Skip to content

Commit c73a27f

Browse files
committed
Fold result claims and notification bindings into ClientSession
Claims arrive keyed by their owning extension identifier; at adopt() the active set is computed for the negotiated version and the tools/call result adapter is rebuilt as a discriminated union (zero active claims keeps the module-level adapter, byte-identical parsing). The capability ad is built per version through the same association, so an extension whose claims are all inactive is not advertised — the ad and the claims dissolve together, on the initialize, discover-probe, and modern-stamp paths alike. call_tool gains allow_claimed: a claimed shape raises UnexpectedClaimedResult carrying the parsed value unless allowed. Notification bindings deliver through per-binding bounded FIFOs with one serialized consumer task each; enqueue never awaits, and overflow drops the oldest event with a warning. CORE_RESULT_TYPES derives from the ResultType literal in mcp-types, one source of truth for both the claim constructor and the session.
1 parent 9de9e44 commit c73a27f

7 files changed

Lines changed: 1051 additions & 22 deletions

File tree

‎src/mcp-types/mcp_types/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from mcp_types._types import (
99
CLIENT_CAPABILITIES_META_KEY,
1010
CLIENT_INFO_META_KEY,
11+
CORE_RESULT_TYPES,
1112
DEFAULT_NEGOTIATED_VERSION,
1213
LOG_LEVEL_META_KEY,
1314
PROTOCOL_VERSION_META_KEY,
@@ -231,6 +232,7 @@
231232
"CLIENT_CAPABILITIES_META_KEY",
232233
"LOG_LEVEL_META_KEY",
233234
# Type aliases and variables
235+
"CORE_RESULT_TYPES",
234236
"ContentBlock",
235237
"ElicitRequestedSchema",
236238
"ElicitRequestParams",

‎src/mcp-types/mcp_types/_types.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from __future__ import annotations
1010

11-
from typing import Annotated, Any, ClassVar, Final, Generic, Literal, TypeAlias, TypeVar
11+
from typing import Annotated, Any, ClassVar, Final, Generic, Literal, TypeAlias, TypeVar, get_args
1212

1313
from pydantic import (
1414
BaseModel,
@@ -150,14 +150,19 @@ class Notification(MCPModel, Generic[NotificationParamsT, MethodT]):
150150
params: NotificationParamsT
151151

152152

153-
ResultType = Literal["complete", "input_required"] | str
153+
_CoreResultType = Literal["complete", "input_required"]
154+
155+
ResultType = _CoreResultType | str
154156
"""Tags a `Result` so the client knows how to parse it (2026-07-28).
155157
156158
"complete" means the result is final; "input_required" means it is an
157159
`InputRequiredResult`. The union is open (the tasks extension reserves "task").
158160
Absent `resultType` is equivalent to "complete".
159161
"""
160162

163+
CORE_RESULT_TYPES: Final[frozenset[str]] = frozenset(get_args(_CoreResultType))
164+
"""The `resultType` tags owned by the core protocol vocabulary; extension claims may not re-key them."""
165+
161166

162167
class Result(MCPModel):
163168
"""Base class for JSON-RPC results.

‎src/mcp/client/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
ClientExtension,
1818
NotificationBinding,
1919
ResultClaim,
20+
UnexpectedClaimedResult,
2021
advertise,
2122
)
2223
from mcp.client.session import ClientSession
@@ -37,5 +38,6 @@
3738
"ResponseCacheStore",
3839
"ResultClaim",
3940
"Transport",
41+
"UnexpectedClaimedResult",
4042
"advertise",
4143
]

‎src/mcp/client/extension.py‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from dataclasses import dataclass
1515
from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, get_args
1616

17-
from mcp_types import CallToolResult, InputRequiredResult, Result
17+
from mcp_types import CORE_RESULT_TYPES, CallToolResult, InputRequiredResult, Result
1818
from mcp_types.version import MODERN_PROTOCOL_VERSIONS
1919
from pydantic import BaseModel
2020

@@ -28,6 +28,7 @@
2828
"ClientExtension",
2929
"NotificationBinding",
3030
"ResultClaim",
31+
"UnexpectedClaimedResult",
3132
"advertise",
3233
]
3334

@@ -81,7 +82,7 @@ class ResultClaim(Generic[ClaimedT]):
8182
protocol_versions: frozenset[str] | None = None
8283

8384
def __post_init__(self) -> None:
84-
if self.result_type in ("complete", "input_required"):
85+
if self.result_type in CORE_RESULT_TYPES:
8586
raise ValueError(f"resultType {self.result_type!r} is core protocol vocabulary")
8687
if issubclass(self.model, CallToolResult | InputRequiredResult):
8788
raise ValueError("claim models must not subclass core result types")
@@ -98,9 +99,30 @@ def __post_init__(self) -> None:
9899
)
99100

100101

102+
class UnexpectedClaimedResult(RuntimeError):
103+
"""A claimed (extension) result shape arrived on a `call_tool` that did not opt in.
104+
105+
Raised by `ClientSession.call_tool` when a claimed shape parses and
106+
`allow_claimed` is False. By the time this raises the server may have
107+
durably created state (e.g. a task) — the parsed value is carried as
108+
`result` so the caller can reach its id to clean up, not just read a
109+
message. To handle claimed shapes, pass the owning extension to
110+
`Client(extensions=[...])` (the transparent path) or call with
111+
`allow_claimed=True` and handle the shape yourself.
112+
"""
113+
114+
def __init__(self, result: Result) -> None:
115+
super().__init__(
116+
f"Server returned a claimed result ({type(result).__name__}); pass the owning extension to "
117+
"Client(extensions=[...]) for transparent resolution, or call with allow_claimed=True "
118+
"and handle the shape. The carried result may reference server-side state needing cleanup."
119+
)
120+
self.result = result
121+
122+
101123
@dataclass(frozen=True, kw_only=True)
102124
class NotificationBinding(Generic[NotifyParamsT]):
103-
"""Deliver server notifications for `method` to `handler` (today: silently dropped).
125+
"""Deliver server notifications for `method` to `handler` (unbound methods stay silently dropped).
104126
105127
Observation-only: the handler receives validated params, returns None, and
106128
cannot short-circuit anything. Delivery is per-binding serialized through a

0 commit comments

Comments
 (0)