diff --git a/README.md b/README.md index 2747f4d4..c5ce7f02 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,11 @@ mcpServers: type: remote url: "https://pubmed.mcp.example.com" noOAuth: true # skip OAuth for public servers that don't require auth + + builds: + type: remote + url: "https://builds.mcp.example.com" + tasksMode: always # async task execution — see MCP Tasks below ``` ## CLI Reference @@ -626,6 +631,36 @@ 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. +### MCP Tasks (long-running tools) + +Kit advertises [MCP task support](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) +during `initialize`, so cooperating MCP servers can respond to `tools/call` +with a `taskId` instead of blocking the connection. Kit then polls +`tasks/get` / `tasks/result` until the task reaches a terminal state, and +best-effort `tasks/cancel`s on context cancellation. + +Defaults are safe — a server that doesn't advertise task capability runs +synchronously, exactly as before. Opt in per server via `tasksMode` in +`.kit.yml` (`auto` | `never` | `always`) or programmatically through the SDK: + +```go +host, _ := kit.New(ctx, &kit.Options{ + MCPTaskMode: map[string]kit.MCPTaskMode{ + "build-server": kit.MCPTaskModeAlways, + }, + MCPTaskTimeout: 15 * time.Minute, + MCPTaskProgress: func(p kit.MCPTaskProgress) { + log.Printf("%s: %s", p.TaskID, p.Status) + }, +}) + +tasks, _ := host.ListMCPTasks(ctx, "build-server") +_, _ = host.CancelMCPTask(ctx, "build-server", tasks[0].TaskID) +``` + +See the [configuration docs](/configuration#mcp-tasks-long-running-tools) and +[SDK options → MCP Tasks](/sdk/options#mcp-tasks) for the full surface. + ### 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 24e2d978..5e06ccfb 100644 --- a/pkg/kit/README.md +++ b/pkg/kit/README.md @@ -190,6 +190,41 @@ msg, err := host.GetMCPPrompt(ctx, "server-name", "prompt-name", map[string]stri }) ``` +### MCP Tasks (long-running tools) + +Kit advertises [MCP task support](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) +during `initialize`. Cooperating servers can respond to `tools/call` with a +`taskId` immediately; Kit then polls `tasks/get` / `tasks/result` until the +task reaches a terminal state, and best-effort `tasks/cancel`s on context +cancellation. Servers that don't advertise the capability keep their previous +synchronous behaviour. + +```go +host, _ := kit.New(ctx, &kit.Options{ + // Per-server mode: auto (default), never, or always. + MCPTaskMode: map[string]kit.MCPTaskMode{ + "build-server": kit.MCPTaskModeAlways, + }, + MCPTaskTimeout: 15 * time.Minute, // total wall-clock cap + MCPTaskProgress: func(p kit.MCPTaskProgress) { + log.Printf("%s/%s: %s", p.Server, p.TaskID, p.Status) + }, +}) + +// Inspect / cancel in-flight tasks +tasks, _ := host.ListMCPTasks(ctx, "build-server") +t, _ := host.GetMCPTask(ctx, "build-server", tasks[0].TaskID) +if !t.Status.IsTerminal() { + _, _ = host.CancelMCPTask(ctx, "build-server", t.TaskID) +} +``` + +The progress handler fires once when a task is accepted and again on every +observed status transition; the final invocation always carries a terminal +status (`MCPTaskStatusCompleted`, `MCPTaskStatusFailed`, or +`MCPTaskStatusCancelled`). Don't block in the handler — dispatch long work on +a goroutine. + ### Session Management Maintain conversation context: diff --git a/www/pages/configuration.md b/www/pages/configuration.md index b1587265..5afd8552 100644 --- a/www/pages/configuration.md +++ b/www/pages/configuration.md @@ -88,6 +88,11 @@ mcpServers: type: remote url: "https://pubmed.mcp.example.com" noOAuth: true # skip OAuth for public servers + + builds: + type: remote + url: "https://builds.mcp.example.com" + tasksMode: always # always run tools/call as async tasks (Phase 1 MVP) ``` ### MCP server fields @@ -101,9 +106,34 @@ mcpServers: | `allowedTools` | list | Whitelist of tool names to expose | | `excludedTools` | list | Blacklist of tool names to hide | | `noOAuth` | bool | Skip OAuth for this server (for public servers that don't require auth) | +| `tasksMode` | string | When to augment `tools/call` with MCP task metadata: `auto` (default — only when the server advertises task support), `never`, or `always`. See [MCP tasks](#mcp-tasks-long-running-tools). | A legacy format with `transport`, `args`, `env`, and `headers` fields is also supported. +### MCP tasks (long-running tools) + +Kit advertises [MCP task support](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) +during `initialize` so servers can respond to `tools/call` with a +`CreateTaskResult` (a task ID + `working` status) instead of blocking until +the operation finishes. Kit then polls `tasks/get` / `tasks/result` until the +task reaches a terminal state, and best-effort `tasks/cancel`s on context +cancellation. + +This avoids HTTP/SSE proxy timeouts on long builds, deploys, and batch jobs, +and lets the user/agent abort cleanly with Ctrl-C. + +**Per-server `tasksMode`:** + +| Value | Behaviour | +|-------|-----------| +| `auto` (default) | Augment `tools/call` with task metadata only when the server advertised `tasks/toolCalls` capability. Servers that don't advertise it run synchronously, exactly as before. | +| `never` | Always issue `tools/call` synchronously, regardless of server capability. | +| `always` | Always opt into task augmentation, even when the server didn't advertise the capability. The server may still respond synchronously — this just expresses client intent unconditionally. | + +Defaults are safe: any existing MCP server keeps its previous behaviour +bit-for-bit. SDK consumers can also override the mode programmatically and +plug in a progress callback — see [SDK options](/sdk/options#mcp-tasks). + ## Custom models Define custom models in your `.kit.yml` for use with the `custom` provider. This is useful for self-hosted models or API endpoints not in the built-in database: diff --git a/www/pages/sdk/options.md b/www/pages/sdk/options.md index be9d643c..00fe433e 100644 --- a/www/pages/sdk/options.md +++ b/www/pages/sdk/options.md @@ -169,6 +169,12 @@ when embedding Kit as a library. | `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) | +| `MCPTaskMode` | `map[string]MCPTaskMode` | — | Per-server override for task-augmented `tools/call`. Keys are server names; missing entries fall back to the `tasksMode` field of the matching `MCPServerConfig`. See [MCP Tasks](#mcp-tasks). | +| `MCPTaskTimeout` | `time.Duration` | `15m` | Maximum wall-clock to wait for a task to reach a terminal state. Independent of any per-call context deadline. | +| `MCPTaskTTL` | `time.Duration` | — | TTL hint sent in `TaskParams` for every task-augmented call. Zero omits the field and lets the server pick. | +| `MCPTaskPollInterval` | `time.Duration` | `1s` | Fallback interval between `tasks/get` requests when the server does not suggest one. | +| `MCPTaskMaxPollInterval` | `time.Duration` | `5s` | Cap on the polling interval (a server-supplied `pollInterval` can otherwise grow without bound). | +| `MCPTaskProgress` | `MCPTaskProgressHandler` | — | Optional callback invoked once when a task is accepted and on every observed status transition. The final invocation always carries a terminal status. | ## MCP OAuth Authorization @@ -248,6 +254,79 @@ authorization URL and hang until the 2-minute callback timeout fires. Always set `OnAuthURL`, or use a higher-level wrapper like `CLIMCPAuthHandler`. ::: +## MCP Tasks + +The [MCP Tasks utility](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) +turns a synchronous `tools/call` into a pollable async job: the server +returns a `taskId` with status `working` immediately, and the client polls +`tasks/get` / `tasks/result` until the task reaches a terminal state. + +Kit advertises task support during `initialize` and, by default, augments +`tools/call` with task metadata only when the server advertises +`tasks/toolCalls` capability — so any existing MCP server keeps its previous +synchronous behaviour bit-for-bit. Long-running tools (builds, deployments, +batch jobs, sub-agent runs) get HTTP/SSE timeout-resistance and clean +cancellation "for free" once both sides opt in. + +### Per-server mode + +```go +import "time" + +host, _ := kit.New(ctx, &kit.Options{ + MCPTaskMode: map[string]kit.MCPTaskMode{ + "build-server": kit.MCPTaskModeAlways, // force task-augmented calls + "chat-server": kit.MCPTaskModeNever, // force synchronous calls + // any server not in the map honours its `tasksMode` config field + // (default "auto") + }, +}) +``` + +| Mode | Behaviour | +|---|---| +| `MCPTaskModeAuto` (default) | Augment `tools/call` with `TaskParams` only when the server advertised `tasks/toolCalls`. | +| `MCPTaskModeNever` | Always issue `tools/call` synchronously, ignoring server capability. | +| `MCPTaskModeAlways` | Always opt in, even when the server didn't advertise the capability. The server may still respond synchronously. | + +### Progress callbacks + +```go +host, _ := kit.New(ctx, &kit.Options{ + MCPTaskTimeout: 15 * time.Minute, // total wall-clock cap + MCPTaskTTL: 30 * time.Minute, // server retention hint + MCPTaskProgress: func(p kit.MCPTaskProgress) { + log.Printf("%s/%s: %s %s", p.Server, p.TaskID, p.Status, p.Message) + }, +}) +``` + +The handler fires once when a task is accepted and again on every observed +status transition. The final call always carries a terminal status +(`MCPTaskStatusCompleted`, `MCPTaskStatusFailed`, or `MCPTaskStatusCancelled`). +Do not block in the handler — dispatch long work on a goroutine. + +### Inspecting and cancelling tasks + +```go +tasks, _ := host.ListMCPTasks(ctx, "build-server") +for _, t := range tasks { + fmt.Printf("%s: %s (%s)\n", t.TaskID, t.Status, t.StatusMessage) +} + +t, _ := host.GetMCPTask(ctx, "build-server", taskID) +if !t.Status.IsTerminal() { + _, _ = host.CancelMCPTask(ctx, "build-server", taskID) +} +``` + +`Kit.ListMCPTasks`, `Kit.GetMCPTask`, and `Kit.CancelMCPTask` work against any +loaded MCP server that advertises the corresponding capability. +`MCPTaskStatus.IsTerminal()` is the canonical check for completion. + +Context cancellation also works end-to-end: cancelling the `ctx` passed to a +tool execution triggers a best-effort `tasks/cancel` before the call returns. + ## Precedence For any given generation or provider field, the effective value is resolved diff --git a/www/pages/sdk/overview.md b/www/pages/sdk/overview.md index 7a22c93c..79a18c35 100644 --- a/www/pages/sdk/overview.md +++ b/www/pages/sdk/overview.md @@ -215,6 +215,33 @@ resources := host.ListMCPResources() content, _ := host.ReadMCPResource(ctx, "server", "file:///path") ``` +## MCP tasks (long-running tools) + +Kit advertises [MCP task support](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) +during `initialize`, so cooperating servers can return a `taskId` immediately +and let Kit poll `tasks/get` / `tasks/result` until the operation completes. +This avoids HTTP/SSE proxy timeouts on long tools and gives you clean +cancellation via context. + +```go +host, _ := kit.New(ctx, &kit.Options{ + MCPTaskMode: map[string]kit.MCPTaskMode{ + "build-server": kit.MCPTaskModeAlways, + }, + MCPTaskProgress: func(p kit.MCPTaskProgress) { + log.Printf("%s: %s", p.TaskID, p.Status) + }, +}) + +// Inspect / cancel in-flight tasks +tasks, _ := host.ListMCPTasks(ctx, "build-server") +_, _ = host.CancelMCPTask(ctx, "build-server", tasks[0].TaskID) +``` + +Defaults to `MCPTaskModeAuto` per server, so any existing MCP server keeps +its previous synchronous behaviour. See [SDK options → MCP Tasks](/sdk/options#mcp-tasks) +for the full surface. + ## Context and compaction Monitor and manage context usage: