Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,3 +546,43 @@ that optional capabilities outside the core protocol can be declared on the
wire. Keys are namespaced as `"{vendor-prefix}/{extension-name}"`; values
are per-extension settings objects.

Use `AddExtension` to declare one and `HasExtension` to test for one:

```go
caps := &mcp.ClientCapabilities{}
caps.AddExtension("io.example/my-extension", nil)
client := mcp.NewClient(impl, &mcp.ClientOptions{Capabilities: caps})

cs, err := client.Connect(ctx, transport, nil)
...
if cs.InitializeResult().Capabilities.HasExtension("io.example/my-extension") {
// The server declared it too.
}
```

#### Tasks

[SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md)
moved tasks out of the core protocol and into the
[tasks extension](https://github.com/modelcontextprotocol/ext-tasks/blob/main/specification/draft/tasks.md),
identified by the `mcp.ExtensionTasks` constant. A server that has negotiated
it may answer a request with a durable task handle instead of the result that
was asked for, which the client then polls to completion.

**The SDK does not implement task execution**, and does not declare the
extension by default. Declaring it is a promise to the peer: a client that
declares it must be prepared for any eligible request to return a task handle
instead of a result. Only declare it if you implement that polling flow
yourself.

If a server returns a task handle anyway, decoding fails with
`*mcp.UnsupportedTaskResultError`, which carries the task ID:

```go
res, err := cs.CallTool(ctx, params)
var terr *mcp.UnsupportedTaskResultError
if errors.As(err, &terr) {
log.Printf("server created task %s, which this SDK cannot resolve", terr.TaskID)
}
```

30 changes: 30 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,36 @@ capabilities outside the core protocol can be declared on the wire. Keys
are namespaced as `"{vendor-prefix}/{extension-name}"`; values are
per-extension settings objects.

Use `AddExtension` to declare one, and `HasExtension` to test what the client
declared. Client capabilities are read from the request, since as of protocol
version 2026-07-28 they travel in each request's `_meta` rather than in the
initialize handshake:

```go
caps := &mcp.ServerCapabilities{}
caps.AddExtension("io.example/my-extension", nil)
server := mcp.NewServer(impl, &mcp.ServerOptions{Capabilities: caps})

// Inside a tool handler:
if req.ClientCapabilities().HasExtension("io.example/my-extension") {
// The client declared it too.
}
```

#### Tasks

[SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md)
moved tasks out of the core protocol and into the
[tasks extension](https://github.com/modelcontextprotocol/ext-tasks/blob/main/specification/draft/tasks.md),
identified by the `mcp.ExtensionTasks` constant. A server that has negotiated
it may answer a request with a durable task handle instead of the result that
was asked for, which the client then polls to completion.

**The SDK does not implement task execution**, and does not declare the
extension by default. Declaring it is a promise to the peer: a server that
declares it must serve `tasks/get`, `tasks/update` and `tasks/cancel`. Only
declare it if you implement those yourself.

### Pagination

Server-side feature lists may be
Expand Down
40 changes: 40 additions & 0 deletions internal/docs/client.src.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,43 @@ that optional capabilities outside the core protocol can be declared on the
wire. Keys are namespaced as `"{vendor-prefix}/{extension-name}"`; values
are per-extension settings objects.

Use `AddExtension` to declare one and `HasExtension` to test for one:

```go
caps := &mcp.ClientCapabilities{}
caps.AddExtension("io.example/my-extension", nil)
client := mcp.NewClient(impl, &mcp.ClientOptions{Capabilities: caps})

cs, err := client.Connect(ctx, transport, nil)
...
if cs.InitializeResult().Capabilities.HasExtension("io.example/my-extension") {
// The server declared it too.
}
```

#### Tasks

[SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md)
moved tasks out of the core protocol and into the
[tasks extension](https://github.com/modelcontextprotocol/ext-tasks/blob/main/specification/draft/tasks.md),
identified by the `mcp.ExtensionTasks` constant. A server that has negotiated
it may answer a request with a durable task handle instead of the result that
was asked for, which the client then polls to completion.

**The SDK does not implement task execution**, and does not declare the
extension by default. Declaring it is a promise to the peer: a client that
declares it must be prepared for any eligible request to return a task handle
instead of a result. Only declare it if you implement that polling flow
yourself.

If a server returns a task handle anyway, decoding fails with
`*mcp.UnsupportedTaskResultError`, which carries the task ID:

```go
res, err := cs.CallTool(ctx, params)
var terr *mcp.UnsupportedTaskResultError
if errors.As(err, &terr) {
log.Printf("server created task %s, which this SDK cannot resolve", terr.TaskID)
}
```

30 changes: 30 additions & 0 deletions internal/docs/server.src.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,36 @@ capabilities outside the core protocol can be declared on the wire. Keys
are namespaced as `"{vendor-prefix}/{extension-name}"`; values are
per-extension settings objects.

Use `AddExtension` to declare one, and `HasExtension` to test what the client
declared. Client capabilities are read from the request, since as of protocol
version 2026-07-28 they travel in each request's `_meta` rather than in the
initialize handshake:

```go
caps := &mcp.ServerCapabilities{}
caps.AddExtension("io.example/my-extension", nil)
server := mcp.NewServer(impl, &mcp.ServerOptions{Capabilities: caps})

// Inside a tool handler:
if req.ClientCapabilities().HasExtension("io.example/my-extension") {
// The client declared it too.
}
```

#### Tasks

[SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md)
moved tasks out of the core protocol and into the
[tasks extension](https://github.com/modelcontextprotocol/ext-tasks/blob/main/specification/draft/tasks.md),
identified by the `mcp.ExtensionTasks` constant. A server that has negotiated
it may answer a request with a durable task handle instead of the result that
was asked for, which the client then polls to completion.

**The SDK does not implement task execution**, and does not declare the
extension by default. Declaring it is a promise to the peer: a server that
declares it must serve `tasks/get`, `tasks/update` and `tasks/cancel`. Only
declare it if you implement those yourself.

### Pagination

Server-side feature lists may be
Expand Down
37 changes: 37 additions & 0 deletions mcp/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ const (
// input before it can complete the request. The client should fulfill the
// InputRequests and retry the call with the responses.
resultTypeInputRequired resultType = "input_required"

// resultTypeTask is reserved by the io.modelcontextprotocol/tasks
// extension to discriminate a CreateTaskResult from a standard result.
// See [ExtensionTasks].
resultTypeTask resultType = "task"
)

type completeResultWithType struct {
Expand Down Expand Up @@ -470,10 +475,14 @@ func (x *CallToolResult) UnmarshalJSON(data []byte) error {
Content []*wireContent `json:"content"`
StructuredContent json.RawMessage `json:"structuredContent"`
ResultType resultType `json:"resultType"`
TaskID string `json:"taskId"`
}
if err := internaljson.Unmarshal(data, &wire); err != nil {
return err
}
if wire.ResultType == resultTypeTask {
return &UnsupportedTaskResultError{TaskID: wire.TaskID}
}
if len(wire.StructuredContent) > 0 {
unmarshal := internaljson.UnmarshalUseNumber
if structuredcontentfloat64 == "1" {
Expand Down Expand Up @@ -598,6 +607,16 @@ func (c *ClientCapabilities) AddExtension(name string, settings map[string]any)
c.Extensions[name] = settings
}

// HasExtension reports whether c declares the extension with the given name.
// It is safe to call on a nil *ClientCapabilities.
func (c *ClientCapabilities) HasExtension(name string) bool {
if c == nil {
return false
}
_, ok := c.Extensions[name]
return ok
}

// clone returns a copy of the ClientCapabilities.
// Values in the Extensions and Experimental maps are shallow-copied.
func (c *ClientCapabilities) clone() *ClientCapabilities {
Expand Down Expand Up @@ -1116,10 +1135,14 @@ func (x *GetPromptResult) UnmarshalJSON(data []byte) error {
var wire struct {
res
ResultType resultType `json:"resultType"`
TaskID string `json:"taskId"`
}
if err := internaljson.Unmarshal(data, &wire); err != nil {
return err
}
if wire.ResultType == resultTypeTask {
return &UnsupportedTaskResultError{TaskID: wire.TaskID}
}
wire.res.resultType = wire.ResultType
*x = GetPromptResult(wire.res)
return nil
Expand Down Expand Up @@ -1747,10 +1770,14 @@ func (x *ReadResourceResult) UnmarshalJSON(data []byte) error {
var wire struct {
res
ResultType resultType `json:"resultType"`
TaskID string `json:"taskId"`
}
if err := internaljson.Unmarshal(data, &wire); err != nil {
return err
}
if wire.ResultType == resultTypeTask {
return &UnsupportedTaskResultError{TaskID: wire.TaskID}
}
wire.res.resultType = wire.ResultType
*x = ReadResourceResult(wire.res)
return nil
Expand Down Expand Up @@ -2406,6 +2433,16 @@ func (c *ServerCapabilities) AddExtension(name string, settings map[string]any)
c.Extensions[name] = settings
}

// HasExtension reports whether c declares the extension with the given name.
// It is safe to call on a nil *ServerCapabilities.
func (c *ServerCapabilities) HasExtension(name string) bool {
if c == nil {
return false
}
_, ok := c.Extensions[name]
return ok
}

// clone returns a copy of the ServerCapabilities.
// Values in the Extensions and Experimental maps are shallow-copied.
func (c *ServerCapabilities) clone() *ServerCapabilities {
Expand Down
88 changes: 88 additions & 0 deletions mcp/tasks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by the license
// that can be found in the LICENSE file.

package mcp

import (
"crypto/rand"
"fmt"
)

// ExtensionTasks identifies the MCP Tasks extension, which lets a server answer
// a request with a durable task handle instead of the request's normal result.
//
// This SDK does not implement task execution, and does not declare the
// extension by default: declaring it obliges a client to poll a task handle to
// completion, and a server to serve the tasks/* methods.
//
// See https://github.com/modelcontextprotocol/ext-tasks/blob/main/specification/draft/tasks.md.
const ExtensionTasks = "io.modelcontextprotocol/tasks"

// UnsupportedTaskResultError reports that a peer answered a request with a task
// handle from the [ExtensionTasks] extension, which this SDK cannot resolve.
type UnsupportedTaskResultError struct {
// TaskID identifies the created task, for manual polling or cancellation.
TaskID string
}

func (e *UnsupportedTaskResultError) Error() string {
return fmt.Sprintf("peer created task %q: the %s extension is not implemented", e.TaskID, ExtensionTasks)
}

// TaskStatus is the state of a task in the [ExtensionTasks] extension.
// Values outside the constants below are preserved: the extension may add
// statuses, and a peer's status must round-trip unchanged.
//
// See https://github.com/modelcontextprotocol/ext-tasks/blob/main/specification/draft/tasks.md.
type TaskStatus string

const (
// TaskStatusWorking means the request is currently being processed.
TaskStatusWorking TaskStatus = "working"
// TaskStatusInputRequired means the server needs input from the client.
TaskStatusInputRequired TaskStatus = "input_required"
// TaskStatusCompleted means the request finished and its result is available.
TaskStatusCompleted TaskStatus = "completed"
// TaskStatusCancelled means the request was cancelled before completion.
TaskStatusCancelled TaskStatus = "cancelled"
// TaskStatusFailed means the request failed with a JSON-RPC error.
TaskStatusFailed TaskStatus = "failed"
)

// Task is the operational metadata for a task in the [ExtensionTasks] extension.
// Derived shapes that carry inputRequests, result, or error are not modeled
// here; those belong to task execution, which this SDK does not implement.
//
// Timestamps are strings, matching [Annotations.LastModified]: the extension
// types them as ISO 8601 text, and parsing them as time.Time would reject
// forms the spec allows and rewrite the value on the way back out.
//
// See https://github.com/modelcontextprotocol/ext-tasks/blob/main/specification/draft/tasks.md.
type Task struct {
// TaskID is the server-generated identifier for this task.
TaskID string `json:"taskId"`
// Status is the current task state.
Status TaskStatus `json:"status"`
// StatusMessage is an optional description of the current state.
StatusMessage string `json:"statusMessage,omitempty"`
// CreatedAt is the ISO 8601 timestamp when the task was created.
CreatedAt string `json:"createdAt"`
// LastUpdatedAt is the ISO 8601 timestamp when the task was last updated.
LastUpdatedAt string `json:"lastUpdatedAt"`
// TTLMs is the time-to-live from creation, in milliseconds.
// Nil encodes as JSON null, which the extension defines as unlimited.
// The field is required, so a nil pointer is sent as null. int64 holds
// a TTL past the 32-bit range: a year is about 3.15e10 ms.
TTLMs *int64 `json:"ttlMs"`
// PollIntervalMs is the suggested polling interval in milliseconds.
// Omitted when unset.
PollIntervalMs *int64 `json:"pollIntervalMs,omitempty"`
}

// newTaskID returns an unguessable task ID. The extension requires
// server-generated IDs with enough entropy that a third party cannot
// enumerate them. [crypto/rand.Text] supplies at least 128 bits.
func newTaskID() string {
return rand.Text()
}
Loading