Protocol types for 2026-07-28: superset monolith, committed per-version packages, and wire-method maps (#2849)

This commit is contained in:
Max 2026-06-16 17:40:14 +01:00 • committed by GitHub
commit 65be5a7147
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 19076 additions and 371 deletions

View file

@ -30,6 +30,11 @@ jobs:
env:
SKIP: no-commit-to-branch,readme-v1-frozen
- name: Surface types match vendored schema
run: |
uv sync --group codegen --frozen
uv run --frozen --group codegen python scripts/gen_surface_types.py --check
# TODO(Max): Drop this in v2. Deliberate updates (e.g. the v2 status
# banner) go through the 'override-readme-freeze' label.
- name: Check README.md is not modified

View file

@ -221,6 +221,14 @@ Common renames:
Because `populate_by_name=True` is set, the old camelCase names still work as constructor kwargs (e.g., `Tool(inputSchema={...})` is accepted), but attribute access must use snake_case (`tool.input_schema`).
### Server handler results are validated against the protocol schema
Results returned from server handlers are now validated against the negotiated protocol version's schema before being sent. A result that does not conform raises on the server side and the client receives an `INTERNAL_ERROR` response. The case most existing code will hit is `Tool.inputSchema`: the spec requires it to contain `"type": "object"`, so an empty `{}` is now rejected.
### Client validates inbound traffic against the protocol schema
`ClientSession` now validates server requests, notifications, and results against the negotiated protocol version's schema before parsing them into `mcp.types` models. Spec-invalid server output that the previous monolith parse tolerated may now raise `pydantic.ValidationError` from `list_tools()`, `call_tool()`, and similar calls. `_meta` remains the sanctioned place for result extras (and `experimental` for capability extras).
### `args` parameter removed from `ClientSessionGroup.call_tool()`
The deprecated `args` parameter has been removed from `ClientSessionGroup.call_tool()`. Use `arguments` instead.
@ -531,6 +539,10 @@ await ctx.log(level="info", data="hello")
Positional calls (`await ctx.info("hello")`) are unaffected.
### `Context.elicit()` schema gate validates the rendered schema
`Context.elicit()` (and `elicit_with_validation()`) now render the schema first and validate each property against the spec's `PrimitiveSchemaDefinition`, raising `TypeError` at the call site for anything outside it. `Optional[T]` fields render as `{"type": ...}` with the field omitted from `required` (previously the non-spec `anyOf` shape). A bare `list[str]` field is rejected because it renders without the required enum items; use `list[Literal[...]]` or `list[str]` with `json_schema_extra` supplying the items. Unions of multiple primitives (e.g. `int | str`) and nested models are rejected.
### Replace `RootModel` by union types with `TypeAdapter` validation
The following union types are no longer `RootModel` subclasses:
@ -939,7 +951,7 @@ from mcp.types import (
)
async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
return ListToolsResult(tools=[Tool(name="my_tool", description="A tool", input_schema={})])
return ListToolsResult(tools=[Tool(name="my_tool", description="A tool", input_schema={"type": "object"})])
async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
@ -1181,7 +1193,7 @@ Behavior changes:
### Experimental Tasks support removed
Tasks (SEP-1686) have been removed from the MCP specification and are no longer part of this SDK. The `mcp.client.experimental`, `mcp.server.experimental`, `mcp.shared.experimental`, and `mcp.server.lowlevel.experimental` modules have been removed, along with all `Task*` types, the `tasks` capability fields, `Tool.execution`, and the `experimental` properties on `ClientSession`, `ServerSession`, `Server`, and `ServerRequestContext`.
Tasks (SEP-1686) have been removed from the MCP specification and are no longer part of this SDK. The `mcp.client.experimental`, `mcp.server.experimental`, `mcp.shared.experimental`, and `mcp.server.lowlevel.experimental` modules have been removed, along with the `experimental` properties on `ClientSession`, `ServerSession`, `Server`, and `ServerRequestContext`. The corresponding `Task*` types remain in `mcp.types` as types-only definitions.
Tasks are expected to return as a separate MCP extension in a future release.
@ -1227,6 +1239,10 @@ If you relied on extra fields round-tripping through MCP types, move that data i
## New Features
### 2025-11-25 and 2026-07-28 protocol fields modeled
`mcp.types` models the 2025-11-25 and 2026-07-28 protocol fields (e.g. `resultType`, `ttlMs`/`cacheScope` on cacheable results, `inputResponses`/`requestState` on retried requests), so inbound payloads carrying these keys parse into typed fields and round-trip. `ttlMs`/`cacheScope` default to `None`; `resultType` defaults to `"complete"` on concrete results (`None` on `EmptyResult`); the server strips all of them from the wire at pre-2026 versions.
### `streamable_http_app()` available on lowlevel Server
The `streamable_http_app()` method is now available directly on the lowlevel `Server` class, not just `MCPServer`. This allows using the streamable HTTP transport without the MCPServer wrapper.

View file

@ -104,6 +104,7 @@ docs = [
"mkdocs-material[imaging]>=9.5.45",
"mkdocstrings-python>=2.0.1",
]
codegen = ["datamodel-code-generator==0.57.0"]
[build-system]
requires = ["hatchling", "uv-dynamic-versioning"]
@ -177,6 +178,8 @@ max-complexity = 24 # Default is 10
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
# Generated by scripts/gen_surface_types.py: raw datamodel-codegen output (TID251 lifts the repo-wide RootModel ban for these generated validators).
"src/mcp/types/v*/__init__.py" = ["D212", "E501", "I001", "TID251", "UP007", "UP037"]
"tests/server/mcpserver/test_func_metadata.py" = ["E501"]
"tests/shared/test_progress_notifications.py" = ["PLW0603"]

4057
schema/2025-11-25.json Normal file

File diff suppressed because it is too large Load diff

3873
schema/2026-07-28.json Normal file

File diff suppressed because it is too large Load diff

14
schema/PINNED.json Normal file
View file

@ -0,0 +1,14 @@
[
{
"protocol_version": "2025-11-25",
"source_path_in_spec_repo": "schema/2025-11-25/schema.json",
"spec_commit": "6d441518de8a9d5adbab0b10a76a667a63f90665",
"sha256": "4e01628360a2149892eab8f298ceee626d24a58862184eb8ec85d95b8f353e31"
},
{
"protocol_version": "2026-07-28",
"source_path_in_spec_repo": "schema/draft/schema.json",
"spec_commit": "6d441518de8a9d5adbab0b10a76a667a63f90665",
"sha256": "bce2e7c9622bb0b449475ba6d8d80228c71190a09250e75dabd502b280ecf3cb"
}
]

12
schema/README.md Normal file
View file

@ -0,0 +1,12 @@
# Vendored protocol schemas
JSON Schema files for each protocol version the SDK has a wire-shape surface
package for, vendored from the [spec repository] at the commit recorded in
`PINNED.json`. `scripts/gen_surface_types.py` reads these to regenerate
`src/mcp/types/v<version>/__init__.py`; CI runs the generator with `--check`.
To bump: drop the new `schema.json` here as `<protocol-version>.json`, update
the matching entry in `PINNED.json` (commit + sha256), and run
`uv run --frozen --group codegen python scripts/gen_surface_types.py`.
[spec repository]: https://github.com/modelcontextprotocol/modelcontextprotocol

View file

@ -0,0 +1,252 @@
"""Regenerate the per-version wire-shape surface packages from vendored schemas.
Runs `datamodel-code-generator` over each `schema/PINNED.json` entry and
writes the result to `src/mcp/types/v<version>/__init__.py` with only the
fixes the raw output needs: a small JSON pre-patch for the known
`number`-as-`integer` schema.json defect, a header, and per-version
epilogue aliases. Run with `uv run --frozen --group codegen python scripts/gen_surface_types.py [--check]`.
"""
from __future__ import annotations
import argparse
import difflib
import hashlib
import json
import re
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parent.parent
SCHEMA_DIR = REPO_ROOT / "schema"
TYPES_DIR = REPO_ROOT / "src" / "mcp" / "types"
# schema.ts -> schema.json renders TypeScript `number` as JSON Schema
# `integer` at these sites; patch the JSON before codegen so floats validate.
# Patched to `["integer", "number"]` (not bare `"number"`) so codegen emits
# `int | float` and pydantic's smart-union preserves ints on round-trip.
# TODO: drop once modelcontextprotocol/modelcontextprotocol fixes the schema.ts -> schema.json number rendering.
SCHEMA_PATCHES: dict[str, list[tuple[str, Any, Any]]] = {
"2025-11-25": [
("$defs/NumberSchema/properties/default/type", "integer", ["integer", "number"]),
("$defs/NumberSchema/properties/maximum/type", "integer", ["integer", "number"]),
("$defs/NumberSchema/properties/minimum/type", "integer", ["integer", "number"]),
# `null` arm is monolith superset leniency: hosts may answer optional form fields with null.
(
"$defs/ElicitResult/properties/content/additionalProperties/anyOf/1/type",
["string", "integer", "boolean"],
["string", "integer", "number", "boolean", "null"],
),
# Older python-sdk releases emit `anyOf` for Optional fields; the callback's
# own schema validation is the real gate, so accept any property shape inbound.
# PrimitiveSchemaDefinition becomes an orphan $def after this patch but
# datamodel-codegen still emits it; elicitation.py imports it as the gate type.
(
"$defs/ElicitRequestFormParams/properties/requestedSchema/properties/properties/additionalProperties",
{"$ref": "#/$defs/PrimitiveSchemaDefinition"},
{},
),
],
"2026-07-28": [
("$defs/NumberSchema/properties/default/type", "number", ["integer", "number"]),
("$defs/NumberSchema/properties/maximum/type", "number", ["integer", "number"]),
("$defs/NumberSchema/properties/minimum/type", "number", ["integer", "number"]),
# `null` arm is monolith superset leniency: hosts may answer optional form fields with null.
(
"$defs/ElicitResult/properties/content/additionalProperties/anyOf/1/type",
["string", "integer", "boolean"],
["string", "integer", "number", "boolean", "null"],
),
# Spec `JSONValue` includes `number` and `null`; the ts->json render dropped both.
(
"$defs/JSONValue/anyOf/2/type",
["string", "integer", "boolean"],
["string", "integer", "number", "boolean", "null"],
),
# Older python-sdk releases emit `anyOf` for Optional fields; the callback's
# own schema validation is the real gate, so accept any property shape inbound.
(
"$defs/ElicitRequestFormParams/properties/requestedSchema/properties/properties/additionalProperties",
{"$ref": "#/$defs/PrimitiveSchemaDefinition"},
{},
),
],
}
# 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
# 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"}),
"2026-07-28": frozenset({"MetaObject", "RequestMetaObject", "InputSchema", "OutputSchema", "Result"}),
}
# Hand-written union aliases the wire-method maps reference by value; the schema
# has no named definition for "everything tools/call may return", so name it here.
EPILOGUES: dict[str, str] = {
"2026-07-28": (
"AnyCallToolResult = CallToolResult | InputRequiredResult\n"
"AnyGetPromptResult = GetPromptResult | InputRequiredResult\n"
"AnyReadResourceResult = ReadResourceResult | InputRequiredResult\n"
),
}
HEADER = (
'"""Internal wire-shape models for protocol {version}. Generated; do not edit.\n'
"\n"
"Regenerate with `scripts/gen_surface_types.py` from `schema/{version}.json`\n"
'(sha256 `{sha}`)."""\n'
"# pyright: reportIncompatibleVariableOverride=false, reportGeneralTypeIssues=false\n"
)
def load_pinned() -> list[dict[str, str]]:
"""Read `schema/PINNED.json` and verify each vendored file's sha256."""
entries: list[dict[str, str]] = json.loads((SCHEMA_DIR / "PINNED.json").read_text())
for entry in entries:
path = SCHEMA_DIR / f"{entry['protocol_version']}.json"
actual = hashlib.sha256(path.read_bytes()).hexdigest()
if actual != entry["sha256"]:
raise SystemExit(f"sha256 mismatch for {path.name}: PINNED={entry['sha256']} disk={actual}")
return entries
def patch_schema(schema: dict[str, Any], patches: list[tuple[str, Any, Any]]) -> None:
"""Apply `(path, old, new)` JSON-pointer-ish patches in place, asserting the old value."""
for path, old, new in patches:
*parts, leaf = path.split("/")
node: Any = schema
for part in parts:
node = node[int(part) if part.isdigit() else part]
if node[leaf] != old:
raise SystemExit(f"schema patch {path}: expected {old!r}, found {node[leaf]!r}")
node[leaf] = new
def run_codegen(schema_path: Path, output_path: Path) -> None:
"""Run datamodel-code-generator at the version pinned in the `codegen` dependency group."""
# fmt: off
result = subprocess.run(
[
"uv", "run", "--frozen", "--group", "codegen", "datamodel-codegen",
"--input", str(schema_path),
"--input-file-type", "jsonschema",
"--output", str(output_path),
"--output-model-type", "pydantic_v2.BaseModel",
"--target-python-version", "3.10",
"--base-class", "mcp.types._wire_base.WireModel",
"--snake-case-field", "--remove-special-field-name-prefix",
"--use-annotated", "--use-field-description", "--use-schema-description",
"--enum-field-as-literal", "all",
"--use-union-operator", "--use-double-quotes",
"--extra-fields", "ignore",
# JSON Schema `format` is annotation-only; codegen's defaults
# (Base64Str, AnyUrl) over-assert and reject valid wire data.
"--type-mappings", "byte=string", "uri=string", "uri-template=string",
"--disable-timestamp",
],
capture_output=True, text=True,
)
# fmt: on
if result.returncode != 0:
raise SystemExit(f"datamodel-codegen failed:\n{result.stderr}")
def allow_open_class_extras(source: str, open_classes: frozenset[str]) -> str:
"""Restore `extra="allow"` on `open_classes` only.
Every other class uses `extra="ignore"` so the surface acts as a sieve;
`open_classes` are the places the spec defines as open key-value bags.
"""
def patch(match: re.Match[str]) -> str:
if match.group(1) not in open_classes:
return match.group(0)
return match.group(0).replace('extra="ignore"', 'extra="allow"')
source = re.sub(
r'^class (\w+)\(WireModel\):\n(?: {4}.*\n|\n)*? {4}model_config = ConfigDict\(\n {8}extra="ignore",\n {4}\)\n',
patch,
source,
flags=re.MULTILINE,
)
# Drift guard: substitution count must match the allow-list.
assert source.count('extra="allow"') == len(open_classes), (source.count('extra="allow"'), open_classes)
return source
def build(entry: dict[str, str]) -> str:
"""Generate, post-process, and format one version's surface module text."""
version = entry["protocol_version"]
schema = json.loads((SCHEMA_DIR / f"{version}.json").read_text())
patch_schema(schema, SCHEMA_PATCHES.get(version, []))
with tempfile.TemporaryDirectory() as tmp:
patched = Path(tmp) / "schema.json"
patched.write_text(json.dumps(schema))
raw = Path(tmp) / "raw.py"
run_codegen(patched, raw)
source = raw.read_text()
source = re.sub(r"\A# generated by datamodel-codegen:\n#[^\n]*\n", "", source)
source = re.sub(r"^class Model\(RootModel\[Any\]\):\n {4}root: Any\n+", "", source, count=1, flags=re.MULTILINE)
# Codegen appends `| None` to forward refs of nullable models, which is a
# runtime TypeError on a string ref and redundant since `JSONValue` includes None.
source = source.replace('"JSONValue" | None', '"JSONValue"')
source = allow_open_class_extras(source, OPEN_CLASSES[version])
if epilogue := EPILOGUES.get(version, ""):
# Insert before the trailing model_rebuild() block: pyright's evaluation
# order for the recursive RootModel block is sensitive to placement.
match = re.search(r"^\w+\.model_rebuild\(\)$", source, flags=re.MULTILINE)
cut = match.start() if match else len(source)
source = f"{source[:cut]}{epilogue}\n\n{source[cut:]}"
source = HEADER.format(version=version, sha=entry["sha256"]) + source
staging = TYPES_DIR / f"_staging_{version}.py"
try:
staging.write_text(source)
subprocess.run(
["uv", "run", "--frozen", "ruff", "format", "--no-cache", str(staging)],
cwd=REPO_ROOT, capture_output=True, check=True,
) # fmt: skip
return staging.read_text()
finally:
staging.unlink(missing_ok=True)
def main(argv: list[str] | None = None) -> int:
"""CLI entry point: write each surface package, or diff under `--check`."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true", help="diff regenerated output against committed files")
args = parser.parse_args(argv)
drift = False
for entry in load_pinned():
target = TYPES_DIR / ("v" + entry["protocol_version"].replace("-", "_")) / "__init__.py"
candidate = build(entry)
if not args.check:
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(candidate)
print(f"{entry['protocol_version']}: wrote {target.relative_to(REPO_ROOT)} ({len(candidate)} bytes)")
continue
committed = target.read_text() if target.is_file() else ""
if committed != candidate:
drift = True
sys.stderr.writelines(
difflib.unified_diff(
committed.splitlines(keepends=True),
candidate.splitlines(keepends=True),
fromfile=str(target.relative_to(REPO_ROOT)),
tofile="<regenerated>",
)
)
return 1 if drift else 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -4,7 +4,7 @@ import logging
from collections.abc import Mapping
from dataclasses import dataclass
from types import TracebackType
from typing import Any, Protocol, cast, get_args
from typing import Any, Protocol, cast
import anyio
import anyio.abc
@ -22,7 +22,8 @@ from mcp.shared.message import ClientMessageMetadata, SessionMessage
from mcp.shared.session import RequestResponder
from mcp.shared.transport_context import TransportContext
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
from mcp.types import RequestId, RequestParamsMeta
from mcp.types import INTERNAL_ERROR, METHOD_NOT_FOUND, RequestId, RequestParamsMeta
from mcp.types import methods as _methods
DEFAULT_CLIENT_INFO = types.Implementation(name="mcp", version="0.1.0")
@ -116,14 +117,6 @@ async def _default_logging_callback(
ClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter(types.ClientResult | types.ErrorData)
_SERVER_REQUEST_METHODS: frozenset[str] = frozenset(
cast(type[BaseModel], arm).model_fields["method"].default for arm in get_args(types.ServerRequest)
)
"""Method names in the SDK's `ServerRequest` union, derived from the
discriminator literal on each arm. Requests for any other method — including
spec methods this SDK deliberately doesn't model, like `tasks/*` — are
answered with METHOD_NOT_FOUND instead of failing union validation."""
class ClientSession:
"""Client half of an MCP connection, running on a `Dispatcher`.
@ -244,6 +237,12 @@ class ClientSession:
# The spec forbids cancelling initialize.
opts["cancel_on_abandon"] = False
raw = await self._dispatcher.send_raw_request(method, data.get("params"), opts)
# Literal fallback covers pre-handshake and stateless; matches runner.py.
version = self.protocol_version or "2025-11-25"
try:
_methods.validate_server_result(method, version, raw)
except KeyError:
pass
return result_type.model_validate(raw, by_name=False)
async def send_notification(self, notification: types.ClientNotification) -> None:
@ -308,6 +307,11 @@ class ClientSession:
"""
return self._initialize_result
@property
def protocol_version(self) -> str | None:
"""The negotiated protocol version. None until `initialize()` has completed."""
return self._initialize_result.protocol_version if self._initialize_result else None
async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
"""Send a ping request."""
return await self.send_request(types.PingRequest(params=types.RequestParams(_meta=meta)), types.EmptyResult)
@ -506,12 +510,14 @@ class ClientSession:
self, dctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None
) -> dict[str, Any]:
"""Answer a server-initiated request via the registered callbacks."""
if method not in _SERVER_REQUEST_METHODS:
raise MCPError(code=types.METHOD_NOT_FOUND, message="Method not found", data=method)
payload: dict[str, Any] = {"method": method}
if params is not None:
payload["params"] = dict(params)
request = types.server_request_adapter.validate_python(payload, by_name=False)
# Literal, not LATEST_PROTOCOL_VERSION: the fallback covers the initialize
# handshake (which only exists at <=2025) and stateless until the header
# is plumbed; its meaning is fixed regardless of LATEST bumps.
version = self.protocol_version or "2025-11-25"
try:
request = cast(types.ServerRequest, _methods.parse_server_request(method, version, params))
except KeyError:
raise MCPError(code=METHOD_NOT_FOUND, message="Method not found", data=method) from None
response: types.ClientResult | types.ErrorData
if isinstance(request, types.PingRequest):
@ -532,19 +538,27 @@ class ClientSession:
client_response = ClientResponse.validate_python(response)
if isinstance(client_response, types.ErrorData):
raise MCPError.from_error_data(client_response)
return client_response.model_dump(by_alias=True, mode="json", exclude_none=True)
dumped = client_response.model_dump(by_alias=True, mode="json", exclude_none=True)
try:
_methods.validate_client_result(method, version, dumped)
except ValidationError:
logger.exception("client callback for %r returned an invalid result", method)
raise MCPError(code=INTERNAL_ERROR, message="Client callback returned an invalid result") from None
return dumped
async def _on_notify(
self, dctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None
) -> None:
"""Route a server notification: validate, run the typed callback, tee to message_handler."""
payload: dict[str, Any] = {"method": method}
if params is not None:
payload["params"] = dict(params)
# Same fallback as `_on_request`: covers pre-handshake and stateless.
version = self.protocol_version or "2025-11-25"
try:
notification = types.server_notification_adapter.validate_python(payload, by_name=False)
notification = cast(types.ServerNotification, _methods.parse_server_notification(method, version, params))
except KeyError:
logger.debug("dropped %r: not defined at %s", method, version)
return
except ValidationError:
logger.warning("Failed to validate notification: %s", payload, exc_info=True)
logger.warning("Failed to validate notification: %s", method, exc_info=True)
return
if isinstance(notification, types.CancelledNotification):
# The dispatcher already applied the cancellation; not surfaced to message_handler.

View file

@ -38,6 +38,7 @@ from mcp.types import (
PingRequest,
Request,
)
from mcp.types import methods as _methods
__all__ = ["Connection"]
@ -175,6 +176,13 @@ class Connection:
KeyError: `result_type` omitted for a non-spec request type.
"""
raw = await self.send_raw_request(req.method, dump_params(req.params), opts)
# Literal fallback covers pre-handshake and stateless; matches runner.py.
version = self.protocol_version or "2025-11-25"
if req.method in _methods.MONOLITH_REQUESTS:
try:
_methods.validate_client_result(req.method, version, raw)
except KeyError:
pass
cls = result_type if result_type is not None else _RESULT_FOR[type(req)]
return cls.model_validate(raw, by_name=False)

