From 73d90185a4bd412debf30edda85f3f7d1d7a1f57 Mon Sep 17 00:00:00 2001
From: Jonathan Hefner
Date: Tue, 15 Jul 2025 09:36:38 -0500
Subject: [PATCH] Transport-agnostic resumable requests
This adds a transport-agnostic mechanism for resuming requests after
disconnections. Using this mechanism:
- Clients and servers can disconnect and reconnect without losing
progress.
- Servers can communicate expire-after-disconnect timeouts and reclaim
resources thereafter.
- Clients can check request status after disconnect without having to
fetch undelivered messages.
- All of the above works regardless of transport (HTTP, WebSocket,
stdio, etc.).
__Motivation__
- **Addressing limitations of resumability when using the Streamable
HTTP transport.**
- SSE-based resume requires the server to send at least one event in
order for the client to obtain a `Last-Event-ID`. If a connection
is lost before an event is sent, there is no way for the client to
resume the SSE stream. This is especially problematic because the
spec currently says that disconnection should not be interpreted as
the client cancelling its request.
- The spec does not indicate whether a server can delete previously
missed SSE events once they have been confirmed delivered by a
resume. The spec could explicitly allow this, but resuming is done
via HTTP GET, and HTTP GET requests should be read-only.
- There is no mechanism for a server to communicate that it will
expire a request after a certain duration of client inactivity.
- **Extending resumability to other transports.**
- Because resumability is defined by the transport layer, the burden
of creating new or custom transports is higher.
- If each transport defines its own version of resumability, it is
more difficult to develop MCP features without accounting for (or
relying on) the nuances of a particular transport.
- **Enabling robust handling of long-running requests such as tool
calls.**
- The spec does not allow servers to close a connection while
computing a result. In other words, servers must maintain
potentially long-running connections.
- There is no mechanism for a client to check the status of a request
after disconnection without having to fetch undelivered messages.
This commit addresses the above issues in the following ways:
- The server sends `notifications/requests/resumePolicy` notification as
soon as possible after determining a request should be resumable.
This causes the Streamable HTTP transport to send a usable
`Last-Event-ID` to the client.
- Because a client resumes using a request ID instead of solely an event
ID, there is no expectation for servers to retain messages that have
been confirmed delivered. Furthermore, for the Streamable HTTP
transport, `requests/resume` is sent via POST, not GET, allowing
servers to delete delivered messages as part of the resume request.
- The `notifications/requests/resumePolicy` notification includes an
optional `maxWait` parameter, informing the client of the maximum
number of seconds it may wait after a disconnection before resuming
the request or checking its status. After this time has elapsed, the
server MAY cancel the request and free all associated resources.
- Because resumability is handled at the application layer via
`notifications/requests/resumePolicy` and `requests/resume`, it works
the same for all transports.
- After sending a `notifications/requests/resumePolicy` notification,
the server is allowed to disconnect at will. Thus the server is not
required to maintain a long-running connection.
- The client can use `requests/getStatus` to check the status of a
request after disconnection without having to fetch undelivered
messages.
Co-authored-by: Connor Peet
---
docs/specification/draft/basic/lifecycle.mdx | 76 +++++++--
docs/specification/draft/basic/transports.mdx | 4 +-
docs/specification/draft/schema.mdx | 43 ++++-
schema/draft/schema.json | 152 ++++++++++++++++++
schema/draft/schema.ts | 145 ++++++++++++++++-
5 files changed, 402 insertions(+), 18 deletions(-)
diff --git a/docs/specification/draft/basic/lifecycle.mdx b/docs/specification/draft/basic/lifecycle.mdx
index b6a84a3f0..f5f513a99 100644
--- a/docs/specification/draft/basic/lifecycle.mdx
+++ b/docs/specification/draft/basic/lifecycle.mdx
@@ -150,18 +150,19 @@ available during the session.
Key capabilities include:
-| Category | Capability | Description |
-| -------- | -------------- | ------------------------------------------------------------------------------------ |
-| 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 | `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) |
-| Server | `tools` | Exposes callable [tools](/specification/draft/server/tools) |
-| Server | `logging` | Emits structured [log messages](/specification/draft/server/utilities/logging) |
-| Server | `completions` | Supports argument [autocompletion](/specification/draft/server/utilities/completion) |
-| Server | `experimental` | Describes support for non-standard experimental features |
+| Category | Capability | Description |
+| -------- | ------------------- | ------------------------------------------------------------------------------------ |
+| 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 | `resumableRequests` | Support for [resumable requests](#resuming-requests) |
+| 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) |
+| Server | `tools` | Exposes callable [tools](/specification/draft/server/tools) |
+| Server | `logging` | Emits structured [log messages](/specification/draft/server/utilities/logging) |
+| Server | `completions` | Supports argument [autocompletion](/specification/draft/server/utilities/completion) |
+| Server | `experimental` | Describes support for non-standard experimental features |
Capability objects can describe sub-capabilities like:
@@ -179,6 +180,57 @@ Both parties **SHOULD**:
- Respect the negotiated protocol version
- Only use capabilities that were successfully negotiated
+#### Resuming Requests
+
+MCP provides a transport-agnostic mechanism for resuming requests across disconnections.
+
+If a client has advertised the [`resumableRequests`][] capability, a server **MAY** send a [`notifications/requests/resumePolicy`][] notification when responding to a request. The notification will specify the resume policy for the request in the event of disconnection, and will include a token that the client can use to resume the request.
+
+After the resume policy is sent, both the client and the server **MAY** disconnect at will. This allows servers to handle long-running requests without maintaining a constant connection.
+
+After a disconnection, clients can resume the request by sending a [`requests/resume`][] request with the **same message ID** as the original request, plus the server-issued token as a parameter. If the ID and token are valid per the resume policy, the server **SHOULD** reset policy-related timers, send any pending messages (e.g., progress notifications), and then continue as if it were handling the original request.
+
+
+
+When using the [Streamable HTTP transport](/specification/draft/basic/transports#streamable-http) to stream messages, a server cannot confirm whether a message has been delivered. Clients use the `Last-Event-ID` HTTP header upon reconnect to indicate the last delivered message.
+
+In order to provide message delivery guarantees, servers SHOULD maintain a buffer of unconfirmed messages and use the `Last-Event-ID` header to determine which messages in the buffer should be sent to the client. For example, when using incremental event IDs, a server might retain all messages and, upon reconnection, discard messages with an event ID <= `Last-Event-ID` before sending the remainder to the client.
+
+
+
+Alternatively, after a disconnection, a client can send a [`requests/getStatus`][] request to get the status of the original request without fetching pending messages. If the parameters of the `requests/getStatus` request are valid per the request policy, the server **SHOULD** reset policy-related timers and then return the status of the original request.
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant Server
+
+ Client->>+Server: Request (e.g., tools/call) { id: 123, params: { ... } }
+
+ Server-->>Client: notifications/requests/resumePolicy { params: { requestId: 123, resumeToken: "abc" } }
+ loop
+ Server-->>Client: Messages (e.g., notifications/progress)
+ end
+ Server--x-Client: Disconnection occurs
+
+ Note over Client: Client checks request status (optional)
+ Client->>+Server: requests/getStatus { params: { requestId: 123, resumeToken: "abc" } }
+ Server-->>-Client: GetRequestStatusResult
+
+ Note over Client: Client decides to resume
+ Client->>+Server: requests/resume { id: 123, params: { resumeToken: "abc" } } [Same `id` as original request]
+ Server-->>Client: Undelivered messages
+ loop
+ Server-->>Client: Messages (e.g., notifications/progress)
+ end
+ Server-->>-Client: CallToolResult { id: 123, result: { ... } }
+```
+
+[`notifications/requests/resumePolicy`]: /specification/draft/schema#notifications%2Frequests%2Fresumepolicy
+[`resumableRequests`]: /specification/draft/schema#clientcapabilities-resumablerequests
+[`requests/getStatus`]: /specification/draft/schema#requests%2Fgetstatus
+[`requests/resume`]: /specification/draft/schema#requests%2Fresume
+
### Shutdown
During the shutdown phase, one side (usually the client) cleanly terminates the protocol
diff --git a/docs/specification/draft/basic/transports.mdx b/docs/specification/draft/basic/transports.mdx
index 582976b06..4cb8b47eb 100644
--- a/docs/specification/draft/basic/transports.mdx
+++ b/docs/specification/draft/basic/transports.mdx
@@ -107,8 +107,8 @@ MCP endpoint.
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.
+ or [_resume policy notification_](/specification/draft/basic/lifecycle#resuming-requests)
+ for the received JSON-RPC _request_, 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..bbca01a1f 100644
--- a/docs/specification/draft/schema.mdx
+++ b/docs/specification/draft/schema.mdx
@@ -27,7 +27,7 @@ the data is entirely optional.
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.
Optionalelicitation
elicitation?:object
Present if the client supports elicitation from the server.
Optionalexperimental
experimental?:{[key:string]:object}
Experimental, non-standard capabilities that the client supports.
Optionalroots
roots?:{listChanged?:boolean}
Present if the client supports listing roots.
Type declaration
OptionallistChanged?: boolean
Whether the client supports notifications for changes to the roots list.
Optionalsampling
sampling?:object
Present if the client supports sampling from an LLM.
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.
Optionalelicitation
elicitation?:object
Present if the client supports elicitation from the server.
Optionalexperimental
experimental?:{[key:string]:object}
Experimental, non-standard capabilities that the client supports.
OptionalresumableRequests
resumableRequests?:object
Present if the client supports resumable requests.
Optionalroots
roots?:{listChanged?:boolean}
Present if the client supports listing roots.
Type declaration
OptionallistChanged?: boolean
Whether the client supports notifications for changes to the roots list.
Optionalsampling
sampling?:object
Present if the client supports sampling from an LLM.
An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client.
The maximum number of seconds a client may wait after a disconnection
+before resuming the request or checking its status. After this time has
+elapsed, the server MAY cancel the request and free all associated
+resources.
If this number is omitted, the server provides no guarantee, and MAY
+cancel the request at its own discretion.
OptionalminWait?: number
The minimum number of seconds a client SHOULD wait after a disconnection
+before resuming the request or checking its status.
An opaque token that the client MUST send back to the server when
+resuming the request or checking its status.
This token should be treated as sensitive information because it can be
+used to access messages related to the request.
+
## `notifications/resources/list_changed`
### `ResourceListChangedNotification`
@@ -354,6 +369,32 @@ If provided, the server should return results starting after this cursor.
A request from the client to the server to resume a prior resumable request.
+The ID of this request MUST be the same as the ID of the resumable request.
If the resumable request was cancelled (due to the resume policy or due to an
+explicit cancellation request from the client), the server SHOULD respond
+with an error.
params
params:{resumeToken:string}
Type declaration
resumeToken: string
The resume token for the original request issued by the server via a notifications/requests/resumePolicy notification.
If this value does not match the token issued by the server, the server
+MUST respond with an error.
+
## `resources/list`
### `ListResourcesRequest`
diff --git a/schema/draft/schema.json b/schema/draft/schema.json
index 8fea15032..4ffb07440 100644
--- a/schema/draft/schema.json
+++ b/schema/draft/schema.json
@@ -230,6 +230,12 @@
"description": "Experimental, non-standard capabilities that the client supports.",
"type": "object"
},
+ "resumableRequests": {
+ "additionalProperties": true,
+ "description": "Present if the client supports resumable requests.",
+ "properties": {},
+ "type": "object"
+ },
"roots": {
"description": "Present if the client supports listing roots.",
"properties": {
@@ -305,6 +311,12 @@
},
{
"$ref": "#/definitions/CompleteRequest"
+ },
+ {
+ "$ref": "#/definitions/ResumeRequest"
+ },
+ {
+ "$ref": "#/definitions/GetRequestStatusRequest"
}
]
},
@@ -764,6 +776,75 @@
],
"type": "object"
},
+ "GetRequestStatusRequest": {
+ "description": "A request from the client to the server to get the status of a resumable\nrequest.\n\nWhen handling this request, the server SHOULD also reset policy-related\ntimers in the same way as for `requests/resume`.",
+ "properties": {
+ "method": {
+ "const": "requests/getStatus",
+ "type": "string"
+ },
+ "params": {
+ "properties": {
+ "requestId": {
+ "$ref": "#/definitions/RequestId",
+ "description": "The ID of the resumable request."
+ },
+ "resumeToken": {
+ "description": "The resume token for the resumable request issued by the server via a\n`notifications/requests/resumePolicy` notification.\n\nIf this value does not match the token issued by the server, the server\nMUST respond with an error.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "requestId",
+ "resumeToken"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "method",
+ "params"
+ ],
+ "type": "object"
+ },
+ "GetRequestStatusResult": {
+ "description": "A response to a `requests/getStatus` request.",
+ "properties": {
+ "_meta": {
+ "additionalProperties": {},
+ "description": "See [specification/draft/basic/index#general-fields] for notes on _meta usage.",
+ "type": "object"
+ },
+ "hasInputRequest": {
+ "description": "Whether the server has requested additional input from the client, such as\nwhen making a sampling request.",
+ "type": "boolean"
+ },
+ "hasPendingMessage": {
+ "description": "Whether there are pending messages for the client that are related to the\nresumable request, such as progress notifications or sampling requests.",
+ "type": "boolean"
+ },
+ "requestId": {
+ "$ref": "#/definitions/RequestId",
+ "description": "The ID of the resumable request."
+ },
+ "status": {
+ "description": "The current status of the resumable request.\n\n- `\"processing\"` indicates that the server is computing a response.\n- `\"completed\"` indicates that the server has computed a final response.\n- `\"failed\"` indicates that the server has a final response, but the\n response is an error.",
+ "enum": [
+ "completed",
+ "failed",
+ "processing"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "hasInputRequest",
+ "hasPendingMessage",
+ "requestId",
+ "status"
+ ],
+ "type": "object"
+ },
"ImageContent": {
"description": "An image provided to or from an LLM.",
"properties": {
@@ -1993,6 +2074,71 @@
},
"type": "object"
},
+ "ResumePolicyNotification": {
+ "description": "A notification to the client that a request is resumable, subject to the\nspecified policy.\n\nThe server SHOULD send this notification as soon as possible after\ndetermining a request is resumable.",
+ "properties": {
+ "method": {
+ "const": "notifications/requests/resumePolicy",
+ "type": "string"
+ },
+ "params": {
+ "properties": {
+ "maxWait": {
+ "description": "The maximum number of seconds a client may wait after a disconnection\nbefore resuming the request or checking its status. After this time has\nelapsed, the server MAY cancel the request and free all associated\nresources.\n\nIf this number is omitted, the server provides no guarantee, and MAY\ncancel the request at its own discretion.",
+ "type": "integer"
+ },
+ "minWait": {
+ "description": "The minimum number of seconds a client SHOULD wait after a disconnection\nbefore resuming the request or checking its status.",
+ "type": "integer"
+ },
+ "requestId": {
+ "$ref": "#/definitions/RequestId",
+ "description": "The ID of the request."
+ },
+ "resumeToken": {
+ "description": "An opaque token that the client MUST send back to the server when\nresuming the request or checking its status.\n\nThis token should be treated as sensitive information because it can be\nused to access messages related to the request.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "requestId",
+ "resumeToken"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "method",
+ "params"
+ ],
+ "type": "object"
+ },
+ "ResumeRequest": {
+ "description": "A request from the client to the server to resume a prior resumable request.\nThe ID of this request MUST be the same as the ID of the resumable request.\n\nIf the resumable request was cancelled (due to the resume policy or due to an\nexplicit cancellation request from the client), the server SHOULD respond\nwith an error.",
+ "properties": {
+ "method": {
+ "const": "requests/resume",
+ "type": "string"
+ },
+ "params": {
+ "properties": {
+ "resumeToken": {
+ "description": "The resume token for the original request issued by the server via a\n`notifications/requests/resumePolicy` notification.\n\nIf this value does not match the token issued by the server, the server\nMUST respond with an error.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "resumeToken"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "method",
+ "params"
+ ],
+ "type": "object"
+ },
"Role": {
"description": "The sender or recipient of messages and data in a conversation.",
"enum": [
@@ -2157,6 +2303,9 @@
},
{
"$ref": "#/definitions/LoggingMessageNotification"
+ },
+ {
+ "$ref": "#/definitions/ResumePolicyNotification"
}
]
},
@@ -2207,6 +2356,9 @@
},
{
"$ref": "#/definitions/CompleteResult"
+ },
+ {
+ "$ref": "#/definitions/GetRequestStatusResult"
}
]
},
diff --git a/schema/draft/schema.ts b/schema/draft/schema.ts
index 01c25000a..55cce4aae 100644
--- a/schema/draft/schema.ts
+++ b/schema/draft/schema.ts
@@ -235,6 +235,10 @@ export interface ClientCapabilities {
* Present if the client supports elicitation from the server.
*/
elicitation?: object;
+ /**
+ * Present if the client supports resumable requests.
+ */
+ resumableRequests?: object;
}
/**
@@ -1471,6 +1475,137 @@ export interface ElicitResult extends Result {
content?: { [key: string]: string | number | boolean };
}
+/**
+ * A notification to the client that a request is resumable, subject to the
+ * specified policy.
+ *
+ * The server SHOULD send this notification as soon as possible after
+ * determining a request is resumable.
+ *
+ * @category notifications/requests/resumePolicy
+ */
+export interface ResumePolicyNotification extends Notification {
+ method: "notifications/requests/resumePolicy";
+ params: {
+ /**
+ * The ID of the request.
+ */
+ requestId: RequestId;
+
+ /**
+ * An opaque token that the client MUST send back to the server when
+ * resuming the request or checking its status.
+ *
+ * This token should be treated as sensitive information because it can be
+ * used to access messages related to the request.
+ */
+ resumeToken: string;
+
+ /**
+ * The minimum number of seconds a client SHOULD wait after a disconnection
+ * before resuming the request or checking its status.
+ */
+ minWait?: number;
+
+ /**
+ * The maximum number of seconds a client may wait after a disconnection
+ * before resuming the request or checking its status. After this time has
+ * elapsed, the server MAY cancel the request and free all associated
+ * resources.
+ *
+ * If this number is omitted, the server provides no guarantee, and MAY
+ * cancel the request at its own discretion.
+ */
+ maxWait?: number;
+ }
+}
+
+/**
+ * A request from the client to the server to resume a prior resumable request.
+ * The ID of this request MUST be the same as the ID of the resumable request.
+ *
+ * If the resumable request was cancelled (due to the resume policy or due to an
+ * explicit cancellation request from the client), the server SHOULD respond
+ * with an error.
+ *
+ * @category requests/resume
+ */
+export interface ResumeRequest extends Request {
+ method: "requests/resume";
+ params: {
+ /**
+ * The resume token for the original request issued by the server via a
+ * `notifications/requests/resumePolicy` notification.
+ *
+ * If this value does not match the token issued by the server, the server
+ * MUST respond with an error.
+ */
+ resumeToken: string;
+ };
+}
+
+/**
+ * A request from the client to the server to get the status of a resumable
+ * request.
+ *
+ * When handling this request, the server SHOULD also reset policy-related
+ * timers in the same way as for `requests/resume`.
+ *
+ * @category requests/getStatus
+ */
+export interface GetRequestStatusRequest extends Request {
+ method: "requests/getStatus";
+ params: {
+ /**
+ * The ID of the resumable request.
+ */
+ requestId: RequestId;
+
+ /**
+ * The resume token for the resumable request issued by the server via a
+ * `notifications/requests/resumePolicy` notification.
+ *
+ * If this value does not match the token issued by the server, the server
+ * MUST respond with an error.
+ */
+ resumeToken: string;
+ };
+}
+
+/**
+ * A response to a `requests/getStatus` request.
+ *
+ * @category requests/getStatus
+ */
+export interface GetRequestStatusResult extends Result {
+ /**
+ * The ID of the resumable request.
+ */
+ requestId: RequestId;
+
+ /**
+ * The current status of the resumable request.
+ *
+ * - `"processing"` indicates that the server is computing a response.
+ * - `"completed"` indicates that the server has computed a final response.
+ * - `"failed"` indicates that the server has a final response, but the
+ * response is an error.
+ */
+ status: "processing" | "completed" | "failed";
+
+ /**
+ * Whether there are pending messages for the client that are related to the
+ * resumable request, such as progress notifications or sampling requests.
+ */
+ hasPendingMessage: boolean;
+
+ /**
+ * Whether the server has requested additional input from the client, such as
+ * when making a sampling request.
+ */
+ hasInputRequest: boolean;
+}
+
/* Client messages */
/** @internal */
export type ClientRequest =
@@ -1486,7 +1621,9 @@ export type ClientRequest =
| SubscribeRequest
| UnsubscribeRequest
| CallToolRequest
- | ListToolsRequest;
+ | ListToolsRequest
+ | ResumeRequest
+ | GetRequestStatusRequest;
/** @internal */
export type ClientNotification =
@@ -1518,7 +1655,8 @@ export type ServerNotification =
| ResourceUpdatedNotification
| ResourceListChangedNotification
| ToolListChangedNotification
- | PromptListChangedNotification;
+ | PromptListChangedNotification
+ | ResumePolicyNotification;
/** @internal */
export type ServerResult =
@@ -1531,4 +1669,5 @@ export type ServerResult =
| ListResourcesResult
| ReadResourceResult
| CallToolResult
- | ListToolsResult;
+ | ListToolsResult
+ | GetRequestStatusResult;
elicitation?: object;
experimental?: { [key: string]: object };
roots?: { listChanged?: boolean };
sampling?: object;
}