diff --git a/schema/2026-07-28.json b/schema/2026-07-28.json index 7025ee7d4..87116a420 100644 --- a/schema/2026-07-28.json +++ b/schema/2026-07-28.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/PINNED.json b/schema/PINNED.json index e4022e86c..9b1739d29 100644 --- a/schema/PINNED.json +++ b/schema/PINNED.json @@ -8,7 +8,7 @@ { "protocol_version": "2026-07-28", "source_path_in_spec_repo": "schema/draft/schema.json", - "spec_commit": "2852f30e26ca5fb779565741ec042094cb110abd", - "sha256": "ed1ad4ba94aaeb2068b78969ef901b1150f7b2f06cf86472b3032abee1380b6a" + "spec_commit": "ead35b59b4fda8b32e276810025d8f92bdcec1b6", + "sha256": "e00f675287e8cf078688c26c8a89d283ff2613da3b76d5cd15aff9d189df639c" } ] diff --git a/scripts/gen_surface_types.py b/scripts/gen_surface_types.py index df99b33eb..f33862909 100644 --- a/scripts/gen_surface_types.py +++ b/scripts/gen_surface_types.py @@ -85,7 +85,15 @@ OPEN_CLASSES: dict[str, frozenset[str]] = { "2025-11-25": frozenset({"Meta", "InputSchema", "OutputSchema", "Result", "GetTaskPayloadResult", "Data"}), "2026-07-28": frozenset( - {"MetaObject", "NotificationMetaObject", "RequestMetaObject", "InputSchema", "OutputSchema", "Result"} + { + "MetaObject", + "NotificationMetaObject", + "RequestMetaObject", + "SubscriptionsListenResultMeta", + "InputSchema", + "OutputSchema", + "Result", + } ), } diff --git a/src/mcp-types/mcp_types/__init__.py b/src/mcp-types/mcp_types/__init__.py index c90062e41..2ed97cba3 100644 --- a/src/mcp-types/mcp_types/__init__.py +++ b/src/mcp-types/mcp_types/__init__.py @@ -160,6 +160,7 @@ SubscriptionsAcknowledgedNotificationParams, SubscriptionsListenRequest, SubscriptionsListenRequestParams, + SubscriptionsListenResult, Task, TaskMetadata, TasksCallCapability, @@ -385,6 +386,7 @@ "ListTasksResult", "ListToolsResult", "ReadResourceResult", + "SubscriptionsListenResult", # Error data payloads "MissingRequiredClientCapabilityErrorData", "UnsupportedProtocolVersionErrorData", diff --git a/src/mcp-types/mcp_types/_types.py b/src/mcp-types/mcp_types/_types.py index 09bf94d22..34dc10083 100644 --- a/src/mcp-types/mcp_types/_types.py +++ b/src/mcp-types/mcp_types/_types.py @@ -1063,6 +1063,21 @@ class SubscriptionsAcknowledgedNotification( params: SubscriptionsAcknowledgedNotificationParams +class SubscriptionsListenResult(Result): + """Signals that a `subscriptions/listen` stream has ended gracefully (2026-07-28). + + Because the listen stream is long-lived, this result is sent only when the + server tears the subscription down (for example during shutdown); an abrupt + transport close carries no response. The body is otherwise empty: the + `_meta["io.modelcontextprotocol/subscriptionId"]` key is required on the + wire and equals the JSON-RPC id of the originating `subscriptions/listen` + request. + """ + + result_type: ResultType = "complete" + """See `ResultType`. Always serialized; older peers ignore it.""" + + class ListPromptsRequest(PaginatedRequest[Literal["prompts/list"]]): """Sent from the client to request a list of prompts and prompt templates the server has.""" @@ -2156,6 +2171,7 @@ def _require_one_field(self) -> Self: | ReadResourceResult | CallToolResult | ListToolsResult + | SubscriptionsListenResult | InputRequiredResult ) """Union of every result payload a server can return for a client request. diff --git a/src/mcp-types/mcp_types/methods.py b/src/mcp-types/mcp_types/methods.py index 5bc7c1ff9..824dcfdfe 100644 --- a/src/mcp-types/mcp_types/methods.py +++ b/src/mcp-types/mcp_types/methods.py @@ -292,7 +292,7 @@ ("resources/read", "2026-07-28"): v2026.AnyReadResourceResult, ("resources/templates/list", "2026-07-28"): v2026.ListResourceTemplatesResult, ("server/discover", "2026-07-28"): v2026.DiscoverResult, - ("subscriptions/listen", "2026-07-28"): v2026.EmptyResult, + ("subscriptions/listen", "2026-07-28"): v2026.SubscriptionsListenResult, ("tools/call", "2026-07-28"): v2026.AnyCallToolResult, ("tools/list", "2026-07-28"): v2026.ListToolsResult, } @@ -396,7 +396,7 @@ # smart-union ties resolve leftmost. Pinned by tests/types/test_methods.py. "sampling/createMessage": types.CreateMessageResult | types.CreateMessageResultWithTools, "server/discover": types.DiscoverResult, - "subscriptions/listen": types.EmptyResult, + "subscriptions/listen": types.SubscriptionsListenResult, "tools/call": types.CallToolResult | types.InputRequiredResult, "tools/list": types.ListToolsResult, } diff --git a/src/mcp-types/mcp_types/v2026_07_28/__init__.py b/src/mcp-types/mcp_types/v2026_07_28/__init__.py index 9ab30a346..2963c1323 100644 --- a/src/mcp-types/mcp_types/v2026_07_28/__init__.py +++ b/src/mcp-types/mcp_types/v2026_07_28/__init__.py @@ -1,7 +1,7 @@ """Internal wire-shape models for protocol 2026-07-28. Generated; do not edit. Regenerate with `scripts/gen_surface_types.py` from `schema/2026-07-28.json` -(sha256 `ed1ad4ba94aaeb2068b78969ef901b1150f7b2f06cf86472b3032abee1380b6a`).""" +(sha256 `e00f675287e8cf078688c26c8a89d283ff2613da3b76d5cd15aff9d189df639c`).""" # pyright: reportIncompatibleVariableOverride=false, reportGeneralTypeIssues=false from __future__ import annotations @@ -909,6 +909,25 @@ class SubscriptionFilter(WireModel): """ +class SubscriptionsListenResultMeta(WireModel): + """ + Extends {@link MetaObject} with the subscription-stream identifier carried by a + {@link SubscriptionsListenResult}. All key naming rules from `MetaObject` apply. + """ + + model_config = ConfigDict( + extra="allow", + ) + io_modelcontextprotocol_subscription_id: Annotated[RequestId, Field(alias="io.modelcontextprotocol/subscriptionId")] + """ + 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`). + """ + + class TextResourceContents(WireModel): model_config = ConfigDict( extra="ignore", @@ -2056,6 +2075,31 @@ class SubscriptionsAcknowledgedNotificationParams(WireModel): """ +class SubscriptionsListenResult(WireModel): + """ + The response to a {@link SubscriptionsListenRequestsubscriptions/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. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[SubscriptionsListenResultMeta, Field(alias="_meta")] + result_type: Annotated[str, Field(alias="resultType")] + """ + 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"`. + """ + + class TextContent(WireModel): """ Text provided to or from an LLM. @@ -3544,6 +3588,7 @@ class ServerResult( | ListResourcesResult | ListResourceTemplatesResult | ReadResourceResult + | SubscriptionsListenResult | ListPromptsResult | GetPromptResult | ListToolsResult @@ -3558,6 +3603,7 @@ class ServerResult( | ListResourcesResult | ListResourceTemplatesResult | ReadResourceResult + | SubscriptionsListenResult | ListPromptsResult | GetPromptResult | ListToolsResult diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index bbd2ff331..c10ff82f3 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -182,7 +182,7 @@ def __init__( | None = None, on_subscriptions_listen: Callable[ [ServerRequestContext[LifespanResultT], types.SubscriptionsListenRequestParams], - Awaitable[types.EmptyResult], + Awaitable[types.SubscriptionsListenResult], ] | None = None, on_list_prompts: Callable[ @@ -264,7 +264,7 @@ def __init__( | None = None, on_subscriptions_listen: Callable[ [ServerRequestContext[LifespanResultT], types.SubscriptionsListenRequestParams], - Awaitable[types.EmptyResult], + Awaitable[types.SubscriptionsListenResult], ] | None = None, on_list_prompts: Callable[ @@ -355,7 +355,7 @@ def __init__( | None = None, on_subscriptions_listen: Callable[ [ServerRequestContext[LifespanResultT], types.SubscriptionsListenRequestParams], - Awaitable[types.EmptyResult], + Awaitable[types.SubscriptionsListenResult], ] | None = None, on_list_prompts: Callable[ diff --git a/tests/types/test_methods.py b/tests/types/test_methods.py index ab72aa389..79ea067c6 100644 --- a/tests/types/test_methods.py +++ b/tests/types/test_methods.py @@ -268,7 +268,7 @@ ("resources/read", "2026-07-28"): (v2026.ReadResourceResult, v2026.InputRequiredResult), ("resources/templates/list", "2026-07-28"): v2026.ListResourceTemplatesResult, ("server/discover", "2026-07-28"): v2026.DiscoverResult, - ("subscriptions/listen", "2026-07-28"): v2026.EmptyResult, + ("subscriptions/listen", "2026-07-28"): v2026.SubscriptionsListenResult, ("tools/call", "2026-07-28"): (v2026.CallToolResult, v2026.InputRequiredResult), ("tools/list", "2026-07-28"): v2026.ListToolsResult, } @@ -290,9 +290,7 @@ ("sampling/createMessage", "2025-11-25"): v2025.CreateMessageResult, } -EMPTY_SERVER_RESPONSE_METHODS = frozenset( - {"logging/setLevel", "ping", "resources/subscribe", "resources/unsubscribe", "subscriptions/listen"} -) +EMPTY_SERVER_RESPONSE_METHODS = frozenset({"logging/setLevel", "ping", "resources/subscribe", "resources/unsubscribe"}) EMPTY_CLIENT_RESPONSE_METHODS = frozenset({"ping"}) # Pre-2026 versions share the 2025-11-25 surface package. @@ -404,7 +402,10 @@ "ttlMs": 0, "cacheScope": "private", }, - v2026.EmptyResult: {"resultType": "complete"}, + v2026.SubscriptionsListenResult: { + "resultType": "complete", + "_meta": {"io.modelcontextprotocol/subscriptionId": 1}, + }, v2026.ListPromptsResult: {"prompts": [], "resultType": "complete", "ttlMs": 0, "cacheScope": "private"}, v2026.ListResourcesResult: {"resources": [], "resultType": "complete", "ttlMs": 0, "cacheScope": "private"}, v2026.ListResourceTemplatesResult: { @@ -844,7 +845,9 @@ def test_validate_functions_accept_reject_and_gate_like_their_parse_siblings(): ttl_ms=0, cache_scope="private", ), - "subscriptions/listen": types.EmptyResult(result_type="complete"), + "subscriptions/listen": types.SubscriptionsListenResult.model_validate( + {"_meta": {"io.modelcontextprotocol/subscriptionId": 1}} + ), "tools/call": types.CallToolResult(content=[]), "tools/list": types.ListToolsResult(tools=[], ttl_ms=0, cache_scope="private"), } diff --git a/tests/types/test_parity.py b/tests/types/test_parity.py index 8a06073ac..080f343c3 100644 --- a/tests/types/test_parity.py +++ b/tests/types/test_parity.py @@ -122,6 +122,7 @@ "v2026_07_28.RequestedSchema", "v2026_07_28.ResourceRequestParams", "v2026_07_28.StringSchema", + "v2026_07_28.SubscriptionsListenResultMeta", "v2026_07_28.TitledMultiSelectEnumSchema", "v2026_07_28.TitledSingleSelectEnumSchema", "v2026_07_28.UnsupportedProtocolVersionError",