# 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.
