mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-21 06:30:09 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a8e684dff | |||
| 7ef99ac60f |
@@ -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
@@ -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
|
||||
|
||||
+24
-17
@@ -980,15 +980,23 @@ type Options struct {
|
||||
Debug bool
|
||||
|
||||
// MCPAuthHandler handles OAuth authorization for remote MCP servers.
|
||||
// When set, remote transports (streamable HTTP, SSE) are configured with
|
||||
// OAuth support. If the server returns a 401, the handler is invoked to
|
||||
// let the user authorize via browser.
|
||||
// When set, remote transports (streamable HTTP, SSE) are configured
|
||||
// with OAuth support. If the server returns a 401, the handler is
|
||||
// invoked to let the user authorize.
|
||||
//
|
||||
// If nil, a [DefaultMCPAuthHandler] is created automatically — opening the
|
||||
// system browser and listening on a local callback server.
|
||||
// If nil, OAuth is disabled: remote MCP servers requiring authorization
|
||||
// will fail to connect and the underlying authorization-required error
|
||||
// is surfaced to the caller. The SDK deliberately does not construct a
|
||||
// default handler — doing so would bind a local TCP port and trigger
|
||||
// presentation I/O (browser open, stderr writes) without the consumer
|
||||
// opting in, which is wrong for library, daemon, or web-app embedders.
|
||||
//
|
||||
// Set to a custom implementation to control the authorization UX (e.g.
|
||||
// display a URL in a custom UI, redirect to a web app, etc.).
|
||||
// CLI consumers: pass [NewCLIMCPAuthHandler] to get the standard
|
||||
// "open browser + print status" behavior.
|
||||
//
|
||||
// Custom UX: implement [MCPAuthHandler] directly, or use
|
||||
// [DefaultMCPAuthHandler] and set its OnAuthURL hook to plug in your
|
||||
// own presentation (TUI modal, QR code, web redirect, etc.).
|
||||
MCPAuthHandler MCPAuthHandler
|
||||
|
||||
// MCPTokenStoreFactory, if non-nil, is called to create a token store for
|
||||
@@ -1362,20 +1370,19 @@ func New(ctx context.Context, opts *Options) (*Kit, error) {
|
||||
OnMCPServerLoaded: opts.OnMCPServerLoaded,
|
||||
}
|
||||
|
||||
// Set up OAuth handler for remote MCP servers.
|
||||
// Set up OAuth handler for remote MCP servers. The SDK does not create
|
||||
// a default handler: auto-construction would bind a local TCP port and
|
||||
// (historically) shell out to a browser without the consumer asking,
|
||||
// which is a surprise for library/daemon/web-app embedders. Consumers
|
||||
// that want CLI behavior pass a [CLIMCPAuthHandler] explicitly; other
|
||||
// consumers implement [MCPAuthHandler] themselves. If nil, remote MCP
|
||||
// servers requiring OAuth will fail to connect with the underlying
|
||||
// authorization-required error surfaced to the caller.
|
||||
//
|
||||
// The SDK MCPAuthHandler interface is structurally identical to
|
||||
// tools.MCPAuthHandler, so any implementation satisfies both.
|
||||
if opts.MCPAuthHandler != nil {
|
||||
setupOpts.AuthHandler = opts.MCPAuthHandler
|
||||
} else {
|
||||
// Create a default handler that opens the system browser.
|
||||
defaultHandler, authErr := NewDefaultMCPAuthHandler()
|
||||
if authErr != nil {
|
||||
// Non-fatal: OAuth just won't be available for remote servers.
|
||||
log.Printf("WARN Failed to create OAuth handler; remote MCP servers requiring auth will fail: %v", authErr)
|
||||
} else {
|
||||
setupOpts.AuthHandler = defaultHandler
|
||||
}
|
||||
}
|
||||
|
||||
// Set up custom token store factory for MCP OAuth tokens.
|
||||
|
||||
+45
-45
@@ -5,18 +5,18 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MCPAuthHandler handles OAuth authorization for MCP servers.
|
||||
// Implementations control the user experience — opening a browser, showing a
|
||||
// prompt, displaying a URL, etc.
|
||||
// prompt, displaying a URL, posting to a message bus, etc.
|
||||
//
|
||||
// The default implementation ([DefaultMCPAuthHandler]) opens the system browser
|
||||
// and starts a local HTTP callback server to receive the authorization code.
|
||||
// [DefaultMCPAuthHandler] provides the transport mechanics (port reservation
|
||||
// and callback server) but performs no user-facing I/O on its own; consumers
|
||||
// wire presentation via [DefaultMCPAuthHandler.OnAuthURL] or implement
|
||||
// MCPAuthHandler from scratch.
|
||||
type MCPAuthHandler interface {
|
||||
// RedirectURI returns the OAuth redirect URI that the callback server
|
||||
// will listen on. This is called during MCP transport setup — before any
|
||||
@@ -37,23 +37,44 @@ type MCPAuthHandler interface {
|
||||
HandleAuth(ctx context.Context, serverName string, authURL string) (callbackURL string, err error)
|
||||
}
|
||||
|
||||
// DefaultMCPAuthHandler opens the system browser and starts a local HTTP
|
||||
// callback server to receive the OAuth authorization code. It eagerly reserves
|
||||
// a TCP port on construction so [RedirectURI] is stable for the lifetime of
|
||||
// the handler.
|
||||
// DefaultMCPAuthHandler provides the transport mechanics of an OAuth flow —
|
||||
// reserving a local TCP port and running a one-shot HTTP callback server —
|
||||
// without making any user-experience decisions. It performs no browser opens,
|
||||
// no printing, no TUI calls; consumers attach presentation by setting
|
||||
// [DefaultMCPAuthHandler.OnAuthURL] or by wrapping the handler.
|
||||
//
|
||||
// Create instances with [NewDefaultMCPAuthHandler] (random port) or
|
||||
// [NewDefaultMCPAuthHandlerWithPort] (explicit port).
|
||||
// The handler eagerly reserves a TCP port on construction so [RedirectURI] is
|
||||
// stable for the lifetime of the handler. Create instances with
|
||||
// [NewDefaultMCPAuthHandler] (random port) or [NewDefaultMCPAuthHandlerWithPort]
|
||||
// (explicit port). Always call [DefaultMCPAuthHandler.Close] when done to
|
||||
// release the port.
|
||||
type DefaultMCPAuthHandler struct {
|
||||
listener net.Listener
|
||||
port int
|
||||
mu sync.Mutex // guards listener lifecycle
|
||||
|
||||
// OnAuthURL, if set, is invoked exactly once per [HandleAuth] call with
|
||||
// the authorization URL the user must visit. This is where consumers
|
||||
// plug in their UX: open a browser, print to stderr, post to a TUI
|
||||
// stream, render a QR code, etc. The handler performs no I/O on the
|
||||
// URL itself; if OnAuthURL is nil the URL is silently dropped and the
|
||||
// user has no way to complete the flow.
|
||||
//
|
||||
// OnAuthURL is called synchronously before the handler blocks on the
|
||||
// callback. It must not block indefinitely — long-running work should
|
||||
// be dispatched to a goroutine.
|
||||
OnAuthURL func(serverName, authURL string)
|
||||
}
|
||||
|
||||
// NewDefaultMCPAuthHandler creates a handler that listens on a random
|
||||
// available port on localhost. The port is reserved immediately so
|
||||
// [RedirectURI] returns a stable value. Call [DefaultMCPAuthHandler.Close]
|
||||
// when the handler is no longer needed to release the port.
|
||||
//
|
||||
// The returned handler has no OnAuthURL hook configured and will therefore
|
||||
// appear to hang on HandleAuth until the context deadline fires. Set
|
||||
// OnAuthURL before using the handler, or use a higher-level wrapper such
|
||||
// as [CLIMCPAuthHandler].
|
||||
func NewDefaultMCPAuthHandler() (*DefaultMCPAuthHandler, error) {
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
@@ -88,9 +109,9 @@ func (h *DefaultMCPAuthHandler) Port() int {
|
||||
return h.port
|
||||
}
|
||||
|
||||
// HandleAuth opens the system browser to authURL and waits for the OAuth
|
||||
// callback on the local server. It returns the full callback URL including
|
||||
// query parameters (code, state, etc.).
|
||||
// HandleAuth invokes [OnAuthURL] with the authorization URL (if configured)
|
||||
// and waits for the OAuth callback on the local server. It returns the full
|
||||
// callback URL including query parameters (code, state, etc.).
|
||||
//
|
||||
// If the context has no deadline, a default 2-minute timeout is applied.
|
||||
// The callback server is started for each HandleAuth call and shut down
|
||||
@@ -136,19 +157,13 @@ func (h *DefaultMCPAuthHandler) HandleAuth(ctx context.Context, serverName strin
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
// Start serving on the pre-reserved listener. We need to create a new
|
||||
// listener on the same port because http.Server.Serve takes ownership
|
||||
// and closes the listener when done. The original listener is kept open
|
||||
// to reserve the port; we create a second listener via SO_REUSEADDR
|
||||
// semantics (Go's default on most platforms) or, more reliably, we
|
||||
// temporarily release and re-acquire.
|
||||
//
|
||||
// Strategy: use the held listener directly for Serve. After Serve
|
||||
// returns (due to Shutdown), re-acquire the listener to keep the port
|
||||
// reserved for future HandleAuth calls.
|
||||
// Start serving on the pre-reserved listener. http.Server.Serve takes
|
||||
// ownership and closes the listener when Shutdown is called, so we
|
||||
// re-acquire a fresh listener on the same port in the deferred cleanup
|
||||
// below to keep the port reserved for subsequent HandleAuth calls.
|
||||
h.mu.Lock()
|
||||
serveListener := h.listener
|
||||
h.listener = nil // Serve will close it
|
||||
h.listener = nil
|
||||
h.mu.Unlock()
|
||||
|
||||
if serveListener == nil {
|
||||
@@ -184,10 +199,11 @@ func (h *DefaultMCPAuthHandler) HandleAuth(ctx context.Context, serverName strin
|
||||
}
|
||||
}()
|
||||
|
||||
// Open the system browser.
|
||||
if err := openBrowser(authURL); err != nil {
|
||||
// Browser open is best-effort; the user can still navigate manually.
|
||||
_ = err
|
||||
// Surface the authorization URL to the consumer. This is the single
|
||||
// presentation seam: the SDK itself does not open browsers, print,
|
||||
// or otherwise touch the user's environment.
|
||||
if h.OnAuthURL != nil {
|
||||
h.OnAuthURL(serverName, authURL)
|
||||
}
|
||||
|
||||
// Wait for the callback, a server error, or context cancellation.
|
||||
@@ -214,22 +230,6 @@ func (h *DefaultMCPAuthHandler) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// openBrowser opens the default system browser to the given URL. This is a
|
||||
// best-effort operation — errors are returned but callers typically ignore
|
||||
// them since the user can navigate manually.
|
||||
func openBrowser(url string) error {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return exec.Command("xdg-open", url).Start()
|
||||
case "windows":
|
||||
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||
case "darwin":
|
||||
return exec.Command("open", url).Start()
|
||||
default:
|
||||
return fmt.Errorf("unsupported platform: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
// oauthSuccessHTML is the HTML page returned to the browser after a
|
||||
// successful OAuth callback.
|
||||
const oauthSuccessHTML = `<!DOCTYPE html>
|
||||
|
||||
+47
-15
@@ -5,32 +5,49 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// CLIMCPAuthHandler wraps a [DefaultMCPAuthHandler] and prints status messages
|
||||
// to a writer (typically stderr) so the user knows what's happening during
|
||||
// OAuth authorization. This is the handler used by the CLI/TUI binary.
|
||||
// CLIMCPAuthHandler is the MCP OAuth handler for CLI/TUI consumers. It wraps
|
||||
// a [DefaultMCPAuthHandler] and layers standard CLI behavior on top of the
|
||||
// underlying transport mechanics:
|
||||
//
|
||||
// For TUI integration, set NotifyFunc to route messages through the TUI's
|
||||
// event system instead of (or in addition to) the writer.
|
||||
// - Opens the authorization URL in the system browser
|
||||
// - Prints status messages (or routes them to a TUI via [NotifyFunc])
|
||||
//
|
||||
// Non-CLI consumers (web apps, daemons, custom TUIs) should not use this
|
||||
// handler; implement [MCPAuthHandler] directly or configure a
|
||||
// [DefaultMCPAuthHandler] with a custom OnAuthURL instead.
|
||||
type CLIMCPAuthHandler struct {
|
||||
inner *DefaultMCPAuthHandler
|
||||
w io.Writer
|
||||
|
||||
// NotifyFunc, when set, is called with status messages instead of writing
|
||||
// to the writer. This allows the TUI to display system messages in the
|
||||
// chat stream. If nil, messages are written to w.
|
||||
// NotifyFunc, when set, is called with status messages instead of
|
||||
// writing to the writer. This allows the TUI to display system
|
||||
// messages in the chat stream. If nil, messages are written to w.
|
||||
NotifyFunc func(serverName, message string)
|
||||
}
|
||||
|
||||
// NewCLIMCPAuthHandler creates a CLI auth handler that prints status messages
|
||||
// to stderr and delegates the actual OAuth flow to a [DefaultMCPAuthHandler].
|
||||
// to stderr, opens the authorization URL in the system browser, and delegates
|
||||
// the callback-server mechanics to a [DefaultMCPAuthHandler].
|
||||
func NewCLIMCPAuthHandler() (*CLIMCPAuthHandler, error) {
|
||||
inner, err := NewDefaultMCPAuthHandler()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &CLIMCPAuthHandler{inner: inner, w: os.Stderr}, nil
|
||||
h := &CLIMCPAuthHandler{inner: inner, w: os.Stderr}
|
||||
// Wire the CLI presentation policy into the inner handler's hook.
|
||||
// This is the one place in the codebase where OAuth triggers a
|
||||
// browser open; the SDK core remains I/O-free.
|
||||
inner.OnAuthURL = func(serverName, authURL string) {
|
||||
h.notify(serverName, fmt.Sprintf("🔐 MCP server %q requires authentication. Opening browser...", serverName))
|
||||
h.notify(serverName, fmt.Sprintf(" If the browser doesn't open, visit:\n %s", authURL))
|
||||
// Browser open is best-effort; the user can still navigate manually.
|
||||
_ = openBrowser(authURL)
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// RedirectURI returns the OAuth redirect URI from the inner handler.
|
||||
@@ -38,17 +55,15 @@ func (h *CLIMCPAuthHandler) RedirectURI() string {
|
||||
return h.inner.RedirectURI()
|
||||
}
|
||||
|
||||
// HandleAuth prints status messages and delegates to the inner handler.
|
||||
// HandleAuth delegates to the inner handler (which invokes OnAuthURL, runs
|
||||
// the callback server, and returns the full callback URL) and emits a final
|
||||
// success or failure notification.
|
||||
func (h *CLIMCPAuthHandler) HandleAuth(ctx context.Context, serverName string, authURL string) (string, error) {
|
||||
h.notify(serverName, fmt.Sprintf("🔐 MCP server %q requires authentication. Opening browser...", serverName))
|
||||
h.notify(serverName, fmt.Sprintf(" If the browser doesn't open, visit:\n %s", authURL))
|
||||
|
||||
callbackURL, err := h.inner.HandleAuth(ctx, serverName, authURL)
|
||||
if err != nil {
|
||||
h.notify(serverName, fmt.Sprintf("✗ Authentication failed for %q: %v", serverName, err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
h.notify(serverName, fmt.Sprintf("✓ Authenticated with %q", serverName))
|
||||
return callbackURL, nil
|
||||
}
|
||||
@@ -66,3 +81,20 @@ func (h *CLIMCPAuthHandler) notify(serverName, message string) {
|
||||
}
|
||||
_, _ = fmt.Fprintln(h.w, message)
|
||||
}
|
||||
|
||||
// openBrowser opens the system default browser at url. Intentionally
|
||||
// unexported: browser opening is CLI policy, not SDK surface. Consumers
|
||||
// that need similar behavior for their own UX should bring their own
|
||||
// helper (or use a third-party package like github.com/pkg/browser).
|
||||
func openBrowser(url string) error {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return exec.Command("xdg-open", url).Start()
|
||||
case "windows":
|
||||
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||
case "darwin":
|
||||
return exec.Command("open", url).Start()
|
||||
default:
|
||||
return fmt.Errorf("unsupported platform: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
+70
-3
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user