diff --git a/README.md b/README.md index 4653d1ca..70d3c986 100644 --- a/README.md +++ b/README.md @@ -588,6 +588,38 @@ are pointer types so explicit `0.0` is distinguishable from "leave alone"; a non-zero `MaxTokens` suppresses automatic right-sizing the same way `--max-tokens` does on the CLI. +### MCP OAuth (remote MCP servers) + +When a remote MCP server returns 401, Kit runs the full OAuth flow (dynamic +client registration → PKCE → token exchange → persistence) but delegates the +user-facing step — showing the authorization URL and receiving the callback — +to an `MCPAuthHandler` that you pass explicitly via `Options.MCPAuthHandler`. +If nil, OAuth is disabled and the authorization-required error surfaces to the +caller; the SDK never auto-opens a browser or binds a localhost port. + +```go +// CLI/TUI apps: opens the system browser + prints status to stderr. +authHandler, _ := kit.NewCLIMCPAuthHandler() +defer authHandler.Close() + +host, _ := kit.New(ctx, &kit.Options{ + MCPAuthHandler: authHandler, +}) + +// Custom UX: reuse the SDK's port + callback server, supply your own +// presentation via OnAuthURL (TUI modal, QR code, web redirect, etc.). +// h, _ := kit.NewDefaultMCPAuthHandler() +// h.OnAuthURL = func(server, authURL string) { myUI.Show(server, authURL) } +// +// Full control (web apps, daemons): implement kit.MCPAuthHandler yourself — +// no localhost binding, no side effects. +``` + +Tokens are persisted to `$XDG_CONFIG_HOME/.kit/mcp_tokens.json` by default; swap +in a custom `MCPTokenStoreFactory` for encrypted, DB-backed, or in-memory +storage. See the [SDK options docs](/sdk/options#mcp-oauth-authorization) for +the full matrix. + ### Custom Tools Create custom tools with automatic schema generation — no external dependencies needed: diff --git a/pkg/kit/README.md b/pkg/kit/README.md index 88664016..f7f451df 100644 --- a/pkg/kit/README.md +++ b/pkg/kit/README.md @@ -224,10 +224,13 @@ kit.LLMResponse // {Content, FinishReason, Usage} kit.LLMFilePart // {Filename, Data []byte, MediaType} // MCP OAuth types -kit.MCPServer // *server.MCPServer for in-process MCP transport -kit.MCPServerConfig // Configuration for an MCP server (stdio, SSE, or in-process) -kit.MCPTokenStore // Persists OAuth tokens for a single MCP server -kit.MCPToken // OAuth token (access token, refresh token, expiry) +kit.MCPServer // *server.MCPServer for in-process MCP transport +kit.MCPServerConfig // Configuration for an MCP server (stdio, SSE, or in-process) +kit.MCPAuthHandler // Interface: handles user-facing OAuth authorization +kit.DefaultMCPAuthHandler // Port + callback-server mechanics; set OnAuthURL for presentation +kit.CLIMCPAuthHandler // CLI wrapper: opens browser, prints status +kit.MCPTokenStore // Persists OAuth tokens for a single MCP server +kit.MCPToken // OAuth token (access token, refresh token, expiry) kit.MCPTokenStoreFactory // Creates an MCPTokenStore for a given server URL // Conversion helpers diff --git a/skills/kit-sdk/SKILL.md b/skills/kit-sdk/SKILL.md index 0dc1a02e..84f40128 100644 --- a/skills/kit-sdk/SKILL.md +++ b/skills/kit-sdk/SKILL.md @@ -125,7 +125,12 @@ host, err := kit.New(ctx, &kit.Options{ AutoCompact: true, // auto-compact near context limit CompactionOptions: &kit.CompactionOptions{...}, // nil = defaults - // MCP OAuth + // MCP OAuth — both fields are opt-in. If MCPAuthHandler is nil, + // remote MCP servers that require OAuth will fail to connect with + // an authorization-required error instead of silently opening a + // browser. CLI consumers use NewCLIMCPAuthHandler; other embedders + // implement MCPAuthHandler or configure DefaultMCPAuthHandler. + MCPAuthHandler: mcpAuthHandler, // nil = OAuth disabled MCPTokenStoreFactory: func(serverURL string) (kit.MCPTokenStore, error) { return myCustomStore(serverURL), nil // custom OAuth token storage }, @@ -820,9 +825,65 @@ err = host.SubscribeMCPResource(ctx, "myserver", "file:///path/to/file") err = host.UnsubscribeMCPResource(ctx, "myserver", "file:///path/to/file") ``` +### MCP OAuth Authorization + +When a remote MCP server requires OAuth, Kit runs the full authorization flow +(dynamic client registration → PKCE → user consent → token exchange → token +persistence) but delegates the **user-facing step** — displaying the +authorization URL and receiving the callback — to an `MCPAuthHandler`. + +The SDK ships three building blocks: + +| Building block | When to use | +|---|---| +| **No handler** (`Options.MCPAuthHandler = nil`) | Default. OAuth is disabled; 401s from remote MCP servers surface as errors. Correct for library, daemon, and web-app embedders that don't want side effects. | +| **`kit.NewCLIMCPAuthHandler()`** | CLI/TUI apps. Opens the system browser, prints status to stderr (or via `NotifyFunc`), runs a localhost callback server. This is what the `kit` binary uses. | +| **`kit.NewDefaultMCPAuthHandler()` + `OnAuthURL`** | Custom UX. Get the transport mechanics (port reservation + callback server) from the SDK; wire your own presentation in the `OnAuthURL(serverName, authURL)` closure. | +| **Implement `kit.MCPAuthHandler` directly** | Full control. No localhost binding — e.g. return the URL from an HTTP endpoint and have the consumer POST the callback URL back. | + +**CLI-style embedder (browser + stderr):** + +```go +authHandler, err := kit.NewCLIMCPAuthHandler() +if err != nil { + log.Fatal(err) +} +defer authHandler.Close() // release the reserved port + +host, _ := kit.New(ctx, &kit.Options{ + MCPAuthHandler: authHandler, +}) +``` + +**Custom UX embedder (TUI modal, QR code, web redirect, etc.):** + +```go +authHandler, _ := kit.NewDefaultMCPAuthHandler() +authHandler.OnAuthURL = func(serverName, authURL string) { + // Render the URL however you like — no browser or terminal assumptions. + myUI.ShowAuthPrompt(serverName, authURL) +} +defer authHandler.Close() + +host, _ := kit.New(ctx, &kit.Options{ + MCPAuthHandler: authHandler, +}) +``` + +**Important:** `DefaultMCPAuthHandler` with no `OnAuthURL` set will silently +drop the authorization URL and block until the 2-minute callback timeout +fires. Always set `OnAuthURL`, or use a higher-level wrapper like +`CLIMCPAuthHandler`. + ### MCP OAuth Token Storage -For remote MCP servers that use OAuth, you can provide a custom token store: +Once authorization succeeds, the resulting access/refresh tokens are persisted +by an `MCPTokenStore`. By default tokens are written to +`$XDG_CONFIG_HOME/.kit/mcp_tokens.json` (fallback `~/.config/.kit/mcp_tokens.json`), +keyed by server URL, with `0600` file permissions. + +Provide a custom store for encrypted storage, database persistence, or +in-memory-only flows: ```go host, _ := kit.New(ctx, &kit.Options{ @@ -832,7 +893,7 @@ host, _ := kit.New(ctx, &kit.Options{ }) ``` -The `MCPTokenStore` interface requires `GetToken`/`SetToken`/`DeleteToken` methods. Return `kit.ErrMCPNoToken` from `GetToken` when no token is stored. When nil (default), tokens are persisted to `$XDG_CONFIG_HOME/.kit/mcp_tokens.json`. +The `MCPTokenStore` interface requires `GetToken`/`SetToken`/`DeleteToken` methods. Return `kit.ErrMCPNoToken` from `GetToken` when no token is stored. --- @@ -1015,6 +1076,12 @@ kit.LLMFilePart // {Filename, Data []byte, MediaType} kit.CompactionResult, kit.CompactionOptions // MCP OAuth types +kit.MCPAuthHandler // interface: RedirectURI() + HandleAuth(ctx, server, authURL) for OAuth UX +kit.DefaultMCPAuthHandler // SDK-provided transport mechanics (port + callback server); set OnAuthURL hook +kit.CLIMCPAuthHandler // CLI wrapper around DefaultMCPAuthHandler: opens browser, prints status +kit.NewDefaultMCPAuthHandler() // random port, no UX side effects +kit.NewDefaultMCPAuthHandlerWithPort() // fixed port (useful when registering a stable redirect URI) +kit.NewCLIMCPAuthHandler() // CLI handler: browser + stderr + localhost callback kit.MCPTokenStore // interface for custom OAuth token storage kit.MCPToken // OAuth token struct (access, refresh, expiry) kit.MCPTokenStoreFactory // func(serverURL string) (MCPTokenStore, error) diff --git a/www/pages/sdk/options.md b/www/pages/sdk/options.md index 29a55911..1fd74564 100644 --- a/www/pages/sdk/options.md +++ b/www/pages/sdk/options.md @@ -65,7 +65,11 @@ host, err := kit.New(ctx, &kit.Options{ // Session (advanced) SessionManager: myCustomSession, // custom SessionManager implementation - // MCP OAuth + // MCP OAuth — both opt-in. Leave MCPAuthHandler nil to disable + // OAuth entirely (remote MCP 401s bubble up as errors). CLI apps + // pass kit.NewCLIMCPAuthHandler(); custom UX embedders implement + // MCPAuthHandler or configure DefaultMCPAuthHandler + OnAuthURL. + MCPAuthHandler: authHandler, // nil = OAuth disabled MCPTokenStoreFactory: func(serverURL string) (kit.MCPTokenStore, error) { return myStore(serverURL), nil }, @@ -162,9 +166,88 @@ when embedding Kit as a library. |-------|------|---------|-------------| | `AutoCompact` | `bool` | `false` | Auto-compact when near context limit | | `CompactionOptions` | `*CompactionOptions` | — | Configuration for auto-compaction | -| `MCPTokenStoreFactory` | `func` | — | Custom OAuth token storage for MCP servers | +| `MCPAuthHandler` | `MCPAuthHandler` | — | OAuth handler for remote MCP servers. `nil` disables OAuth (servers returning 401 fail with the authorization-required error). See [MCP OAuth](#mcp-oauth-authorization) below. | +| `MCPTokenStoreFactory` | `func` | — | Custom OAuth token storage for MCP servers (default: JSON file in `$XDG_CONFIG_HOME/.kit/mcp_tokens.json`). | | `InProcessMCPServers` | `map[string]*MCPServer` | — | In-process mcp-go servers (no subprocess) | +## MCP OAuth Authorization + +When a remote MCP server (SSE or Streamable HTTP) requires OAuth, Kit runs +the full authorization flow (dynamic client registration → PKCE → user +consent → token exchange → token persistence) but delegates the **user-facing +step** — displaying the authorization URL and receiving the callback — to +an `MCPAuthHandler`. + +The SDK is deliberately inert when `MCPAuthHandler` is `nil`: it does **not** +auto-construct a default handler, bind a local TCP port, or open a browser. +This keeps library, daemon, and web-app embedders free of surprise I/O. +Consumers opt in by passing a handler explicitly. + +| Building block | When to use | +|---|---| +| `MCPAuthHandler = nil` (default) | OAuth disabled. Remote MCP servers requiring auth fail with a clear error. Correct for libraries, daemons, and web apps. | +| `kit.NewCLIMCPAuthHandler()` | CLI/TUI apps. Opens the system browser, prints status to stderr (or via `NotifyFunc`), runs a localhost callback server. Used by the `kit` binary. | +| `kit.NewDefaultMCPAuthHandler()` + `OnAuthURL` | Custom UX. Use the SDK's port reservation and callback server; plug in your own presentation via the `OnAuthURL(serverName, authURL)` closure. | +| Implement `kit.MCPAuthHandler` directly | Full control. No localhost binding — e.g. return the URL from an HTTP endpoint and have the consumer POST the callback URL back. | + +**CLI-style embedder:** + +```go +authHandler, err := kit.NewCLIMCPAuthHandler() +if err != nil { + log.Fatal(err) +} +defer authHandler.Close() // release the reserved port + +host, _ := kit.New(ctx, &kit.Options{ + MCPAuthHandler: authHandler, +}) +``` + +**Custom UX embedder (TUI modal, QR code, web redirect, etc.):** + +```go +authHandler, _ := kit.NewDefaultMCPAuthHandler() +authHandler.OnAuthURL = func(serverName, authURL string) { + // No browser or terminal assumptions — render however you like. + myUI.ShowAuthPrompt(serverName, authURL) +} +defer authHandler.Close() + +host, _ := kit.New(ctx, &kit.Options{ + MCPAuthHandler: authHandler, +}) +``` + +**Fully custom handler (no local port binding at all):** + +```go +type WebAuthHandler struct { + redirectURI string + callbacks chan string +} + +func (h *WebAuthHandler) RedirectURI() string { return h.redirectURI } + +func (h *WebAuthHandler) HandleAuth(ctx context.Context, serverName, authURL string) (string, error) { + // Push the URL to the user's existing browser session via your web app, + // then block on the callback that your HTTP handler pushes onto the channel. + h.pushToUserSession(serverName, authURL) + select { + case callbackURL := <-h.callbacks: + return callbackURL, nil + case <-ctx.Done(): + return "", ctx.Err() + } +} +``` + +::: warning +`DefaultMCPAuthHandler` with no `OnAuthURL` set will silently drop the +authorization URL and hang until the 2-minute callback timeout fires. Always +set `OnAuthURL`, or use a higher-level wrapper like `CLIMCPAuthHandler`. +::: + ## Precedence For any given generation or provider field, the effective value is resolved