-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Transport-agnostic resumable streams #899
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
connor4312
wants to merge
26
commits into
modelcontextprotocol:main
from
connor4312:connor4312/543-streams
Closed
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
fa81940
Initial take on transport-agnostic resumable streams
connor4312 7700c47
update
connor4312 a02fa0e
Response => Result
jonathanhefner 3f0e51e
Repurpose `stream/poll` for stream status
jonathanhefner 0fd29c8
Add `stream/poll/all`
jonathanhefner dd71c19
Paginate `stream/poll/all`
jonathanhefner 6daaa7f
Merge pull request #2 from jonathanhefner/connor4312/543-streams
jonathanhefner 3971b3b
add docs
connor4312 f28eabb
rm basemetadata
connor4312 31877a3
add a client capability
connor4312 90df79a
generate:json
connor4312 f962e00
format
connor4312 3130fee
comments
connor4312 916a187
Fix typos and address feedback
jonathanhefner 594449f
Merge pull request #3 from jonathanhefner/connor4312/543-streams/feed…
jonathanhefner cbb379d
Merge branch 'main' into connor4312/543-streams/feedback-2
jonathanhefner 124ba59
Allow at-will disconnect
jonathanhefner 3e4e66d
Remove `stream/poll/all` for now
jonathanhefner 3896196
Add `resumeToken` and make `streamId` sharable
jonathanhefner 521c9b2
Add JSDoc `@category` tags
jonathanhefner ef8b9ec
Tweak documentation
jonathanhefner b90eace
Update generated files
jonathanhefner 98434f3
Format with Prettier
jonathanhefner 81f8599
Merge pull request #4 from jonathanhefner/connor4312/543-streams/feed…
jonathanhefner 6225ba2
Tweak documentation
jonathanhefner fa99a55
Merge pull request #5 from jonathanhefner/connor4312/543-streams/feed…
jonathanhefner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| --- | ||
| title: Streams | ||
| --- | ||
|
|
||
| <div id="enable-section-numbers" /> | ||
|
|
||
| <Info>**Protocol Revision**: draft</Info> | ||
|
|
||
| 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<br>{ 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<br>{ streamId: "123", resumeToken: "abc" } | ||
| Server-->>-Client: StreamPollResult | ||
|
|
||
| Note over Client: Client decides to resume | ||
| Client->>+Server: stream/resume<br>{ 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<br>{ 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<br>{ streamId: "123", resumeToken: "abc" } | ||
| Server-->>Client: Initial messages | ||
| Server--x-Client: Disconnection occurs | ||
|
|
||
| Client->>+Server: stream/resume<br>{ streamId: "123", resumeToken: "abc" } | ||
| Server-->>Client: Undelivered messages | ||
| Server-->>-Client: notifications/stream/end<br>{ streamId: "123" } | ||
| ``` | ||
|
|
||
| <Note> | ||
|
|
||
| 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. | ||
|
|
||
| </Note> | ||
|
|
||
| ### 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<br>{ 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<br>{ streamId: "123", resumeToken: "abc" } | ||
|
|
||
| loop | ||
| Server-->>Client: notifications/progress | ||
| Server-->>Client: Other messages | ||
| end | ||
|
|
||
| Server-->>Client: CallToolResult | ||
| Server-->>-Client: notifications/stream/end<br>{ 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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The end is missing here