diff --git a/docs/servers/structured-output.md b/docs/servers/structured-output.md index 792bcb0c6c..fbe09ba817 100644 --- a/docs/servers/structured-output.md +++ b/docs/servers/structured-output.md @@ -169,6 +169,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} The keys must be `str`. A `dict[int, float]` can't be a JSON object, so it falls back to the `{"result": ...}` wrapper. +Dictionary results use Pydantic's `TypeAdapter` for validation and serialization. If you inspect a tool's `FuncMetadata.output_model`, it holds the dictionary type annotation with its schema title. + ## Validation `output_schema` is not documentation. Whatever your function returns is **validated against it** before it leaves the server. diff --git a/pyproject.toml b/pyproject.toml index d12cb7485e..536ef5278a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -219,6 +219,7 @@ ignore = ["PERF203"] [tool.ruff.lint.flake8-tidy-imports.banned-api] "pydantic.RootModel".msg = "Use `pydantic.TypeAdapter` instead." +"pydantic.root_model.RootModel".msg = "Use `pydantic.TypeAdapter` instead." [tool.ruff.lint.mccabe] @@ -228,8 +229,8 @@ max-complexity = 24 # Default is 10 "__init__.py" = ["F401"] # The mcp.types package is an alias that mirrors mcp_types namespaces by design. "src/mcp/types/*.py" = ["F403"] -# 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/mcp_types/_v*/__init__.py" = ["D212", "E501", "I001", "TID251", "UP007", "UP037"] +# Generated by scripts/gen_surface_types.py. +"src/mcp-types/mcp_types/_v*/__init__.py" = ["D212", "E501", "I001", "UP007", "UP037"] "tests/server/mcpserver/test_func_metadata.py" = ["E501"] # Inline snapshots of the translation tool's output carry long status/prompt lines verbatim. "tests/docs/test_translations.py" = ["E501"] diff --git a/scripts/gen_surface_types.py b/scripts/gen_surface_types.py index a9887dd4bb..9e0279fffa 100644 --- a/scripts/gen_surface_types.py +++ b/scripts/gen_surface_types.py @@ -5,13 +5,14 @@ underscore marks these as internal validators, not public API) with only the fixes the raw output needs: a small JSON pre-patch for the known `number`-as-`integer` schema.json defect, a header, full URLs for the spec's -site-absolute doc links, and per-version epilogue aliases. Run with +site-absolute doc links, plain type aliases, 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 ast import difflib import hashlib import json @@ -195,6 +196,7 @@ def run_codegen(schema_path: Path, output_path: Path) -> None: "--use-annotated", "--use-field-description", "--use-schema-description", "--enum-field-as-literal", "all", "--use-union-operator", "--use-double-quotes", + "--use-type-alias", "--skip-root-model", "--extra-fields", "ignore", # JSON Schema `format` is annotation-only; codegen's defaults # (Base64Str, AnyUrl) over-assert and reject valid wire data. @@ -237,6 +239,10 @@ def build(entry: dict[str, str]) -> str: schema = json.loads((SCHEMA_DIR / f"{version}.json").read_text(encoding="utf-8")) patch_schema(schema, SCHEMA_PATCHES.get(version, [])) make_server_info_opaque(schema) + if "JSONValue" in schema["$defs"]: + # A single recursive alias avoids mutually recursive alias evaluation in type checkers. + assert schema["$defs"]["JSONValue"]["anyOf"][0] == {"$ref": "#/$defs/JSONObject"} + schema["$defs"]["JSONValue"]["anyOf"][0] = schema["$defs"]["JSONObject"] with tempfile.TemporaryDirectory() as tmp: patched = Path(tmp) / "schema.json" @@ -246,7 +252,27 @@ def build(entry: dict[str, str]) -> str: source = raw.read_text(encoding="utf-8") 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) + # Keep named aliases only for recursive types; other aliases remain ordinary Python types and unions. + for node in reversed(ast.parse(source).body): + if not ( + isinstance(node, ast.Assign) + and isinstance(node.value, ast.Call) + and isinstance(node.value.func, ast.Name) + and node.value.func.id == "TypeAliasType" + ): + continue + value = node.value.args[1] + if any( + isinstance(part, ast.Constant) and isinstance(part.value, str) and part.value in schema["$defs"] + for part in ast.walk(value) + ): + continue + original = ast.get_source_segment(source, node.value) + replacement = ast.get_source_segment(source, value) + assert original is not None and replacement is not None + source = source.replace(original, f"({replacement})", 1) + if "= TypeAliasType(" not in source: + source = source.replace("from typing_extensions import TypeAliasType\n", "") # 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"') @@ -256,8 +282,7 @@ def build(entry: dict[str, str]) -> str: source = source.replace("](/", "](https://modelcontextprotocol.io/") 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. + # Resolve aliases before rebuilding models with forward references. 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:]}" diff --git a/src/mcp-types/mcp_types/_v2025_11_25/__init__.py b/src/mcp-types/mcp_types/_v2025_11_25/__init__.py index b5f5b9673f..5ffd15f0ec 100644 --- a/src/mcp-types/mcp_types/_v2025_11_25/__init__.py +++ b/src/mcp-types/mcp_types/_v2025_11_25/__init__.py @@ -9,7 +9,7 @@ from typing import Annotated, Any, Literal from mcp_types._wire_base import WireModel -from pydantic import ConfigDict, Field, RootModel +from pydantic import ConfigDict, Field class BaseMetadata(WireModel): @@ -285,11 +285,10 @@ class CompleteResult(WireModel): completion: Completion -class Cursor(RootModel[str]): - root: str - """ - An opaque token used to represent a cursor for pagination. - """ +Cursor = str +""" +An opaque token used to represent a cursor for pagination. +""" class RequestedSchema(WireModel): @@ -556,27 +555,13 @@ class LegacyTitledEnumSchema(WireModel): type: Literal["string"] -class LoggingLevel( - RootModel[ - Literal[ - "alert", - "critical", - "debug", - "emergency", - "error", - "info", - "notice", - "warning", - ] - ] -): - root: Literal["alert", "critical", "debug", "emergency", "error", "info", "notice", "warning"] - """ - The severity of a log message. +LoggingLevel = Literal["alert", "critical", "debug", "emergency", "error", "info", "notice", "warning"] +""" +The severity of a log message. - These map to syslog message severities, as specified in RFC-5424: - https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 - """ +These map to syslog message severities, as specified in RFC-5424: +https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 +""" class LoggingMessageNotificationParams(WireModel): @@ -723,11 +708,10 @@ class PaginatedResult(WireModel): """ -class ProgressToken(RootModel[str | int]): - root: str | int - """ - A progress token, used to associate progress notifications with the original request. - """ +ProgressToken = str | int +""" +A progress token, used to associate progress notifications with the original request. +""" class PromptArgument(WireModel): @@ -853,11 +837,10 @@ class Request(WireModel): params: dict[str, Any] | None = None -class RequestId(RootModel[str | int]): - root: str | int - """ - A uniquely identifying ID for a request in JSON-RPC. - """ +RequestId = str | int +""" +A uniquely identifying ID for a request in JSON-RPC. +""" class RequestParams(WireModel): @@ -970,11 +953,10 @@ class Result(WireModel): """ -class Role(RootModel[Literal["assistant", "user"]]): - root: Literal["assistant", "user"] - """ - The sender or recipient of messages and data in a conversation. - """ +Role = Literal["assistant", "user"] +""" +The sender or recipient of messages and data in a conversation. +""" class Root(WireModel): @@ -1216,11 +1198,10 @@ class TaskMetadata(WireModel): """ -class TaskStatus(RootModel[Literal["cancelled", "completed", "failed", "input_required", "working"]]): - root: Literal["cancelled", "completed", "failed", "input_required", "working"] - """ - The status of a task. - """ +TaskStatus = Literal["cancelled", "completed", "failed", "input_required", "working"] +""" +The status of a task. +""" class TextResourceContents(WireModel): @@ -1867,26 +1848,16 @@ class EmbeddedResource(WireModel): type: Literal["resource"] -class EmptyResult(RootModel[Result]): - root: Result +EmptyResult = Result -class EnumSchema( - RootModel[ - UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ] -): - root: ( - UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ) +EnumSchema = ( + UntitledSingleSelectEnumSchema + | TitledSingleSelectEnumSchema + | UntitledMultiSelectEnumSchema + | TitledMultiSelectEnumSchema + | LegacyTitledEnumSchema +) class GetPromptRequestParams(WireModel): @@ -2115,8 +2086,7 @@ class LoggingMessageNotification(WireModel): params: LoggingMessageNotificationParams -class MultiSelectEnumSchema(RootModel[UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema]): - root: UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema +MultiSelectEnumSchema = UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema class PaginatedRequestParams(WireModel): @@ -2152,32 +2122,20 @@ class PingRequest(WireModel): params: RequestParams | None = None -class PrimitiveSchemaDefinition( - RootModel[ - StringSchema - | NumberSchema - | BooleanSchema - | UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ] -): - root: ( - StringSchema - | NumberSchema - | BooleanSchema - | UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ) - """ - Restricted schema definitions that only allow primitive types - without nested objects or arrays. - """ +PrimitiveSchemaDefinition = ( + StringSchema + | NumberSchema + | BooleanSchema + | UntitledSingleSelectEnumSchema + | TitledSingleSelectEnumSchema + | UntitledMultiSelectEnumSchema + | TitledMultiSelectEnumSchema + | LegacyTitledEnumSchema +) +""" +Restricted schema definitions that only allow primitive types +without nested objects or arrays. +""" class ProgressNotificationParams(WireModel): @@ -2499,8 +2457,7 @@ class SetLevelRequest(WireModel): params: SetLevelRequestParams -class SingleSelectEnumSchema(RootModel[UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema]): - root: UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema +SingleSelectEnumSchema = UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema class SubscribeRequest(WireModel): @@ -2793,8 +2750,7 @@ class CompleteRequest(WireModel): params: CompleteRequestParams -class ContentBlock(RootModel[TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource]): - root: TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource +ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource class CreateTaskResult(WireModel): @@ -2812,11 +2768,10 @@ class CreateTaskResult(WireModel): task: Task -class ElicitRequestParams(RootModel[ElicitRequestURLParams | ElicitRequestFormParams]): - root: ElicitRequestURLParams | ElicitRequestFormParams - """ - The parameters for a request to elicit additional information from the user via the client. - """ +ElicitRequestParams = ElicitRequestURLParams | ElicitRequestFormParams +""" +The parameters for a request to elicit additional information from the user via the client. +""" class GetPromptRequest(WireModel): @@ -2857,18 +2812,16 @@ class InitializeRequest(WireModel): params: InitializeRequestParams -class JSONRPCMessage(RootModel[JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse]): - root: JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse - """ - Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. - """ +JSONRPCMessage = JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse +""" +Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. +""" -class JSONRPCResponse(RootModel[JSONRPCResultResponse | JSONRPCErrorResponse]): - root: JSONRPCResultResponse | JSONRPCErrorResponse - """ - A response to a request, containing either the result or error. - """ +JSONRPCResponse = JSONRPCResultResponse | JSONRPCErrorResponse +""" +A response to a request, containing either the result or error. +""" class ListPromptsRequest(WireModel): @@ -3173,64 +3126,34 @@ class CallToolResult(WireModel): """ -class ClientNotification( - RootModel[ - CancelledNotification - | InitializedNotification - | ProgressNotification - | TaskStatusNotification - | RootsListChangedNotification - ] -): - root: ( - CancelledNotification - | InitializedNotification - | ProgressNotification - | TaskStatusNotification - | RootsListChangedNotification - ) - - -class ClientRequest( - RootModel[ - InitializeRequest - | PingRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | SubscribeRequest - | UnsubscribeRequest - | ListPromptsRequest - | GetPromptRequest - | ListToolsRequest - | CallToolRequest - | GetTaskRequest - | GetTaskPayloadRequest - | CancelTaskRequest - | ListTasksRequest - | SetLevelRequest - | CompleteRequest - ] -): - root: ( - InitializeRequest - | PingRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | SubscribeRequest - | UnsubscribeRequest - | ListPromptsRequest - | GetPromptRequest - | ListToolsRequest - | CallToolRequest - | GetTaskRequest - | GetTaskPayloadRequest - | CancelTaskRequest - | ListTasksRequest - | SetLevelRequest - | CompleteRequest - ) +ClientNotification = ( + CancelledNotification + | InitializedNotification + | ProgressNotification + | TaskStatusNotification + | RootsListChangedNotification +) + + +ClientRequest = ( + InitializeRequest + | PingRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscribeRequest + | UnsubscribeRequest + | ListPromptsRequest + | GetPromptRequest + | ListToolsRequest + | CallToolRequest + | GetTaskRequest + | GetTaskPayloadRequest + | CancelTaskRequest + | ListTasksRequest + | SetLevelRequest + | CompleteRequest +) class ElicitRequest(WireModel): @@ -3266,72 +3189,38 @@ class GetPromptResult(WireModel): messages: list[PromptMessage] -class SamplingMessageContentBlock( - RootModel[TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent] -): - root: TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent - - -class ServerNotification( - RootModel[ - CancelledNotification - | ProgressNotification - | ResourceListChangedNotification - | ResourceUpdatedNotification - | PromptListChangedNotification - | ToolListChangedNotification - | TaskStatusNotification - | LoggingMessageNotification - | ElicitationCompleteNotification - ] -): - root: ( - CancelledNotification - | ProgressNotification - | ResourceListChangedNotification - | ResourceUpdatedNotification - | PromptListChangedNotification - | ToolListChangedNotification - | TaskStatusNotification - | LoggingMessageNotification - | ElicitationCompleteNotification - ) - - -class ServerResult( - RootModel[ - Result - | InitializeResult - | ListResourcesResult - | ListResourceTemplatesResult - | ReadResourceResult - | ListPromptsResult - | GetPromptResult - | ListToolsResult - | CallToolResult - | GetTaskResult - | GetTaskPayloadResult - | CancelTaskResult - | ListTasksResult - | CompleteResult - ] -): - root: ( - Result - | InitializeResult - | ListResourcesResult - | ListResourceTemplatesResult - | ReadResourceResult - | ListPromptsResult - | GetPromptResult - | ListToolsResult - | CallToolResult - | GetTaskResult - | GetTaskPayloadResult - | CancelTaskResult - | ListTasksResult - | CompleteResult - ) +SamplingMessageContentBlock = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent + + +ServerNotification = ( + CancelledNotification + | ProgressNotification + | ResourceListChangedNotification + | ResourceUpdatedNotification + | PromptListChangedNotification + | ToolListChangedNotification + | TaskStatusNotification + | LoggingMessageNotification + | ElicitationCompleteNotification +) + + +ServerResult = ( + Result + | InitializeResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | ListPromptsResult + | GetPromptResult + | ListToolsResult + | CallToolResult + | GetTaskResult + | GetTaskPayloadResult + | CancelTaskResult + | ListTasksResult + | CompleteResult +) class CreateMessageResult(WireModel): @@ -3398,28 +3287,16 @@ class SamplingMessage(WireModel): role: Role -class ClientResult( - RootModel[ - Result - | GetTaskResult - | GetTaskPayloadResult - | CancelTaskResult - | ListTasksResult - | CreateMessageResult - | ListRootsResult - | ElicitResult - ] -): - root: ( - Result - | GetTaskResult - | GetTaskPayloadResult - | CancelTaskResult - | ListTasksResult - | CreateMessageResult - | ListRootsResult - | ElicitResult - ) +ClientResult = ( + Result + | GetTaskResult + | GetTaskPayloadResult + | CancelTaskResult + | ListTasksResult + | CreateMessageResult + | ListRootsResult + | ElicitResult +) class CreateMessageRequestParams(WireModel): @@ -3502,25 +3379,13 @@ class CreateMessageRequest(WireModel): params: CreateMessageRequestParams -class ServerRequest( - RootModel[ - PingRequest - | GetTaskRequest - | GetTaskPayloadRequest - | CancelTaskRequest - | ListTasksRequest - | CreateMessageRequest - | ListRootsRequest - | ElicitRequest - ] -): - root: ( - PingRequest - | GetTaskRequest - | GetTaskPayloadRequest - | CancelTaskRequest - | ListTasksRequest - | CreateMessageRequest - | ListRootsRequest - | ElicitRequest - ) +ServerRequest = ( + PingRequest + | GetTaskRequest + | GetTaskPayloadRequest + | CancelTaskRequest + | ListTasksRequest + | CreateMessageRequest + | ListRootsRequest + | ElicitRequest +) diff --git a/src/mcp-types/mcp_types/_v2026_07_28/__init__.py b/src/mcp-types/mcp_types/_v2026_07_28/__init__.py index fb168b3059..869700f272 100644 --- a/src/mcp-types/mcp_types/_v2026_07_28/__init__.py +++ b/src/mcp-types/mcp_types/_v2026_07_28/__init__.py @@ -9,7 +9,8 @@ from typing import Annotated, Any, Literal, Union from mcp_types._wire_base import WireModel -from pydantic import ConfigDict, Field, RootModel +from pydantic import ConfigDict, Field +from typing_extensions import TypeAliasType class BaseMetadata(WireModel): @@ -95,11 +96,10 @@ class Completion(WireModel): """ -class Cursor(RootModel[str]): - root: str - """ - An opaque token used to represent a cursor for pagination. - """ +Cursor = str +""" +An opaque token used to represent a cursor for pagination. +""" class RequestedSchema(WireModel): @@ -415,6 +415,12 @@ class JSONRPCNotification(WireModel): params: dict[str, Any] | None = None +JSONValue = TypeAliasType( + "JSONValue", + Union[dict[str, "JSONValue"], list["JSONValue"], str | int | float | bool | None], +) + + class LegacyTitledEnumSchema(WireModel): """ Use {@link TitledSingleSelectEnumSchema} instead. @@ -436,27 +442,13 @@ class LegacyTitledEnumSchema(WireModel): type: Literal["string"] -class LoggingLevel( - RootModel[ - Literal[ - "alert", - "critical", - "debug", - "emergency", - "error", - "info", - "notice", - "warning", - ] - ] -): - root: Literal["alert", "critical", "debug", "emergency", "error", "info", "notice", "warning"] - """ - The severity of a log message. +LoggingLevel = Literal["alert", "critical", "debug", "emergency", "error", "info", "notice", "warning"] +""" +The severity of a log message. - These map to syslog message severities, as specified in RFC-5424: - https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 - """ +These map to syslog message severities, as specified in RFC-5424: +https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 +""" class MetaObject(WireModel): @@ -624,11 +616,10 @@ class ParseError(WireModel): """ -class ProgressToken(RootModel[str | int]): - root: str | int - """ - A progress token, used to associate progress notifications with the original request. - """ +ProgressToken = str | int +""" +A progress token, used to associate progress notifications with the original request. +""" class PromptArgument(WireModel): @@ -694,11 +685,10 @@ class Request(WireModel): params: dict[str, Any] | None = None -class RequestId(RootModel[str | int]): - root: str | int - """ - A uniquely identifying ID for a request in JSON-RPC. - """ +RequestId = str | int +""" +A uniquely identifying ID for a request in JSON-RPC. +""" class ResourceContents(WireModel): @@ -759,22 +749,20 @@ class ResultMetaObject(WireModel): """ -class ResultType(RootModel[str]): - root: str - """ - Indicates the type of a {@link Result} object, allowing the client to - determine how to parse the response. +ResultType = str +""" +Indicates the type of a {@link Result} object, allowing the client to +determine how to parse the response. - complete - the request completed successfully and the result contains the final content. - input_required - the request requires additional input and the result contains an {@link InputRequiredResult} object with instructions for the client to provide additional input before retrying the original request. - """ +complete - the request completed successfully and the result contains the final content. +input_required - the request requires additional input and the result contains an {@link InputRequiredResult} object with instructions for the client to provide additional input before retrying the original request. +""" -class Role(RootModel[Literal["assistant", "user"]]): - root: Literal["assistant", "user"] - """ - The sender or recipient of messages and data in a conversation. - """ +Role = Literal["assistant", "user"] +""" +The sender or recipient of messages and data in a conversation. +""" class Root(WireModel): @@ -1468,11 +1456,10 @@ class CompleteResultResponse(WireModel): result: CompleteResult -class ElicitRequestParams(RootModel[ElicitRequestFormParams | ElicitRequestURLParams]): - root: ElicitRequestFormParams | ElicitRequestURLParams - """ - The parameters for a request to elicit additional information from the user via the client. - """ +ElicitRequestParams = ElicitRequestFormParams | ElicitRequestURLParams +""" +The parameters for a request to elicit additional information from the user via the client. +""" class EmbeddedResource(WireModel): @@ -1495,22 +1482,13 @@ class EmbeddedResource(WireModel): type: Literal["resource"] -class EnumSchema( - RootModel[ - UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ] -): - root: ( - UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ) +EnumSchema = ( + UntitledSingleSelectEnumSchema + | TitledSingleSelectEnumSchema + | UntitledMultiSelectEnumSchema + | TitledMultiSelectEnumSchema + | LegacyTitledEnumSchema +) class HeaderMismatchError(WireModel): @@ -1553,6 +1531,12 @@ class ImageContent(WireModel): type: Literal["image"] +JSONArray = list[JSONValue] + + +JSONObject = dict[str, JSONValue] + + class JSONRPCErrorResponse(WireModel): """ A response to a request that indicates an error occurred. @@ -1618,8 +1602,7 @@ class ListRootsResult(WireModel): roots: list[Root] -class MultiSelectEnumSchema(RootModel[UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema]): - root: UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema +MultiSelectEnumSchema = UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema class NotificationMetaObject(WireModel): @@ -1680,32 +1663,20 @@ class PaginatedResult(WireModel): """ -class PrimitiveSchemaDefinition( - RootModel[ - StringSchema - | NumberSchema - | BooleanSchema - | UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ] -): - root: ( - StringSchema - | NumberSchema - | BooleanSchema - | UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ) - """ - Restricted schema definitions that only allow primitive types - without nested objects or arrays. - """ +PrimitiveSchemaDefinition = ( + StringSchema + | NumberSchema + | BooleanSchema + | UntitledSingleSelectEnumSchema + | TitledSingleSelectEnumSchema + | UntitledMultiSelectEnumSchema + | TitledMultiSelectEnumSchema + | LegacyTitledEnumSchema +) +""" +Restricted schema definitions that only allow primitive types +without nested objects or arrays. +""" class ProgressNotificationParams(WireModel): @@ -2064,8 +2035,50 @@ class Result(WireModel): """ -class SingleSelectEnumSchema(RootModel[UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema]): - root: UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema +class ServerCapabilities(WireModel): + """ + Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. + """ + + model_config = ConfigDict( + extra="ignore", + ) + completions: JSONObject | None = None + """ + Present if the server supports argument autocompletion suggestions. + """ + experimental: dict[str, JSONObject] | None = None + """ + Experimental, non-standard capabilities that the server supports. + """ + extensions: dict[str, JSONObject] | None = None + """ + Optional MCP extensions that the server supports. Keys are extension identifiers + (e.g., "io.modelcontextprotocol/tasks"), and values are per-extension settings + objects. An empty object indicates support with no settings. + + Keys MUST follow the {@link MetaObject`_meta` key naming rules}, with a + mandatory prefix. + """ + logging: JSONObject | None = None + """ + Present if the server supports sending log messages to the client. + """ + prompts: Prompts | None = None + """ + Present if the server offers any prompt templates. + """ + resources: Resources | None = None + """ + Present if the server offers any resources to read. + """ + tools: Tools | None = None + """ + Present if the server offers any tools to call. + """ + + +SingleSelectEnumSchema = UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema class SubscriptionsAcknowledgedNotificationParams(WireModel): @@ -2237,6 +2250,72 @@ class CancelledNotificationParams(WireModel): """ +class Elicitation(WireModel): + """ + Present if the client supports elicitation from the server. + """ + + model_config = ConfigDict( + extra="ignore", + ) + form: JSONObject | None = None + url: JSONObject | None = None + + +class Sampling(WireModel): + """ + Present if the client supports sampling from an LLM. + """ + + model_config = ConfigDict( + extra="ignore", + ) + context: JSONObject | None = None + """ + Whether the client supports context inclusion via `includeContext` parameter. + If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). + """ + tools: JSONObject | None = None + """ + Whether the client supports tool use via `tools` and `toolChoice` parameters. + """ + + +class ClientCapabilities(WireModel): + """ + Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. + """ + + model_config = ConfigDict( + extra="ignore", + ) + elicitation: Elicitation | None = None + """ + Present if the client supports elicitation from the server. + """ + experimental: dict[str, JSONObject] | None = None + """ + Experimental, non-standard capabilities that the client supports. + """ + extensions: dict[str, JSONObject] | None = None + """ + Optional MCP extensions that the client supports. Keys are extension identifiers + (e.g., "io.modelcontextprotocol/oauth-client-credentials"), and values are + per-extension settings objects. An empty object indicates support with no settings. + + Keys MUST follow the {@link MetaObject`_meta` key naming rules}, with a + mandatory prefix. + """ + roots: dict[str, Any] | None = None + """ + Present if the client supports listing roots. + """ + sampling: Sampling | None = None + """ + Present if the client supports sampling from an LLM. + """ + + class ClientNotification(WireModel): """ This notification is sent by the client to indicate that it is cancelling a request it previously issued. @@ -2256,15 +2335,89 @@ class ClientNotification(WireModel): params: CancelledNotificationParams -class ClientResult(RootModel[Result]): - root: Result +ClientResult = Result +""" +Common result fields. +""" + + +ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource + + +class DiscoverResult(WireModel): """ - Common result fields. + The result returned by the server for a {@link DiscoverRequestserver/discover} request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[ResultMetaObject | None, Field(alias="_meta")] = None + cache_scope: Annotated[Literal["private", "public"], Field(alias="cacheScope")] + """ + Indicates the intended scope of the cached response, analogous to HTTP + `Cache-Control: public` vs `Cache-Control: private`. + + - `"public"`: The response does not contain user-specific data. Any + client or intermediary (e.g., shared gateway, caching proxy) MAY cache + the response and serve it across authorization contexts. + - `"private"`: The response MAY be cached and reused only within the + same authorization context. Caches MUST NOT be shared across + authorization contexts (e.g., a different access token requires a + different cache). + """ + capabilities: ServerCapabilities + """ + The capabilities of the server. + """ + instructions: str | None = None + """ + Natural-language guidance describing the server and its features. + + This can be used by clients to improve an LLM's understanding of + available tools (e.g., by including it in a system prompt). It should + focus on information that helps the model use the server effectively + and should not duplicate information already in tool descriptions. + """ + result_type: Annotated[str, Field(alias="resultType")] + """ + Indicates the type of the result, which allows the client to determine + how to parse the result object. + + Servers implementing this protocol version MUST include this field. + For backward compatibility, when a client receives a result from a + server implementing an earlier protocol version (which does not include + `resultType`), the client MUST treat the absent field as `"complete"`. + """ + supported_versions: Annotated[list[str], Field(alias="supportedVersions")] + """ + MCP Protocol Versions this server supports. The client should choose a + version from this list for use in subsequent requests. + """ + ttl_ms: Annotated[int, Field(alias="ttlMs", ge=0)] + """ + A hint from the server indicating how long (in milliseconds) the + client MAY cache this response before re-fetching. Semantics are + analogous to HTTP Cache-Control max-age. + + - If 0, The response SHOULD be considered immediately stale, + The client MAY re-fetch every time the result is needed. + - If positive, the client SHOULD consider the result fresh for this many + milliseconds after receiving the response. """ -class ContentBlock(RootModel[TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource]): - root: TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource +class DiscoverResultResponse(WireModel): + """ + A successful response from the server for a {@link DiscoverRequestserver/discover} request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + id: RequestId + jsonrpc: Literal["2.0"] + result: DiscoverResult class ElicitRequest(WireModel): @@ -2279,11 +2432,10 @@ class ElicitRequest(WireModel): params: ElicitRequestParams -class EmptyResult(RootModel[Result]): - root: Result - """ - Common result fields. - """ +EmptyResult = Result +""" +Common result fields. +""" class JSONRPCResultResponse(WireModel): @@ -2578,51 +2730,179 @@ class LoggingMessageNotificationParams(WireModel): """ -class ProgressNotification(WireModel): +class Data(WireModel): """ - An out-of-band notification used to inform the receiver of a progress update for a long-running request. + Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). """ model_config = ConfigDict( extra="ignore", ) - jsonrpc: Literal["2.0"] - method: Literal["notifications/progress"] - params: ProgressNotificationParams - - -class PromptMessage(WireModel): + required_capabilities: Annotated[ClientCapabilities, Field(alias="requiredCapabilities")] """ - Describes a message returned as part of a prompt. - - This is similar to {@link SamplingMessage}, but also supports the embedding of - resources from the MCP server. + The capabilities the server requires from the client to process this request. """ + +class Error2(Error): model_config = ConfigDict( extra="ignore", ) - content: ContentBlock - role: Role + code: Literal[-32021] + """ + The error type that occurred. + """ + data: Data + """ + Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). + """ -class ResourceUpdatedNotification(WireModel): +class MissingRequiredClientCapabilityError(WireModel): """ - A notification from the server to the client, informing it that a resource has changed and may need to be read again. This is only sent for resources the client opted in to via the `resourceSubscriptions` field of a {@link SubscriptionsListenRequestsubscriptions/listen} request. + Returned when processing a request requires a capability the client did not + declare in `clientCapabilities`. For HTTP, the response status code MUST be + `400 Bad Request`. """ model_config = ConfigDict( extra="ignore", ) + error: Error2 + id: RequestId | None = None jsonrpc: Literal["2.0"] - method: Literal["notifications/resources/updated"] - params: ResourceUpdatedNotificationParams -class SubscriptionsAcknowledgedNotification(WireModel): +class ProgressNotification(WireModel): """ - Sent by the server to acknowledge that a - {@link SubscriptionsListenRequestsubscriptions/listen} subscription has been + An out-of-band notification used to inform the receiver of a progress update for a long-running request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + jsonrpc: Literal["2.0"] + method: Literal["notifications/progress"] + params: ProgressNotificationParams + + +class PromptMessage(WireModel): + """ + Describes a message returned as part of a prompt. + + This is similar to {@link SamplingMessage}, but also supports the embedding of + resources from the MCP server. + """ + + model_config = ConfigDict( + extra="ignore", + ) + content: ContentBlock + role: Role + + +class RequestMetaObject(WireModel): + """ + Extends {@link MetaObject} with additional request-specific fields. All key naming rules from `MetaObject` apply. + """ + + model_config = ConfigDict( + extra="allow", + ) + io_modelcontextprotocol_client_capabilities: Annotated[ + ClientCapabilities, Field(alias="io.modelcontextprotocol/clientCapabilities") + ] + """ + The client's capabilities for this specific request. Required. + + Capabilities are declared per-request rather than once at initialization; + an empty object means the client supports no optional capabilities. + Servers MUST NOT infer capabilities from prior requests. + """ + io_modelcontextprotocol_client_info: Annotated[ + Implementation | None, Field(alias="io.modelcontextprotocol/clientInfo") + ] = None + """ + Identifies the client software making the request. Clients SHOULD + include this field on every request unless specifically configured not + to do so. + + The {@link Implementation} schema requires `name` and `version`; other + fields are optional. + + The value is self-reported by the client and is not verified by the + protocol. It is intended for display, logging, and debugging. Servers + SHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for + security decisions. + """ + io_modelcontextprotocol_log_level: Annotated[ + LoggingLevel | None, Field(alias="io.modelcontextprotocol/logLevel") + ] = None + """ + The desired log level for this request. Optional. + + If absent, the server MUST NOT send any {@link LoggingMessageNotificationnotifications/message} + notifications for this request. The client opts in to log messages by + explicitly setting a level. Replaces the former `logging/setLevel` RPC. + """ + io_modelcontextprotocol_protocol_version: Annotated[str, Field(alias="io.modelcontextprotocol/protocolVersion")] + """ + The MCP Protocol Version being used for this request. Required. + + For the HTTP transport, this value MUST match the `MCP-Protocol-Version` + header; otherwise the server MUST return a `400 Bad Request`. If the + server does not support the requested version, it MUST return an + {@link UnsupportedProtocolVersionError}. + """ + progress_token: Annotated[ProgressToken | None, Field(alias="progressToken")] = None + """ + If specified, the caller is requesting out-of-band progress notifications for this request (as represented by {@link ProgressNotificationnotifications/progress}). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + """ + + +class RequestParams(WireModel): + """ + Common params for any request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + + +class ResourceRequestParams(WireModel): + """ + Common params for resource-related requests. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + uri: str + """ + The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it. + """ + + +class ResourceUpdatedNotification(WireModel): + """ + A notification from the server to the client, informing it that a resource has changed and may need to be read again. This is only sent for resources the client opted in to via the `resourceSubscriptions` field of a {@link SubscriptionsListenRequestsubscriptions/listen} request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + jsonrpc: Literal["2.0"] + method: Literal["notifications/resources/updated"] + params: ResourceUpdatedNotificationParams + + +class SubscriptionsAcknowledgedNotification(WireModel): + """ + Sent by the server to acknowledge that a + {@link SubscriptionsListenRequestsubscriptions/listen} subscription has been established and to report which notification types it agreed to honor. This notification MUST be the first message the server sends carrying the @@ -2641,6 +2921,23 @@ class SubscriptionsAcknowledgedNotification(WireModel): params: SubscriptionsAcknowledgedNotificationParams +class SubscriptionsListenRequestParams(WireModel): + """ + Parameters for a {@link SubscriptionsListenRequestsubscriptions/listen} request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + notifications: SubscriptionFilter + """ + The notifications the client opts in to on this stream. The server + **MUST NOT** send notification types the client has not explicitly + requested. + """ + + class ToolResultContent(WireModel): """ The result of a tool use, provided by the user back to the assistant. @@ -2750,6 +3047,43 @@ class CancelledNotification(WireModel): params: CancelledNotificationParams +class CompleteRequestParams(WireModel): + """ + Parameters for a `completion/complete` request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + argument: Argument + """ + The argument's information + """ + context: Context | None = None + """ + Additional, optional context for completions + """ + ref: PromptReference | ResourceTemplateReference + + +class DiscoverRequest(WireModel): + """ + A request from the client asking the server to advertise its supported + protocol versions, capabilities, and other metadata. Servers **MUST** + implement `server/discover`. Clients **MAY** call it but are not required + to — version negotiation can also happen inline via per-request `_meta`. + """ + + model_config = ConfigDict( + extra="ignore", + ) + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["server/discover"] + params: RequestParams + + class GetPromptResult(WireModel): """ The result returned by the server for a {@link GetPromptRequestprompts/get} request. @@ -2776,18 +3110,16 @@ class GetPromptResult(WireModel): """ -class JSONRPCMessage(RootModel[JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse]): - root: JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse - """ - Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. - """ +JSONRPCMessage = JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse +""" +Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. +""" -class JSONRPCResponse(RootModel[JSONRPCResultResponse | JSONRPCErrorResponse]): - root: JSONRPCResultResponse | JSONRPCErrorResponse - """ - A response to a request, containing either the result or error. - """ +JSONRPCResponse = JSONRPCResultResponse | JSONRPCErrorResponse +""" +A response to a request, containing either the result or error. +""" class LoggingMessageNotification(WireModel): @@ -2803,34 +3135,65 @@ class LoggingMessageNotification(WireModel): params: LoggingMessageNotificationParams -class SamplingMessageContentBlock( - RootModel[TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent] -): - root: TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent +class PaginatedRequestParams(WireModel): + """ + Common params for paginated requests. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + cursor: str | None = None + """ + An opaque token representing the current pagination position. + If provided, the server should return results starting after this cursor. + """ + +SamplingMessageContentBlock = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent -class ServerNotification( - RootModel[ - CancelledNotification - | ProgressNotification - | ResourceListChangedNotification - | SubscriptionsAcknowledgedNotification - | ResourceUpdatedNotification - | PromptListChangedNotification - | ToolListChangedNotification - | LoggingMessageNotification - ] -): - root: ( - CancelledNotification - | ProgressNotification - | ResourceListChangedNotification - | SubscriptionsAcknowledgedNotification - | ResourceUpdatedNotification - | PromptListChangedNotification - | ToolListChangedNotification - | LoggingMessageNotification + +ServerNotification = ( + CancelledNotification + | ProgressNotification + | ResourceListChangedNotification + | SubscriptionsAcknowledgedNotification + | ResourceUpdatedNotification + | PromptListChangedNotification + | ToolListChangedNotification + | LoggingMessageNotification +) + + +class SubscriptionsListenRequest(WireModel): + """ + Sent from the client to open a long-lived channel for receiving notifications + outside the context of a specific request. Replaces the previous HTTP GET + endpoint and ensures consistent behavior between HTTP and STDIO. + """ + + model_config = ConfigDict( + extra="ignore", + ) + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["subscriptions/listen"] + params: SubscriptionsListenRequestParams + + +class CompleteRequest(WireModel): + """ + A request from the client to the server, to ask for completion options. + """ + + model_config = ConfigDict( + extra="ignore", ) + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["completion/complete"] + params: CompleteRequestParams class CreateMessageResult(WireModel): @@ -2871,43 +3234,34 @@ class CreateMessageResult(WireModel): """ -class InputResponse(RootModel[CreateMessageResult | ListRootsResult | ElicitResult]): - root: CreateMessageResult | ListRootsResult | ElicitResult +InputResponse = CreateMessageResult | ListRootsResult | ElicitResult -class InputResponses(RootModel[dict[str, InputResponse]]): - """ - A map of client responses to server-initiated requests. - Keys correspond to the keys in the {@link InputRequests} map; - values are the client's result for each request. - """ +InputResponses = dict[str, InputResponse] +""" +A map of client responses to server-initiated requests. +Keys correspond to the keys in the {@link InputRequests} map; +values are the client's result for each request. +""" - root: dict[str, InputResponse] - -class SamplingMessage(WireModel): +class ListPromptsRequest(WireModel): """ - Describes a message issued to or received from an LLM API. + Sent from the client to request a list of prompts and prompt templates the server has. """ model_config = ConfigDict( extra="ignore", ) - meta: Annotated[MetaObject | None, Field(alias="_meta")] = None - content: ( - TextContent - | ImageContent - | AudioContent - | ToolUseContent - | ToolResultContent - | list[SamplingMessageContentBlock] - ) - role: Role + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["prompts/list"] + params: PaginatedRequestParams -class CallToolRequest(WireModel): +class ListResourceTemplatesRequest(WireModel): """ - Used by the client to invoke a tool provided by the server. + Sent from the client to request a list of resource templates the server has. """ model_config = ConfigDict( @@ -2915,34 +3269,27 @@ class CallToolRequest(WireModel): ) id: RequestId jsonrpc: Literal["2.0"] - method: Literal["tools/call"] - params: CallToolRequestParams + method: Literal["resources/templates/list"] + params: PaginatedRequestParams -class CallToolRequestParams(WireModel): +class ListResourcesRequest(WireModel): """ - Parameters for a `tools/call` request. + Sent from the client to request a list of resources the server has. """ model_config = ConfigDict( extra="ignore", ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - arguments: dict[str, Any] | None = None - """ - Arguments to use for the tool call. - """ - input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None - name: str - """ - The name of the tool. - """ - request_state: Annotated[str | None, Field(alias="requestState")] = None + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["resources/list"] + params: PaginatedRequestParams -class CallToolResultResponse(WireModel): +class ListToolsRequest(WireModel): """ - A successful response from the server for a {@link CallToolRequesttools/call} request. + Sent from the client to request a list of tools the server has. """ model_config = ConfigDict( @@ -2950,119 +3297,76 @@ class CallToolResultResponse(WireModel): ) id: RequestId jsonrpc: Literal["2.0"] - result: InputRequiredResult | CallToolResult - + method: Literal["tools/list"] + params: PaginatedRequestParams -class Elicitation(WireModel): - """ - Present if the client supports elicitation from the server. - """ +class PaginatedRequest(WireModel): model_config = ConfigDict( extra="ignore", ) - form: JSONObject | None = None - url: JSONObject | None = None + id: RequestId + jsonrpc: Literal["2.0"] + method: str + params: PaginatedRequestParams -class Sampling(WireModel): +class ReadResourceRequestParams(WireModel): """ - Present if the client supports sampling from an LLM. + Parameters for a `resources/read` request. """ model_config = ConfigDict( extra="ignore", ) - context: JSONObject | None = None - """ - Whether the client supports context inclusion via `includeContext` parameter. - If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). - """ - tools: JSONObject | None = None + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None + request_state: Annotated[str | None, Field(alias="requestState")] = None + uri: str """ - Whether the client supports tool use via `tools` and `toolChoice` parameters. + The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it. """ -class ClientCapabilities(WireModel): +class SamplingMessage(WireModel): """ - Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. + Describes a message issued to or received from an LLM API. """ model_config = ConfigDict( extra="ignore", ) - elicitation: Elicitation | None = None - """ - Present if the client supports elicitation from the server. - """ - experimental: dict[str, JSONObject] | None = None - """ - Experimental, non-standard capabilities that the client supports. - """ - extensions: dict[str, JSONObject] | None = None - """ - Optional MCP extensions that the client supports. Keys are extension identifiers - (e.g., "io.modelcontextprotocol/oauth-client-credentials"), and values are - per-extension settings objects. An empty object indicates support with no settings. - - Keys MUST follow the {@link MetaObject`_meta` key naming rules}, with a - mandatory prefix. - """ - roots: dict[str, Any] | None = None - """ - Present if the client supports listing roots. - """ - sampling: Sampling | None = None - """ - Present if the client supports sampling from an LLM. - """ - - -class CompleteRequest(WireModel): - """ - A request from the client to the server, to ask for completion options. - """ - - model_config = ConfigDict( - extra="ignore", + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + content: ( + TextContent + | ImageContent + | AudioContent + | ToolUseContent + | ToolResultContent + | list[SamplingMessageContentBlock] ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["completion/complete"] - params: CompleteRequestParams + role: Role -class CompleteRequestParams(WireModel): +class CallToolRequestParams(WireModel): """ - Parameters for a `completion/complete` request. + Parameters for a `tools/call` request. """ model_config = ConfigDict( extra="ignore", ) meta: Annotated[RequestMetaObject, Field(alias="_meta")] - argument: Argument - """ - The argument's information - """ - context: Context | None = None + arguments: dict[str, Any] | None = None """ - Additional, optional context for completions + Arguments to use for the tool call. """ - ref: PromptReference | ResourceTemplateReference - - -class CreateMessageRequest(WireModel): + input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None + name: str """ - A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. + The name of the tool. """ - - model_config = ConfigDict( - extra="ignore", - ) - method: Literal["sampling/createMessage"] - params: CreateMessageRequestParams + request_state: Annotated[str | None, Field(alias="requestState")] = None class CreateMessageRequestParams(WireModel): @@ -3119,89 +3423,39 @@ class CreateMessageRequestParams(WireModel): """ -class DiscoverRequest(WireModel): - """ - A request from the client asking the server to advertise its supported - protocol versions, capabilities, and other metadata. Servers **MUST** - implement `server/discover`. Clients **MAY** call it but are not required - to — version negotiation can also happen inline via per-request `_meta`. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["server/discover"] - params: RequestParams - - -class DiscoverResult(WireModel): +class GetPromptRequestParams(WireModel): """ - The result returned by the server for a {@link DiscoverRequestserver/discover} request. + Parameters for a `prompts/get` request. """ model_config = ConfigDict( extra="ignore", ) - meta: Annotated[ResultMetaObject | None, Field(alias="_meta")] = None - cache_scope: Annotated[Literal["private", "public"], Field(alias="cacheScope")] - """ - Indicates the intended scope of the cached response, analogous to HTTP - `Cache-Control: public` vs `Cache-Control: private`. - - - `"public"`: The response does not contain user-specific data. Any - client or intermediary (e.g., shared gateway, caching proxy) MAY cache - the response and serve it across authorization contexts. - - `"private"`: The response MAY be cached and reused only within the - same authorization context. Caches MUST NOT be shared across - authorization contexts (e.g., a different access token requires a - different cache). - """ - capabilities: ServerCapabilities - """ - The capabilities of the server. + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + arguments: dict[str, str] | None = None """ - instructions: str | None = None + Arguments to use for templating the prompt. """ - Natural-language guidance describing the server and its features. - - This can be used by clients to improve an LLM's understanding of - available tools (e.g., by including it in a system prompt). It should - focus on information that helps the model use the server effectively - and should not duplicate information already in tool descriptions. + input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None + name: str """ - result_type: Annotated[str, Field(alias="resultType")] + The name of the prompt or prompt template. """ - Indicates the type of the result, which allows the client to determine - how to parse the result object. + request_state: Annotated[str | None, Field(alias="requestState")] = None - Servers implementing this protocol version MUST include this field. - For backward compatibility, when a client receives a result from a - server implementing an earlier protocol version (which does not include - `resultType`), the client MUST treat the absent field as `"complete"`. - """ - supported_versions: Annotated[list[str], Field(alias="supportedVersions")] - """ - MCP Protocol Versions this server supports. The client should choose a - version from this list for use in subsequent requests. - """ - ttl_ms: Annotated[int, Field(alias="ttlMs", ge=0)] - """ - A hint from the server indicating how long (in milliseconds) the - client MAY cache this response before re-fetching. Semantics are - analogous to HTTP Cache-Control max-age. - - If 0, The response SHOULD be considered immediately stale, - The client MAY re-fetch every time the result is needed. - - If positive, the client SHOULD consider the result fresh for this many - milliseconds after receiving the response. - """ +class InputResponseRequestParams(WireModel): + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None + request_state: Annotated[str | None, Field(alias="requestState")] = None -class DiscoverResultResponse(WireModel): +class ReadResourceRequest(WireModel): """ - A successful response from the server for a {@link DiscoverRequestserver/discover} request. + Sent from the client to the server, to read a specific resource URI. """ model_config = ConfigDict( @@ -3209,12 +3463,13 @@ class DiscoverResultResponse(WireModel): ) id: RequestId jsonrpc: Literal["2.0"] - result: DiscoverResult + method: Literal["resources/read"] + params: ReadResourceRequestParams -class GetPromptRequest(WireModel): +class CallToolRequest(WireModel): """ - Used by the client to get a prompt provided by the server. + Used by the client to invoke a tool provided by the server. """ model_config = ConfigDict( @@ -3222,34 +3477,25 @@ class GetPromptRequest(WireModel): ) id: RequestId jsonrpc: Literal["2.0"] - method: Literal["prompts/get"] - params: GetPromptRequestParams + method: Literal["tools/call"] + params: CallToolRequestParams -class GetPromptRequestParams(WireModel): +class CreateMessageRequest(WireModel): """ - Parameters for a `prompts/get` request. + A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. """ model_config = ConfigDict( extra="ignore", ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - arguments: dict[str, str] | None = None - """ - Arguments to use for templating the prompt. - """ - input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None - name: str - """ - The name of the prompt or prompt template. - """ - request_state: Annotated[str | None, Field(alias="requestState")] = None + method: Literal["sampling/createMessage"] + params: CreateMessageRequestParams -class GetPromptResultResponse(WireModel): +class GetPromptRequest(WireModel): """ - A successful response from the server for a {@link GetPromptRequestprompts/get} request. + Used by the client to get a prompt provided by the server. """ model_config = ConfigDict( @@ -3257,7 +3503,18 @@ class GetPromptResultResponse(WireModel): ) id: RequestId jsonrpc: Literal["2.0"] - result: InputRequiredResult | GetPromptResult + method: Literal["prompts/get"] + params: GetPromptRequestParams + + +InputRequest = CreateMessageRequest | ListRootsRequest | ElicitRequest + + +InputRequests = dict[str, InputRequest] +""" +A map of server-initiated requests that the client must fulfill. +Keys are server-assigned identifiers; values are the request objects. +""" class InputRequiredResult(WireModel): @@ -3286,171 +3543,6 @@ class InputRequiredResult(WireModel): """ -class InputResponseRequestParams(WireModel): - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None - request_state: Annotated[str | None, Field(alias="requestState")] = None - - -class ListPromptsRequest(WireModel): - """ - Sent from the client to request a list of prompts and prompt templates the server has. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["prompts/list"] - params: PaginatedRequestParams - - -class ListResourceTemplatesRequest(WireModel): - """ - Sent from the client to request a list of resource templates the server has. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["resources/templates/list"] - params: PaginatedRequestParams - - -class ListResourcesRequest(WireModel): - """ - Sent from the client to request a list of resources the server has. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["resources/list"] - params: PaginatedRequestParams - - -class ListToolsRequest(WireModel): - """ - Sent from the client to request a list of tools the server has. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["tools/list"] - params: PaginatedRequestParams - - -class Data(WireModel): - """ - Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). - """ - - model_config = ConfigDict( - extra="ignore", - ) - required_capabilities: Annotated[ClientCapabilities, Field(alias="requiredCapabilities")] - """ - The capabilities the server requires from the client to process this request. - """ - - -class Error2(Error): - model_config = ConfigDict( - extra="ignore", - ) - code: Literal[-32021] - """ - The error type that occurred. - """ - data: Data - """ - Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). - """ - - -class MissingRequiredClientCapabilityError(WireModel): - """ - Returned when processing a request requires a capability the client did not - declare in `clientCapabilities`. For HTTP, the response status code MUST be - `400 Bad Request`. - """ - - model_config = ConfigDict( - extra="ignore", - ) - error: Error2 - id: RequestId | None = None - jsonrpc: Literal["2.0"] - - -class PaginatedRequest(WireModel): - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: str - params: PaginatedRequestParams - - -class PaginatedRequestParams(WireModel): - """ - Common params for paginated requests. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - cursor: str | None = None - """ - An opaque token representing the current pagination position. - If provided, the server should return results starting after this cursor. - """ - - -class ReadResourceRequest(WireModel): - """ - Sent from the client to the server, to read a specific resource URI. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["resources/read"] - params: ReadResourceRequestParams - - -class ReadResourceRequestParams(WireModel): - """ - Parameters for a `resources/read` request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None - request_state: Annotated[str | None, Field(alias="requestState")] = None - uri: str - """ - The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it. - """ - - class ReadResourceResultResponse(WireModel): """ A successful response from the server for a {@link ReadResourceRequestresources/read} request. @@ -3464,139 +3556,52 @@ class ReadResourceResultResponse(WireModel): result: InputRequiredResult | ReadResourceResult -class RequestMetaObject(WireModel): - """ - Extends {@link MetaObject} with additional request-specific fields. All key naming rules from `MetaObject` apply. - """ +ServerResult = ( + Result + | InputRequiredResult + | DiscoverResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | SubscriptionsListenResult + | ListPromptsResult + | GetPromptResult + | ListToolsResult + | CallToolResult + | CompleteResult +) - model_config = ConfigDict( - extra="allow", - ) - io_modelcontextprotocol_client_capabilities: Annotated[ - ClientCapabilities, Field(alias="io.modelcontextprotocol/clientCapabilities") - ] - """ - The client's capabilities for this specific request. Required. - Capabilities are declared per-request rather than once at initialization; - an empty object means the client supports no optional capabilities. - Servers MUST NOT infer capabilities from prior requests. - """ - io_modelcontextprotocol_client_info: Annotated[ - Implementation | None, Field(alias="io.modelcontextprotocol/clientInfo") - ] = None - """ - Identifies the client software making the request. Clients SHOULD - include this field on every request unless specifically configured not - to do so. - - The {@link Implementation} schema requires `name` and `version`; other - fields are optional. - - The value is self-reported by the client and is not verified by the - protocol. It is intended for display, logging, and debugging. Servers - SHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for - security decisions. - """ - io_modelcontextprotocol_log_level: Annotated[ - LoggingLevel | None, Field(alias="io.modelcontextprotocol/logLevel") - ] = None - """ - The desired log level for this request. Optional. - - If absent, the server MUST NOT send any {@link LoggingMessageNotificationnotifications/message} - notifications for this request. The client opts in to log messages by - explicitly setting a level. Replaces the former `logging/setLevel` RPC. - """ - io_modelcontextprotocol_protocol_version: Annotated[str, Field(alias="io.modelcontextprotocol/protocolVersion")] - """ - The MCP Protocol Version being used for this request. Required. - - For the HTTP transport, this value MUST match the `MCP-Protocol-Version` - header; otherwise the server MUST return a `400 Bad Request`. If the - server does not support the requested version, it MUST return an - {@link UnsupportedProtocolVersionError}. - """ - progress_token: Annotated[ProgressToken | None, Field(alias="progressToken")] = None - """ - If specified, the caller is requesting out-of-band progress notifications for this request (as represented by {@link ProgressNotificationnotifications/progress}). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. - """ - - -class RequestParams(WireModel): - """ - Common params for any request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - - -class ResourceRequestParams(WireModel): +class CallToolResultResponse(WireModel): """ - Common params for resource-related requests. + A successful response from the server for a {@link CallToolRequesttools/call} request. """ model_config = ConfigDict( extra="ignore", ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - uri: str - """ - The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it. - """ - - -class ServerCapabilities(WireModel): - """ - Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. - """ + id: RequestId + jsonrpc: Literal["2.0"] + result: InputRequiredResult | CallToolResult - model_config = ConfigDict( - extra="ignore", - ) - completions: JSONObject | None = None - """ - Present if the server supports argument autocompletion suggestions. - """ - experimental: dict[str, JSONObject] | None = None - """ - Experimental, non-standard capabilities that the server supports. - """ - extensions: dict[str, JSONObject] | None = None - """ - Optional MCP extensions that the server supports. Keys are extension identifiers - (e.g., "io.modelcontextprotocol/tasks"), and values are per-extension settings - objects. An empty object indicates support with no settings. - Keys MUST follow the {@link MetaObject`_meta` key naming rules}, with a - mandatory prefix. - """ - logging: JSONObject | None = None - """ - Present if the server supports sending log messages to the client. - """ - prompts: Prompts | None = None - """ - Present if the server offers any prompt templates. - """ - resources: Resources | None = None - """ - Present if the server offers any resources to read. - """ - tools: Tools | None = None - """ - Present if the server offers any tools to call. - """ +ClientRequest = ( + DiscoverRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscriptionsListenRequest + | ListPromptsRequest + | GetPromptRequest + | ListToolsRequest + | CallToolRequest + | CompleteRequest +) -class SubscriptionsListenRequest(WireModel): +class GetPromptResultResponse(WireModel): """ - Sent from the client to open a long-lived channel for receiving notifications - outside the context of a specific request. Replaces the previous HTTP GET - endpoint and ensures consistent behavior between HTTP and STDIO. + A successful response from the server for a {@link GetPromptRequestprompts/get} request. """ model_config = ConfigDict( @@ -3604,143 +3609,9 @@ class SubscriptionsListenRequest(WireModel): ) id: RequestId jsonrpc: Literal["2.0"] - method: Literal["subscriptions/listen"] - params: SubscriptionsListenRequestParams - - -class SubscriptionsListenRequestParams(WireModel): - """ - Parameters for a {@link SubscriptionsListenRequestsubscriptions/listen} request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - notifications: SubscriptionFilter - """ - The notifications the client opts in to on this stream. The server - **MUST NOT** send notification types the client has not explicitly - requested. - """ - - -class InputRequest(RootModel[CreateMessageRequest | ListRootsRequest | ElicitRequest]): - root: CreateMessageRequest | ListRootsRequest | ElicitRequest - - -class ServerResult( - RootModel[ - Result - | InputRequiredResult - | DiscoverResult - | ListResourcesResult - | ListResourceTemplatesResult - | ReadResourceResult - | SubscriptionsListenResult - | ListPromptsResult - | GetPromptResult - | ListToolsResult - | CallToolResult - | CompleteResult - ] -): - root: ( - Result - | InputRequiredResult - | DiscoverResult - | ListResourcesResult - | ListResourceTemplatesResult - | ReadResourceResult - | SubscriptionsListenResult - | ListPromptsResult - | GetPromptResult - | ListToolsResult - | CallToolResult - | CompleteResult - ) - - -class ClientRequest( - RootModel[ - DiscoverRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | SubscriptionsListenRequest - | ListPromptsRequest - | GetPromptRequest - | ListToolsRequest - | CallToolRequest - | CompleteRequest - ] -): - root: ( - DiscoverRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | SubscriptionsListenRequest - | ListPromptsRequest - | GetPromptRequest - | ListToolsRequest - | CallToolRequest - | CompleteRequest - ) - - -class InputRequests(RootModel[dict[str, InputRequest]]): - """ - A map of server-initiated requests that the client must fulfill. - Keys are server-assigned identifiers; values are the request objects. - """ - - root: dict[str, InputRequest] - - -class JSONArray(RootModel[list["JSONValue"]]): - root: list["JSONValue"] - - -class JSONObject(RootModel[dict[str, "JSONValue"]]): - root: dict[str, "JSONValue"] - - -class JSONValue(RootModel[Union[JSONObject, list["JSONValue"], str | int | float | bool | None]]): - root: Union[JSONObject, list["JSONValue"], str | int | float | bool | None] + result: InputRequiredResult | GetPromptResult AnyCallToolResult = CallToolResult | InputRequiredResult AnyGetPromptResult = GetPromptResult | InputRequiredResult AnyReadResourceResult = ReadResourceResult | InputRequiredResult - - -CallToolRequest.model_rebuild() -CallToolRequestParams.model_rebuild() -CallToolResultResponse.model_rebuild() -Elicitation.model_rebuild() -Sampling.model_rebuild() -ClientCapabilities.model_rebuild() -CompleteRequest.model_rebuild() -CompleteRequestParams.model_rebuild() -CreateMessageRequest.model_rebuild() -CreateMessageRequestParams.model_rebuild() -DiscoverRequest.model_rebuild() -DiscoverResult.model_rebuild() -GetPromptRequest.model_rebuild() -GetPromptRequestParams.model_rebuild() -GetPromptResultResponse.model_rebuild() -InputRequiredResult.model_rebuild() -InputResponseRequestParams.model_rebuild() -ListPromptsRequest.model_rebuild() -ListResourceTemplatesRequest.model_rebuild() -ListResourcesRequest.model_rebuild() -ListToolsRequest.model_rebuild() -PaginatedRequest.model_rebuild() -PaginatedRequestParams.model_rebuild() -ReadResourceRequest.model_rebuild() -ReadResourceRequestParams.model_rebuild() -ServerCapabilities.model_rebuild() -SubscriptionsListenRequest.model_rebuild() -JSONArray.model_rebuild() -JSONObject.model_rebuild() diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index aa2406dc7f..a618112153 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -80,19 +80,12 @@ def _clamp_inbound_ttl(raw: dict[str, Any]) -> None: @cache def _wire_fields(target: type[BaseModel] | UnionType) -> frozenset[str]: - """Top-level wire keys `target` declares (its members', for a union). - - A `RootModel` row (e.g. an empty result carried as `RootModel[Result]`) - reports its wrapped type's keys, not the pydantic-internal `root`. - """ + """Top-level wire keys `target` declares (its members', for a union).""" members: tuple[Any, ...] = get_args(target) if isinstance(target, UnionType) else (target,) models = [m for m in members if isinstance(m, type) and issubclass(m, BaseModel)] fields: set[str] = set() for model in models: - if getattr(model, "__pydantic_root_model__", False): # a RootModel wrapper row - fields |= _wire_fields(model.model_fields["root"].annotation) - else: - fields.update(field.alias or name for name, field in model.model_fields.items()) + fields.update(field.alias or name for name, field in model.model_fields.items()) return frozenset(fields) diff --git a/src/mcp/server/auth/handlers/authorize.py b/src/mcp/server/auth/handlers/authorize.py index 5cf93cf8c2..91955eb937 100644 --- a/src/mcp/server/auth/handlers/authorize.py +++ b/src/mcp/server/auth/handlers/authorize.py @@ -2,8 +2,7 @@ from dataclasses import dataclass from typing import Any, Literal -# TODO(Marcelo): We should drop the `RootModel`. -from pydantic import AnyUrl, BaseModel, Field, RootModel, ValidationError # noqa: TID251 +from pydantic import AnyUrl, BaseModel, Field, TypeAdapter, ValidationError from starlette.datastructures import FormData, QueryParams from starlette.requests import Request from starlette.responses import RedirectResponse, Response @@ -20,6 +19,7 @@ from mcp.shared.auth import InvalidRedirectUriError, InvalidScopeError logger = logging.getLogger(__name__) +_ANY_URL_ADAPTER = TypeAdapter(AnyUrl) class AuthorizationRequest(BaseModel): @@ -59,10 +59,6 @@ def best_effort_extract_string(key: str, params: None | FormData | QueryParams) return None -class AnyUrlModel(RootModel[AnyUrl]): - root: AnyUrl - - @dataclass class AuthorizationHandler: provider: OAuthAuthorizationServerProvider[Any, Any, Any] @@ -107,9 +103,9 @@ async def error_response( if params is not None and "redirect_uri" not in params: raw_redirect_uri = None else: - raw_redirect_uri = AnyUrlModel.model_validate( + raw_redirect_uri = _ANY_URL_ADAPTER.validate_python( best_effort_extract_string("redirect_uri", params) - ).root + ) redirect_uri = client.validate_redirect_uri(raw_redirect_uri) except (ValidationError, InvalidRedirectUriError): # if the redirect URI is invalid, ignore it & just return the diff --git a/src/mcp/server/elicitation.py b/src/mcp/server/elicitation.py index 26425c1338..650378886b 100644 --- a/src/mcp/server/elicitation.py +++ b/src/mcp/server/elicitation.py @@ -8,7 +8,7 @@ # Internal surface package; imported as the gate's source of truth for spec-valid property schemas. from mcp_types._v2025_11_25 import PrimitiveSchemaDefinition -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel, TypeAdapter, ValidationError from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue from pydantic_core import core_schema from typing_extensions import TypeAliasType @@ -16,6 +16,7 @@ from mcp.server.session import ServerSession ElicitSchemaModelT = TypeVar("ElicitSchemaModelT", bound=BaseModel) +_PRIMITIVE_SCHEMA_ADAPTER = TypeAdapter[PrimitiveSchemaDefinition](PrimitiveSchemaDefinition) class AcceptedElicitation(BaseModel, Generic[ElicitSchemaModelT]): @@ -79,7 +80,7 @@ def _validate_rendered_properties(json_schema: dict[str, Any]) -> None: """ for field_name, prop in json_schema.get("properties", {}).items(): try: - PrimitiveSchemaDefinition.model_validate(prop) + _PRIMITIVE_SCHEMA_ADAPTER.validate_python(prop) except ValidationError: raise TypeError( f"Elicitation schema field {field_name!r} rendered as {prop!r}, " diff --git a/src/mcp/server/mcpserver/utilities/func_metadata.py b/src/mcp/server/mcpserver/utilities/func_metadata.py index cc32433568..0ffac07c4e 100644 --- a/src/mcp/server/mcpserver/utilities/func_metadata.py +++ b/src/mcp/server/mcpserver/utilities/func_metadata.py @@ -114,15 +114,15 @@ def model_dump_one_level(self) -> dict[str, Any]: class FuncMetadata(BaseModel): """A tool function's argument model plus, for structured output, the published `output_schema` and the - `output_model` results are validated against. Constructing one with an `output_model` and no schema derives - the schema (and raises if pydantic can't); the fields are read live, so clearing or reassigning them later + `output_model` type annotation results are validated against. Constructing one with an `output_model` and no + schema derives the schema (and raises if pydantic can't); the fields are read live, so reassigning them later takes effect on the next call.""" arg_model: Annotated[type[ArgModelBase], WithJsonSchema(None)] output_schema: dict[str, Any] | None = None - output_model: Annotated[type[Any], WithJsonSchema(None)] | None = None + output_model: Annotated[Any, WithJsonSchema(None)] = None wrap_output: bool = False - _adapter: tuple[type[Any], TypeAdapter[Any]] | None = PrivateAttr(default=None) + _adapter: tuple[Any, TypeAdapter[Any]] | None = PrivateAttr(default=None) def model_post_init(self, context: Any, /) -> None: if self.output_model is not None and self.output_schema is None: @@ -130,7 +130,7 @@ def model_post_init(self, context: Any, /) -> None: schema = self._output_adapter(self.output_model).json_schema(schema_generator=StrictJsonSchema) self.output_schema = _inline_root_ref(schema) - def _output_adapter(self, output_model: type[Any]) -> TypeAdapter[Any]: + def _output_adapter(self, output_model: Any) -> TypeAdapter[Any]: """The validator/serializer for `output_model`, built once and rebuilt only if the field is reassigned.""" if self._adapter is None or self._adapter[0] is not output_model: self._adapter = (output_model, TypeAdapter(_pydantic_readable_typeddict(output_model))) @@ -477,7 +477,7 @@ def func_metadata( return FuncMetadata(arg_model=arguments_model) -def _create_output_model(original_annotation: Any, type_expr: Any, func_name: str) -> tuple[type[Any] | None, bool]: +def _create_output_model(original_annotation: Any, type_expr: Any, func_name: str) -> tuple[Any, bool]: """Pick the type structured output is validated against for the given return annotation. Args: @@ -491,7 +491,7 @@ def _create_output_model(original_annotation: Any, type_expr: Any, func_name: st Model is None if the type cannot carry structured output. wrap_output is True if the result needs to be wrapped in {"result": ...} """ - model: type[Any] | None = None + model: Any = None wrap_output = False # First handle special case: None @@ -503,13 +503,12 @@ def _create_output_model(original_annotation: Any, type_expr: Any, func_name: st elif isinstance(type_expr, GenericAlias): origin = get_origin(type_expr) - # Special case: dict with string keys can use RootModel if origin is dict: args = get_args(type_expr) if len(args) == 2 and args[0] is str: # TODO: should we use the original annotation? We are losing any potential `Annotated` # metadata for Pydantic here: - model = _create_dict_model(func_name, type_expr) + model = Annotated[type_expr, Field(title=f"{func_name}DictOutput")] else: # dict with non-str keys needs wrapping model = _create_wrapped_model(func_name, original_annotation) @@ -622,21 +621,6 @@ def _create_wrapped_model(func_name: str, annotation: Any) -> type[BaseModel]: return create_model(model_name, result=annotation) -def _create_dict_model(func_name: str, dict_annotation: Any) -> type[BaseModel]: - """Create a RootModel for dict[str, T] types.""" - # TODO(Marcelo): We should not rely on RootModel for this. - from pydantic import RootModel # noqa: TID251 - - class DictModel(RootModel[dict_annotation]): - pass - - # Give it a meaningful name - DictModel.__name__ = f"{func_name}DictOutput" - DictModel.__qualname__ = f"{func_name}DictOutput" - - return DictModel - - def _convert_to_content(result: Any) -> list[ContentBlock]: """Convert a result to a sequence of content objects. diff --git a/tests/types/test_methods.py b/tests/types/test_methods.py index bd0d1f9b5b..d38dfa0993 100644 --- a/tests/types/test_methods.py +++ b/tests/types/test_methods.py @@ -454,11 +454,20 @@ def test_elicit_result_surface_accepts_null_content_values_at_every_version_that surface.model_validate({"action": "accept", "content": {"name": "x", "age": None}}) -def test_server_capabilities_extensions_with_null_json_value_round_trips_at_2026(): - """Spec `JSONValue` includes `null`; the ts->json render dropped it from the vendored schema.""" - raw: dict[str, Any] = {"extensions": {"x": {"k": None}}} - parsed = v2026.ServerCapabilities.model_validate(raw) - assert parsed.model_dump(mode="json")["extensions"] == {"x": {"k": None}} +def test_discovery_capabilities_preserve_nested_json_values() -> None: + """Spec `JSONValue` permits nested containers and every JSON scalar, including numbers and null.""" + extensions = {"x": {"nested": [None, True, 1, 1.5, "text", {"child": [False]}]}} + raw = { + "supportedVersions": ["2026-07-28"], + "capabilities": {"extensions": extensions}, + "resultType": "complete", + "ttlMs": 0, + "cacheScope": "private", + } + parsed = methods.parse_server_result("server/discover", "2026-07-28", raw) + assert isinstance(parsed, types.DiscoverResult) + assert parsed.capabilities.extensions == extensions + assert methods.serialize_server_result("server/discover", "2026-07-28", raw) == raw def test_elicit_request_surface_accepts_loose_property_schemas(): diff --git a/tests/types/test_parity.py b/tests/types/test_parity.py index 3531992141..fe8ac758c1 100644 --- a/tests/types/test_parity.py +++ b/tests/types/test_parity.py @@ -153,8 +153,6 @@ def _surface_classes(module: ModuleType) -> list[tuple[str, type[BaseModel]]]: continue if obj.__module__ != module.__name__ or obj.__name__ != name: continue # re-export or alias to another model - if getattr(obj, "__pydantic_root_model__", False): - continue # RootModel alias wrapper; the field-subset property does not apply out.append((f"{tail}.{name}", obj)) return out