Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
13 changes: 13 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,19 @@ func (c *Client) CancelTask(
return mcp.ParseCancelTaskResult(response)
}

// UpdateTask delivers inputResponses to a task that is waiting for client input
func (c *Client) UpdateTask(
ctx context.Context,
request mcp.UpdateTaskRequest,
) (*mcp.UpdateTaskResult, error) {
response, err := c.sendRequest(ctx, string(mcp.MethodTasksUpdate), request.Params, outboundHeader(request.Header, request.Method))
if err != nil {
return nil, err
}

return mcp.ParseUpdateTaskResult(response)
}

// ListTasks returns the list of tasks
func (c *Client) ListTasks(
ctx context.Context,
Expand Down
5 changes: 5 additions & 0 deletions mcp/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const (
// [InputRequiredResult.InputRequests] and retries the original request with
// the answers attached. See the multi round-trip request pattern.
ResultTypeInputRequired ResultType = "input_required"

// ResultTypeTask indicates the server has created an asynchronous task to
// handle the request. The client polls tasks/get until the task reaches a
// terminal state.
ResultTypeTask ResultType = "task"
)

// IsComplete reports whether the result type denotes a final result. An empty
Expand Down
19 changes: 18 additions & 1 deletion mcp/tasks.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
package mcp

import (
"fmt"
"time"
)

// ExtensionTasks is the identifier for the MCP Tasks extension.
const ExtensionTasks = "io.modelcontextprotocol/tasks"

// UnsupportedTaskResultError is returned when a peer responded to a request
// with a task handle (resultType:"task") from the ExtensionTasks extension,
// but the local SDK cannot automatically resolve it.
type UnsupportedTaskResultError struct {
TaskID string
}

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

// TaskOption is a function that configures a Task.
// It provides a flexible way to set various properties of a Task using the functional options pattern.
type TaskOption func(*Task)
Expand Down Expand Up @@ -80,9 +95,11 @@ func NewTaskParams(ttlMs *int64) TaskParams {
}

// NewCreateTaskResult creates a CreateTaskResult with the given task.
// Tasks extension wire format, set Legacy=false.
func NewCreateTaskResult(task Task) CreateTaskResult {
return CreateTaskResult{
Task: task,
Task: task,
Legacy: true,
}
}

Expand Down
159 changes: 157 additions & 2 deletions mcp/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,11 @@ const (
// https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks
MethodTasksGet MCPMethod = "tasks/get"

// MethodTasksUpdate submits responses to outstanding inputRequests for a
// task in the input_required state.
MethodTasksUpdate MCPMethod = "tasks/update"

// MethodTasksList lists all tasks for the current session.
// https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks
MethodTasksList MCPMethod = "tasks/list"

// MethodTasksResult retrieves the result of a completed task.
Expand Down Expand Up @@ -147,6 +150,10 @@ const (
// https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks
MethodNotificationTasksStatus = "notifications/tasks/status"

// MethodNotificationTasks is the SEP-2663 tasks extension notification
// method for task status changes.
MethodNotificationTasks = "notifications/tasks"

// MethodCompletionComplete returns completion suggestions for a given argument
// https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion
MethodCompletionComplete MCPMethod = "completion/complete"
Expand Down Expand Up @@ -625,6 +632,17 @@ type ClientCapabilities struct {
Tasks *TasksCapability `json:"tasks,omitempty"`
}

// HasExtension reports whether the client declared support for the named
// extension. The name should be a fully-qualified extension identifier such as
// "io.modelcontextprotocol/tasks".
func (c *ClientCapabilities) HasExtension(name string) bool {
if c == nil {
return false
}
_, ok := c.Extensions[name]
return ok
}

// ServerCapabilities represents capabilities that a server may support. Known
// capabilities are defined here, in this schema, but this is not a closed set: any
// server can define its own, additional capabilities.
Expand Down Expand Up @@ -1724,6 +1742,36 @@ func (t Task) GetName() string {
return t.TaskId
}

// MarshalJSONLegacyTask serializes the Task of legacy 2025-11-25 wire format.
func (r CreateTaskResult) MarshalJSONLegacyTask() ([]byte, error) {
taskFields := map[string]any{
"taskId": r.Task.TaskId,
"status": r.Task.Status,
"createdAt": r.Task.CreatedAt,
"lastUpdatedAt": r.Task.LastUpdatedAt,
"ttl": r.Task.TTL,
}
if r.Task.StatusMessage != "" {
taskFields["statusMessage"] = r.Task.StatusMessage
}
if r.Task.PollInterval != nil {
taskFields["pollInterval"] = *r.Task.PollInterval
}
raw, err := json.Marshal(taskFields)
if err != nil {
return nil, err
}
m := map[string]json.RawMessage{"task": raw}
if r.Meta != nil {
metaBytes, err := json.Marshal(r.Meta)
if err != nil {
return nil, err
}
m["_meta"] = metaBytes
}
return json.Marshal(m)
}

// TaskParams represents the task metadata included when augmenting a request.
type TaskParams struct {
// Requested duration in milliseconds to retain task from creation.
Expand All @@ -1734,10 +1782,76 @@ type TaskParams struct {
// It contains task metadata rather than the actual operation result.
type CreateTaskResult struct {
Result
Task Task `json:"task"`
Task Task `json:"-"`
Content []Content `json:"-"`
StructuredContent any `json:"-"`
IsError bool `json:"-"`
// Legacy instructs MarshalJSON to use the 2025-11-25 wire format.
Legacy bool `json:"-"`
}

// MarshalJSON implements custom JSON marshaling for CreateTaskResult.
func (r CreateTaskResult) MarshalJSON() ([]byte, error) {
if r.Legacy {
return r.MarshalJSONLegacyTask()
}
m := map[string]any{
"resultType": ResultTypeTask,
"taskId": r.Task.TaskId,
"status": r.Task.Status,
"createdAt": r.Task.CreatedAt,
"lastUpdatedAt": r.Task.LastUpdatedAt,
"ttlMs": r.Task.TTL,
}
if r.Task.StatusMessage != "" {
m["statusMessage"] = r.Task.StatusMessage
}
if r.Task.PollInterval != nil {
m["pollIntervalMs"] = *r.Task.PollInterval
}
if r.Meta != nil {
m["_meta"] = r.Meta
}
return json.Marshal(m)
}

// UnmarshalJSON handles both wire formats SEP-2663 and Legacy(2025-11-25).
func (r *CreateTaskResult) UnmarshalJSON(data []byte) error {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
// Always decode the common Result fields (_meta, resultType) first.
var base struct {
Meta *Meta `json:"_meta"`
ResultType ResultType `json:"resultType"`
Task *Task `json:"task"`
TaskId string `json:"taskId"`
}
if err := json.Unmarshal(data, &base); err != nil {
return err
}
r.Meta = base.Meta
r.ResultType = base.ResultType

if base.Task != nil {
// Legacy format: task nested under "task" key.
r.Task = *base.Task
r.Legacy = true
return nil
}
// SEP-2663 inline format: task fields at the top level.
type inlineTask struct {
TaskId string `json:"taskId"`
Status TaskStatus `json:"status"`
StatusMessage string `json:"statusMessage,omitempty"`
CreatedAt string `json:"createdAt"`
LastUpdatedAt string `json:"lastUpdatedAt"`
TTL *int64 `json:"ttlMs"`
PollInterval *int64 `json:"pollIntervalMs,omitempty"`
}
var inline inlineTask
if err := json.Unmarshal(data, &inline); err != nil {
return err
}
r.Task = Task(inline)
return nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

// GetTaskRequest retrieves the current status of a task.
Expand All @@ -1755,6 +1869,11 @@ type GetTaskParams struct {
type GetTaskResult struct {
Result
Task
TaskResult json.RawMessage `json:"result,omitempty"`
TaskError *JSONRPCErrorDetails `json:"error,omitempty"`
// InputRequests holds outstanding server-to-client requests when Status
// is input_required.
InputRequests InputRequests `json:"inputRequests,omitempty"`
}

// ListTasksRequest retrieves a paginated list of tasks.
Expand Down Expand Up @@ -1807,6 +1926,23 @@ type CancelTaskResult struct {
Task
}

// MarshalJSON serialises CancelTaskResult.
func (r CancelTaskResult) MarshalJSON() ([]byte, error) {
if r.TaskId == "" {
// Ack-only.
type resultOnly struct {
Result
}
return json.Marshal(resultOnly{r.Result})
}
// Legacy: include all task fields inline.
type flat struct {
Result
Task
}
return json.Marshal(flat(r))
}

// TaskStatusNotification is sent when a task's status changes.
type TaskStatusNotification struct {
Notification
Expand All @@ -1817,6 +1953,25 @@ type TaskStatusNotificationParams struct {
Task
}

// UpdateTaskRequest submits responses to outstanding inputRequests for a task
// in the input_required state.
type UpdateTaskRequest struct {
Request
Header http.Header `json:"-"`
Params UpdateTaskParams `json:"params"`
}

// UpdateTaskParams are the parameters for tasks/update.
type UpdateTaskParams struct {
TaskId string `json:"taskId"`
InputResponses InputResponses `json:"inputResponses"`
}

// UpdateTaskResult is the ack-only result returned by tasks/update.
type UpdateTaskResult struct {
Result
}

// ClientRequest represents any request that can be sent from client to server.
type ClientRequest any

Expand Down
Loading
Loading