Description
When a tool registered with AddTool + WithTaskSupport(TaskSupportOptional) is called asynchronously (client sends task params), the handler's context gets canceled immediately after the CreateTaskResult response is written.
Root Cause
In server.go, executeRegularToolAsTask derives the task context from the HTTP request context:
// handleToolCall passes ctx derived from r.Context()
go s.executeRegularToolAsTask(ctx, entry, regularTool, request)
// executeRegularToolAsTask (line ~1793)
taskCtx, cancel := context.WithCancel(ctx) // ctx is still bound to r.Context()
Once the HTTP handler returns (after writing the CreateTaskResult JSON response), the HTTP server cancels r.Context(), which cascades into taskCtx and kills the tool handler goroutine.
The same issue exists in executeTaskTool for AddTaskTool handlers.
Reproduction
- Register a tool with
AddTool + WithTaskSupport(mcp.TaskSupportOptional)
- Have the handler do any work that takes more than a few milliseconds (e.g. an HTTP call to an LLM)
- Call the tool via streamable HTTP transport with task params
- Observe: handler fails with
context canceled
Expected Behavior
The task goroutine should run to completion regardless of the HTTP request lifecycle. The context should only be canceled when:
- The task's own timeout/TTL expires
- A client sends
tasks/cancel
- The server shuts down
Suggested Fix
Use context.WithoutCancel (Go 1.21+) to detach from the HTTP request lifecycle while preserving context values (session info, etc.), then derive a cancellable context for task-level cancellation:
// executeRegularToolAsTask
taskCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
This preserves:
- Context values (session metadata, etc.)
- Task-level cancellation via
cancel() (wired to tasks/cancel)
While removing:
- Unwanted cancellation cascade from the HTTP request lifecycle
Workaround
Handlers can work around this today by detaching at the top:
s.AddTool(tool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
ctx = context.WithoutCancel(ctx) // detach from HTTP request lifecycle
// ... long-running work ...
})
Downside: this also makes tasks/cancel non-functional for that handler.
Environment
- mcp-go version: v0.47.1
- Go version: 1.23+
- Transport: streamable HTTP
Description
When a tool registered with
AddTool+WithTaskSupport(TaskSupportOptional)is called asynchronously (client sends task params), the handler's context gets canceled immediately after theCreateTaskResultresponse is written.Root Cause
In
server.go,executeRegularToolAsTaskderives the task context from the HTTP request context:Once the HTTP handler returns (after writing the
CreateTaskResultJSON response), the HTTP server cancelsr.Context(), which cascades intotaskCtxand kills the tool handler goroutine.The same issue exists in
executeTaskToolforAddTaskToolhandlers.Reproduction
AddTool+WithTaskSupport(mcp.TaskSupportOptional)context canceledExpected Behavior
The task goroutine should run to completion regardless of the HTTP request lifecycle. The context should only be canceled when:
tasks/cancelSuggested Fix
Use
context.WithoutCancel(Go 1.21+) to detach from the HTTP request lifecycle while preserving context values (session info, etc.), then derive a cancellable context for task-level cancellation:This preserves:
cancel()(wired totasks/cancel)While removing:
Workaround
Handlers can work around this today by detaching at the top:
Downside: this also makes
tasks/cancelnon-functional for that handler.Environment