Logging, Progress, and Cancellation

Purpose and Scope

This page explains how an MCP server should communicate during work that may take time: progress updates for a single request, log messages for a connected client, cancellation-aware handler structure, and related patterns for operations that need user input or may fail. In the TypeScript SDK documentation, every handler receives a context object as its second argument, and the request-scoped helpers for progress, logging, and cancellation live under ctx.mcpReq. Treat that object as the boundary between your business logic and the MCP request lifecycle: it carries request metadata, can send request-associated notifications, and exposes the signal you should consult when work is no longer needed.

Sources: docs/servers/logging-progress-cancellation.md

Long-running MCP handlers are easiest to reason about when they produce a final result exactly once and use separate protocol channels for everything that happens before that result. Progress notifications report measurable advancement while the original call remains pending. Logging reports diagnostic or status information, but MCP logging is deprecated for newer protocol revisions and should not be the default observability strategy for new production code. Cancellation is the client’s way to say that the result is no longer wanted. These behaviors are not replacements for tool results, resource contents, prompt messages, or structured errors; they are request-lifecycle signals that surround those outcomes.

Sources: docs/servers/logging-progress-cancellation.md, docs/servers/errors.md

Relevant Source Files

  • docs/servers/logging-progress-cancellation.md - Primary how-to for request-scoped progress, MCP logging, and cancellation helpers exposed through ctx.mcpReq.
  • docs/servers/completion.md - Shows a related interactive server capability, completion/complete, and helps distinguish autocomplete from long-running operation progress.
  • docs/servers/elicitation.md - Documents the older push-style server request flow through ctx.mcpReq.elicitInput, which matters when long-running work needs user input on 2025-era connections.
  • docs/servers/errors.md - Defines the difference between model-visible tool errors and JSON-RPC protocol errors, which is essential when a long-running operation cannot finish successfully.
  • docs/servers/input-required.md - Documents the input_required result pattern used by 2026-07-28 handlers to pause work until the client returns input and retries the call.
  • docs/servers/notifications.md - Explains server notifications, including list-changed and resource-updated messages, so progress notifications can be understood as one member of the broader notification family.

Request Context and Lifecycle Signals

The core implementation model is simple: register a tool, resource, prompt, or other server callback, and read the second handler argument when you need request-lifecycle services. The logging-progress-cancellation guide names this argument as the handler context and places the helpers on ctx.mcpReq. That placement is important because the helpers are scoped to the in-flight MCP request rather than to the process or the McpServer object as a whole. When a tool processes three files, progress belongs to that one tools/call, not to every client connection or to the server’s global state.

Sources: docs/servers/logging-progress-cancellation.md

Request-scoped design also clarifies what not to do. Do not invent a separate side channel for progress when the client has already opted into MCP progress for a call. Do not store a progress token globally and reuse it for later calls. Do not treat cancellation as a generic server shutdown flag. Each of these signals is tied to a particular request, and the handler should read or send them as part of that request’s context. If the operation spans many internal steps, pass only the necessary scoped values into helper functions rather than passing the whole server instance around.

Sources: docs/servers/logging-progress-cancellation.md

Reporting Progress

A client asks for progress by putting a progressToken into the request _meta. The SDK’s client helper does this automatically when the caller supplies an onprogress callback. On the server side, the handler reads ctx.mcpReq._meta?.progressToken; if it is present, the handler sends a notifications/progress notification with ctx.mcpReq.notify. The documented example registers a process-files tool, loops through the requested files, and after each file sends progress, total, and a human-readable message using the same token. The tool still returns a normal final result after the loop finishes.

Sources: docs/servers/logging-progress-cancellation.md

server.registerTool(
    'process-files',
    {
        description: 'Process files with progress updates',
        inputSchema: z.object({ files: z.array(z.string()) })
    },
    async ({ files }, ctx) => {
        const progressToken = ctx.mcpReq._meta?.progressToken;
 
        for (let i = 0; i < files.length; i++) {
            if (progressToken !== undefined) {
                await ctx.mcpReq.notify({
                    method: 'notifications/progress',
                    params: { progressToken, progress: i + 1, total: files.length, message: `Processed ${files[i]}` }
                });
            }
        }
 
        return { content: [{ type: 'text', text: `Processed ${files.length} files` }] };
    }
);

Progress must be optional from the server’s point of view. The same guide explicitly shows the client calling the tool without onprogress; the request then arrives with no progressToken, and the guard sends no notifications. That is the right behavior for hosts that do not need or cannot display progress. The documented rule is that progress must increase on every notification for the same token, while total and message are optional. Use total when the amount of work is known, and use message for short user-facing status, not for verbose diagnostic output.

Sources: docs/servers/logging-progress-cancellation.md

On the client side, client.callTool accepts an onprogress option. In the guide, calling process-files with three files causes the callback to fire once per file before the call resolves with final content. That sequence is the expected user experience: progress updates should make pending work visible without changing the result shape. If a handler cannot determine precise percentages, report monotonic units such as records processed, files uploaded, or stages completed. If a client did not ask for progress, the handler should still do the work and return the same final result.

