Most tool calls in an agent protocol are synchronous: the model asks for an operation, the server executes it, and the result returns in the same request-response cycle. That works for quick lookups, but it is a poor fit for indexing repositories, processing large batches, running long diagnostics, or waiting on external systems.
Task-augmented MCP tools split acceptance from completion. The server accepts a tool call, returns a task identifier immediately, executes work in the background, and exposes the final result through task polling or notifications. This lets the host keep the model responsive without pretending that every operation can finish within a single tool timeout.
The mechanism is protocol-level, not just a library helper. Capabilities advertise whether task augmentation is supported, individual tools can forbid, allow, or require task execution, and task result retrieval becomes a separate operation with its own cancellation, listing, expiry, and concurrency controls.
A normal tool call returns the operation result directly. A task-augmented call returns a task record with a status such as working, plus metadata such as a task identifier, timestamps, time-to-live, and a suggested polling interval. The actual tool result is retrieved later when the task completes.
Task support is negotiated during initialization. A client should only send task metadata for request types the receiver declared. This prevents accidental task creation against older servers that only understand synchronous requests.
Tool declarations add another layer. A tool may mark task support as forbidden, optional, or required. Optional tools can run synchronously for small inputs and asynchronously for large inputs. Required tools force clients to create a task because the operation is known to be long-running.
Returning a task ID means the server has accepted responsibility for lifecycle state: execution, completion, failure, cancellation, expiry, and result retrieval. Treating task creation as a simple goroutine spawn without durable state is unsafe for production hosts.
In a Go MCP server, a long-running tool can be declared with task support and registered with a task handler. The server exposes task capabilities, optionally caps concurrent work, and returns a task result instead of blocking the tool call until all work is done.
package main
import (
"context"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func main() {
s := server.NewMCPServer(
"Batch Processor",
"1.0.0",
server.WithTaskCapabilities(true, true, true),
server.WithMaxConcurrentTasks(10),
)
tool := mcp.NewTool(
"process_batch",
mcp.WithDescription("Process a batch asynchronously"),
mcp.WithTaskSupport(mcp.TaskSupportRequired),
mcp.WithArray("items",
mcp.WithStringItems(),
mcp.Required(),
),
)
s.AddTaskTool(tool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CreateTaskResult, error) {
items := req.GetStringSlice("items", nil)
for _, item := range items {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
processItem(item)
}
}
return &mcp.CreateTaskResult{Task: mcp.Task{}}, nil
})
_ = server.ServeStdio(s)
}
The protocol expects task state to be observable after creation. Clients poll for results, list tasks when supported, and cancel tasks when cancellation was negotiated. Servers should enforce task time-to-live values and garbage-collect old results so a long-lived agent host does not become an unbounded result store.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "process_batch",
"arguments": { "items": ["a", "b", "c"] },
"task": { "ttl": 60000 }
}
}
Indexing a repository, parsing a large document set, or embedding a corpus should not block the model turn. A task lets the agent start the job, continue planning, and retrieve the result when the index is ready.
Incident-response tools often collect logs, query metrics, run traces, and summarize findings. Task augmentation gives these tools a clean way to expose progress and cancellation while keeping the host UI responsive.
When a task may take minutes, users need state: queued, working, waiting for approval, failed, cancelled, or complete. MCP task records provide a protocol surface for that state instead of burying it in arbitrary tool text.
Admission validates arguments, checks authorization, allocates a task record, and returns quickly. Execution runs in a worker pool with bounded concurrency. This separation prevents a malicious or accidental burst of task calls from becoming unlimited goroutines.
type TaskStore interface {
Create(ctx context.Context, req TaskRequest) (Task, error)
MarkRunning(ctx context.Context, id string) error
Complete(ctx context.Context, id string, result any) error
Fail(ctx context.Context, id string, err error) error
Cancel(ctx context.Context, id string) error
}
func admitTask(ctx context.Context, store TaskStore, q chan<- string, req TaskRequest) (Task, error) {
task, err := store.Create(ctx, req)
if err != nil {
return Task{}, err
}
select {
case q <- task.ID:
return task, nil
default:
_ = store.Fail(ctx, task.ID, ErrOverloaded)
return Task{}, ErrOverloaded
}
}
Task workers may crash or be cancelled. Use idempotency keys, checkpointing, and safe retry boundaries so a restarted task does not duplicate side effects or corrupt partially written outputs.