cxxmcp 1.1.6
C++ MCP SDK
Loading...
Searching...
No Matches
Request Lifecycle

This document records the SDK-level lifecycle contract for requests, timeouts, cancellation, progress, and shutdown. It applies to the public Peer / Service path and to compatibility wrappers that delegate to the same SDK behavior.

Request Ownership

Synchronous helpers send one JSON-RPC request and return the parsed typed result or a stable core::Error. Asynchronous helpers return a RequestHandle<T>. The handle represents one pending request and owns the wait path for that request result.

Request ids are generated by the owning peer or client. A late response for a timed-out or cancelled request is discarded deterministically instead of being matched to a later request.

Raw JSON-RPC helpers follow the same lifecycle rules as typed helpers. They are the escape hatch for vendor methods and future MCP methods, not a separate transport path.

Initialization Boundary

Standard server transports own MCP session initialization state. Before a session has completed initialize and sent notifications/initialized, the built-in stdio, process-stdio, role-generic server transport, and Streamable HTTP paths accept only lifecycle-safe requests such as initialize and ping; normal business requests fail with a stable protocol or transport error.

Direct calls to Server::handle_request() are dispatcher calls, not a complete session boundary. Embedded callers that bypass the SDK transports must enforce the initialize / initialized sequence themselves or route requests through ServerPeer / Service with a role-generic transport.

Timeouts

Requests may carry RequestOptions with a timeout. When a timeout expires:

  • the pending response slot is removed;
  • the public result is a timeout error;
  • later responses for the same id are ignored;
  • a protocol cancellation notification is sent when the SDK can identify the request id and the active transport can still send notifications.

Timeouts do not kill user code already running inside a handler. Handler code must observe its cancellation token for cooperative stop behavior.

Cancellation

Cancellation is cooperative. CancellationSource owns cancellation state and CancellationToken is the cheap copyable value passed to SDK internals and handlers. CancellationToken::cancelled() uses atomic_bool with memory_order_acquire for the fast path. CancellationToken::wait_for_cancel() blocks on a condition variable until cancellation is requested (zero CPU), with optional timeout and deadline overloads.

Outbound RequestHandle<T> cancellation uses token callback registration to complete the handle and send one protocol cancellation notification. It does not dedicate request executor workers to long-lived cancellation watchers.

The SDK propagates cancellation tokens into handler contexts where there is a meaningful execution boundary:

  • tool handlers receive ToolContext::cancellation_token;
  • prompt handlers receive PromptContext::cancellation_token;
  • resource handlers receive ResourceContext::cancellation_token;
  • completion, sampling, and contract-style request handlers receive a cancellation token when the public overload exposes one;
  • client-side inbound roots, sampling, elicitation, and custom request handlers can receive a CancellationToken through their cancellation-aware overloads;
  • service receive loops observe the service cancellation token;
  • request timeout and explicit cancellation paths clean up pending response state.

For both concrete Client and native ClientPeer, an incoming notifications/cancelled message cancels the matching inbound request token before the application handler observes subsequent cancellation checks. This keeps server-to-client requests on the same cooperative lifecycle model as client-to-server tool, prompt, resource, completion, sampling, and task flows.

Application handlers should check context.cancelled() or context.cancellation_token.cancelled() before expensive work and between blocking operations. Throwing from a handler is converted to a controlled internal error; returning a typed error is preferred when the failure is part of application behavior.

Progress

Progress is modeled through MCP progress tokens in request metadata and notifications/progress payloads. Typed helpers preserve metadata so progress tokens can round-trip through client/server, HTTP, stdio, and process-stdio paths.

Progress notifications are best effort: they are sent only while the related session or transport is still open. A closed transport or cancelled service stops future progress delivery.

Shutdown

Service<Role> owns the active receive loop for a peer. serve(peer) returns RunningService<Role>, which is the lifecycle owner applications should keep.

  • close() closes transports, cancels the service token, and unblocks receive loops.
  • stop() is a non-throwing best-effort stop path for compatibility with older code.
  • wait() waits until the service loop has exited.
  • destroying a running service closes it before releasing owned state.
  • moved-from RunningService values are inert and can be stopped or waited safely.

For server peers with multiple transports, shutdown is transport-wide: all attached transports are closed and pending receive loops observe the same service cancellation.

Process-level SIGINT / SIGTERM or console-control handling remains an application responsibility. See the signal handling section below for the recommended atomic-flag pattern and transport-specific shutdown notes.

Transport Notes

All built-in role-generic transports follow the same high-level lifecycle contract:

  • send returns a stable transport or protocol error instead of leaking backend-specific exceptions;
  • receive is sequential, and std::nullopt means end-of-stream or a closed transport;
  • close unblocks pending receive operations where the platform allows it.

Streamable HTTP additionally scopes requests, progress, cancellation, and server-to-client callbacks to the active MCP session id. Stale or deleted sessions reject new requests and drain pending state.

Reconnect And Retry Boundary

Reconnect is transport-specific, not a generic Transport<Role> contract. The public transport boundary intentionally stays narrow: send, receive, close, diagnostics, and documented concurrency behavior. Adding a generic reconnect() method would be a public contract change and needs a design note because different transports have different ownership models.

Streamable HTTP and legacy SSE-compatible paths have scoped recovery behavior: HTTP session-terminated client requests can reinitialize and retry once when the client has cached initialize parameters, and SSE delivery can use its configured reconnect interval. This does not imply that stdio, process stdio, custom byte streams, or application-owned transports have automatic reconnect.

Applications that need cross-transport reconnect orchestration should own that policy above Peer / Service: construct a new transport, perform a new initialize exchange, replay any application-owned subscriptions or roots, and decide which in-flight operations are safe to retry.