View file

@ -2,15 +2,18 @@
from __future__ import annotations
import types
from collections.abc import Sequence
from typing import Generic, Literal, TypeVar, Union, get_args, get_origin
from typing import Any, Generic, Literal, TypeVar
from pydantic import BaseModel
from pydantic import BaseModel, ValidationError
from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
from pydantic_core import core_schema
from mcp.server.session import ServerSession
from mcp.types import RequestId
# Internal surface package; imported as the gate's source of truth for spec-valid property schemas.
from mcp.types.v2025_11_25 import PrimitiveSchemaDefinition
ElicitSchemaModelT = TypeVar("ElicitSchemaModelT", bound=BaseModel)
@ -45,61 +48,38 @@ class AcceptedUrlElicitation(BaseModel):
UrlElicitationResult = AcceptedUrlElicitation | DeclinedElicitation | CancelledElicitation
# Primitive types allowed in elicitation schemas
_ELICITATION_PRIMITIVE_TYPES = (str, int, float, bool)
class _ElicitationJsonSchema(GenerateJsonSchema):
"""JSON-Schema generator that flattens `T | None` to `T` and drops `None` defaults.
The spec's `PrimitiveSchemaDefinition` admits no `anyOf` or null type; an
optional field is expressed by leaving it out of `required`, which pydantic
already does for any field with a default.
"""
def nullable_schema(self, schema: core_schema.NullableSchema) -> JsonSchemaValue:
return self.generate_inner(schema["schema"])
def default_schema(self, schema: core_schema.WithDefaultSchema) -> JsonSchemaValue:
result = super().default_schema(schema)
if result.get("default") is None:
result.pop("default", None)
return result
def _validate_elicitation_schema(schema: type[BaseModel]) -> None:
"""Validate that a Pydantic model only contains primitive field types."""
for field_name, field_info in schema.model_fields.items():
annotation = field_info.annotation
def _validate_rendered_properties(json_schema: dict[str, Any]) -> None:
"""Reject any `properties` entry the spec's `PrimitiveSchemaDefinition` won't accept.
if annotation is None or annotation is types.NoneType: # pragma: no cover
continue
elif _is_primitive_field(annotation):
continue
elif _is_string_sequence(annotation):
continue
else:
raise TypeError(
f"Elicitation schema field '{field_name}' must be a primitive type "
f"{_ELICITATION_PRIMITIVE_TYPES}, a sequence of strings (list[str], etc.), "
f"or Optional of these types. Nested models and complex types are not allowed."
)
def _is_string_sequence(annotation: type) -> bool:
"""Check if annotation is a sequence of strings (list[str], Sequence[str], etc)."""
origin = get_origin(annotation)
# Check if it's a sequence-like type with str elements
if origin:
Catches whatever the renderer let through that isn't spec-valid: bare
`list[str]` (no enum), multi-primitive unions, nested models.
"""
for field_name, prop in json_schema.get("properties", {}).items():
try:
if issubclass(origin, Sequence):
args = get_args(annotation)
# Should have single str type arg
return len(args) == 1 and args[0] is str
except TypeError: # pragma: no cover
# origin is not a class, so it can't be a subclass of Sequence
pass
return False
def _is_primitive_field(annotation: type) -> bool:
"""Check if a field is a primitive type allowed in elicitation schemas."""
# Handle basic primitive types
if annotation in _ELICITATION_PRIMITIVE_TYPES:
return True
# Handle Union types
origin = get_origin(annotation)
if origin is Union or origin is types.UnionType:
args = get_args(annotation)
# All args must be primitive types, None, or string sequences
return all(
arg is types.NoneType or arg in _ELICITATION_PRIMITIVE_TYPES or _is_string_sequence(arg) for arg in args
)
return False
PrimitiveSchemaDefinition.model_validate(prop)
except ValidationError:
raise TypeError(
f"Elicitation schema field {field_name!r} rendered as {prop!r}, "
f"which is not a valid PrimitiveSchemaDefinition"
) from None
async def elicit_with_validation(
@ -118,10 +98,8 @@ async def elicit_with_validation(
For sensitive data like credentials or OAuth flows, use elicit_url() instead.
"""
# Validate that schema only contains primitive types and fail loudly if not
_validate_elicitation_schema(schema)
json_schema = schema.model_json_schema()
json_schema = schema.model_json_schema(schema_generator=_ElicitationJsonSchema)
_validate_rendered_properties(json_schema)
result = await session.elicit_form(
message=message,

View file

@ -19,7 +19,7 @@ import logging
from collections.abc import Mapping
from dataclasses import dataclass, field
from functools import partial, reduce
from typing import TYPE_CHECKING, Any, Generic, cast, get_args
from typing import TYPE_CHECKING, Any, Generic, cast
import anyio.abc
from opentelemetry.trace import SpanKind, StatusCode
@ -38,19 +38,18 @@ from mcp.shared.message import ServerMessageMetadata
from mcp.shared.transport_context import TransportContext
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
from mcp.types import (
INTERNAL_ERROR,
INVALID_PARAMS,
LATEST_PROTOCOL_VERSION,
METHOD_NOT_FOUND,
ClientRequest,
ErrorData,
Implementation,
InitializeRequestParams,
InitializeResult,
NotificationParams,
RequestParams,
RequestParamsMeta,
client_request_adapter,
)
from mcp.types import methods as _methods
if TYPE_CHECKING:
from mcp.server.lowlevel.server import Server
@ -80,14 +79,6 @@ def _extract_meta(params: Mapping[str, Any] | None) -> RequestParamsMeta | None:
return None
_SPEC_CLIENT_METHODS: frozenset[str] = frozenset(
cast(type[BaseModel], arm).model_fields["method"].default for arm in get_args(ClientRequest)
)
"""Method names in the spec `ClientRequest` union, derived from the
discriminator literal on each arm. Used to gate upfront validation so custom
methods registered via `add_request_handler` are not rejected."""
def otel_middleware(next_on_request: OnRequest) -> OnRequest:
"""Dispatch-tier middleware that wraps each request in an OpenTelemetry span.
@ -228,16 +219,22 @@ class ServerRunner(Generic[LifespanT]):
params: Mapping[str, Any] | None,
) -> dict[str, Any]:
ctx = self._make_context(dctx, _extract_meta(params))
# Literal, not LATEST_PROTOCOL_VERSION: the fallback covers the initialize
# handshake (which only exists at <=2025) and stateless until the header
# is plumbed; its meaning is fixed regardless of LATEST bumps.
version = self.connection.protocol_version or "2025-11-25"
is_spec_method = method in _methods.SPEC_CLIENT_METHODS
async def _inner() -> HandlerResult:
# TODO(maxisbey): pinned compat: spec methods are validated against
# the ClientRequest union before lookup, so malformed params are
# INVALID_PARAMS even with no handler registered.
if method in _SPEC_CLIENT_METHODS:
payload: dict[str, Any] = {"method": method}
if params is not None:
payload["params"] = dict(params)
client_request_adapter.validate_python(payload, by_name=False)
# Pinned compat: spec methods are surface-validated before lookup,
# so malformed params are INVALID_PARAMS even with no handler
# registered. Custom methods miss the monolith map and fall through
# to `entry.params_type` exactly as before.
if is_spec_method:
try:
_methods.validate_client_request(method, version, params)
except KeyError:
raise MCPError(code=METHOD_NOT_FOUND, message="Method not found", data=method) from None
# TODO(maxisbey): the 2026-07-28 spec drops the handshake; this branch and
# the gate become a per-version legacy path then. Initialize runs inline
# (read loop parked), so awaiting the peer anywhere on this path deadlocks.
@ -265,6 +262,21 @@ class ServerRunner(Generic[LifespanT]):
call = self._compose_server_middleware(ctx, method, params, _inner)
result = _dump_result(await call())
# TODO: reject resultType values outside {"complete", "input_required"} unless the
# corresponding extension is in this request's _meta clientCapabilities.extensions; the
# explicit MUST-reject is client-side (basic/index.mdx ResultType), this enforces it proactively.
if is_spec_method:
try:
result = _methods.serialize_server_result(method, version, result)
except KeyError:
# Middleware short-circuited a wrong-version spec method without
# calling `call_next`; it owns the result shape.
pass
except ValidationError:
# Server bug, not client fault. Detail stays in the server log:
# pydantic messages echo the result body.
logger.exception("handler for %r returned an invalid result", method)
raise MCPError(code=INTERNAL_ERROR, message="Handler returned an invalid result") from None
if method == "initialize":
# Commit only on chain success, so a middleware veto leaves no state.
# Race-free: the read loop is parked until this call returns.
@ -278,18 +290,23 @@ class ServerRunner(Generic[LifespanT]):
params: Mapping[str, Any] | None,
) -> None:
ctx = self._make_context(dctx, _extract_meta(params))
# Same fallback as `_on_request`: covers pre-handshake and stateless.
version = self.connection.protocol_version or "2025-11-25"
async def _inner() -> None:
if method in _methods.SPEC_CLIENT_NOTIFICATION_METHODS:
try:
_methods.validate_client_notification(method, version, params)
except KeyError:
logger.debug("dropped %r: not defined at %s", method, version)
return
except ValidationError:
logger.warning("dropped %r: malformed params", method)
return
if method == "notifications/initialized":
# Validate before committing so a malformed notification leaves
# state untouched; then fall through so a registered handler
# observes an initialized connection.
if params is not None:
try:
NotificationParams.model_validate(params, by_name=False)
except ValidationError:
logger.warning("dropped %r: malformed params", method)
return
# Surface validation above already rejected a malformed body, so
# commit; fall through so a registered handler observes an
# initialized connection.
self.connection.initialized.set()
elif not self.connection.initialize_accepted:
logger.debug("dropped %s: received before initialization", method)

