diff --git a/docs/specification/draft/basic/patterns/subscriptions.mdx b/docs/specification/draft/basic/patterns/subscriptions.mdx index b85db7072..92ebb0e02 100644 --- a/docs/specification/draft/basic/patterns/subscriptions.mdx +++ b/docs/specification/draft/basic/patterns/subscriptions.mdx @@ -114,12 +114,43 @@ A subscription ends when: - The **client** cancels it — close the SSE stream (HTTP) or send `notifications/cancelled` referencing the `subscriptions/listen` request ID (stdio). -- The **server** tears it down (e.g., during shutdown) — it **MUST** close the SSE - stream (HTTP) or send `notifications/cancelled` referencing the - `subscriptions/listen` request ID (stdio). +- The **server** tears it down (e.g., during shutdown) — it **SHOULD** send the + empty `subscriptions/listen` response to signal a graceful end (see + [Graceful Closure](#graceful-closure)), then close the stream. - The underlying transport closes (HTTP timeout, TCP disconnect, stdio process exit). +### Graceful Closure + +When the server ends a subscription on its own initiative (for example, during +shutdown), it **SHOULD** respond to the original `subscriptions/listen` request +with an empty result before closing the stream. This is the JSON-RPC response to +the long-lived request, correlated by its `id`, and signals that the subscription +ended gracefully — as opposed to an abrupt transport drop, which carries no +response. + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "resultType": "complete", + "_meta": { + "io.modelcontextprotocol/subscriptionId": 1 + } + } +} +``` + +Like every other message on the stream, the response carries +`io.modelcontextprotocol/subscriptionId` in `_meta`, identifying which +subscription it closes. The value matches the JSON-RPC `id` of the originating +`subscriptions/listen` request. + +A client that receives this response knows the subscription closed cleanly; a +transport that closes without it indicates an unexpected disconnect, which the +client **MAY** treat as a trigger to reconnect. + On **stdio**, if the connection is terminated and then re-established, the client **MUST** re-send `subscriptions/listen` to re-establish its subscriptions — the server holds no subscription state across reconnections. diff --git a/docs/specification/draft/schema.mdx b/docs/specification/draft/schema.mdx index cd11cd51b..1a49f0d76 100644 --- a/docs/specification/draft/schema.mdx +++ b/docs/specification/draft/schema.mdx @@ -699,7 +699,7 @@ deprecated features registry.

interface PromptListChangedNotification {
  jsonrpc: "2.0";
  method: "notifications/prompts/list_changed";
  params?: NotificationParams;
}

An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This is only delivered on a subscriptions/listen stream when the client requested it via the promptsListChanged filter field.

Example: Prompts list changed
{
"jsonrpc": "2.0",
"method": "notifications/prompts/list_changed"
}
+
interface PromptListChangedNotification {
  jsonrpc: "2.0";
  method: "notifications/prompts/list_changed";
  params?: NotificationParams;
}

An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This is only delivered on a subscriptions/listen stream when the client requested it via the promptsListChanged filter field.

Example: Prompts list changed
{
"jsonrpc": "2.0",
"method": "notifications/prompts/list_changed",
"params": {
"_meta": {
"io.modelcontextprotocol/subscriptionId": "listen-1"
}
}
}
@@ -710,7 +710,7 @@ deprecated features registry.

interface ResourceListChangedNotification {
  jsonrpc: "2.0";
  method: "notifications/resources/list_changed";
  params?: NotificationParams;
}

An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This is only delivered on a subscriptions/listen stream when the client requested it via the resourcesListChanged filter field.

Example: Resources list changed
{
"jsonrpc": "2.0",
"method": "notifications/resources/list_changed"
}
+
interface ResourceListChangedNotification {
  jsonrpc: "2.0";
  method: "notifications/resources/list_changed";
  params?: NotificationParams;
}

An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This is only delivered on a subscriptions/listen stream when the client requested it via the resourcesListChanged filter field.

Example: Resources list changed
{
"jsonrpc": "2.0",
"method": "notifications/resources/list_changed",
"params": {
"_meta": {
"io.modelcontextprotocol/subscriptionId": "listen-1"
}
}
}
@@ -721,7 +721,7 @@ deprecated features registry.

interface ResourceUpdatedNotification {
  jsonrpc: "2.0";
  method: "notifications/resources/updated";
  params: ResourceUpdatedNotificationParams;
}

A notification from the server to the client, informing it that a resource has changed and may need to be read again. This is only sent for resources the client opted in to via the resourceSubscriptions field of a subscriptions/listen request.

Example: File resource updated notification
{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": {
"uri": "file:///project/src/main.rs"
}
}
+
interface ResourceUpdatedNotification {
  jsonrpc: "2.0";
  method: "notifications/resources/updated";
  params: ResourceUpdatedNotificationParams;
}

A notification from the server to the client, informing it that a resource has changed and may need to be read again. This is only sent for resources the client opted in to via the resourceSubscriptions field of a subscriptions/listen request.

Example: File resource updated notification
{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": {
"_meta": {
"io.modelcontextprotocol/subscriptionId": "listen-1"
},
"uri": "file:///project/src/main.rs"
}
}
@@ -764,7 +764,7 @@ the server has no prompts), it is omitted from this set.

