docs(sdk): document MCPAuthHandler and OAuth opt-in behavior

Reflect the refactor that made MCPAuthHandler an explicit, opt-in
dependency for remote MCP OAuth. Four surfaces updated:

- README.md: new 'MCP OAuth (remote MCP servers)' subsection under the
  Go SDK section, outlining the three consumer patterns (nil / CLI /
  custom) and linking to the full options docs.
- pkg/kit/README.md: type cheat-sheet now lists MCPAuthHandler,
  DefaultMCPAuthHandler, and CLIMCPAuthHandler alongside the existing
  MCPTokenStore entries.
- skills/kit-sdk/SKILL.md: Options example annotated with nil-disables-
  OAuth semantics; new 'MCP OAuth Authorization' section precedes the
  existing token-storage section; re-exported types list expanded.
- www/pages/sdk/options.md: Options fields table gains MCPAuthHandler
  row; new top-level 'MCP OAuth Authorization' section with consumer
  matrix, CLI/custom/fully-custom code samples, and a warning callout
  about the OnAuthURL nil-hang footgun.
This commit is contained in:
Ed Zynda
2026-04-17 15:30:10 +03:00
parent 7ef99ac60f
commit 8a8e684dff
4 changed files with 194 additions and 9 deletions
+32
View File
@@ -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:
+7 -4
View File
@@ -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
+70 -3
View File
@@ -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)
+85 -2
View File
@@ -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