diff --git a/docs/servers/structured-output.md b/docs/servers/structured-output.md index 792bcb0c..fbe09ba8 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 d12cb748..536ef527 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 a9887dd4..9e0279ff 100644 --- a/scripts/gen_surface_types.py +++ b/scripts/gen_surface_types.py @@ -5,13 +5,14 @@ writes the result to `src/mcp-types/mcp_types/_v/__init__.py` (the 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 b5f5b967..5ffd15f0 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 __future__ import annotations 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 - ) +ClientNotification = ( + 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 - ) +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 +SamplingMessageContentBlock = 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 - ) +ServerNotification = ( + 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 - ) +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 fb168b30..869700f2 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 __future__ import annotations 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,6 +2730,49 @@ class LoggingMessageNotificationParams(WireModel): """ +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 ProgressNotification(WireModel): """ An out-of-band notification used to inform the receiver of a progress update for a long-running request. @@ -2606,6 +2801,91 @@ class PromptMessage(WireModel): 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. @@ -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. + """ - -class ServerNotification( - RootModel[ - CancelledNotification - | ProgressNotification - | ResourceListChangedNotification - | SubscriptionsAcknowledgedNotification - | ResourceUpdatedNotification - | PromptListChangedNotification - | ToolListChangedNotification - | LoggingMessageNotification - ] -): - root: ( - CancelledNotification - | ProgressNotification - | ResourceListChangedNotification - | SubscriptionsAcknowledgedNotification - | ResourceUpdatedNotification - | PromptListChangedNotification - | ToolListChangedNotification - | LoggingMessageNotification + 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 + + +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,428 +3234,15 @@ 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. - """ - - root: dict[str, InputResponse] - - -class SamplingMessage(WireModel): - """ - Describes a message issued to or received from an LLM API. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[MetaObject | None, Field(alias="_meta")] = None - content: ( - TextContent - | ImageContent - | AudioContent - | ToolUseContent - | ToolResultContent - | list[SamplingMessageContentBlock] - ) - role: Role - - -class CallToolRequest(WireModel): - """ - Used by the client to invoke a tool provided by the server. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["tools/call"] - params: CallToolRequestParams - - -class CallToolRequestParams(WireModel): - """ - Parameters for a `tools/call` request. - """ - - 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 - - -class CallToolResultResponse(WireModel): - """ - A successful response from the server for a {@link CallToolRequesttools/call} request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - result: InputRequiredResult | CallToolResult - - -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 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 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 CreateMessageRequest(WireModel): - """ - 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", - ) - method: Literal["sampling/createMessage"] - params: CreateMessageRequestParams - - -class CreateMessageRequestParams(WireModel): - """ - Parameters for a `sampling/createMessage` request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - include_context: Annotated[ - Literal["allServers", "none", "thisServer"] | None, - Field(alias="includeContext"), - ] = None - """ - A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. - The client MAY ignore this request. - - Default is `"none"`. The values `"thisServer"` and `"allServers"` are deprecated (SEP-2596): servers SHOULD - omit this field or use `"none"`, and SHOULD only use the deprecated values if the client declares - {@link ClientCapabilities.sampling.context}. - """ - max_tokens: Annotated[int, Field(alias="maxTokens")] - """ - The requested maximum number of tokens to sample (to prevent runaway completions). - - The client MAY choose to sample fewer tokens than the requested maximum. - """ - messages: list[SamplingMessage] - metadata: JSONObject | None = None - """ - Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. - """ - model_preferences: Annotated[ModelPreferences | None, Field(alias="modelPreferences")] = None - """ - The server's preferences for which model to select. The client MAY ignore these preferences. - """ - stop_sequences: Annotated[list[str] | None, Field(alias="stopSequences")] = None - system_prompt: Annotated[str | None, Field(alias="systemPrompt")] = None - """ - An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. - """ - temperature: float | None = None - tool_choice: Annotated[ToolChoice | None, Field(alias="toolChoice")] = None - """ - Controls how the model uses tools. - The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. - Default is `{ mode: "auto" }`. - """ - tools: list[Tool] | None = None - """ - Tools that the model may use during generation. - The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. - """ - - -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): - """ - 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 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 GetPromptRequest(WireModel): - """ - Used by the client to get a prompt provided by the server. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["prompts/get"] - params: GetPromptRequestParams - - -class GetPromptRequestParams(WireModel): - """ - Parameters for a `prompts/get` request. - """ - - 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 - - -class GetPromptResultResponse(WireModel): - """ - A successful response from the server for a {@link GetPromptRequestprompts/get} request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - result: InputRequiredResult | GetPromptResult - - -class InputRequiredResult(WireModel): - """ - An InputRequiredResult sent by the server to indicate that additional input is needed - before the request can be completed. - - At least one of `inputRequests` or `requestState` MUST be present. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[ResultMetaObject | None, Field(alias="_meta")] = None - input_requests: Annotated[InputRequests | None, Field(alias="inputRequests")] = None - request_state: Annotated[str | None, Field(alias="requestState")] = None - 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"`. - """ - - -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 +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. +""" class ListPromptsRequest(WireModel): @@ -3351,49 +3301,6 @@ class ListToolsRequest(WireModel): 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", @@ -3404,36 +3311,6 @@ class PaginatedRequest(WireModel): 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. @@ -3451,6 +3328,221 @@ class ReadResourceRequestParams(WireModel): """ +class SamplingMessage(WireModel): + """ + Describes a message issued to or received from an LLM API. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + content: ( + TextContent + | ImageContent + | AudioContent + | ToolUseContent + | ToolResultContent + | list[SamplingMessageContentBlock] + ) + role: Role + + +class CallToolRequestParams(WireModel): + """ + Parameters for a `tools/call` request. + """ + + 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 + + +class CreateMessageRequestParams(WireModel): + """ + Parameters for a `sampling/createMessage` request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + include_context: Annotated[ + Literal["allServers", "none", "thisServer"] | None, + Field(alias="includeContext"), + ] = None + """ + A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. + The client MAY ignore this request. + + Default is `"none"`. The values `"thisServer"` and `"allServers"` are deprecated (SEP-2596): servers SHOULD + omit this field or use `"none"`, and SHOULD only use the deprecated values if the client declares + {@link ClientCapabilities.sampling.context}. + """ + max_tokens: Annotated[int, Field(alias="maxTokens")] + """ + The requested maximum number of tokens to sample (to prevent runaway completions). + + The client MAY choose to sample fewer tokens than the requested maximum. + """ + messages: list[SamplingMessage] + metadata: JSONObject | None = None + """ + Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. + """ + model_preferences: Annotated[ModelPreferences | None, Field(alias="modelPreferences")] = None + """ + The server's preferences for which model to select. The client MAY ignore these preferences. + """ + stop_sequences: Annotated[list[str] | None, Field(alias="stopSequences")] = None + system_prompt: Annotated[str | None, Field(alias="systemPrompt")] = None + """ + An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. + """ + temperature: float | None = None + tool_choice: Annotated[ToolChoice | None, Field(alias="toolChoice")] = None + """ + Controls how the model uses tools. + The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. + Default is `{ mode: "auto" }`. + """ + tools: list[Tool] | None = None + """ + Tools that the model may use during generation. + The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. + """ + + +class GetPromptRequestParams(WireModel): + """ + Parameters for a `prompts/get` request. + """ + + 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 + + +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 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 CallToolRequest(WireModel): + """ + Used by the client to invoke a tool provided by the server. + """ + + model_config = ConfigDict( + extra="ignore", + ) + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["tools/call"] + params: CallToolRequestParams + + +class CreateMessageRequest(WireModel): + """ + 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", + ) + method: Literal["sampling/createMessage"] + params: CreateMessageRequestParams + + +class GetPromptRequest(WireModel): + """ + Used by the client to get a prompt provided by the server. + """ + + model_config = ConfigDict( + extra="ignore", + ) + id: RequestId + jsonrpc: Literal["2.0"] + 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): + """ + An InputRequiredResult sent by the server to indicate that additional input is needed + before the request can be completed. + + At least one of `inputRequests` or `requestState` MUST be present. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[ResultMetaObject | None, Field(alias="_meta")] = None + input_requests: Annotated[InputRequests | None, Field(alias="inputRequests")] = None + request_state: Annotated[str | None, Field(alias="requestState")] = None + 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"`. + """ + + class ReadResourceResultResponse(WireModel): """ A successful response from the server for a {@link ReadResourceRequestresources/read} request. @@ -3464,139 +3556,25 @@ class ReadResourceResultResponse(WireModel): result: InputRequiredResult | ReadResourceResult -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. - """ +ServerResult = ( + Result + | InputRequiredResult + | DiscoverResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | SubscriptionsListenResult + | ListPromptsResult + | GetPromptResult + | ListToolsResult + | CallToolResult + | CompleteResult +) -class RequestParams(WireModel): +class CallToolResultResponse(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 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. - """ - - -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. + A successful response from the server for a {@link CallToolRequesttools/call} request. """ model_config = ConfigDict( @@ -3604,143 +3582,36 @@ class SubscriptionsListenRequest(WireModel): ) id: RequestId jsonrpc: Literal["2.0"] - method: Literal["subscriptions/listen"] - params: SubscriptionsListenRequestParams + result: InputRequiredResult | CallToolResult -class SubscriptionsListenRequestParams(WireModel): +ClientRequest = ( + DiscoverRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscriptionsListenRequest + | ListPromptsRequest + | GetPromptRequest + | ListToolsRequest + | CallToolRequest + | CompleteRequest +) + + +class GetPromptResultResponse(WireModel): """ - Parameters for a {@link SubscriptionsListenRequestsubscriptions/listen} request. + A successful response from the server for a {@link GetPromptRequestprompts/get} 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] + id: RequestId + jsonrpc: Literal["2.0"] + 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 aa2406dc..a6181121 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 5cf93cf8..91955eb9 100644 --- a/src/mcp/server/auth/handlers/authorize.py +++ b/src/mcp/server/auth/handlers/authorize.py @@ -2,8 +2,7 @@ import logging 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.server.auth.provider import ( 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 @@ class AuthorizationHandler: 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 26425c13..65037888 100644 --- a/src/mcp/server/elicitation.py +++ b/src/mcp/server/elicitation.py @@ -8,7 +8,7 @@ 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 -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 typing_extensions import TypeAliasType 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 cc324335..0ffac07c 100644 --- a/src/mcp/server/mcpserver/utilities/func_metadata.py +++ b/src/mcp/server/mcpserver/utilities/func_metadata.py @@ -114,15 +114,15 @@ class ArgModelBase(BaseModel): 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 @@ class FuncMetadata(BaseModel): 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 bd0d1f9b..d38dfa09 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 35319921..fe8ac758 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