### `ToolListChangedNotification` -
interface ToolListChangedNotification {
  jsonrpc: "2.0";
  method: "notifications/tools/list_changed";
  params?: NotificationParams;
}

An optional notification from the server to the client, informing it that the list of tools it offers has changed. This is only delivered on a subscriptions/listen stream when the client requested it via the toolsListChanged filter field.

Example: Tools list changed
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}
+
interface ToolListChangedNotification {
  jsonrpc: "2.0";
  method: "notifications/tools/list_changed";
  params?: NotificationParams;
}

An optional notification from the server to the client, informing it that the list of tools it offers has changed. This is only delivered on a subscriptions/listen stream when the client requested it via the toolsListChanged filter field.

Example: Tools list changed
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed",
"params": {
"_meta": {
"io.modelcontextprotocol/subscriptionId": "listen-1"
}
}
}
@@ -1344,6 +1344,21 @@ requested.

+
+ +### `SubscriptionsListenResult` + +
interface SubscriptionsListenResult {
  resultType: string;
  _meta: SubscriptionsListenResultMeta;
  [key: string]: unknown;
}

The response to a subscriptions/listen +request, signalling that the subscription has ended gracefully (for example, +during server shutdown). Because the listen stream is long-lived, this result +is sent only when the server tears the subscription down; an abrupt transport +close carries no response. The result body is otherwise empty.

Example: Subscription closed gracefully
{
"resultType": "complete",
"_meta": {
"io.modelcontextprotocol/subscriptionId": "listen-1"
}
}

Indicates the type of the result, which allows the client to determine +how to parse the result object.

Servers implementing this protocol version MUST include this field. +For backward compatibility, when a client receives a result from a +server implementing an earlier protocol version (which does not include resultType), the client MUST treat the absent field as "complete".

+
+ +
### `SubscriptionFilter` @@ -1354,6 +1369,17 @@ Replaces the former resources/subscribe RPC.

+
+ +### `SubscriptionsListenResultMeta` + +
interface SubscriptionsListenResultMeta {
  "io.modelcontextprotocol/subscriptionId": RequestId;
  [key: string]: unknown;
}

Extends MetaObject with the subscription-stream identifier carried by a SubscriptionsListenResult. All key naming rules from MetaObject apply.

MetaObject for key naming rules and reserved prefixes.

Identifies the subscription stream this response closes, so the client can +correlate it with the originating subscription — mirroring the same key on +the stream's notifications. The value is the JSON-RPC ID of the subscriptions/listen request that opened the stream (and equals this +response's id).