View file

@ -20,6 +20,7 @@ from mcp.shared.dispatcher import CallOptions, ProgressFnT
from mcp.shared.exceptions import NoBackChannelError, StatelessModeNotSupported
from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher
from mcp.shared.message import ServerMessageMetadata
from mcp.types import methods as _methods
__all__ = ["ServerSession"]
@ -96,6 +97,12 @@ class ServerSession:
result = await self._dispatcher.send_raw_request(
data["method"], data.get("params"), opts or None, _related_request_id=related
)
# Literal fallback covers pre-handshake and stateless; matches runner.py.
version = self.protocol_version or "2025-11-25"
try:
_methods.validate_client_result(request.method, version, result)
except KeyError:
pass
return result_type.model_validate(result, by_name=False)
async def send_notification(

View file

@ -16,6 +16,7 @@ KNOWN_PROTOCOL_VERSIONS: Final[tuple[str, ...]] = (
"2025-03-26",
"2025-06-18",
"2025-11-25",
"2026-07-28",
)
"""Every released protocol revision, oldest to newest."""

View file

@ -1,26 +1,36 @@
"""This module defines the types for the MCP protocol.
Check the latest schema at:
https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.json
https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.json
"""
# Re-export everything from _types for backward compatibility
from mcp.types._types import (
CLIENT_CAPABILITIES_META_KEY,
CLIENT_INFO_META_KEY,
DEFAULT_NEGOTIATED_VERSION,
LATEST_PROTOCOL_VERSION,
LOG_LEVEL_META_KEY,
PROTOCOL_VERSION_META_KEY,
Annotations,
AudioContent,
BaseMetadata,
BlobResourceContents,
CacheableResult,
CallToolRequest,
CallToolRequestParams,
CallToolResult,
CancelledNotification,
CancelledNotificationParams,
CancelTaskRequest,
CancelTaskRequestParams,
CancelTaskResult,
ClientCapabilities,
ClientNotification,
ClientRequest,
ClientResult,
ClientTasksCapability,
ClientTasksRequestsCapability,
CompleteRequest,
CompleteRequestParams,
CompleteResult,
@ -33,6 +43,9 @@ from mcp.types._types import (
CreateMessageRequestParams,
CreateMessageResult,
CreateMessageResultWithTools,
CreateTaskResult,
DiscoverRequest,
DiscoverResult,
ElicitationCapability,
ElicitationRequiredErrorData,
ElicitCompleteNotification,
@ -49,6 +62,12 @@ from mcp.types._types import (
GetPromptRequest,
GetPromptRequestParams,
GetPromptResult,
GetTaskPayloadRequest,
GetTaskPayloadRequestParams,
GetTaskPayloadResult,
GetTaskRequest,
GetTaskRequestParams,
GetTaskResult,
Icon,
IconTheme,
ImageContent,
@ -58,6 +77,12 @@ from mcp.types._types import (
InitializeRequest,
InitializeRequestParams,
InitializeResult,
InputRequest,
InputRequests,
InputRequiredResult,
InputResponse,
InputResponseRequestParams,
InputResponses,
ListPromptsRequest,
ListPromptsResult,
ListResourcesRequest,
@ -66,12 +91,15 @@ from mcp.types._types import (
ListResourceTemplatesResult,
ListRootsRequest,
ListRootsResult,
ListTasksRequest,
ListTasksResult,
ListToolsRequest,
ListToolsResult,
LoggingCapability,
LoggingLevel,
LoggingMessageNotification,
LoggingMessageNotificationParams,
MissingRequiredClientCapabilityErrorData,
ModelHint,
ModelPreferences,
Notification,
@ -92,6 +120,7 @@ from mcp.types._types import (
ReadResourceRequest,
ReadResourceRequestParams,
ReadResourceResult,
RelatedTaskMetadata,
Request,
RequestParams,
RequestParamsMeta,
@ -105,6 +134,7 @@ from mcp.types._types import (
ResourceUpdatedNotification,
ResourceUpdatedNotificationParams,
Result,
ResultType,
Role,
Root,
RootsCapability,
@ -119,22 +149,44 @@ from mcp.types._types import (
ServerNotification,
ServerRequest,
ServerResult,
ServerTasksCapability,
ServerTasksRequestsCapability,
SetLevelRequest,
SetLevelRequestParams,
StopReason,
SubscribeRequest,
SubscribeRequestParams,
SubscriptionFilter,
SubscriptionsAcknowledgedNotification,
SubscriptionsAcknowledgedNotificationParams,
SubscriptionsListenRequest,
SubscriptionsListenRequestParams,
Task,
TaskMetadata,
TasksCallCapability,
TasksCancelCapability,
TasksCreateElicitationCapability,
TasksCreateMessageCapability,
TasksElicitationCapability,
TasksListCapability,
TasksSamplingCapability,
TaskStatus,
TaskStatusNotification,
TaskStatusNotificationParams,
TasksToolsCapability,
TextContent,
TextResourceContents,
Tool,
ToolAnnotations,
ToolChoice,
ToolExecution,
ToolListChangedNotification,
ToolResultContent,
ToolsCapability,
ToolUseContent,
UnsubscribeRequest,
UnsubscribeRequestParams,
UnsupportedProtocolVersionErrorData,
UrlElicitationCapability,
client_notification_adapter,
client_request_adapter,
@ -150,9 +202,12 @@ from mcp.types.jsonrpc import (
INTERNAL_ERROR,
INVALID_PARAMS,
INVALID_REQUEST,
JSONRPC_VERSION,
METHOD_NOT_FOUND,
MISSING_REQUIRED_CLIENT_CAPABILITY,
PARSE_ERROR,
REQUEST_TIMEOUT,
UNSUPPORTED_PROTOCOL_VERSION,
URL_ELICITATION_REQUIRED,
ErrorData,
JSONRPCError,
@ -168,17 +223,28 @@ __all__ = [
# Protocol version constants
"LATEST_PROTOCOL_VERSION",
"DEFAULT_NEGOTIATED_VERSION",
# Reserved request _meta keys
"PROTOCOL_VERSION_META_KEY",
"CLIENT_INFO_META_KEY",
"CLIENT_CAPABILITIES_META_KEY",
"LOG_LEVEL_META_KEY",
# Type aliases and variables
"ContentBlock",
"ElicitRequestedSchema",
"ElicitRequestParams",
"IncludeContext",
"InputRequest",
"InputRequests",
"InputResponse",
"InputResponses",
"LoggingLevel",
"ProgressToken",
"ResultType",
"Role",
"SamplingContent",
"SamplingMessageContentBlock",
"StopReason",
"TaskStatus",
# Base classes
"BaseMetadata",
"Request",
@ -186,13 +252,17 @@ __all__ = [
"Result",
"RequestParams",
"RequestParamsMeta",
"InputResponseRequestParams",
"NotificationParams",
"PaginatedRequest",
"PaginatedRequestParams",
"PaginatedResult",
"CacheableResult",
"EmptyResult",
# Capabilities
"ClientCapabilities",
"ClientTasksCapability",
"ClientTasksRequestsCapability",
"CompletionsCapability",
"ElicitationCapability",
"FormElicitationCapability",
@ -204,6 +274,16 @@ __all__ = [
"SamplingContextCapability",
"SamplingToolsCapability",
"ServerCapabilities",
"ServerTasksCapability",
"ServerTasksRequestsCapability",
"TasksCallCapability",
"TasksCancelCapability",
"TasksCreateElicitationCapability",
"TasksCreateMessageCapability",
"TasksElicitationCapability",
"TasksListCapability",
"TasksSamplingCapability",
"TasksToolsCapability",
"ToolsCapability",
"UrlElicitationCapability",
# Content types
@ -236,27 +316,40 @@ __all__ = [
"ResourceTemplateReference",
"Root",
"SamplingMessage",
"SubscriptionFilter",
"Task",
"TaskMetadata",
"RelatedTaskMetadata",
"Tool",
"ToolAnnotations",
"ToolChoice",
"ToolExecution",
# Requests
"CallToolRequest",
"CallToolRequestParams",
"CompleteRequest",
"CompleteRequestParams",
"CancelTaskRequest",
"CancelTaskRequestParams",
"CreateMessageRequest",
"CreateMessageRequestParams",
"DiscoverRequest",
"ElicitRequest",
"ElicitRequestFormParams",
"ElicitRequestURLParams",
"GetPromptRequest",
"GetPromptRequestParams",
"GetTaskPayloadRequest",
"GetTaskPayloadRequestParams",
"GetTaskRequest",
"GetTaskRequestParams",
"InitializeRequest",
"InitializeRequestParams",
"ListPromptsRequest",
"ListResourcesRequest",
"ListResourceTemplatesRequest",
"ListRootsRequest",
"ListTasksRequest",
"ListToolsRequest",
"PingRequest",
"ReadResourceRequest",
@ -265,23 +358,35 @@ __all__ = [
"SetLevelRequestParams",
"SubscribeRequest",
"SubscribeRequestParams",
"SubscriptionsListenRequest",
"SubscriptionsListenRequestParams",
"UnsubscribeRequest",
"UnsubscribeRequestParams",
# Results
"CallToolResult",
"CancelTaskResult",
"CompleteResult",
"CreateMessageResult",
"CreateMessageResultWithTools",
"CreateTaskResult",
"DiscoverResult",
"ElicitResult",
"ElicitationRequiredErrorData",
"GetPromptResult",
"GetTaskPayloadResult",
"GetTaskResult",
"InitializeResult",
"InputRequiredResult",
"ListPromptsResult",
"ListResourcesResult",
"ListResourceTemplatesResult",
"ListRootsResult",
"ListTasksResult",
"ListToolsResult",
"ReadResourceResult",
# Error data payloads
"MissingRequiredClientCapabilityErrorData",
"UnsupportedProtocolVersionErrorData",
# Notifications
"CancelledNotification",
"CancelledNotificationParams",
@ -297,6 +402,10 @@ __all__ = [
"ResourceUpdatedNotification",
"ResourceUpdatedNotificationParams",
"RootsListChangedNotification",
"SubscriptionsAcknowledgedNotification",
"SubscriptionsAcknowledgedNotificationParams",
"TaskStatusNotification",
"TaskStatusNotificationParams",
"ToolListChangedNotification",
# Union types for request/response routing
"ClientNotification",
@ -317,9 +426,12 @@ __all__ = [
"INTERNAL_ERROR",
"INVALID_PARAMS",
"INVALID_REQUEST",
"JSONRPC_VERSION",
"METHOD_NOT_FOUND",
"MISSING_REQUIRED_CLIENT_CAPABILITY",
"PARSE_ERROR",
"REQUEST_TIMEOUT",
"UNSUPPORTED_PROTOCOL_VERSION",
"URL_ELICITATION_REQUIRED",
"ErrorData",
"JSONRPCError",

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,9 @@
"""Shared pydantic base for the generated `mcp.types.v*` wire-shape packages."""
from pydantic import BaseModel, ConfigDict
class WireModel(BaseModel):
"""Base for generated wire models: enables `populate_by_name`; subclasses set `extra` themselves."""
model_config = ConfigDict(populate_by_name=True)

View file

@ -2,13 +2,16 @@
from __future__ import annotations
from typing import Annotated, Any, Literal
from typing import Annotated, Any, Final, Literal
from pydantic import BaseModel, Field, TypeAdapter
RequestId = Annotated[int, Field(strict=True)] | str
"""The ID of a JSON-RPC request."""
JSONRPC_VERSION: Final[Literal["2.0"]] = "2.0"
"""The JSON-RPC version string carried by every MCP message envelope."""
class JSONRPCRequest(BaseModel):
"""A JSON-RPC request that expects a response."""
@ -27,9 +30,11 @@ class JSONRPCNotification(BaseModel):
params: dict[str, Any] | None = None
# TODO(Marcelo): This is actually not correct. A JSONRPCResponse is the union of a successful response and an error.
class JSONRPCResponse(BaseModel):
"""A successful (non-error) response to a request."""
"""A successful (non-error) response to a request.
Named `JSONRPCResultResponse` in the 2025-11-25+ schemas; the SDK keeps the original name.
"""
jsonrpc: Literal["2.0"]
id: RequestId
@ -38,18 +43,41 @@ class JSONRPCResponse(BaseModel):
# MCP-specific error codes in the range [-32000, -32099]
URL_ELICITATION_REQUIRED = -32042
"""Error code indicating that a URL mode elicitation is required before the request can be processed."""
"""A URL-mode elicitation is required before the request can be processed (protocol 2025-11-25 only)."""
# SDK error codes
MISSING_REQUIRED_CLIENT_CAPABILITY = -32003
"""The server requires a client capability the request did not declare (protocol 2026-07-28)."""
UNSUPPORTED_PROTOCOL_VERSION = -32004
"""The request's protocol version is not supported by the server (protocol 2026-07-28)."""
# SDK error codes: SDK-internal allocations in the JSON-RPC server-error range
# [-32000, -32099]; not defined by the MCP schema. New values must avoid codes
# the spec has allocated above.
CONNECTION_CLOSED = -32000
"""SDK-only: the connection closed before a response arrived; never emitted on the wire."""
REQUEST_TIMEOUT = -32001
"""SDK-only: a request timed out waiting for its response."""
# Standard JSON-RPC error codes
PARSE_ERROR = -32700
"""Standard JSON-RPC: invalid JSON was received."""
INVALID_REQUEST = -32600
"""Standard JSON-RPC: the message is not a valid request object."""
METHOD_NOT_FOUND = -32601
"""Standard JSON-RPC: the requested method does not exist or is not available."""
INVALID_PARAMS = -32602
"""Standard JSON-RPC: invalid method parameters."""
INTERNAL_ERROR = -32603
"""Standard JSON-RPC: an internal error occurred on the receiver.
The SDK uses the generic `ErrorData` envelope; the schema's per-code wrapper types are not constructed.
"""
class ErrorData(BaseModel):
@ -76,8 +104,15 @@ class JSONRPCError(BaseModel):
jsonrpc: Literal["2.0"]
id: RequestId | None
"""The id of the request this error responds to.
Required but nullable per JSON-RPC 2.0: `None` encodes `"id": null` (the id could not be determined).
"""
error: ErrorData
JSONRPCMessage = JSONRPCRequest | JSONRPCNotification | JSONRPCResponse | JSONRPCError
"""Any JSON-RPC envelope that can be decoded off the wire or encoded to be sent."""
jsonrpc_message_adapter: TypeAdapter[JSONRPCMessage] = TypeAdapter(JSONRPCMessage)

710
src/mcp/types/methods.py Normal file
View file

@ -0,0 +1,710 @@
"""Per-version method maps and parse/serialize functions for MCP traffic.
This module is supported public API; the `mcp.types.v*` packages it draws on
are internal validators and not for direct import.
Surface maps key `(method, version)` to per-version wire types (key absence is
the version gate; shape validation is per schema era, i.e. 2025-11-25 for every
pre-2026 version and 2026-07-28 for 2026). Monolith maps key `method` to the
version-free `mcp.types` models user code receives."""
from __future__ import annotations
from collections.abc import Mapping
from functools import cache
from types import MappingProxyType, UnionType
from typing import Any, Final, TypeVar
from pydantic import BaseModel, TypeAdapter
import mcp.types as types
import mcp.types.v2025_11_25 as v2025
import mcp.types.v2026_07_28 as v2026
from mcp.shared.version import KNOWN_PROTOCOL_VERSIONS
__all__ = [
"CLIENT_NOTIFICATIONS",
"CLIENT_REQUESTS",
"CLIENT_RESULTS",
"MONOLITH_NOTIFICATIONS",
"MONOLITH_REQUESTS",
"MONOLITH_RESULTS",
"SERVER_NOTIFICATIONS",
"SERVER_REQUESTS",
"SERVER_RESULTS",
"SPEC_CLIENT_METHODS",
"SPEC_CLIENT_NOTIFICATION_METHODS",
"parse_client_notification",
"parse_client_request",
"parse_client_result",
"parse_server_notification",
"parse_server_request",
"parse_server_result",
"serialize_server_result",
"validate_client_notification",
"validate_client_request",
"validate_client_result",
"validate_server_result",
]
# --- Surface maps: client-to-server ---
CLIENT_REQUESTS: Final[Mapping[tuple[str, str], type[BaseModel]]] = MappingProxyType(
{
# 2024-11-05
("completion/complete", "2024-11-05"): v2025.CompleteRequest,
("initialize", "2024-11-05"): v2025.InitializeRequest,
("logging/setLevel", "2024-11-05"): v2025.SetLevelRequest,
("ping", "2024-11-05"): v2025.PingRequest,
("prompts/get", "2024-11-05"): v2025.GetPromptRequest,
("prompts/list", "2024-11-05"): v2025.ListPromptsRequest,
("resources/list", "2024-11-05"): v2025.ListResourcesRequest,
("resources/read", "2024-11-05"): v2025.ReadResourceRequest,
("resources/subscribe", "2024-11-05"): v2025.SubscribeRequest,
("resources/templates/list", "2024-11-05"): v2025.ListResourceTemplatesRequest,
("resources/unsubscribe", "2024-11-05"): v2025.UnsubscribeRequest,
("tools/call", "2024-11-05"): v2025.CallToolRequest,
("tools/list", "2024-11-05"): v2025.ListToolsRequest,
# 2025-03-26
("completion/complete", "2025-03-26"): v2025.CompleteRequest,
("initialize", "2025-03-26"): v2025.InitializeRequest,
("logging/setLevel", "2025-03-26"): v2025.SetLevelRequest,
("ping", "2025-03-26"): v2025.PingRequest,
("prompts/get", "2025-03-26"): v2025.GetPromptRequest,
("prompts/list", "2025-03-26"): v2025.ListPromptsRequest,
("resources/list", "2025-03-26"): v2025.ListResourcesRequest,
("resources/read", "2025-03-26"): v2025.ReadResourceRequest,
("resources/subscribe", "2025-03-26"): v2025.SubscribeRequest,
("resources/templates/list", "2025-03-26"): v2025.ListResourceTemplatesRequest,
("resources/unsubscribe", "2025-03-26"): v2025.UnsubscribeRequest,
("tools/call", "2025-03-26"): v2025.CallToolRequest,
("tools/list", "2025-03-26"): v2025.ListToolsRequest,
# 2025-06-18
("completion/complete", "2025-06-18"): v2025.CompleteRequest,
("initialize", "2025-06-18"): v2025.InitializeRequest,
("logging/setLevel", "2025-06-18"): v2025.SetLevelRequest,
("ping", "2025-06-18"): v2025.PingRequest,
("prompts/get", "2025-06-18"): v2025.GetPromptRequest,
("prompts/list", "2025-06-18"): v2025.ListPromptsRequest,
("resources/list", "2025-06-18"): v2025.ListResourcesRequest,
("resources/read", "2025-06-18"): v2025.ReadResourceRequest,
("resources/subscribe", "2025-06-18"): v2025.SubscribeRequest,
("resources/templates/list", "2025-06-18"): v2025.ListResourceTemplatesRequest,
("resources/unsubscribe", "2025-06-18"): v2025.UnsubscribeRequest,
("tools/call", "2025-06-18"): v2025.CallToolRequest,
("tools/list", "2025-06-18"): v2025.ListToolsRequest,
# 2025-11-25 (tasks/* deliberately absent)
("completion/complete", "2025-11-25"): v2025.CompleteRequest,
("initialize", "2025-11-25"): v2025.InitializeRequest,
("logging/setLevel", "2025-11-25"): v2025.SetLevelRequest,
("ping", "2025-11-25"): v2025.PingRequest,
("prompts/get", "2025-11-25"): v2025.GetPromptRequest,
("prompts/list", "2025-11-25"): v2025.ListPromptsRequest,
("resources/list", "2025-11-25"): v2025.ListResourcesRequest,
("resources/read", "2025-11-25"): v2025.ReadResourceRequest,
("resources/subscribe", "2025-11-25"): v2025.SubscribeRequest,
("resources/templates/list", "2025-11-25"): v2025.ListResourceTemplatesRequest,
("resources/unsubscribe", "2025-11-25"): v2025.UnsubscribeRequest,
("tools/call", "2025-11-25"): v2025.CallToolRequest,
("tools/list", "2025-11-25"): v2025.ListToolsRequest,
# 2026-07-28 (lifecycle, logging, subscribe pair removed; discover/listen added)
("completion/complete", "2026-07-28"): v2026.CompleteRequest,
("prompts/get", "2026-07-28"): v2026.GetPromptRequest,
("prompts/list", "2026-07-28"): v2026.ListPromptsRequest,
("resources/list", "2026-07-28"): v2026.ListResourcesRequest,
("resources/read", "2026-07-28"): v2026.ReadResourceRequest,
("resources/templates/list", "2026-07-28"): v2026.ListResourceTemplatesRequest,
("server/discover", "2026-07-28"): v2026.DiscoverRequest,
("subscriptions/listen", "2026-07-28"): v2026.SubscriptionsListenRequest,
("tools/call", "2026-07-28"): v2026.CallToolRequest,
("tools/list", "2026-07-28"): v2026.ListToolsRequest,
}
)
CLIENT_NOTIFICATIONS: Final[Mapping[tuple[str, str], type[BaseModel]]] = MappingProxyType(
{
# 2024-11-05
("notifications/cancelled", "2024-11-05"): v2025.CancelledNotification,
("notifications/initialized", "2024-11-05"): v2025.InitializedNotification,
("notifications/progress", "2024-11-05"): v2025.ProgressNotification,
("notifications/roots/list_changed", "2024-11-05"): v2025.RootsListChangedNotification,
# 2025-03-26
("notifications/cancelled", "2025-03-26"): v2025.CancelledNotification,
("notifications/initialized", "2025-03-26"): v2025.InitializedNotification,
("notifications/progress", "2025-03-26"): v2025.ProgressNotification,
("notifications/roots/list_changed", "2025-03-26"): v2025.RootsListChangedNotification,
# 2025-06-18
("notifications/cancelled", "2025-06-18"): v2025.CancelledNotification,
("notifications/initialized", "2025-06-18"): v2025.InitializedNotification,
("notifications/progress", "2025-06-18"): v2025.ProgressNotification,
("notifications/roots/list_changed", "2025-06-18"): v2025.RootsListChangedNotification,
# 2025-11-25 (tasks/status deliberately absent)
("notifications/cancelled", "2025-11-25"): v2025.CancelledNotification,
("notifications/initialized", "2025-11-25"): v2025.InitializedNotification,
("notifications/progress", "2025-11-25"): v2025.ProgressNotification,
("notifications/roots/list_changed", "2025-11-25"): v2025.RootsListChangedNotification,
# 2026-07-28 (initialized and roots/list_changed removed)
("notifications/cancelled", "2026-07-28"): v2026.CancelledNotification,
("notifications/progress", "2026-07-28"): v2026.ProgressNotification,
}
)
# --- Surface maps: server-to-client ---
SERVER_REQUESTS: Final[Mapping[tuple[str, str], type[BaseModel]]] = MappingProxyType(
{
# 2024-11-05
("ping", "2024-11-05"): v2025.PingRequest,
("roots/list", "2024-11-05"): v2025.ListRootsRequest,
("sampling/createMessage", "2024-11-05"): v2025.CreateMessageRequest,
# 2025-03-26
("ping", "2025-03-26"): v2025.PingRequest,
("roots/list", "2025-03-26"): v2025.ListRootsRequest,
("sampling/createMessage", "2025-03-26"): v2025.CreateMessageRequest,
# 2025-06-18 (adds elicitation/create)
("elicitation/create", "2025-06-18"): v2025.ElicitRequest,
("ping", "2025-06-18"): v2025.PingRequest,
("roots/list", "2025-06-18"): v2025.ListRootsRequest,
("sampling/createMessage", "2025-06-18"): v2025.CreateMessageRequest,
# 2025-11-25 (tasks/* deliberately absent)
("elicitation/create", "2025-11-25"): v2025.ElicitRequest,
("ping", "2025-11-25"): v2025.PingRequest,
("roots/list", "2025-11-25"): v2025.ListRootsRequest,
("sampling/createMessage", "2025-11-25"): v2025.CreateMessageRequest,
# 2026-07-28: none (schema defines no ServerRequest union)
}
)
SERVER_NOTIFICATIONS: Final[Mapping[tuple[str, str], type[BaseModel]]] = MappingProxyType(
{
# 2024-11-05
("notifications/cancelled", "2024-11-05"): v2025.CancelledNotification,
("notifications/message", "2024-11-05"): v2025.LoggingMessageNotification,
("notifications/progress", "2024-11-05"): v2025.ProgressNotification,
("notifications/prompts/list_changed", "2024-11-05"): v2025.PromptListChangedNotification,
("notifications/resources/list_changed", "2024-11-05"): v2025.ResourceListChangedNotification,
("notifications/resources/updated", "2024-11-05"): v2025.ResourceUpdatedNotification,
("notifications/tools/list_changed", "2024-11-05"): v2025.ToolListChangedNotification,
# 2025-03-26
("notifications/cancelled", "2025-03-26"): v2025.CancelledNotification,
("notifications/message", "2025-03-26"): v2025.LoggingMessageNotification,
("notifications/progress", "2025-03-26"): v2025.ProgressNotification,
("notifications/prompts/list_changed", "2025-03-26"): v2025.PromptListChangedNotification,
("notifications/resources/list_changed", "2025-03-26"): v2025.ResourceListChangedNotification,
("notifications/resources/updated", "2025-03-26"): v2025.ResourceUpdatedNotification,
("notifications/tools/list_changed", "2025-03-26"): v2025.ToolListChangedNotification,
# 2025-06-18
("notifications/cancelled", "2025-06-18"): v2025.CancelledNotification,
("notifications/message", "2025-06-18"): v2025.LoggingMessageNotification,
("notifications/progress", "2025-06-18"): v2025.ProgressNotification,
("notifications/prompts/list_changed", "2025-06-18"): v2025.PromptListChangedNotification,
("notifications/resources/list_changed", "2025-06-18"): v2025.ResourceListChangedNotification,
("notifications/resources/updated", "2025-06-18"): v2025.ResourceUpdatedNotification,
("notifications/tools/list_changed", "2025-06-18"): v2025.ToolListChangedNotification,
# 2025-11-25 (adds elicitation/complete; tasks/status deliberately absent)
("notifications/cancelled", "2025-11-25"): v2025.CancelledNotification,
("notifications/elicitation/complete", "2025-11-25"): v2025.ElicitationCompleteNotification,
("notifications/message", "2025-11-25"): v2025.LoggingMessageNotification,
("notifications/progress", "2025-11-25"): v2025.ProgressNotification,
("notifications/prompts/list_changed", "2025-11-25"): v2025.PromptListChangedNotification,
("notifications/resources/list_changed", "2025-11-25"): v2025.ResourceListChangedNotification,
("notifications/resources/updated", "2025-11-25"): v2025.ResourceUpdatedNotification,
("notifications/tools/list_changed", "2025-11-25"): v2025.ToolListChangedNotification,
# 2026-07-28 (adds subscriptions/acknowledged)
("notifications/cancelled", "2026-07-28"): v2026.CancelledNotification,
("notifications/elicitation/complete", "2026-07-28"): v2026.ElicitationCompleteNotification,
("notifications/message", "2026-07-28"): v2026.LoggingMessageNotification,
("notifications/progress", "2026-07-28"): v2026.ProgressNotification,
("notifications/prompts/list_changed", "2026-07-28"): v2026.PromptListChangedNotification,
("notifications/resources/list_changed", "2026-07-28"): v2026.ResourceListChangedNotification,
("notifications/resources/updated", "2026-07-28"): v2026.ResourceUpdatedNotification,
("notifications/subscriptions/acknowledged", "2026-07-28"): v2026.SubscriptionsAcknowledgedNotification,
("notifications/tools/list_changed", "2026-07-28"): v2026.ToolListChangedNotification,
}
)
# --- Surface maps: results ---
SERVER_RESULTS: Final[Mapping[tuple[str, str], type[BaseModel] | UnionType]] = MappingProxyType(
{
# 2024-11-05
("completion/complete", "2024-11-05"): v2025.CompleteResult,
("initialize", "2024-11-05"): v2025.InitializeResult,
("logging/setLevel", "2024-11-05"): v2025.EmptyResult,
("ping", "2024-11-05"): v2025.EmptyResult,
("prompts/get", "2024-11-05"): v2025.GetPromptResult,
("prompts/list", "2024-11-05"): v2025.ListPromptsResult,
("resources/list", "2024-11-05"): v2025.ListResourcesResult,
("resources/read", "2024-11-05"): v2025.ReadResourceResult,
("resources/subscribe", "2024-11-05"): v2025.EmptyResult,
("resources/templates/list", "2024-11-05"): v2025.ListResourceTemplatesResult,
("resources/unsubscribe", "2024-11-05"): v2025.EmptyResult,
("tools/call", "2024-11-05"): v2025.CallToolResult,
("tools/list", "2024-11-05"): v2025.ListToolsResult,
# 2025-03-26
("completion/complete", "2025-03-26"): v2025.CompleteResult,
("initialize", "2025-03-26"): v2025.InitializeResult,
("logging/setLevel", "2025-03-26"): v2025.EmptyResult,
("ping", "2025-03-26"): v2025.EmptyResult,
("prompts/get", "2025-03-26"): v2025.GetPromptResult,
("prompts/list", "2025-03-26"): v2025.ListPromptsResult,
("resources/list", "2025-03-26"): v2025.ListResourcesResult,
("resources/read", "2025-03-26"): v2025.ReadResourceResult,
("resources/subscribe", "2025-03-26"): v2025.EmptyResult,
("resources/templates/list", "2025-03-26"): v2025.ListResourceTemplatesResult,
("resources/unsubscribe", "2025-03-26"): v2025.EmptyResult,
("tools/call", "2025-03-26"): v2025.CallToolResult,
("tools/list", "2025-03-26"): v2025.ListToolsResult,
# 2025-06-18
("completion/complete", "2025-06-18"): v2025.CompleteResult,
("initialize", "2025-06-18"): v2025.InitializeResult,
("logging/setLevel", "2025-06-18"): v2025.EmptyResult,
("ping", "2025-06-18"): v2025.EmptyResult,
("prompts/get", "2025-06-18"): v2025.GetPromptResult,
("prompts/list", "2025-06-18"): v2025.ListPromptsResult,
("resources/list", "2025-06-18"): v2025.ListResourcesResult,
("resources/read", "2025-06-18"): v2025.ReadResourceResult,
("resources/subscribe", "2025-06-18"): v2025.EmptyResult,
("resources/templates/list", "2025-06-18"): v2025.ListResourceTemplatesResult,
("resources/unsubscribe", "2025-06-18"): v2025.EmptyResult,
("tools/call", "2025-06-18"): v2025.CallToolResult,
("tools/list", "2025-06-18"): v2025.ListToolsResult,
# 2025-11-25
("completion/complete", "2025-11-25"): v2025.CompleteResult,
("initialize", "2025-11-25"): v2025.InitializeResult,
("logging/setLevel", "2025-11-25"): v2025.EmptyResult,
("ping", "2025-11-25"): v2025.EmptyResult,
("prompts/get", "2025-11-25"): v2025.GetPromptResult,
("prompts/list", "2025-11-25"): v2025.ListPromptsResult,
("resources/list", "2025-11-25"): v2025.ListResourcesResult,
("resources/read", "2025-11-25"): v2025.ReadResourceResult,
("resources/subscribe", "2025-11-25"): v2025.EmptyResult,
("resources/templates/list", "2025-11-25"): v2025.ListResourceTemplatesResult,
("resources/unsubscribe", "2025-11-25"): v2025.EmptyResult,
("tools/call", "2025-11-25"): v2025.CallToolResult,
("tools/list", "2025-11-25"): v2025.ListToolsResult,
# 2026-07-28 (dual-result rows use the version's union aliases)
("completion/complete", "2026-07-28"): v2026.CompleteResult,
("prompts/get", "2026-07-28"): v2026.AnyGetPromptResult,
("prompts/list", "2026-07-28"): v2026.ListPromptsResult,
("resources/list", "2026-07-28"): v2026.ListResourcesResult,
("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,
("tools/call", "2026-07-28"): v2026.AnyCallToolResult,
("tools/list", "2026-07-28"): v2026.ListToolsResult,
}
)
"""Results servers send, keyed by the originating client request's (method, version)."""
CLIENT_RESULTS: Final[Mapping[tuple[str, str], type[BaseModel] | UnionType]] = MappingProxyType(
{
# 2024-11-05
("ping", "2024-11-05"): v2025.EmptyResult,
("roots/list", "2024-11-05"): v2025.ListRootsResult,
("sampling/createMessage", "2024-11-05"): v2025.CreateMessageResult,
# 2025-03-26
("ping", "2025-03-26"): v2025.EmptyResult,
("roots/list", "2025-03-26"): v2025.ListRootsResult,
("sampling/createMessage", "2025-03-26"): v2025.CreateMessageResult,
# 2025-06-18
("elicitation/create", "2025-06-18"): v2025.ElicitResult,
("ping", "2025-06-18"): v2025.EmptyResult,
("roots/list", "2025-06-18"): v2025.ListRootsResult,
("sampling/createMessage", "2025-06-18"): v2025.CreateMessageResult,
# 2025-11-25
("elicitation/create", "2025-11-25"): v2025.ElicitResult,
("ping", "2025-11-25"): v2025.EmptyResult,
("roots/list", "2025-11-25"): v2025.ListRootsResult,
("sampling/createMessage", "2025-11-25"): v2025.CreateMessageResult,
# 2026-07-28: none (no server-to-client requests at this version)
}
)
"""Results clients send, keyed by the originating server request's (method, version)."""
# --- Direction-specific method sets ---
SPEC_CLIENT_METHODS: Final[frozenset[str]] = frozenset(m for m, _ in CLIENT_REQUESTS)
"""Spec request methods a client may send (any version); the server-side spec-method discriminator."""
SPEC_CLIENT_NOTIFICATION_METHODS: Final[frozenset[str]] = frozenset(m for m, _ in CLIENT_NOTIFICATIONS)
"""Spec notification methods a client may send (any version); the server-side spec-method discriminator."""
# --- Monolith maps ---
MONOLITH_REQUESTS: Final[Mapping[str, type[types.Request[Any, Any]]]] = MappingProxyType(
{
"completion/complete": types.CompleteRequest,
"elicitation/create": types.ElicitRequest,
"initialize": types.InitializeRequest,
"logging/setLevel": types.SetLevelRequest,
"ping": types.PingRequest,
"prompts/get": types.GetPromptRequest,
"prompts/list": types.ListPromptsRequest,
"resources/list": types.ListResourcesRequest,
"resources/read": types.ReadResourceRequest,
"resources/subscribe": types.SubscribeRequest,
"resources/templates/list": types.ListResourceTemplatesRequest,
"resources/unsubscribe": types.UnsubscribeRequest,
"roots/list": types.ListRootsRequest,
"sampling/createMessage": types.CreateMessageRequest,
"server/discover": types.DiscoverRequest,
"subscriptions/listen": types.SubscriptionsListenRequest,
"tools/call": types.CallToolRequest,
"tools/list": types.ListToolsRequest,
}
)
"""Monolith request model per method, both directions."""
MONOLITH_NOTIFICATIONS: Final[Mapping[str, type[types.Notification[Any, Any]]]] = MappingProxyType(
{
"notifications/cancelled": types.CancelledNotification,
"notifications/elicitation/complete": types.ElicitCompleteNotification,
"notifications/initialized": types.InitializedNotification,
"notifications/message": types.LoggingMessageNotification,
"notifications/progress": types.ProgressNotification,
"notifications/prompts/list_changed": types.PromptListChangedNotification,
"notifications/resources/list_changed": types.ResourceListChangedNotification,
"notifications/resources/updated": types.ResourceUpdatedNotification,
"notifications/roots/list_changed": types.RootsListChangedNotification,
"notifications/subscriptions/acknowledged": types.SubscriptionsAcknowledgedNotification,
"notifications/tools/list_changed": types.ToolListChangedNotification,
}
)
"""Monolith notification model per method, both directions."""
MONOLITH_RESULTS: Final[Mapping[str, type[types.Result] | UnionType]] = MappingProxyType(
{
"completion/complete": types.CompleteResult,
"elicitation/create": types.ElicitResult,
"initialize": types.InitializeResult,
"logging/setLevel": types.EmptyResult,
"ping": types.EmptyResult,
"prompts/get": types.GetPromptResult | types.InputRequiredResult,
"prompts/list": types.ListPromptsResult,
"resources/list": types.ListResourcesResult,
"resources/read": types.ReadResourceResult | types.InputRequiredResult,
"resources/subscribe": types.EmptyResult,
"resources/templates/list": types.ListResourceTemplatesResult,
"resources/unsubscribe": types.EmptyResult,
"roots/list": types.ListRootsResult,
# Arm order load-bearing: a single-block body satisfies both arms and
# 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,
"tools/call": types.CallToolResult | types.InputRequiredResult,
"tools/list": types.ListToolsResult,
}
)
"""Monolith result model (or two-arm union) per request method."""
# --- Parse functions ---
# Envelope stubs merged into bodies for surface validation (surface classes are full frames).
_REQUEST_STUB: Final[Mapping[str, Any]] = MappingProxyType({"jsonrpc": "2.0", "id": 0})
_NOTIFICATION_STUB: Final[Mapping[str, Any]] = MappingProxyType({"jsonrpc": "2.0"})
def _check_known_version(version: str) -> None:
"""Raise ValueError for unknown `version` so a typo cannot silently gate every method."""
if version not in KNOWN_PROTOCOL_VERSIONS:
raise ValueError(f"version must be a known protocol version, got {version!r}")
def _body(method: str, params: Mapping[str, Any] | None) -> dict[str, Any]:
"""Build a JSON-RPC body, omitting `params` when None."""
body: dict[str, Any] = {"method": method}
if params is not None:
body["params"] = params
return body
@cache
def _adapter(target: type[BaseModel] | UnionType) -> TypeAdapter[Any]:
return TypeAdapter(target)
_MonolithT = TypeVar("_MonolithT")
def _monolith_row(monolith: Mapping[str, _MonolithT], method: str) -> _MonolithT:
"""Look up `method` in `monolith`, raising RuntimeError on miss.
Not KeyError: the surface row already matched, so a miss is inconsistent
extension maps and must not be caught by the session's `except KeyError` gate.
"""
try:
return monolith[method]
except KeyError:
raise RuntimeError(f"inconsistent extension maps: surface defines {method!r} but monolith does not") from None
def validate_client_request(
method: str,
version: str,
params: Mapping[str, Any] | None,
*,
surface: Mapping[tuple[str, str], type[BaseModel]] = CLIENT_REQUESTS,
) -> None:
"""Validate a client request against `surface` only.
Raises:
ValueError: `version` is not a known protocol version.
KeyError: `(method, version)` is not in `surface` (the version gate).
pydantic.ValidationError: body fails surface validation.
"""
_check_known_version(version)
surface[(method, version)].model_validate({**_REQUEST_STUB, **_body(method, params)}, by_name=False)
def parse_client_request(
method: str,
version: str,
params: Mapping[str, Any] | None,
*,
surface: Mapping[tuple[str, str], type[BaseModel]] = CLIENT_REQUESTS,
monolith: Mapping[str, type[types.Request[Any, Any]]] = MONOLITH_REQUESTS,
) -> types.Request[Any, Any]:
"""Validate a client request against `surface`, then parse and return its `monolith` model.
Args:
surface: `(method, version)` to wire-type map; the version-gate lookup
and (per-schema-era) shape check run against this. Pass an extended
map to admit custom methods.
monolith: `method` to version-free model map; the returned instance is
parsed from this row. Must cover every method `surface` admits.
Raises:
ValueError: `version` is not a known protocol version.
KeyError: `(method, version)` is not in `surface` (the version gate).
pydantic.ValidationError: body fails surface or monolith validation.
RuntimeError: surface matched but `method` has no monolith row.
"""
validate_client_request(method, version, params, surface=surface)
return _monolith_row(monolith, method).model_validate(_body(method, params), by_name=False)
def parse_server_request(
method: str,
version: str,
params: Mapping[str, Any] | None,
*,
surface: Mapping[tuple[str, str], type[BaseModel]] = SERVER_REQUESTS,
monolith: Mapping[str, type[types.Request[Any, Any]]] = MONOLITH_REQUESTS,
) -> types.Request[Any, Any]:
"""Validate a server request against `surface`, then parse and return its `monolith` model.
Args:
surface: `(method, version)` to wire-type map; the version-gate lookup
and (per-schema-era) shape check run against this. Pass an extended
map to admit custom methods.
monolith: `method` to version-free model map; the returned instance is
parsed from this row. Must cover every method `surface` admits.
Raises:
ValueError: `version` is not a known protocol version.
KeyError: `(method, version)` is not in `surface` (the version gate).
pydantic.ValidationError: body fails surface or monolith validation.
RuntimeError: surface matched but `method` has no monolith row.
"""
_check_known_version(version)
surface_type = surface[(method, version)]
surface_type.model_validate({**_REQUEST_STUB, **_body(method, params)}, by_name=False)
return _monolith_row(monolith, method).model_validate(_body(method, params), by_name=False)
def validate_client_notification(
method: str,
version: str,
params: Mapping[str, Any] | None,
*,
surface: Mapping[tuple[str, str], type[BaseModel]] = CLIENT_NOTIFICATIONS,
) -> None:
"""Validate a client notification against `surface` only.
Raises:
ValueError: `version` is not a known protocol version.
KeyError: `(method, version)` is not in `surface`.
pydantic.ValidationError: body fails surface validation.
"""
_check_known_version(version)
surface[(method, version)].model_validate({**_NOTIFICATION_STUB, **_body(method, params)}, by_name=False)
def parse_client_notification(
method: str,
version: str,
params: Mapping[str, Any] | None,
*,
surface: Mapping[tuple[str, str], type[BaseModel]] = CLIENT_NOTIFICATIONS,
monolith: Mapping[str, type[types.Notification[Any, Any]]] = MONOLITH_NOTIFICATIONS,
) -> types.Notification[Any, Any]:
"""Validate a client notification against `surface`, then parse and return its `monolith` model.
Args:
surface: `(method, version)` to wire-type map; the version-gate lookup
and (per-schema-era) shape check run against this. Pass an extended
map to admit custom methods.
monolith: `method` to version-free model map; the returned instance is
parsed from this row. Must cover every method `surface` admits.
Raises:
ValueError: `version` is not a known protocol version.
KeyError: `(method, version)` is not in `surface`.
pydantic.ValidationError: body fails surface or monolith validation.
RuntimeError: surface matched but `method` has no monolith row.
"""
validate_client_notification(method, version, params, surface=surface)
return _monolith_row(monolith, method).model_validate(_body(method, params), by_name=False)
def parse_server_notification(
method: str,
version: str,
params: Mapping[str, Any] | None,
*,
surface: Mapping[tuple[str, str], type[BaseModel]] = SERVER_NOTIFICATIONS,
monolith: Mapping[str, type[types.Notification[Any, Any]]] = MONOLITH_NOTIFICATIONS,
) -> types.Notification[Any, Any]:
"""Validate a server notification against `surface`, then parse and return its `monolith` model.
Args:
surface: `(method, version)` to wire-type map; the version-gate lookup
and (per-schema-era) shape check run against this. Pass an extended
map to admit custom methods.
monolith: `method` to version-free model map; the returned instance is
parsed from this row. Must cover every method `surface` admits.
Raises:
ValueError: `version` is not a known protocol version.
KeyError: `(method, version)` is not in `surface`.
pydantic.ValidationError: body fails surface or monolith validation.
RuntimeError: surface matched but `method` has no monolith row.
"""
_check_known_version(version)
surface_type = surface[(method, version)]
surface_type.model_validate({**_NOTIFICATION_STUB, **_body(method, params)}, by_name=False)
return _monolith_row(monolith, method).model_validate(_body(method, params), by_name=False)
def serialize_server_result(
method: str,
version: str,
data: Mapping[str, Any],
*,
surface: Mapping[tuple[str, str], type[BaseModel] | UnionType] = SERVER_RESULTS,
) -> 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.
Raises:
ValueError: `version` is not a known protocol version.
KeyError: `(method, version)` is not in `surface`.
pydantic.ValidationError: result fails surface validation.
"""
_check_known_version(version)
adapter = _adapter(surface[(method, version)])
return adapter.dump_python(
adapter.validate_python(data, by_name=False), by_alias=True, mode="json", exclude_none=True
)
def validate_server_result(
method: str,
version: str,
data: Mapping[str, Any],
*,
surface: Mapping[tuple[str, str], type[BaseModel] | UnionType] = SERVER_RESULTS,
) -> None:
"""Validate a server result against `surface` only.
Raises:
ValueError: `version` is not a known protocol version.
KeyError: `(method, version)` is not in `surface`.
pydantic.ValidationError: result fails surface validation.
"""
serialize_server_result(method, version, data, surface=surface)
def parse_server_result(
method: str,
version: str,
data: Mapping[str, Any],
*,
surface: Mapping[tuple[str, str], type[BaseModel] | UnionType] = SERVER_RESULTS,
monolith: Mapping[str, type[types.Result] | UnionType] = MONOLITH_RESULTS,
) -> types.Result:
"""Validate a server result against `surface`, then parse and return its `monolith` model.
Args:
surface: `(method, version)` to wire-type map; the version-gate lookup
and (per-schema-era) shape check run against this. Pass an extended
map to admit custom methods.
monolith: `method` to version-free model map; the returned instance is
parsed from this row. Must cover every method `surface` admits.
Raises:
ValueError: `version` is not a known protocol version.
KeyError: `(method, version)` is not in `surface`.
pydantic.ValidationError: result fails surface or monolith validation.
RuntimeError: surface matched but `method` has no monolith row.
"""
validate_server_result(method, version, data, surface=surface)
result: types.Result = _adapter(_monolith_row(monolith, method)).validate_python(data, by_name=False)
return result
def validate_client_result(
method: str,
version: str,
data: Mapping[str, Any],
*,
surface: Mapping[tuple[str, str], type[BaseModel] | UnionType] = CLIENT_RESULTS,
) -> None:
"""Validate a client result against `surface` only.
Raises:
ValueError: `version` is not a known protocol version.
KeyError: `(method, version)` is not in `surface`.
pydantic.ValidationError: result fails surface validation.
"""
_check_known_version(version)
_adapter(surface[(method, version)]).validate_python(data, by_name=False)
def parse_client_result(
method: str,
version: str,
data: Mapping[str, Any],
*,
surface: Mapping[tuple[str, str], type[BaseModel] | UnionType] = CLIENT_RESULTS,
monolith: Mapping[str, type[types.Result] | UnionType] = MONOLITH_RESULTS,
) -> types.Result:
"""Validate a client result against `surface`, then parse and return its `monolith` model.
Args:
surface: `(method, version)` to wire-type map; the version-gate lookup
and (per-schema-era) shape check run against this. Pass an extended
map to admit custom methods.
monolith: `method` to version-free model map; the returned instance is
parsed from this row. Must cover every method `surface` admits.
Raises:
ValueError: `version` is not a known protocol version.
KeyError: `(method, version)` is not in `surface`.
pydantic.ValidationError: result fails surface or monolith validation.
RuntimeError: surface matched but `method` has no monolith row.
"""
validate_client_result(method, version, data, surface=surface)
result: types.Result = _adapter(_monolith_row(monolith, method)).validate_python(data, by_name=False)
return result

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more