Task-Augmented MCP Tools

Protocols & Interoperability advanced Building with AI ~35 min read

Overview

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.

Core Concepts

Two-Phase Tool Results

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.

Capability Negotiation

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-Level Task Policy

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.

Key Concept: A Task Is a Protocol Promise

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.

How It Works

Declare Task-Capable Tools

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)
}

Poll, Cancel, and Expire

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 }
  }
}

Practical Applications

Repository and Document Indexing

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.

Operations Diagnostics

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.

Human-Visible Background Work

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.

Implementation Patterns

Separate Admission from Execution

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
    }
}

Design Idempotent Work Units

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.

Performance Considerations

Common Pitfalls

Further Reading