Protocol types for 2026-07-28: superset monolith, committed per-version packages, and wire-method maps (#2849)
This commit is contained in:
parent
2693fb95ae
commit
65be5a7147
37 changed files with 19076 additions and 371 deletions
5
.github/workflows/shared.yml
vendored
5
.github/workflows/shared.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
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
3873
schema/2026-07-28.json
Normal file
File diff suppressed because it is too large
Load diff
14
schema/PINNED.json
Normal file
14
schema/PINNED.json
Normal 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
12
schema/README.md
Normal 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
|
||||
252
scripts/gen_surface_types.py
Normal file
252
scripts/gen_surface_types.py
Normal 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())
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
9
src/mcp/types/_wire_base.py
Normal file
9
src/mcp/types/_wire_base.py
Normal 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)
|
||||
|
|
@ -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
710
src/mcp/types/methods.py
Normal 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
|
||||
3526
src/mcp/types/v2025_11_25/__init__.py
Normal file
3526
src/mcp/types/v2025_11_25/__init__.py
Normal file
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
Loading…
Reference in a new issue