+
+ + ## `tools/call` diff --git a/schema/draft/examples/PromptListChangedNotification/prompts-list-changed.json b/schema/draft/examples/PromptListChangedNotification/prompts-list-changed.json index 858cd5d87..1fb5771b2 100644 --- a/schema/draft/examples/PromptListChangedNotification/prompts-list-changed.json +++ b/schema/draft/examples/PromptListChangedNotification/prompts-list-changed.json @@ -1,4 +1,9 @@ { "jsonrpc": "2.0", - "method": "notifications/prompts/list_changed" + "method": "notifications/prompts/list_changed", + "params": { + "_meta": { + "io.modelcontextprotocol/subscriptionId": "listen-1" + } + } } diff --git a/schema/draft/examples/ResourceListChangedNotification/resources-list-changed.json b/schema/draft/examples/ResourceListChangedNotification/resources-list-changed.json index 6ba5e168e..8c894de0e 100644 --- a/schema/draft/examples/ResourceListChangedNotification/resources-list-changed.json +++ b/schema/draft/examples/ResourceListChangedNotification/resources-list-changed.json @@ -1,4 +1,9 @@ { "jsonrpc": "2.0", - "method": "notifications/resources/list_changed" + "method": "notifications/resources/list_changed", + "params": { + "_meta": { + "io.modelcontextprotocol/subscriptionId": "listen-1" + } + } } diff --git a/schema/draft/examples/ResourceUpdatedNotification/file-resource-updated-notification.json b/schema/draft/examples/ResourceUpdatedNotification/file-resource-updated-notification.json index b5f9ef67f..0b85f54ba 100644 --- a/schema/draft/examples/ResourceUpdatedNotification/file-resource-updated-notification.json +++ b/schema/draft/examples/ResourceUpdatedNotification/file-resource-updated-notification.json @@ -2,6 +2,9 @@ "jsonrpc": "2.0", "method": "notifications/resources/updated", "params": { + "_meta": { + "io.modelcontextprotocol/subscriptionId": "listen-1" + }, "uri": "file:///project/src/main.rs" } } diff --git a/schema/draft/examples/SubscriptionsListenResult/listen-closed.json b/schema/draft/examples/SubscriptionsListenResult/listen-closed.json new file mode 100644 index 000000000..2e7e50720 --- /dev/null +++ b/schema/draft/examples/SubscriptionsListenResult/listen-closed.json @@ -0,0 +1,6 @@ +{ + "resultType": "complete", + "_meta": { + "io.modelcontextprotocol/subscriptionId": "listen-1" + } +} diff --git a/schema/draft/examples/ToolListChangedNotification/tools-list-changed.json b/schema/draft/examples/ToolListChangedNotification/tools-list-changed.json index a28e84676..89cd4e1a6 100644 --- a/schema/draft/examples/ToolListChangedNotification/tools-list-changed.json +++ b/schema/draft/examples/ToolListChangedNotification/tools-list-changed.json @@ -1,4 +1,9 @@ { "jsonrpc": "2.0", - "method": "notifications/tools/list_changed" + "method": "notifications/tools/list_changed", + "params": { + "_meta": { + "io.modelcontextprotocol/subscriptionId": "listen-1" + } + } } diff --git a/schema/draft/schema.json b/schema/draft/schema.json index be1ae2f93..82cac4d58 100644 --- a/schema/draft/schema.json +++ b/schema/draft/schema.json @@ -3220,6 +3220,9 @@ { "$ref": "#/$defs/ReadResourceResult" }, + { + "$ref": "#/$defs/SubscriptionsListenResult" + }, { "$ref": "#/$defs/ListPromptsResult" }, @@ -3389,6 +3392,36 @@ ], "type": "object" }, + "SubscriptionsListenResult": { + "description": "The response to a {@link SubscriptionsListenRequestsubscriptions/listen}\nrequest, signalling that the subscription has ended gracefully (for example,\nduring server shutdown). Because the listen stream is long-lived, this result\nis sent only when the server tears the subscription down; an abrupt transport\nclose carries no response. The result body is otherwise empty.", + "properties": { + "_meta": { + "$ref": "#/$defs/SubscriptionsListenResultMeta" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + } + }, + "required": [ + "_meta", + "resultType" + ], + "type": "object" + }, + "SubscriptionsListenResultMeta": { + "description": "Extends {@link MetaObject} with the subscription-stream identifier carried by a\n{@link SubscriptionsListenResult}. All key naming rules from `MetaObject` apply.", + "properties": { + "io.modelcontextprotocol/subscriptionId": { + "$ref": "#/$defs/RequestId", + "description": "Identifies the subscription stream this response closes, so the client can\ncorrelate it with the originating subscription — mirroring the same key on\nthe stream's notifications. The value is the JSON-RPC ID of the\n`subscriptions/listen` request that opened the stream (and equals this\nresponse's `id`)." + } + }, + "required": [ + "io.modelcontextprotocol/subscriptionId" + ], + "type": "object" + }, "TextContent": { "description": "Text provided to or from an LLM.", "properties": { diff --git a/schema/draft/schema.ts b/schema/draft/schema.ts index c48f9d719..10b5d7475 100644 --- a/schema/draft/schema.ts +++ b/schema/draft/schema.ts @@ -1300,6 +1300,40 @@ export interface SubscriptionsListenRequest extends JSONRPCRequest { params: SubscriptionsListenRequestParams; } +/** + * Extends {@link MetaObject} with the subscription-stream identifier carried by a + * {@link SubscriptionsListenResult}. All key naming rules from `MetaObject` apply. + * + * @see {@link MetaObject} for key naming rules and reserved prefixes. + * @category `subscriptions/listen` + */ +export interface SubscriptionsListenResultMeta extends MetaObject { + /** + * Identifies the subscription stream this response closes, so the client can + * correlate it with the originating subscription — mirroring the same key on + * the stream's notifications. The value is the JSON-RPC ID of the + * `subscriptions/listen` request that opened the stream (and equals this + * response's `id`). + */ + "io.modelcontextprotocol/subscriptionId": RequestId; +} + +/** + * The response to a {@link SubscriptionsListenRequest | subscriptions/listen} + * request, signalling that the subscription has ended gracefully (for example, + * during server shutdown). Because the listen stream is long-lived, this result + * is sent only when the server tears the subscription down; an abrupt transport + * close carries no response. The result body is otherwise empty. + * + * @example Subscription closed gracefully + * {@includeCode ./examples/SubscriptionsListenResult/listen-closed.json} + * + * @category `subscriptions/listen` + */ +export interface SubscriptionsListenResult extends Result { + _meta: SubscriptionsListenResultMeta; +} + /** * Parameters for a {@link SubscriptionsAcknowledgedNotification | notifications/subscriptions/acknowledged} notification. * @@ -3134,6 +3168,7 @@ export type ServerResult = | ListResourceTemplatesResult | ListResourcesResult | ReadResourceResult + | SubscriptionsListenResult | CallToolResult | ListToolsResult | InputRequiredResult;