Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions scripts/gen_surface_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,26 @@
}

# Classes the spec defines as open key-value bags: `_meta` content, the
# JSON-Schema-document fields on `Tool`, and the schemas with explicit
# `additionalProperties: {}`. These keep `extra="allow"` so the sieve preserves
# arbitrary keys; every other class ignores extras. Per-version because codegen
# JSON-Schema-document fields on `Tool`, the schemas with explicit
# `additionalProperties: {}`, and the capability objects whose descriptions say
# they are not a closed set. These keep `extra="allow"` so the sieve preserves
# arbitrary keys; every other class ignores extras. Capability names that are
# real fields on another schema era are removed by `serialize_server_result`
# (`tasks` at 2026-07-28, `extensions` before it). Per-version because codegen
# reuses class names across versions for unrelated schemas (e.g. `Data`).
OPEN_CLASSES: dict[str, frozenset[str]] = {
"2025-11-25": frozenset({"Meta", "InputSchema", "OutputSchema", "Result", "GetTaskPayloadResult", "Data"}),
"2025-11-25": frozenset(
{
"Meta",
"InputSchema",
"OutputSchema",
"Result",
"GetTaskPayloadResult",
"Data",
"ClientCapabilities",
"ServerCapabilities",
}
),
"2026-07-28": frozenset(
{
"MetaObject",
Expand All @@ -112,6 +126,8 @@
"InputSchema",
"OutputSchema",
"Result",
"ClientCapabilities",
"ServerCapabilities",
}
),
}
Expand Down
15 changes: 13 additions & 2 deletions src/mcp-types/mcp_types/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,10 +389,13 @@ class ClientTasksCapability(MCPModel):
class ClientCapabilities(MCPModel):
"""Capabilities a client may support.

Not a closed set: any client can define additional capabilities. Sent once in
Not a closed set: any client can define additional capabilities. Unknown keys
are retained. Nested known capability objects stay closed. Sent once in
`initialize` through 2025-11-25; per-request in `_meta` on 2026-07-28.
"""

model_config = ConfigDict(extra="allow")

experimental: dict[str, dict[str, Any]] | None = None
"""Experimental, non-standard capabilities that the client supports."""
sampling: SamplingCapability | None = None
Expand Down Expand Up @@ -483,7 +486,15 @@ class ServerTasksCapability(MCPModel):


class ServerCapabilities(MCPModel):
"""Capabilities that a server may support. Not a closed set."""
"""Capabilities that a server may support. Not a closed set.

Unknown keys are retained (a draft `events` object, or any other
self-hosted capability). Nested known capability objects stay closed.
Names that belong only to another protocol era are removed when a result
is serialized for that era.
"""

model_config = ConfigDict(extra="allow")

experimental: dict[str, dict[str, Any]] | None = None
"""Experimental, non-standard capabilities that the server supports."""
Expand Down
4 changes: 2 additions & 2 deletions src/mcp-types/mcp_types/_v2025_11_25/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class ClientCapabilities(WireModel):
"""

model_config = ConfigDict(
extra="ignore",
extra="allow",
)
elicitation: Elicitation | None = None
"""
Expand Down Expand Up @@ -1102,7 +1102,7 @@ class ServerCapabilities(WireModel):
"""

model_config = ConfigDict(
extra="ignore",
extra="allow",
)
completions: dict[str, Any] | None = None
"""
Expand Down
4 changes: 2 additions & 2 deletions src/mcp-types/mcp_types/_v2026_07_28/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2041,7 +2041,7 @@ class ServerCapabilities(WireModel):
"""

model_config = ConfigDict(
extra="ignore",
extra="allow",
)
completions: JSONObject | None = None
"""
Expand Down Expand Up @@ -2287,7 +2287,7 @@ class ClientCapabilities(WireModel):
"""

model_config = ConfigDict(
extra="ignore",
extra="allow",
)
elicitation: Elicitation | None = None
"""
Expand Down
50 changes: 47 additions & 3 deletions src/mcp-types/mcp_types/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,47 @@ def parse_server_notification(
return _monolith_row(monolith, method).model_validate(_body(method, params), by_name=False)


def _wire_names(model: type[BaseModel]) -> frozenset[str]:
"""Wire keys of `model`'s declared fields (alias when one is set)."""
return frozenset((field.alias or field.serialization_alias or name) for name, field in model.model_fields.items())


# Pre-2026 versions share the 2025-11-25 surface (see the module docstring).
_SERVER_CAPABILITY_MODEL: Final[Mapping[str, type[BaseModel]]] = MappingProxyType(
{"2025-11-25": v2025.ServerCapabilities, "2026-07-28": v2026.ServerCapabilities}
)
_ALL_SERVER_CAPABILITY_NAMES: Final[frozenset[str]] = _wire_names(v2025.ServerCapabilities) | _wire_names(
v2026.ServerCapabilities
)
# Results whose top-level `capabilities` object is `ServerCapabilities`.
_CAPABILITY_RESULT_METHODS: Final[frozenset[str]] = frozenset({"initialize", "server/discover"})


def _capability_era(version: str) -> str:
"""Schema era for capability shape. Every pre-2026 version uses 2025-11-25."""
return "2026-07-28" if version >= "2026-07-28" else "2025-11-25"


def _drop_cross_era_server_capability_keys(method: str, version: str, data: dict[str, Any]) -> None:
"""Drop capability names declared only on another schema era.

`ServerCapabilities` is an open set, so the wire model keeps unknown keys.
A name that is a real field on the other era is still version vocabulary and
must not leak: `tasks` on 2026-07-28, `extensions` on every earlier version.
Truly unknown keys, such as a draft `events` object, stay. Known fields were
already validated by the surface model; this only removes the cross-era names.
"""
if method not in _CAPABILITY_RESULT_METHODS:
return
capabilities = data.get("capabilities")
if not isinstance(capabilities, dict):
return
era_names = _wire_names(_SERVER_CAPABILITY_MODEL[_capability_era(version)])
owned = cast("dict[str, Any]", capabilities)
for name in _ALL_SERVER_CAPABILITY_NAMES - era_names:
owned.pop(name, None)


def serialize_server_result(
method: str,
version: str,
Expand All @@ -640,8 +681,9 @@ def serialize_server_result(
) -> dict[str, Any]:
"""Validate `data` against `surface` and return its surface-shaped dump.

The surface model carries `extra="ignore"`, so fields not in `version`'s
schema are dropped from the returned dict.
Closed surface models carry `extra="ignore"`, so fields not in `version`'s
schema are dropped. `ServerCapabilities` is an open set: unknown keys are
kept, and names that belong only to another schema era are removed.

Raises:
ValueError: `version` is not a known protocol version.
Expand All @@ -650,9 +692,11 @@ def serialize_server_result(
"""
_check_known_version(version)
adapter = _adapter(surface[(method, version)])
return adapter.dump_python(
dumped = adapter.dump_python(
adapter.validate_python(data, by_name=False), by_alias=True, mode="json", exclude_none=True
)
_drop_cross_era_server_capability_keys(method, version, dumped)
return dumped


def validate_server_result(
Expand Down
Loading
Loading