Sources: docs/servers/logging-progress-cancellation.md

Logging and Deprecation Status

MCP logging is a protocol feature, but the server documentation marks it as deprecated as of protocol version 2026-07-28 under SEP-2577. The guide recommends logging to stderr for stdio servers or using OpenTelemetry instead. Existing logging support remains functional through the deprecation window, but new server designs should treat MCP logging as compatibility behavior rather than a primary observability system. This matters because logs often contain operational detail, while progress messages are part of a user-visible request experience. Keeping those channels separate helps hosts display concise progress without becoming a log viewer.

Sources: docs/servers/logging-progress-cancellation.md

When MCP logging is used, the server declares the logging capability at construction time. The documented example constructs new McpServer({ name: 'file-processor', version: '1.0.0' }, { capabilities: { logging: {} } }). Inside a handler, ctx.mcpReq.log(level, data) sends a notifications/message notification, and data can be any JSON value. Because this is a request-scoped helper, it is appropriate for messages related to the currently executing handler, such as the start of validation or a summary of detected invalid records.

Sources: docs/servers/logging-progress-cancellation.md

const server = new McpServer(
    { name: 'file-processor', version: '1.0.0' },
    { capabilities: { logging: {} } }
);
 
await ctx.mcpReq.log('info', `Validating ${records.length} records`);

Cancellation-Aware Handler Structure

The logging-progress-cancellation guide states that the cancellation signal is available on ctx.mcpReq alongside progress and logging helpers. Design long-running handlers so they have natural cancellation checkpoints: before starting a costly external request, between files or batches, and after awaiting any operation that may take noticeable time. The goal is not merely to stop sending progress; it is to avoid continuing work whose result the client no longer needs. Keep cleanup idempotent, because cancellation can arrive after some side effects have already occurred.

Sources: docs/servers/logging-progress-cancellation.md

For multi-step work, pair cancellation checks with clear result and error boundaries. If the handler completes normally, return the tool result, resource contents, or prompt messages expected by the original method. If the operation fails in a way the model can recover from, use the tool-error shape described in the errors guide: a successful tools/call result with isError: true and helpful text. If the request itself is invalid, resource, prompt, and completion callbacks should throw protocol errors because they do not have the tool isError channel. This distinction keeps cancellation, user-visible failure, and JSON-RPC failure from being conflated.

Sources: docs/servers/errors.md, docs/servers/logging-progress-cancellation.md

Some long-running operations are waiting on a human rather than on CPU, I/O, or a remote service. The elicitation guide documents ctx.mcpReq.elicitInput, where a handler asks the connected client to present a form or URL flow and then resumes when the answer arrives. That push-style helper is explicitly called out as a 2025-era mechanism: on a 2026-07-28 connection, the handler should return an input request instead. Do not model human confirmation as progress; progress says work is advancing, while elicitation or input-required says the server needs information before it can continue.

Sources: docs/servers/elicitation.md, docs/servers/input-required.md

For 2026-07-28, the input-required guide documents an input_required result for tools/call, prompts/get, or resources/read. A handler returns embedded input requests, the client answers them, and the client retries the original call with inputResponses; on re-entry the handler reads accepted content and finishes. The guide also notes that embedded requests are checked against client-declared capabilities, and a missing capability rejects the call with -32021 before anything reaches the wire. This pattern is especially useful for deployments, deletes, purchases, or any operation that must pause safely until the user confirms.

Sources: docs/servers/input-required.md

Completion and notifications are adjacent but different concepts. Completion is server-side autocomplete for prompt arguments and resource template variables; the client sends a partial value and the server returns suggestions through completion/complete. Notifications are one-way pushes from server to client, including list-changed and resource-updated messages. Progress notifications are tied to a specific request token, while list-changed notifications tell clients that cached tool, prompt, or resource lists are stale. Choose the protocol feature that matches the user problem rather than using a generic notification for every asynchronous event.

Sources: docs/servers/completion.md, docs/servers/notifications.md, docs/servers/logging-progress-cancellation.md

Implementation Checklist

Use this checklist when adding a long-running server handler. First, decide whether the work has measurable units; if so, read ctx.mcpReq._meta?.progressToken and send monotonic notifications/progress updates only when the token exists. Second, decide whether status text is progress, diagnostics, or a final model-facing message. Put user-visible advancement in progress messages, operational telemetry in stderr or OpenTelemetry, and recoverable failure guidance in a tool result with isError: true. Third, structure the loop around cancellation checkpoints so the handler can stop between units without corrupting state.

Sources: docs/servers/logging-progress-cancellation.md, docs/servers/errors.md

Fourth, if the handler needs user input, select the protocol-era pattern intentionally: ctx.mcpReq.elicitInput for documented 2025-era push flows, or an input_required result for 2026-07-28 requests. Fifth, keep cache and capability notifications separate from request progress. Use list-changed and resource-updated notifications when server state changes and clients need to refresh cached data. Next, read server-errors for failure semantics, input-required for retry-based human input, and server-notifications for server-originated change messages that are not tied to a single pending call.

Sources: docs/servers/elicitation.md, docs/servers/input-required.md, docs/servers/notifications.md