From 4b358ef5ea0d450d808143879c6743bd9ef74ccb Mon Sep 17 00:00:00 2001 From: kurtisvg <31518063+kurtisvg@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:53:46 -0600 Subject: [PATCH 1/4] feat(schema): add subscriptions/listen response subscriptions/listen was the only request without a response object. Add an empty SubscriptionsListenResult, sent by the server to signal a graceful end of the subscription (e.g. during shutdown), distinct from an abrupt transport drop which carries no response. Like other stream messages it carries the subscriptionId in _meta. Also add the subscriptionId to the stream notification examples that were missing it, matching the spec requirement that all stream messages carry it. --- .../draft/basic/patterns/subscriptions.mdx | 37 +++++++++++++++++-- docs/specification/draft/schema.mdx | 34 +++++++++++++++-- .../prompts-list-changed.json | 7 +++- .../resources-list-changed.json | 7 +++- .../file-resource-updated-notification.json | 3 ++ .../listen-closed.json | 6 +++ .../tools-list-changed.json | 7 +++- schema/draft/schema.json | 29 +++++++++++++++ schema/draft/schema.ts | 35 ++++++++++++++++++ 9 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 schema/draft/examples/SubscriptionsListenResult/listen-closed.json 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..bec7094d0 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..81358761e 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,32 @@ ], "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": [ + "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`)." + } + }, + "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..c4fc0229e 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; From 1d61a794cd3693c6d2b181460641bda34bc36532 Mon Sep 17 00:00:00 2001 From: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> Date: Mon, 22 Jun 2026 09:39:55 -0600 Subject: [PATCH 2/4] Update schema/draft/schema.ts Co-authored-by: Peter Alexander --- schema/draft/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/draft/schema.ts b/schema/draft/schema.ts index c4fc0229e..d5c3ab50e 100644 --- a/schema/draft/schema.ts +++ b/schema/draft/schema.ts @@ -1315,7 +1315,7 @@ export interface SubscriptionsListenResultMeta extends MetaObject { * `subscriptions/listen` request that opened the stream (and equals this * response's `id`). */ - "io.modelcontextprotocol/subscriptionId"?: RequestId; + "io.modelcontextprotocol/subscriptionId": RequestId; } /** From 1c1e965c272efc662acde88525b09d1aa298854a Mon Sep 17 00:00:00 2001 From: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> Date: Mon, 22 Jun 2026 09:40:04 -0600 Subject: [PATCH 3/4] Update schema/draft/schema.ts Co-authored-by: Peter Alexander --- schema/draft/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/draft/schema.ts b/schema/draft/schema.ts index d5c3ab50e..10b5d7475 100644 --- a/schema/draft/schema.ts +++ b/schema/draft/schema.ts @@ -1331,7 +1331,7 @@ export interface SubscriptionsListenResultMeta extends MetaObject { * @category `subscriptions/listen` */ export interface SubscriptionsListenResult extends Result { - _meta?: SubscriptionsListenResultMeta; + _meta: SubscriptionsListenResultMeta; } /** From c2262ec0484565ab4432e90626de2a8350f20e24 Mon Sep 17 00:00:00 2001 From: kurtisvg <31518063+kurtisvg@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:25:24 -0600 Subject: [PATCH 4/4] chore(schema): regenerate draft schema after listen result _meta change --- docs/specification/draft/schema.mdx | 6 +++--- schema/draft/schema.json | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/specification/draft/schema.mdx b/docs/specification/draft/schema.mdx index bec7094d0..1a49f0d76 100644 --- a/docs/specification/draft/schema.mdx +++ b/docs/specification/draft/schema.mdx @@ -1348,14 +1348,14 @@ requested.

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

The response to a subscriptions/listen +

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

+server implementing an earlier protocol version (which does not include resultType), the client MUST treat the absent field as "complete".

@@ -1373,7 +1373,7 @@ 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 +

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

diff --git a/schema/draft/schema.json b/schema/draft/schema.json index 81358761e..82cac4d58 100644 --- a/schema/draft/schema.json +++ b/schema/draft/schema.json @@ -3404,6 +3404,7 @@ } }, "required": [ + "_meta", "resultType" ], "type": "object" @@ -3416,6 +3417,9 @@ "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": {