Skip to content

Task goroutine context is canceled when HTTP response is flushed (executeRegularToolAsTask) #897

Description

@alejandro5042

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

  1. Register a tool with AddTool + WithTaskSupport(mcp.TaskSupportOptional)
  2. Have the handler do any work that takes more than a few milliseconds (e.g. an HTTP call to an LLM)
  3. Call the tool via streamable HTTP transport with task params
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions