diff --git a/docs/docs.json b/docs/docs.json index 701b79659..7e3e735d3 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -437,6 +437,7 @@ "specification/draft/basic/index", "specification/draft/basic/lifecycle", "specification/draft/basic/transports", + "specification/draft/basic/streams", "specification/draft/basic/authorization", "specification/draft/basic/security_best_practices", { diff --git a/docs/specification/draft/basic/lifecycle.mdx b/docs/specification/draft/basic/lifecycle.mdx index b6a84a3f0..b32ee52b0 100644 --- a/docs/specification/draft/basic/lifecycle.mdx +++ b/docs/specification/draft/basic/lifecycle.mdx @@ -155,6 +155,7 @@ Key capabilities include: | Client | `roots` | Ability to provide filesystem [roots](/specification/draft/client/roots) | | Client | `sampling` | Support for LLM [sampling](/specification/draft/client/sampling) requests | | Client | `elicitation` | Support for server [elicitation](/specification/draft/client/elicitation) requests | +| Client | `streams` | Support for [resumable streams](/specification/draft/basic/streams) | | Client | `experimental` | Describes support for non-standard experimental features | | Server | `prompts` | Offers [prompt templates](/specification/draft/server/prompts) | | Server | `resources` | Provides readable [resources](/specification/draft/server/resources) | diff --git a/docs/specification/draft/basic/streams.mdx b/docs/specification/draft/basic/streams.mdx new file mode 100644 index 000000000..33043bc9d --- /dev/null +++ b/docs/specification/draft/basic/streams.mdx @@ -0,0 +1,230 @@ +--- +title: Streams +--- + +
+ +**Protocol Revision**: draft + +MCP defines a transport-agnostic mechanism for resumable streams that allow clients and servers to maintain state across disconnections. Using resumable streams: + +- Clients and servers can disconnect and reconnect without losing progress. +- Servers can communicate expiration timeouts and reclaim resources thereafter. +- Clients can poll stream status after disconnect without having to fetch undelivered messages. +- All of the above works regardless of transport (HTTP, WebSocket, stdio, etc.). + +## Lifecycle + +A stream follows a defined lifecycle: + +1. **Creation**: Server creates a stream and sends a [`notifications/stream/create`][] notification with a unique stream ID. +2. **Live**: Messages are sent as part of the stream. +3. **Completion**: Server sends a [`notifications/stream/end`][] notification when the stream is complete. + +In the event of disconnection, the client may send a [`stream/poll`][] to check the stream's status, and a [`stream/resume`][] request to receive messages from the stream. + +```mermaid +sequenceDiagram + participant Client + participant Server + + Client->>+Server: Request (e.g., tools/call) + Note over Server: Server creates stream + Server-->>Client: notifications/stream/create
{ streamId: "123", resumeToken: "abc" } + loop + Server-->>Client: Messages (e.g., notifications/progress) + end + Server--x-Client: Disconnection occurs + + Note over Client: Client polls stream status (optional) + Client->>+Server: stream/poll
{ streamId: "123", resumeToken: "abc" } + Server-->>-Client: StreamPollResult + + Note over Client: Client decides to resume + Client->>+Server: stream/resume
{ streamId: "123", resumeToken: "abc" } + Server-->>Client: Undelivered messages + loop + Server-->>Client: Messages (e.g., notifications/progress) + end + Server-->>Client: CallToolResult + + Note over Server: Server terminates stream + Server-->>-Client: notifications/stream/end
{ streamId: "123" } +``` + +[`notifications/stream/create`]: /specification/draft/schema#notifications%2Fstream%2Fcreate +[`notifications/stream/end`]: /specification/draft/schema#notifications%2Fstream%2Fend +[`stream/poll`]: /specification/draft/schema#stream%2Fpoll +[`stream/resume`]: /specification/draft/schema#stream%2Fresume + +### Creation + +When a client sends a request that might cause a long-running response, the server **MAY** create a stream by sending a [`notifications/stream/create`][] notification with a unique stream ID, a resume token, and resume interval parameters. + +```json +{ + "jsonrpc": "2.0", + "method": "notifications/stream/create", + "params": { + "stream": { + "streamId": "550e8400-e29b-41d4-a716-446655440000", + "resumeToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "requestId": "1", + "resumeInterval": { + "min": 5, + "max": 3600 + } + } + } +} +``` + +- `streamId` and `resumeToken` are used to resume or poll the stream in the event of disconnection. +- `resumeInterval.min` specifies the minimum seconds a client **SHOULD** wait before resuming or polling the stream in order to prevent excessive reconnections. +- `resumeInterval.max` specifies the maximum seconds a client may wait before the server considers the stream abandoned. + +### Disconnection and Resumption + +Either the client or server **MAY** disconnect at any time. After a disconnection, the client can resume the stream by sending a [`stream/resume`][] request with the stream ID and resume token. + +```json +{ + "jsonrpc": "2.0", + "method": "stream/resume", + "params": { + "streamId": "550e8400-e29b-41d4-a716-446655440000", + "resumeToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } +} +``` + +When a client sends a `stream/resume` request, the server should: + +1. Send all undelivered messages for the stream to the client. +2. If the stream has ended, send a `notifications/stream/end` notification after sending all other undelivered messages. +3. If the stream ID or resume token is invalid, respond with an error. + +```mermaid +sequenceDiagram + participant Client + participant Server + + Client->>+Server: Request (e.g., tools/call) + Server-->>Client: notifications/stream/create
{ streamId: "123", resumeToken: "abc" } + Server-->>Client: Initial messages + Server--x-Client: Disconnection occurs + + Client->>+Server: stream/resume
{ streamId: "123", resumeToken: "abc" } + Server-->>Client: Undelivered messages + Server-->>-Client: notifications/stream/end
{ streamId: "123" } +``` + + + +When using the [Streamable HTTP transport](/specification/draft/basic/transports#streamable-http) to stream messages, a server cannot definitively know whether a message has been delivered. Clients use the `Last-Event-ID` HTTP header upon reconnect to indicate the last delivered messages. + +In order to provide message delivery guarantees, servers may use monotonic SSE event IDs, and maintain a buffer of recently sent messages (a two minute window aligns with common TCP idle timeout configurations). When a client resumes a stream, the `Last-Event-ID` header can be used to determine which messages in the buffer should be discarded before sending the remainder to the client. + + + +### Polling + +Clients can check a stream's status without fetching all undelivered messages by sending a [`stream/poll`][] request with the stream ID and resume token. + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "method": "stream/poll", + "params": { + "streamId": "550e8400-e29b-41d4-a716-446655440000", + "resumeToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } +} +``` + +The response will indicate the status of the stream as well as whether there are pending messages. + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "streamId": "550e8400-e29b-41d4-a716-446655440000", + "status": "live", + "pendingMessages": true, + "hasRequest": false, + "hasError": false + } +} +``` + +- `status`: The stream status (`"live"` or `"completed"`) +- `pendingMessages`: Whether there are pending messages +- `hasRequest`: Whether any pending messages include a server-to-client request +- `hasError`: Whether any pending messages include an error result + +### Abandonment + +If a client does not resume or poll a stream within the `resumeInterval.max` period, the server **MAY** consider the stream abandoned and reclaim resources. The next time the client attempts to resume or poll the stream, the server **MAY** respond with an error. + +```mermaid +sequenceDiagram + participant Client + participant Server + + Client->>+Server: Request (e.g., tools/call) + Server-->>Client: notifications/stream/create
{ resumeInterval: { max: 3600 } } + Server-->>Client: Initial messages + Server--x-Client: Disconnection occurs + + Note over Client: Client waits > 3600 seconds + + Client->>+Server: stream/resume + Server-->>-Client: Error (stream abandoned) +``` + +### Termination + +When a stream completes, the server **MUST** send a [`notifications/stream/end`][] notification. + +```json +{ + "jsonrpc": "2.0", + "method": "notifications/stream/end", + "params": { + "streamId": "550e8400-e29b-41d4-a716-446655440000" + } +} +``` + +## Using Streams with Tool Calls + +A common pattern is to use streams with tool calls that might take a long time to complete. When using streams with tool calls: + +1. The server sends a `notifications/stream/create` notification immediately after receiving the tool call request. +2. The server can send progress updates and other messages as part of the stream. +3. When the tool call completes, the server sends the result. +4. The server ends the stream with a `notifications/stream/end` notification. + +```mermaid +sequenceDiagram + participant Client + participant Server + + Client->>+Server: tools/call + Server-->>Client: notifications/stream/create
{ streamId: "123", resumeToken: "abc" } + + loop + Server-->>Client: notifications/progress + Server-->>Client: Other messages + end + + Server-->>Client: CallToolResult + Server-->>-Client: notifications/stream/end
{ streamId: "123" } +``` + +## Security Considerations + +- Resume tokens should be treated as sensitive information as they can be used to retrieve message history. +- Servers should validate that clients have appropriate permissions to resume a stream. diff --git a/docs/specification/draft/basic/transports.mdx b/docs/specification/draft/basic/transports.mdx index 582976b06..c2302ebe1 100644 --- a/docs/specification/draft/basic/transports.mdx +++ b/docs/specification/draft/basic/transports.mdx @@ -106,9 +106,10 @@ MCP endpoint. - The server **MAY** send JSON-RPC _requests_ and _notifications_ before sending the JSON-RPC _response_. These messages **SHOULD** relate to the originating client _request_. - - The server **SHOULD NOT** close the SSE stream before sending the JSON-RPC _response_ - for the received JSON-RPC _request_, unless the [session](#session-management) - expires. + - The server **SHOULD NOT** close the SSE stream before sending either the JSON-RPC + _response_ for the received JSON-RPC _request_ or a + [stream creation notification](/specification/draft/basic/streams.mdx#creation), + unless the [session](#session-management) expires. - After the JSON-RPC _response_ has been sent, the server **SHOULD** close the SSE stream. - Disconnection **MAY** occur at any time (e.g., due to network conditions). diff --git a/docs/specification/draft/schema.mdx b/docs/specification/draft/schema.mdx index 458a2d482..174773f87 100644 --- a/docs/specification/draft/schema.mdx +++ b/docs/specification/draft/schema.mdx @@ -27,7 +27,7 @@ the data is entirely optional.

interface ClientCapabilities {
  elicitation?: object;
  experimental?: { [key: string]: object };
  roots?: { listChanged?: boolean };
  sampling?: object;
}

Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities.

elicitation?: object

Present if the client supports elicitation from the server.

experimental?: { [key: string]: object }

Experimental, non-standard capabilities that the client supports.

roots?: { listChanged?: boolean }

Present if the client supports listing roots.

Type declaration
sampling?: object

Present if the client supports sampling from an LLM.

+
interface ClientCapabilities {
  elicitation?: object;
  experimental?: { [key: string]: object };
  roots?: { listChanged?: boolean };
  sampling?: object;
  streams?: object;
}

Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities.

elicitation?: object

Present if the client supports elicitation from the server.

experimental?: { [key: string]: object }

Experimental, non-standard capabilities that the client supports.

roots?: { listChanged?: boolean }

Present if the client supports listing roots.

Type declaration
sampling?: object

Present if the client supports sampling from an LLM.

streams?: object

Present if the client supports resumable streams.

### `ContentBlock` @@ -71,7 +71,8 @@ if present).