diff --git a/docs/migration.md b/docs/migration.md index f6170d0e1..36f7b2943 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -221,6 +221,10 @@ Common renames: Because `populate_by_name=True` is set, the old camelCase names still work as constructor kwargs (e.g., `Tool(inputSchema={...})` is accepted), but attribute access must use snake_case (`tool.input_schema`). +### Results now serialize `resultType` and cache-directive defaults + +Serialized results now include `resultType` by default (and `ttlMs`/`cacheScope` on cacheable results). Peers ignore unknown result fields, so this interoperates across protocol versions, but tests or recorded fixtures that compare exact serialized payloads need the new keys added. + ### `args` parameter removed from `ClientSessionGroup.call_tool()` The deprecated `args` parameter has been removed from `ClientSessionGroup.call_tool()`. Use `arguments` instead. @@ -1181,7 +1185,7 @@ Behavior changes: ### Experimental Tasks support removed -Tasks (SEP-1686) have been removed from the MCP specification and are no longer part of this SDK. The `mcp.client.experimental`, `mcp.server.experimental`, `mcp.shared.experimental`, and `mcp.server.lowlevel.experimental` modules have been removed, along with all `Task*` types, the `tasks` capability fields, `Tool.execution`, and the `experimental` properties on `ClientSession`, `ServerSession`, `Server`, and `ServerRequestContext`. +Tasks (SEP-1686) have been removed from the MCP specification and are no longer part of this SDK. The `mcp.client.experimental`, `mcp.server.experimental`, `mcp.shared.experimental`, and `mcp.server.lowlevel.experimental` modules have been removed, along with the `experimental` properties on `ClientSession`, `ServerSession`, `Server`, and `ServerRequestContext`. The corresponding `Task*` types remain in `mcp.types` as types-only definitions. Tasks are expected to return as a separate MCP extension in a future release. @@ -1227,6 +1231,10 @@ If you relied on extra fields round-tripping through MCP types, move that data i ## New Features +### 2025-11-25 and 2026-07-28 protocol fields modeled + +`mcp.types` models the 2025-11-25 and 2026-07-28 protocol fields (e.g. `resultType`, `ttlMs`/`cacheScope` on cacheable results, `inputResponses`/`requestState` on retried requests), so inbound payloads carrying these keys parse into typed fields and round-trip. Most are optional with `None` defaults; the result-directive fields carry serialized defaults - see [Results now serialize `resultType` and cache-directive defaults](#results-now-serialize-resulttype-and-cache-directive-defaults). + ### `streamable_http_app()` available on lowlevel Server The `streamable_http_app()` method is now available directly on the lowlevel `Server` class, not just `MCPServer`. This allows using the streamable HTTP transport without the MCPServer wrapper. diff --git a/src/mcp/shared/version.py b/src/mcp/shared/version.py index 44154da36..2299de72e 100644 --- a/src/mcp/shared/version.py +++ b/src/mcp/shared/version.py @@ -16,6 +16,7 @@ "2025-03-26", "2025-06-18", "2025-11-25", + "2026-07-28", ) """Every released protocol revision, oldest to newest.""" diff --git a/src/mcp/types/__init__.py b/src/mcp/types/__init__.py index b2d537fb7..7628f9ac2 100644 --- a/src/mcp/types/__init__.py +++ b/src/mcp/types/__init__.py @@ -1,22 +1,30 @@ """This module defines the types for the MCP protocol. Check the latest schema at: -https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.json +https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.json """ # Re-export everything from _types for backward compatibility from mcp.types._types import ( + CLIENT_CAPABILITIES_META_KEY, + CLIENT_INFO_META_KEY, DEFAULT_NEGOTIATED_VERSION, LATEST_PROTOCOL_VERSION, + LOG_LEVEL_META_KEY, + PROTOCOL_VERSION_META_KEY, Annotations, AudioContent, BaseMetadata, BlobResourceContents, + CacheableResult, CallToolRequest, CallToolRequestParams, CallToolResult, CancelledNotification, CancelledNotificationParams, + CancelTaskRequest, + CancelTaskRequestParams, + CancelTaskResult, ClientCapabilities, ClientNotification, ClientRequest, @@ -33,6 +41,9 @@ CreateMessageRequestParams, CreateMessageResult, CreateMessageResultWithTools, + CreateTaskResult, + DiscoverRequest, + DiscoverResult, ElicitationCapability, ElicitationRequiredErrorData, ElicitCompleteNotification, @@ -49,6 +60,12 @@ GetPromptRequest, GetPromptRequestParams, GetPromptResult, + GetTaskPayloadRequest, + GetTaskPayloadRequestParams, + GetTaskPayloadResult, + GetTaskRequest, + GetTaskRequestParams, + GetTaskResult, Icon, IconTheme, ImageContent, @@ -58,6 +75,12 @@ InitializeRequest, InitializeRequestParams, InitializeResult, + InputRequest, + InputRequests, + InputRequiredResult, + InputResponse, + InputResponseRequestParams, + InputResponses, ListPromptsRequest, ListPromptsResult, ListResourcesRequest, @@ -66,12 +89,15 @@ ListResourceTemplatesResult, ListRootsRequest, ListRootsResult, + ListTasksRequest, + ListTasksResult, ListToolsRequest, ListToolsResult, LoggingCapability, LoggingLevel, LoggingMessageNotification, LoggingMessageNotificationParams, + MissingRequiredClientCapabilityErrorData, ModelHint, ModelPreferences, Notification, @@ -92,6 +118,7 @@ ReadResourceRequest, ReadResourceRequestParams, ReadResourceResult, + RelatedTaskMetadata, Request, RequestParams, RequestParamsMeta, @@ -105,6 +132,7 @@ ResourceUpdatedNotification, ResourceUpdatedNotificationParams, Result, + ResultType, Role, Root, RootsCapability, @@ -124,17 +152,29 @@ StopReason, SubscribeRequest, SubscribeRequestParams, + SubscriptionFilter, + SubscriptionsAcknowledgedNotification, + SubscriptionsAcknowledgedNotificationParams, + SubscriptionsListenRequest, + SubscriptionsListenRequestParams, + Task, + TaskMetadata, + TaskStatus, + TaskStatusNotification, + TaskStatusNotificationParams, TextContent, TextResourceContents, Tool, ToolAnnotations, ToolChoice, + ToolExecution, ToolListChangedNotification, ToolResultContent, ToolsCapability, ToolUseContent, UnsubscribeRequest, UnsubscribeRequestParams, + UnsupportedProtocolVersionErrorData, UrlElicitationCapability, client_notification_adapter, client_request_adapter, @@ -150,9 +190,12 @@ INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, + JSONRPC_VERSION, METHOD_NOT_FOUND, + MISSING_REQUIRED_CLIENT_CAPABILITY, PARSE_ERROR, REQUEST_TIMEOUT, + UNSUPPORTED_PROTOCOL_VERSION, URL_ELICITATION_REQUIRED, ErrorData, JSONRPCError, @@ -168,17 +211,28 @@ # Protocol version constants "LATEST_PROTOCOL_VERSION", "DEFAULT_NEGOTIATED_VERSION", + # Reserved request _meta keys + "PROTOCOL_VERSION_META_KEY", + "CLIENT_INFO_META_KEY", + "CLIENT_CAPABILITIES_META_KEY", + "LOG_LEVEL_META_KEY", # Type aliases and variables "ContentBlock", "ElicitRequestedSchema", "ElicitRequestParams", "IncludeContext", + "InputRequest", + "InputRequests", + "InputResponse", + "InputResponses", "LoggingLevel", "ProgressToken", + "ResultType", "Role", "SamplingContent", "SamplingMessageContentBlock", "StopReason", + "TaskStatus", # Base classes "BaseMetadata", "Request", @@ -186,10 +240,12 @@ "Result", "RequestParams", "RequestParamsMeta", + "InputResponseRequestParams", "NotificationParams", "PaginatedRequest", "PaginatedRequestParams", "PaginatedResult", + "CacheableResult", "EmptyResult", # Capabilities "ClientCapabilities", @@ -236,27 +292,40 @@ "ResourceTemplateReference", "Root", "SamplingMessage", + "SubscriptionFilter", + "Task", + "TaskMetadata", + "RelatedTaskMetadata", "Tool", "ToolAnnotations", "ToolChoice", + "ToolExecution", # Requests "CallToolRequest", "CallToolRequestParams", "CompleteRequest", "CompleteRequestParams", + "CancelTaskRequest", + "CancelTaskRequestParams", "CreateMessageRequest", "CreateMessageRequestParams", + "DiscoverRequest", "ElicitRequest", "ElicitRequestFormParams", "ElicitRequestURLParams", "GetPromptRequest", "GetPromptRequestParams", + "GetTaskPayloadRequest", + "GetTaskPayloadRequestParams", + "GetTaskRequest", + "GetTaskRequestParams", "InitializeRequest", "InitializeRequestParams", "ListPromptsRequest", "ListResourcesRequest", "ListResourceTemplatesRequest", "ListRootsRequest", + "ListTasksRequest", "ListToolsRequest", "PingRequest", "ReadResourceRequest", @@ -265,23 +334,35 @@ "SetLevelRequestParams", "SubscribeRequest", "SubscribeRequestParams", + "SubscriptionsListenRequest", + "SubscriptionsListenRequestParams", "UnsubscribeRequest", "UnsubscribeRequestParams", # Results "CallToolResult", + "CancelTaskResult", "CompleteResult", "CreateMessageResult", "CreateMessageResultWithTools", + "CreateTaskResult", + "DiscoverResult", "ElicitResult", "ElicitationRequiredErrorData", "GetPromptResult", + "GetTaskPayloadResult", + "GetTaskResult", "InitializeResult", + "InputRequiredResult", "ListPromptsResult", "ListResourcesResult", "ListResourceTemplatesResult", "ListRootsResult", + "ListTasksResult", "ListToolsResult", "ReadResourceResult", + # Error data payloads + "MissingRequiredClientCapabilityErrorData", + "UnsupportedProtocolVersionErrorData", # Notifications "CancelledNotification", "CancelledNotificationParams", @@ -297,6 +378,10 @@ "ResourceUpdatedNotification", "ResourceUpdatedNotificationParams", "RootsListChangedNotification", + "SubscriptionsAcknowledgedNotification", + "SubscriptionsAcknowledgedNotificationParams", + "TaskStatusNotification", + "TaskStatusNotificationParams", "ToolListChangedNotification", # Union types for request/response routing "ClientNotification", @@ -317,9 +402,12 @@ "INTERNAL_ERROR", "INVALID_PARAMS", "INVALID_REQUEST", + "JSONRPC_VERSION", "METHOD_NOT_FOUND", + "MISSING_REQUIRED_CLIENT_CAPABILITY", "PARSE_ERROR", "REQUEST_TIMEOUT", + "UNSUPPORTED_PROTOCOL_VERSION", "URL_ELICITATION_REQUIRED", "ErrorData", "JSONRPCError", diff --git a/src/mcp/types/_types.py b/src/mcp/types/_types.py index e9d39ef6f..91fa98c84 100644 --- a/src/mcp/types/_types.py +++ b/src/mcp/types/_types.py @@ -1,20 +1,34 @@ +"""Version-superset MCP protocol models. + +One model per protocol construct, carrying every field from every supported +protocol version, so application code sees a single set of types regardless of +the negotiated version. Per-field docstrings note version availability. The +`mcp.types.v*` surface packages carry the schema-exact wire shapes. +""" + from __future__ import annotations -from typing import Annotated, Any, Generic, Literal, TypeAlias, TypeVar +from typing import Annotated, Any, Final, Generic, Literal, TypeAlias, TypeVar -from pydantic import BaseModel, ConfigDict, Field, FileUrl, TypeAdapter +from pydantic import ( + BaseModel, + ConfigDict, + Field, + FileUrl, + TypeAdapter, +) from pydantic.alias_generators import to_camel from typing_extensions import NotRequired, TypedDict from mcp.types.jsonrpc import RequestId -LATEST_PROTOCOL_VERSION = "2025-11-25" -"""The latest version of the Model Context Protocol. +LATEST_PROTOCOL_VERSION: Final[str] = "2025-11-25" +"""The newest protocol version this SDK can negotiate. -You can find the latest specification at https://modelcontextprotocol.io/specification/latest. +See https://modelcontextprotocol.io/specification/latest. """ -DEFAULT_NEGOTIATED_VERSION = "2025-03-26" +DEFAULT_NEGOTIATED_VERSION: Final[str] = "2025-03-26" """The default negotiated version of the Model Context Protocol when no version is specified. We need this to satisfy the MCP specification, which requires the server to assume a specific version if none is @@ -25,9 +39,12 @@ """ ProgressToken = str | int +"""A progress token, used to associate progress notifications with the original request.""" Role = Literal["user", "assistant"] +"""The sender or recipient of messages and data in a conversation.""" IconTheme = Literal["light", "dark"] +"""Theme an icon is designed for. Wire values of `Icon.theme` (2025-11-25+).""" class MCPModel(BaseModel): @@ -38,8 +55,33 @@ class MCPModel(BaseModel): Meta: TypeAlias = dict[str, Any] +PROTOCOL_VERSION_META_KEY = "io.modelcontextprotocol/protocolVersion" +"""Reserved request `_meta` key: the MCP protocol version for this request (2026-07-28). + +SDK-managed; for HTTP its value must match the `MCP-Protocol-Version` header. +""" + +CLIENT_INFO_META_KEY = "io.modelcontextprotocol/clientInfo" +"""Reserved request `_meta` key: the client `Implementation` (2026-07-28). SDK-managed.""" + +CLIENT_CAPABILITIES_META_KEY = "io.modelcontextprotocol/clientCapabilities" +"""Reserved request `_meta` key: per-request `ClientCapabilities` (2026-07-28). SDK-managed.""" + +LOG_LEVEL_META_KEY = "io.modelcontextprotocol/logLevel" +"""Reserved request `_meta` key: desired log level for this request (2026-07-28). + +Deprecated (with the rest of logging) by SEP-2577 in the same revision that +introduces it. If absent, the server must not send log notifications. +""" + class RequestParamsMeta(TypedDict, extra_items=Any): + """The `_meta` object on request params (schema name: `RequestMetaObject`). + + An open map: arbitrary keys round-trip via `extra_items=Any`. Read or set + the reserved `io.modelcontextprotocol/*` keys via the `*_META_KEY` constants. + """ + progress_token: NotRequired[ProgressToken] """ If specified, the caller requests out-of-band progress notifications for @@ -51,6 +93,13 @@ class RequestParamsMeta(TypedDict, extra_items=Any): class RequestParams(MCPModel): meta: RequestParamsMeta | None = Field(alias="_meta", default=None) + """Metadata reserved by MCP for protocol-level concerns (wire name `_meta`). + + Carries the optional progress token and, on 2026-07-28+ sessions, the + reserved `io.modelcontextprotocol/*` keys. Required on the wire for + 2026-07-28+ client requests; the session layer supplies the reserved + entries, so code sending through an SDK session leaves this unset. + """ class PaginatedRequestParams(RequestParams): @@ -75,7 +124,11 @@ class NotificationParams(MCPModel): class Request(MCPModel, Generic[RequestParamsT, MethodT]): - """Base class for JSON-RPC requests.""" + """Base class for JSON-RPC requests. + + The JSON-RPC envelope (`jsonrpc`, `id`) is attached by the session layer + (see `mcp.types.jsonrpc`), not carried here. + """ method: MethodT params: RequestParamsT @@ -85,6 +138,8 @@ class PaginatedRequest(Request[PaginatedRequestParams | None, MethodT], Generic[ """Base class for paginated requests, matching the schema's PaginatedRequest interface.""" params: PaginatedRequestParams | None = None + """Pagination params. Required on the 2026-07-28+ wire (because `_meta` is); + the session layer materializes it there. Optional on earlier versions.""" class Notification(MCPModel, Generic[NotificationParamsT, MethodT]): @@ -94,6 +149,15 @@ class Notification(MCPModel, Generic[NotificationParamsT, MethodT]): params: NotificationParamsT +ResultType = Literal["complete", "input_required"] | str +"""Tags a `Result` so the client knows how to parse it (2026-07-28). + +"complete" means the result is final; "input_required" means it is an +`InputRequiredResult`. The union is open (the tasks extension reserves "task"). +Absent `resultType` is equivalent to "complete". +""" + + class Result(MCPModel): """Base class for JSON-RPC results.""" @@ -112,15 +176,42 @@ class PaginatedResult(Result): """ +class CacheableResult(Result): + """Base class for results that carry client-side caching directives (2026-07-28). + + Both fields are required on the 2026-07-28 wire and always serialized by + this SDK; older peers ignore the extra keys. The defaults are SDK choices + (the schema declares none). + """ + + ttl_ms: Annotated[int, Field(ge=0)] = 0 + """How long (ms) the client MAY cache this response, analogous to HTTP + `Cache-Control: max-age`. 0 means immediately stale.""" + + cache_scope: Literal["public", "private"] = "private" + """Analogous to HTTP `Cache-Control: public` vs `private`: "public" allows + shared caches to serve the response to any user; "private" forbids that.""" + + class EmptyResult(Result): - """A response that indicates success but carries no data.""" + """A result that indicates success but carries no data. + + `result_type` defaults to None so this dumps as `{}`: deployed TypeScript + and Rust SDK clients validate empty results strictly and reject extra keys. + The 2026-07-28 schema requires `resultType`, so code answering an empty + result on a 2026-07-28+ session must pass `result_type="complete"`. + """ + + result_type: ResultType | None = None + """None keeps the dump empty; see the class docstring.""" class BaseMetadata(MCPModel): - """Base class for entities with name and optional title fields.""" + """Base class for entities with a programmatic name and an optional display title.""" name: str - """The programmatic name of the entity.""" + """Intended for programmatic or logical use, but used as a display name in past + specs or fallback (if title isn't present).""" title: str | None = None """ @@ -134,35 +225,30 @@ class BaseMetadata(MCPModel): class Icon(MCPModel): - """An icon for display in user interfaces.""" + """An optionally-sized icon for display in a user interface (2025-11-25+).""" src: str - """URL or data URI for the icon.""" + """A standard URI pointing to an icon resource (`http(s):` or `data:`). + + Consumers SHOULD ensure icon URLs come from a trusted domain and SHOULD + take appropriate precautions when consuming SVGs (which can contain script). + """ mime_type: str | None = None - """Optional MIME type for the icon.""" + """Optional MIME type override if the source MIME type is missing or generic.""" sizes: list[str] | None = None - """Optional list of strings specifying icon dimensions (e.g., ["48x48", "96x96"]).""" + """Optional sizes this icon is available in: WxH (e.g. `"48x48"`) or `"any"`. + If not provided, assume the icon can be used at any size.""" theme: IconTheme | None = None - """Optional theme specifier. - - `"light"` indicates the icon is designed for a light background, `"dark"` indicates the icon - is designed for a dark background. - - See https://modelcontextprotocol.io/specification/2025-11-25/schema#icon for more details. - """ + """The theme this icon is designed for. If not provided, assume any theme.""" class Implementation(BaseMetadata): - """Describes the name and version of an MCP implementation.""" + """Describes the name and version of an MCP implementation (`clientInfo` / `serverInfo`).""" version: str - - title: str | None = None - """An optional human-readable title for this implementation.""" - description: str | None = None """An optional human-readable description of what this implementation does.""" @@ -170,11 +256,15 @@ class Implementation(BaseMetadata): """An optional URL of the website for this implementation.""" icons: list[Icon] | None = None - """An optional list of icons for this implementation.""" + """Optional set of sized icons that the client can display in a user interface.""" class RootsCapability(MCPModel): - """Capability for root operations.""" + """Capability for root operations. + + Deprecated in protocol 2026-07-28 (SEP-2577) but still carried there as an + empty object (`list_changed` exists only through 2025-11-25). + """ list_changed: bool | None = None """Whether the client supports notifications for changes to the roots list.""" @@ -201,7 +291,7 @@ class FormElicitationCapability(MCPModel): class UrlElicitationCapability(MCPModel): - """Capability for URL mode elicitation.""" + """Capability for URL mode elicitation (2025-11-25+).""" class ElicitationCapability(MCPModel): @@ -214,11 +304,11 @@ class ElicitationCapability(MCPModel): """Present if the client supports form mode elicitation.""" url: UrlElicitationCapability | None = None - """Present if the client supports URL mode elicitation.""" + """Present if the client supports URL mode elicitation (2025-11-25 and later).""" class SamplingCapability(MCPModel): - """Sampling capability structure, allowing fine-grained capability advertisement.""" + """Sampling capability structure. Deprecated in 2026-07-28 (SEP-2577); shape unchanged.""" context: SamplingContextCapability | None = None """ @@ -232,8 +322,55 @@ class SamplingCapability(MCPModel): """ +class TasksListCapability(MCPModel): + """Capability for tasks listing operations (2025-11-25 only).""" + + +class TasksCancelCapability(MCPModel): + """Capability for tasks cancel operations (2025-11-25 only).""" + + +class TasksCreateMessageCapability(MCPModel): + """Capability for task-augmented sampling/createMessage requests (2025-11-25 only).""" + + +class TasksSamplingCapability(MCPModel): + """Capability for task-augmented sampling operations (2025-11-25 only).""" + + create_message: TasksCreateMessageCapability | None = None + + +class TasksCreateElicitationCapability(MCPModel): + """Capability for task-augmented elicitation/create requests (2025-11-25 only).""" + + +class TasksElicitationCapability(MCPModel): + """Capability for task-augmented elicitation operations (2025-11-25 only).""" + + create: TasksCreateElicitationCapability | None = None + + +class ClientTasksRequestsCapability(MCPModel): + """Specifies which request types the client can augment with tasks (2025-11-25 only).""" + + sampling: TasksSamplingCapability | None = None + elicitation: TasksElicitationCapability | None = None + + +class ClientTasksCapability(MCPModel): + """Capability for client tasks operations (2025-11-25 only).""" + + list: TasksListCapability | None = None + cancel: TasksCancelCapability | None = None + requests: ClientTasksRequestsCapability | None = None + + class ClientCapabilities(MCPModel): - """Capabilities a client may support.""" + """Capabilities a client may support. + + Not a closed set: any client can define additional capabilities. Sent once in + `initialize` through 2025-11-25; per-request in `_meta` on 2026-07-28. + """ experimental: dict[str, dict[str, Any]] | None = None """Experimental, non-standard capabilities that the client supports.""" @@ -246,6 +383,27 @@ class ClientCapabilities(MCPModel): """Present if the client supports elicitation from the user.""" roots: RootsCapability | None = None """Present if the client supports listing roots.""" + extensions: dict[str, dict[str, Any]] | None = None + """MCP extensions the client supports (2026-07-28). Keys are extension + identifiers; values are per-extension settings (empty object = no settings).""" + tasks: ClientTasksCapability | None = None + """Present if the client supports task-augmented requests (2025-11-25 only).""" + + +class UnsupportedProtocolVersionErrorData(MCPModel): + """Error data for the -32004 unsupported-protocol-version error (2026-07-28).""" + + supported: list[str] + """Protocol versions the server supports; the client should pick one and retry.""" + + requested: str + + +class MissingRequiredClientCapabilityErrorData(MCPModel): + """Error data for the -32003 missing-required-client-capability error (2026-07-28).""" + + required_capabilities: ClientCapabilities + """The capabilities the server requires from the client to process this request.""" class PromptsCapability(MCPModel): @@ -279,14 +437,39 @@ class CompletionsCapability(MCPModel): """Capability for completions operations.""" +class TasksCallCapability(MCPModel): + """Capability for task-augmented tools/call requests (2025-11-25 only).""" + + +class TasksToolsCapability(MCPModel): + """Capability for task-augmented tool operations (2025-11-25 only).""" + + call: TasksCallCapability | None = None + + +class ServerTasksRequestsCapability(MCPModel): + """Specifies which request types the server can augment with tasks (2025-11-25 only).""" + + tools: TasksToolsCapability | None = None + + +class ServerTasksCapability(MCPModel): + """Capability for server tasks operations (2025-11-25 only).""" + + list: TasksListCapability | None = None + cancel: TasksCancelCapability | None = None + requests: ServerTasksRequestsCapability | None = None + + class ServerCapabilities(MCPModel): - """Capabilities that a server may support.""" + """Capabilities that a server may support. Not a closed set.""" experimental: dict[str, dict[str, Any]] | None = None """Experimental, non-standard capabilities that the server supports.""" logging: LoggingCapability | None = None - """Present if the server supports sending log messages to the client.""" + """Present if the server supports sending log messages to the client. + Deprecated in 2026-07-28 (SEP-2577).""" prompts: PromptsCapability | None = None """Present if the server offers any prompt templates.""" @@ -300,9 +483,19 @@ class ServerCapabilities(MCPModel): completions: CompletionsCapability | None = None """Present if the server offers autocompletion suggestions for prompts and resources.""" + extensions: dict[str, dict[str, Any]] | None = None + """MCP extensions the server supports (2026-07-28). Keys are extension + identifiers; values are per-extension settings (empty object = no settings).""" + + tasks: ServerTasksCapability | None = None + """Present if the server supports task-augmented requests (2025-11-25 only).""" + class InitializeRequestParams(RequestParams): - """Parameters for the initialize request.""" + """Parameters for the `initialize` request. + + Removed in protocol 2026-07-28; sent/received on sessions negotiating <= 2025-11-25. + """ protocol_version: str """The latest version of the Model Context Protocol that the client supports.""" @@ -313,6 +506,9 @@ class InitializeRequestParams(RequestParams): class InitializeRequest(Request[InitializeRequestParams, Literal["initialize"]]): """This request is sent from the client to the server when it first connects, asking it to begin initialization. + + Removed in protocol 2026-07-28; sent/received on sessions negotiating <= 2025-11-25. + On 2026-07-28 the handshake is `server/discover` plus per-request `_meta`. """ method: Literal["initialize"] = "initialize" @@ -320,19 +516,29 @@ class InitializeRequest(Request[InitializeRequestParams, Literal["initialize"]]) class InitializeResult(Result): - """After receiving an initialize request from the client, the server sends this.""" + """After receiving an initialize request from the client, the server sends this response. + + Removed in protocol 2026-07-28; sent/received on sessions negotiating <= 2025-11-25. + """ protocol_version: str - """The version of the Model Context Protocol that the server wants to use.""" + """The version of the Model Context Protocol that the server wants to use. + If the client cannot support this version, it MUST disconnect.""" capabilities: ServerCapabilities server_info: Implementation instructions: str | None = None - """Instructions describing how to use the server and its features.""" + """Instructions describing how to use the server and its features. + + Clients may use this to improve an LLM's understanding of available tools, + resources, etc., for example by adding it to the system prompt. + """ class InitializedNotification(Notification[NotificationParams | None, Literal["notifications/initialized"]]): """This notification is sent from the client to the server after initialization has finished. + + Removed in protocol 2026-07-28; sent/received on sessions negotiating <= 2025-11-25. """ method: Literal["notifications/initialized"] = "notifications/initialized" @@ -341,13 +547,181 @@ class InitializedNotification(Notification[NotificationParams | None, Literal["n class PingRequest(Request[RequestParams | None, Literal["ping"]]): """A ping, issued by either the server or the client, to check that the other party is - still alive. + still alive. The receiver must promptly respond, or else may be disconnected. + + Removed in protocol 2026-07-28; sent/received on sessions negotiating <= 2025-11-25. """ method: Literal["ping"] = "ping" params: RequestParams | None = None +class DiscoverRequest(Request[RequestParams | None, Literal["server/discover"]]): + """Asks the server to advertise its supported protocol versions, capabilities, + and other metadata (2026-07-28). + + Servers speaking 2026-07-28 MUST implement this; clients MAY call it but are + not required to (version negotiation can also happen via per-request `_meta`). + """ + + method: Literal["server/discover"] = "server/discover" + params: RequestParams | None = None + """Required on the 2026-07-28 wire (for `_meta`); the session layer materializes it.""" + + +class DiscoverResult(CacheableResult): + """The result returned by the server for a `server/discover` request (2026-07-28).""" + + supported_versions: list[str] + """MCP protocol versions this server supports; the client should pick one for subsequent requests.""" + + capabilities: ServerCapabilities + + server_info: Implementation + + instructions: str | None = None + """Natural-language guidance describing the server and its features, e.g. for + a system prompt. Should not duplicate information already in tool descriptions.""" + + result_type: ResultType = "complete" + """See `ResultType`. Always serialized; required on the 2026-07-28 wire, + ignored by older peers, and defaulted on inbound bodies that omit it.""" + + +# Tasks: introduced in 2025-11-25, removed from the core spec in 2026-07-28 +# (continuing as an extension). Defined here types-only; their methods are not +# in the request/notification unions below, so they are never dispatched. + + +class ToolExecution(MCPModel): + """Execution-related properties for a tool (2025-11-25 only).""" + + task_support: Literal["forbidden", "optional", "required"] | None = None + """Whether this tool supports task-augmented execution. Absent means "forbidden".""" + + +class TaskMetadata(MCPModel): + """Metadata for augmenting a request with task execution (the `task` params field; 2025-11-25 only).""" + + ttl: int | None = None + """Requested duration in milliseconds to retain task from creation.""" + + +class RelatedTaskMetadata(MCPModel): + """Associates a message with a task, via `_meta["io.modelcontextprotocol/related-task"]` (2025-11-25 only).""" + + task_id: str + + +TaskStatus = Literal["working", "input_required", "completed", "failed", "cancelled"] +"""The status of a task (2025-11-25 only).""" + + +class Task(MCPModel): + """Data associated with a task (2025-11-25 only).""" + + task_id: str + + status: TaskStatus + + status_message: str | None = None + """Optional human-readable message describing the current task state.""" + + created_at: str + """ISO 8601 timestamp when the task was created.""" + + last_updated_at: str + """ISO 8601 timestamp when the task was last updated.""" + + ttl: int | None + """Actual retention duration from creation in milliseconds, null for unlimited.""" + + poll_interval: int | None = None + """Suggested polling interval in milliseconds.""" + + +class CreateTaskResult(Result): + """A response to a task-augmented request (2025-11-25 only).""" + + task: Task + + +class GetTaskRequestParams(RequestParams): + task_id: str + + +class GetTaskRequest(Request[GetTaskRequestParams, Literal["tasks/get"]]): + """A request to retrieve the state of a task (2025-11-25 only).""" + + method: Literal["tasks/get"] = "tasks/get" + params: GetTaskRequestParams + + +class GetTaskResult(Result, Task): + """The response to a tasks/get request (2025-11-25 only).""" + + +class CancelTaskRequestParams(RequestParams): + task_id: str + + +class CancelTaskRequest(Request[CancelTaskRequestParams, Literal["tasks/cancel"]]): + """A request to cancel a task (2025-11-25 only).""" + + method: Literal["tasks/cancel"] = "tasks/cancel" + params: CancelTaskRequestParams + + +class CancelTaskResult(Result, Task): + """The response to a tasks/cancel request (2025-11-25 only).""" + + +class TaskStatusNotificationParams(NotificationParams, Task): + """Parameters for a `notifications/tasks/status` notification.""" + + +class TaskStatusNotification(Notification[TaskStatusNotificationParams, Literal["notifications/tasks/status"]]): + """An optional notification informing the requestor that a task's status has changed (2025-11-25 only).""" + + method: Literal["notifications/tasks/status"] = "notifications/tasks/status" + params: TaskStatusNotificationParams + + +class GetTaskPayloadRequestParams(RequestParams): + """Parameters for a tasks/result request.""" + + task_id: str + + +class GetTaskPayloadRequest(Request[GetTaskPayloadRequestParams, Literal["tasks/result"]]): + """A request to retrieve the result of a completed task (2025-11-25 only).""" + + method: Literal["tasks/result"] = "tasks/result" + params: GetTaskPayloadRequestParams + + +class GetTaskPayloadResult(Result): + """The response to a tasks/result request (2025-11-25 only). + + The structure matches the result type of the original request. The payload + arrives as extra wire fields, which `MCPModel` does not retain; validate the + response into the original request's result type (e.g. `CallToolResult`) + instead of this class. + """ + + +class ListTasksRequest(PaginatedRequest[Literal["tasks/list"]]): + """A request to retrieve a list of tasks (2025-11-25 only).""" + + method: Literal["tasks/list"] = "tasks/list" + + +class ListTasksResult(PaginatedResult): + """The response to a tasks/list request (2025-11-25 only).""" + + tasks: list[Task] + + class ProgressNotificationParams(NotificationParams): """Parameters for progress notifications.""" @@ -384,8 +758,14 @@ class ListResourcesRequest(PaginatedRequest[Literal["resources/list"]]): class Annotations(MCPModel): + """Optional annotations the client can use to inform how objects are used or displayed.""" + audience: list[Role] | None = None + """Who the intended audience is, e.g. `["user", "assistant"]`.""" + priority: Annotated[float, Field(ge=0.0, le=1.0)] | None = None + """How important this data is for operating the server: 1 means effectively + required, 0 means entirely optional.""" class Resource(BaseMetadata): @@ -407,15 +787,13 @@ class Resource(BaseMetadata): """ icons: list[Icon] | None = None - """An optional list of icons for this resource.""" + """Optional set of sized icons that the client can display in a user interface.""" annotations: Annotations | None = None + """Optional annotations for the client.""" meta: Meta | None = Field(alias="_meta", default=None) - """ - See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - for notes on _meta usage. - """ + """See the MCP specification for notes on `_meta` usage.""" class ResourceTemplate(BaseMetadata): @@ -425,7 +803,7 @@ class ResourceTemplate(BaseMetadata): """A URI template (according to RFC 6570) that can be used to construct resource URIs.""" description: str | None = None - """A human-readable description of what this template is for.""" + """A description of what this template is for.""" mime_type: str | None = None """The MIME type for all resources that match this template. @@ -434,9 +812,10 @@ class ResourceTemplate(BaseMetadata): """ icons: list[Icon] | None = None - """An optional list of icons for this resource template.""" + """An optional set of sized icons that the client can display in a user interface.""" annotations: Annotations | None = None + """Optional annotations for the client.""" meta: Meta | None = Field(alias="_meta", default=None) """ @@ -445,10 +824,12 @@ class ResourceTemplate(BaseMetadata): """ -class ListResourcesResult(PaginatedResult): +class ListResourcesResult(PaginatedResult, CacheableResult): """The server's response to a resources/list request from the client.""" resources: list[Resource] + result_type: ResultType = "complete" + """See `ResultType`. Always serialized; older peers ignore it.""" class ListResourceTemplatesRequest(PaginatedRequest[Literal["resources/templates/list"]]): @@ -457,19 +838,33 @@ class ListResourceTemplatesRequest(PaginatedRequest[Literal["resources/templates method: Literal["resources/templates/list"] = "resources/templates/list" -class ListResourceTemplatesResult(PaginatedResult): +class ListResourceTemplatesResult(PaginatedResult, CacheableResult): """The server's response to a resources/templates/list request from the client.""" resource_templates: list[ResourceTemplate] + result_type: ResultType = "complete" + """See `ResultType`. Always serialized; older peers ignore it.""" + +class InputResponseRequestParams(RequestParams): + """Base params for client requests that can carry responses to a server's + input requests (2026-07-28 multi-round-trip flow). -class ReadResourceRequestParams(RequestParams): - """Parameters for reading a resource.""" + When a request returns an `InputRequiredResult`, the client retries the + original request with these fields populated. + """ + input_responses: InputResponses | None = None + """Responses to the server's `InputRequiredResult.input_requests`, keyed identically.""" + request_state: str | None = None + """Opaque state from the `InputRequiredResult`, passed back verbatim on retry.""" + + +class ReadResourceRequestParams(InputResponseRequestParams): uri: str """ - The URI of the resource to read. The URI can use any protocol; it is up to the - server how to interpret it. + The URI of the resource. The URI can use any protocol; it is up to the server + how to interpret it. """ @@ -511,10 +906,14 @@ class BlobResourceContents(ResourceContents): """A base64-encoded string representing the binary data of the item.""" -class ReadResourceResult(Result): +class ReadResourceResult(CacheableResult): """The server's response to a resources/read request from the client.""" contents: list[TextResourceContents | BlobResourceContents] + """The contents of the resource or sub-resources that were read.""" + + result_type: ResultType = "complete" + """See `ResultType`. Always serialized; older peers ignore it.""" class ResourceListChangedNotification( @@ -522,6 +921,9 @@ class ResourceListChangedNotification( ): """An optional notification from the server to the client, informing it that the list of resources it can read from has changed. + + May be sent spontaneously through 2025-11-25; on 2026-07-28 sessions the + client must opt in via `subscriptions/listen`. """ method: Literal["notifications/resources/list_changed"] = "notifications/resources/list_changed" @@ -529,7 +931,10 @@ class ResourceListChangedNotification( class SubscribeRequestParams(RequestParams): - """Parameters for subscribing to a resource.""" + """Parameters for subscribing to a resource. + + Removed in protocol 2026-07-28; sent/received on sessions negotiating <= 2025-11-25. + """ uri: str """ @@ -541,6 +946,9 @@ class SubscribeRequestParams(RequestParams): class SubscribeRequest(Request[SubscribeRequestParams, Literal["resources/subscribe"]]): """Sent from the client to request resources/updated notifications from the server whenever a particular resource changes. + + Removed in protocol 2026-07-28; sent/received on sessions negotiating <= 2025-11-25. + On 2026-07-28 use `subscriptions/listen` instead. """ method: Literal["resources/subscribe"] = "resources/subscribe" @@ -548,15 +956,21 @@ class SubscribeRequest(Request[SubscribeRequestParams, Literal["resources/subscr class UnsubscribeRequestParams(RequestParams): - """Parameters for unsubscribing from a resource.""" + """Parameters for a resources/unsubscribe request. + + Removed in protocol 2026-07-28; sent/received on sessions negotiating <= 2025-11-25. + """ uri: str """The URI of the resource to unsubscribe from.""" class UnsubscribeRequest(Request[UnsubscribeRequestParams, Literal["resources/unsubscribe"]]): - """Sent from the client to request cancellation of resources/updated notifications from - the server. + """Sent from the client to request cancellation of resources/updated notifications + from the server. This should follow a previous resources/subscribe request. + + Removed in protocol 2026-07-28; sent/received on sessions negotiating <= 2025-11-25. + On 2026-07-28 use `subscriptions/listen` instead. """ method: Literal["resources/unsubscribe"] = "resources/unsubscribe" @@ -564,8 +978,6 @@ class UnsubscribeRequest(Request[UnsubscribeRequestParams, Literal["resources/un class ResourceUpdatedNotificationParams(NotificationParams): - """Parameters for resource update notifications.""" - uri: str """ The URI of the resource that has been updated. This might be a sub-resource of the @@ -578,23 +990,82 @@ class ResourceUpdatedNotification( ): """A notification from the server to the client, informing it that a resource has changed and may need to be read again. + + Only sent if the client subscribed: via `resources/subscribe` through + 2025-11-25, or `subscriptions/listen` on 2026-07-28. """ method: Literal["notifications/resources/updated"] = "notifications/resources/updated" params: ResourceUpdatedNotificationParams +class SubscriptionFilter(MCPModel): + """The set of notification types a client opts in to via `subscriptions/listen` (2026-07-28). + + Each type is opt-in; the server MUST NOT send types not requested here. + Echoed back in `notifications/subscriptions/acknowledged` as the subset the + server agreed to honor. Extensions merge additional keys (e.g. `taskIds`), + so unknown keys round-trip. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, extra="allow") + + tools_list_changed: bool | None = None + """If true, receive notifications/tools/list_changed.""" + + prompts_list_changed: bool | None = None + """If true, receive notifications/prompts/list_changed.""" + + resources_list_changed: bool | None = None + """If true, receive notifications/resources/list_changed.""" + + resource_subscriptions: list[str] | None = None + """Subscribe to notifications/resources/updated for these resource URIs.""" + + +class SubscriptionsListenRequestParams(RequestParams): + notifications: SubscriptionFilter + """The notifications the client opts in to on this stream.""" + + +class SubscriptionsListenRequest(Request[SubscriptionsListenRequestParams, Literal["subscriptions/listen"]]): + """Opens a long-lived channel for receiving notifications outside the context + of a specific request (2026-07-28). + """ + + method: Literal["subscriptions/listen"] = "subscriptions/listen" + params: SubscriptionsListenRequestParams + + +class SubscriptionsAcknowledgedNotificationParams(NotificationParams): + notifications: SubscriptionFilter + """The subset of requested notification types the server agreed to honor. + Unsupported types are omitted.""" + + +class SubscriptionsAcknowledgedNotification( + Notification[ + SubscriptionsAcknowledgedNotificationParams, + Literal["notifications/subscriptions/acknowledged"], + ] +): + """First message on a `subscriptions/listen` stream: acknowledges the + subscription and reports which notification types the server will honor (2026-07-28). + """ + + method: Literal["notifications/subscriptions/acknowledged"] = "notifications/subscriptions/acknowledged" + params: SubscriptionsAcknowledgedNotificationParams + + class ListPromptsRequest(PaginatedRequest[Literal["prompts/list"]]): - """Sent from the client to request a list of prompts and prompt templates.""" + """Sent from the client to request a list of prompts and prompt templates the server has.""" method: Literal["prompts/list"] = "prompts/list" -class PromptArgument(MCPModel): - """An argument for a prompt template.""" +class PromptArgument(BaseMetadata): + """Describes an argument that a prompt can accept.""" - name: str - """The name of the argument.""" description: str | None = None """A human-readable description of the argument.""" required: bool | None = None @@ -617,15 +1088,15 @@ class Prompt(BaseMetadata): """ -class ListPromptsResult(PaginatedResult): +class ListPromptsResult(PaginatedResult, CacheableResult): """The server's response to a prompts/list request from the client.""" prompts: list[Prompt] + result_type: ResultType = "complete" + """See `ResultType`. Always serialized; older peers ignore it.""" -class GetPromptRequestParams(RequestParams): - """Parameters for getting a prompt.""" - +class GetPromptRequestParams(InputResponseRequestParams): name: str """The name of the prompt or prompt template.""" arguments: dict[str, str] | None = None @@ -640,12 +1111,13 @@ class GetPromptRequest(Request[GetPromptRequestParams, Literal["prompts/get"]]): class TextContent(MCPModel): - """Text content for a message.""" + """Text provided to or from an LLM.""" type: Literal["text"] = "text" text: str """The text content of the message.""" annotations: Annotations | None = None + """Optional annotations for the client.""" meta: Meta | None = Field(alias="_meta", default=None) """ See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) @@ -654,7 +1126,7 @@ class TextContent(MCPModel): class ImageContent(MCPModel): - """Image content for a message.""" + """An image provided to or from an LLM.""" type: Literal["image"] = "image" data: str @@ -665,15 +1137,13 @@ class ImageContent(MCPModel): image types. """ annotations: Annotations | None = None + """Optional annotations for the client.""" meta: Meta | None = Field(alias="_meta", default=None) - """ - See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - for notes on _meta usage. - """ + """See the MCP specification's "General fields: _meta" section for notes on _meta usage.""" class AudioContent(MCPModel): - """Audio content for a message.""" + """Audio provided to or from an LLM.""" type: Literal["audio"] = "audio" data: str @@ -684,6 +1154,7 @@ class AudioContent(MCPModel): audio types. """ annotations: Annotations | None = None + """Optional annotations for the client.""" meta: Meta | None = Field(alias="_meta", default=None) """ See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) @@ -692,11 +1163,11 @@ class AudioContent(MCPModel): class ToolUseContent(MCPModel): - """Content representing an assistant's request to invoke a tool. + """An assistant's request to invoke a tool during sampling (2025-11-25+). - This content type appears in assistant messages when the LLM wants to call a tool - during sampling. The server should execute the tool and return a ToolResultContent - in the next user message. + Appears in `sampling/createMessage` results and replayed assistant messages. + The server should execute the tool and return a `ToolResultContent` in the + next user message. Deprecated in 2026-07-28 (SEP-2577). """ type: Literal["tool_use"] = "tool_use" @@ -712,48 +1183,45 @@ class ToolUseContent(MCPModel): """Arguments to pass to the tool. Must conform to the tool's inputSchema.""" meta: Meta | None = Field(alias="_meta", default=None) - """ - See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - for notes on _meta usage. - """ + """Optional metadata. Clients SHOULD preserve this in subsequent sampling + requests to enable caching optimizations.""" class ToolResultContent(MCPModel): - """Content representing the result of a tool execution. + """The result of a tool use, provided by the user back to the assistant (2025-11-25+). - This content type appears in user messages as a response to a ToolUseContent - from the assistant. It contains the output of executing the requested tool. + Appears in sampling messages as a response to a `ToolUseContent` block. + Requires the `sampling.tools` client capability. Deprecated in 2026-07-28 (SEP-2577). """ type: Literal["tool_result"] = "tool_result" """Discriminator for tool result content.""" tool_use_id: str - """The unique identifier that corresponds to the tool call's id field.""" + """The `id` of the `ToolUseContent` this result corresponds to.""" content: list[ContentBlock] = [] - """ - A list of content objects representing the tool result. - Defaults to empty list if not provided. - """ + """The unstructured result content (same format as `CallToolResult.content`).""" - structured_content: dict[str, Any] | None = None - """ - Optional structured tool output that matches the tool's outputSchema (if defined). - """ + structured_content: Any = None + """An optional structured result value. Any JSON value on 2026-07-28; + restricted to a JSON object on 2025-11-25.""" is_error: bool | None = None - """Whether the tool execution resulted in an error.""" + """Whether the tool use resulted in an error. Absent is equivalent to false.""" meta: Meta | None = Field(alias="_meta", default=None) - """ - See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - for notes on _meta usage. - """ + """Optional metadata. Clients SHOULD preserve this in subsequent sampling + requests to enable caching optimizations.""" SamplingMessageContentBlock: TypeAlias = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent -"""Content block types allowed in sampling messages.""" +"""Content block types allowed in sampling messages. + +This is the widest (2025-11-25+) membership; older sessions allow only a subset +on the wire. Serialization never narrows a value to fit; version gating is the +session layer's responsibility. Deprecated in 2026-07-28 (SEP-2577). +""" SamplingContent: TypeAlias = TextContent | ImageContent | AudioContent """Basic content types for sampling responses (without tool use). @@ -794,6 +1262,7 @@ class EmbeddedResource(MCPModel): type: Literal["resource"] = "resource" resource: TextResourceContents | BlobResourceContents annotations: Annotations | None = None + """Optional annotations for the client.""" meta: Meta | None = Field(alias="_meta", default=None) """ See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) @@ -815,7 +1284,10 @@ class ResourceLink(Resource): class PromptMessage(MCPModel): - """Describes a message returned as part of a prompt.""" + """Describes a message returned as part of a prompt. + + Similar to `SamplingMessage`, but also supports embedded resources. + """ role: Role content: ContentBlock @@ -827,6 +1299,10 @@ class GetPromptResult(Result): description: str | None = None """An optional description for the prompt.""" messages: list[PromptMessage] + """The messages composing the prompt, in the order they should be presented.""" + + result_type: ResultType = "complete" + """See `ResultType`. Always serialized; older peers ignore it.""" class PromptListChangedNotification( @@ -834,6 +1310,9 @@ class PromptListChangedNotification( ): """An optional notification from the server to the client, informing it that the list of prompts it offers has changed. + + May be sent spontaneously through 2025-11-25; on 2026-07-28 sessions the + client must opt in via `subscriptions/listen`. """ method: Literal["notifications/prompts/list_changed"] = "notifications/prompts/list_changed" @@ -898,34 +1377,43 @@ class Tool(BaseMetadata): description: str | None = None """A human-readable description of the tool.""" input_schema: dict[str, Any] - """A JSON Schema object defining the expected parameters for the tool.""" - output_schema: dict[str, Any] | None = None + """A JSON Schema object defining the expected parameters for the tool. + + `type: "object"` is required at the root. 2026-07-28 allows any JSON Schema + 2020-12 keyword; earlier versions define only `type`/`properties`/`required`. """ - An optional JSON Schema object defining the structure of the tool's output - returned in the structured_content field of a CallToolResult. + execution: ToolExecution | None = None + """Execution-related properties (2025-11-25 only; removed in 2026-07-28).""" + output_schema: dict[str, Any] | None = None + """An optional JSON Schema object defining the structure of the tool's output + returned in the `structured_content` field of a `CallToolResult`. + + Restricted to `type: "object"` at the root through 2025-11-25; any valid + JSON Schema 2020-12 on 2026-07-28. """ icons: list[Icon] | None = None - """An optional list of icons for this tool.""" + """Optional set of sized icons for display (2025-11-25+).""" annotations: ToolAnnotations | None = None - """Optional additional tool information.""" + """Optional additional tool information. + Display-name precedence: `title`, `annotations.title`, then `name`.""" meta: Meta | None = Field(alias="_meta", default=None) - """ - See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - for notes on _meta usage. - """ + """See the MCP specification for notes on `_meta` usage.""" -class ListToolsResult(PaginatedResult): +class ListToolsResult(PaginatedResult, CacheableResult): """The server's response to a tools/list request from the client.""" tools: list[Tool] + result_type: ResultType = "complete" + """See `ResultType`. Always serialized; older peers ignore it.""" -class CallToolRequestParams(RequestParams): - """Parameters for calling a tool.""" +class CallToolRequestParams(InputResponseRequestParams): name: str arguments: dict[str, Any] | None = None + task: TaskMetadata | None = None + """If specified, the caller requests task-augmented execution (2025-11-25 only).""" class CallToolRequest(Request[CallToolRequestParams, Literal["tools/call"]]): @@ -936,17 +1424,35 @@ class CallToolRequest(Request[CallToolRequestParams, Literal["tools/call"]]): class CallToolResult(Result): - """The server's response to a tool call.""" + """The server's response to a tool call. + + Errors that originate from the tool SHOULD be reported inside the result + with `is_error` set to true, not as an MCP protocol-level error, so the LLM + can see and self-correct. Errors in finding the tool, or any other + exceptional condition, should be reported as an MCP error response. + """ content: list[ContentBlock] - structured_content: dict[str, Any] | None = None - """An optional JSON object that represents the structured result of the tool call.""" + """A list of content objects that represent the unstructured result of the tool call.""" + structured_content: Any = None + """An optional JSON value representing the structured result of the tool call. + + Any JSON value on 2026-07-28; restricted to a JSON object on 2025-06-18 and + 2025-11-25. + """ is_error: bool = False + """Whether the tool call ended in an error.""" + + result_type: ResultType = "complete" + """See `ResultType`. Always serialized; older peers ignore it.""" class ToolListChangedNotification(Notification[NotificationParams | None, Literal["notifications/tools/list_changed"]]): """An optional notification from the server to the client, informing it that the list of tools it offers has changed. + + May be sent spontaneously through 2025-11-25; on 2026-07-28 sessions the + client must opt in via `subscriptions/listen`. """ method: Literal["notifications/tools/list_changed"] = "notifications/tools/list_changed" @@ -954,25 +1460,36 @@ class ToolListChangedNotification(Notification[NotificationParams | None, Litera LoggingLevel = Literal["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"] +"""The severity of a log message. + +These map to syslog severities (RFC-5424 section 6.2.1). Logging is deprecated +in 2026-07-28 (SEP-2577); the level scale is unchanged across versions. +""" class SetLevelRequestParams(RequestParams): - """Parameters for setting the logging level.""" + """Parameters for setting the logging level. + + Removed in protocol 2026-07-28; sent/received on sessions negotiating <= 2025-11-25. + """ level: LoggingLevel - """The level of logging that the client wants to receive from the server.""" + """The level of logging that the client wants to receive from the server. + The server should send all logs at this level and higher (more severe).""" class SetLevelRequest(Request[SetLevelRequestParams, Literal["logging/setLevel"]]): - """A request from the client to the server, to enable or adjust logging.""" + """A request from the client to the server, to enable or adjust logging. + + Removed in protocol 2026-07-28; sent/received on sessions negotiating <= 2025-11-25. + On 2026-07-28 the client opts in per-request via `_meta` (`LOG_LEVEL_META_KEY`). + """ method: Literal["logging/setLevel"] = "logging/setLevel" params: SetLevelRequestParams class LoggingMessageNotificationParams(NotificationParams): - """Parameters for logging message notifications.""" - level: LoggingLevel """The severity of this log message.""" logger: str | None = None @@ -985,24 +1502,42 @@ class LoggingMessageNotificationParams(NotificationParams): class LoggingMessageNotification(Notification[LoggingMessageNotificationParams, Literal["notifications/message"]]): - """Notification of a log message passed from server to client.""" + """Notification of a log message passed from server to client. + + Through 2025-11-25 the client subscribes via `logging/setLevel`. On + 2026-07-28 the client opts in per-request via `_meta` (`LOG_LEVEL_META_KEY`) + and the server MUST NOT send this without it. Deprecated in 2026-07-28 (SEP-2577). + """ method: Literal["notifications/message"] = "notifications/message" params: LoggingMessageNotificationParams IncludeContext = Literal["none", "thisServer", "allServers"] +"""Scope of MCP-server context a sampling request asks the client to attach. + +"thisServer" and "allServers" are deprecated (SEP-2596). +""" class ModelHint(MCPModel): - """Hints to use for model selection.""" + """Hints to use for model selection. + + Keys not declared here are up to the client to interpret. Deprecated in + 2026-07-28 (SEP-2577) with the rest of sampling. + """ name: str | None = None - """A hint for a model name.""" + """A hint for a model name. + + The client SHOULD treat this as a substring (e.g. `sonnet` matches + `claude-3-5-sonnet-20241022`) and MAY map it to another provider's model + that fills a similar niche. + """ class ModelPreferences(MCPModel): - """The server's preferences for model selection, requested by the client during + """The server's preferences for model selection, requested of the client during sampling. Because LLMs can vary along multiple dimensions, choosing the "best" model is @@ -1014,6 +1549,8 @@ class ModelPreferences(MCPModel): These preferences are always advisory. The client MAY ignore them. It is also up to the client to decide how to interpret these preferences and how to balance them against other considerations. + + Deprecated in 2026-07-28 (SEP-2577) with the rest of sampling. """ hints: list[ModelHint] | None = None @@ -1050,25 +1587,24 @@ class ModelPreferences(MCPModel): class ToolChoice(MCPModel): - """Controls tool usage behavior during sampling. + """Controls tool selection behavior for sampling requests (2025-11-25+). - Allows the server to specify whether and how the LLM should use tools - in its response. + The client MUST return an error if this is received without the + `sampling.tools` capability. Absent means `{"mode": "auto"}`. """ mode: Literal["auto", "required", "none"] | None = None """ - Controls when tools are used: + Controls the tool use ability of the model: - "auto": Model decides whether to use tools (default) - "required": Model MUST use at least one tool before completing - - "none": Model should not use tools + - "none": Model MUST NOT use any tools """ class CreateMessageRequestParams(RequestParams): - """Parameters for creating a message.""" - messages: list[SamplingMessage] + """The conversation to sample from.""" model_preferences: ModelPreferences | None = None """ The server's preferences for which model to select. The client MAY ignore @@ -1078,35 +1614,44 @@ class CreateMessageRequestParams(RequestParams): """An optional system prompt the server wants to use for sampling.""" include_context: IncludeContext | None = None """ - A request to include context from one or more MCP servers (including the caller), to - be attached to the prompt. + 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". "thisServer" and "allServers" are deprecated (SEP-2596). """ temperature: float | None = None max_tokens: int """The maximum number of tokens to sample, as requested by the server.""" stop_sequences: list[str] | None = None metadata: dict[str, Any] | None = None - """Optional metadata to pass through to the LLM provider.""" + """Optional metadata to pass through to the LLM provider. Provider-specific.""" tools: list[Tool] | None = None - """ - Tool definitions for the LLM to use during sampling. - Requires clientCapabilities.sampling.tools to be present. - """ + """Tools the model may use during generation (2025-11-25+). Requires the + `sampling.tools` client capability.""" tool_choice: ToolChoice | None = None - """ - Controls tool usage behavior. - Requires clientCapabilities.sampling.tools and the tools parameter to be present. - """ + """Controls how the model uses tools (2025-11-25+). Requires the + `sampling.tools` client capability.""" + task: TaskMetadata | None = None + """If specified, the caller requests task-augmented execution (2025-11-25 only).""" class CreateMessageRequest(Request[CreateMessageRequestParams, Literal["sampling/createMessage"]]): - """A request from the server to sample an LLM via the client.""" + """A request from the server to sample an LLM via the client. + + The client has full discretion over which model to select and should inform + the user before sampling (human in the loop). A standalone JSON-RPC request + through 2025-11-25; on 2026-07-28 it is embedded in + `InputRequiredResult.input_requests` instead. Deprecated in 2026-07-28 (SEP-2577). + """ method: Literal["sampling/createMessage"] = "sampling/createMessage" params: CreateMessageRequestParams StopReason = Literal["endTurn", "stopSequence", "maxTokens", "toolUse"] | str +"""The reason why sampling stopped, if known. + +An open union to allow provider-specific stop reasons. "toolUse" is 2025-11-25+. +""" class CreateMessageResult(Result): @@ -1114,6 +1659,9 @@ class CreateMessageResult(Result): This is the backwards-compatible version that returns single content (no arrays). Used when the request does not include tools. + + On 2026-07-28 this travels embedded in an `InputResponses` map rather than + as a top-level JSON-RPC result. Deprecated in 2026-07-28 (SEP-2577). """ role: Role @@ -1129,7 +1677,7 @@ class CreateMessageResult(Result): class CreateMessageResultWithTools(Result): """The client's response to a sampling/createMessage request when tools were provided. - This version supports array content for tool use flows. + This version supports array content for tool use flows (2025-11-25 and later). """ role: Role @@ -1162,12 +1710,15 @@ class ResourceTemplateReference(MCPModel): """The URI or URI template of the resource.""" +# Not BaseMetadata: inheriting would reorder dump keys for existing callers. class PromptReference(MCPModel): """Identifies a prompt.""" type: Literal["ref/prompt"] = "ref/prompt" name: str """The name of the prompt or prompt template.""" + title: str | None = None + """Human-readable display title. If not provided, `name` should be used for display.""" class CompletionArgument(MCPModel): @@ -1187,9 +1738,8 @@ class CompletionContext(MCPModel): class CompleteRequestParams(RequestParams): - """Parameters for completion requests.""" - ref: ResourceTemplateReference | PromptReference + """The prompt or resource-template reference to complete against.""" argument: CompletionArgument context: CompletionContext | None = None """Additional, optional context for completions.""" @@ -1223,6 +1773,10 @@ class CompleteResult(Result): """The server's response to a completion/complete request.""" completion: Completion + """The completion values, with optional total / has-more pagination hints.""" + + result_type: ResultType = "complete" + """See `ResultType`. Always serialized; older peers ignore it.""" class ListRootsRequest(Request[RequestParams | None, Literal["roots/list"]]): @@ -1233,14 +1787,22 @@ class ListRootsRequest(Request[RequestParams | None, Literal["roots/list"]]): This request is typically used when the server needs to understand the file system structure or access specific locations that the client has permission to read from. + + A standalone JSON-RPC request through 2025-11-25; on 2026-07-28 it is + embedded in `InputRequiredResult.input_requests`. Deprecated in 2026-07-28 (SEP-2577). """ method: Literal["roots/list"] = "roots/list" params: RequestParams | None = None + """Stays optional on 2026-07-28 (reserved client `_meta` keys do not apply + to server-to-client payloads).""" class Root(MCPModel): - """Represents a root directory or file that the server can operate on.""" + """Represents a root directory or file that the server can operate on. + + Deprecated in 2026-07-28 (SEP-2577) with the rest of roots. + """ uri: FileUrl """ @@ -1264,8 +1826,11 @@ class Root(MCPModel): class ListRootsResult(Result): """The client's response to a roots/list request from the server. - This result contains an array of Root objects, each representing a root directory - or file that the server can operate on. + This result contains an array of Root objects, each representing a root + directory or file that the server can operate on. + + On 2026-07-28 this is carried as an `InputResponses` entry, not a JSON-RPC + result. Deprecated in 2026-07-28 (SEP-2577). """ roots: list[Root] @@ -1280,6 +1845,8 @@ class RootsListChangedNotification( This notification should be sent whenever the client adds, removes, or modifies any root. The server should then request an updated list of roots using the ListRootsRequest. + + Removed in protocol 2026-07-28; sent/received on sessions negotiating <= 2025-11-25. """ method: Literal["notifications/roots/list_changed"] = "notifications/roots/list_changed" @@ -1287,13 +1854,12 @@ class RootsListChangedNotification( class CancelledNotificationParams(NotificationParams): - """Parameters for cancellation notifications.""" - request_id: RequestId | None = None """ The ID of the request to cancel. This MUST correspond to the ID of a request previously issued in the same direction. + Required on the wire through 2025-06-18; optional from 2025-11-25. """ reason: str | None = None """An optional string describing the reason for the cancellation.""" @@ -1302,6 +1868,10 @@ class CancelledNotificationParams(NotificationParams): class CancelledNotification(Notification[CancelledNotificationParams, Literal["notifications/cancelled"]]): """This notification can be sent by either side to indicate that it is canceling a previously-issued request. + + The request SHOULD still be in-flight, but due to communication latency, it + is always possible that this notification MAY arrive after the request has + already finished. A client MUST NOT attempt to cancel its `initialize` request. """ method: Literal["notifications/cancelled"] = "notifications/cancelled" @@ -1325,39 +1895,16 @@ class ElicitCompleteNotification( URLElicitationRequiredError, update the user interface, or otherwise continue an interaction. However, because delivery of the notification is not guaranteed, clients must not wait indefinitely for a notification from the server. + + New in protocol 2025-11-25 with URL mode itself. """ method: Literal["notifications/elicitation/complete"] = "notifications/elicitation/complete" params: ElicitCompleteNotificationParams -ClientRequest = ( - PingRequest - | InitializeRequest - | CompleteRequest - | SetLevelRequest - | GetPromptRequest - | ListPromptsRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | SubscribeRequest - | UnsubscribeRequest - | CallToolRequest - | ListToolsRequest -) -client_request_adapter = TypeAdapter[ClientRequest](ClientRequest) - - -ClientNotification = ( - CancelledNotification | ProgressNotification | InitializedNotification | RootsListChangedNotification -) -client_notification_adapter = TypeAdapter[ClientNotification](ClientNotification) - - # Type for elicitation schema - a JSON Schema dict ElicitRequestedSchema: TypeAlias = dict[str, Any] -"""Schema for elicitation requests.""" class ElicitRequestFormParams(RequestParams): @@ -1379,12 +1926,15 @@ class ElicitRequestFormParams(RequestParams): Only top-level properties are allowed, without nesting. """ + task: TaskMetadata | None = None + """If specified, the caller requests task-augmented execution (2025-11-25 only).""" + class ElicitRequestURLParams(RequestParams): """Parameters for URL mode elicitation requests. URL mode directs users to external URLs for sensitive out-of-band interactions - like OAuth flows, credential collection, or payment processing. + like OAuth flows, credential collection, or payment processing. New in 2025-11-25. """ mode: Literal["url"] = "url" @@ -1402,6 +1952,9 @@ class ElicitRequestURLParams(RequestParams): The client MUST treat this ID as an opaque value. """ + task: TaskMetadata | None = None + """If specified, the caller requests task-augmented execution (2025-11-25 only).""" + # Union type for elicitation request parameters ElicitRequestParams: TypeAlias = ElicitRequestURLParams | ElicitRequestFormParams @@ -1409,7 +1962,7 @@ class ElicitRequestURLParams(RequestParams): class ElicitRequest(Request[ElicitRequestParams, Literal["elicitation/create"]]): - """A request from the server to elicit information from the client.""" + """A request from the server to elicit additional information from the user via the client.""" method: Literal["elicitation/create"] = "elicitation/create" params: ElicitRequestParams @@ -1432,25 +1985,130 @@ class ElicitResult(Result): Contains values matching the requested schema. Values can be strings, integers, floats, booleans, arrays of strings, or null. For URL mode, this field is omitted. + + The null value arm is superset leniency; the schema-exact surface types + declare no null arm. """ class ElicitationRequiredErrorData(MCPModel): - """Error data for URLElicitationRequiredError. + """Error data for the -32042 URL-elicitation-required error. Servers return this when a request cannot be processed until one or more URL mode elicitations are completed. + + Removed in protocol 2026-07-28; sent/received on sessions negotiating 2025-11-25. """ elicitations: list[ElicitRequestURLParams] """List of URL mode elicitations that must be completed.""" +InputRequest: TypeAlias = CreateMessageRequest | ListRootsRequest | ElicitRequest +"""A single server-initiated input request embedded in `InputRequiredResult` (2026-07-28). + +Discriminated by `method`. On 2026-07-28 these embedded payloads take the place +of standalone server-to-client JSON-RPC requests. +""" + +InputRequests: TypeAlias = dict[str, InputRequest] +"""A map of server-initiated requests that the client must fulfill (2026-07-28). + +Keys are server-assigned identifiers. Carried by `InputRequiredResult.input_requests` +and by the tasks extension. +""" + +InputResponse: TypeAlias = CreateMessageResult | CreateMessageResultWithTools | ListRootsResult | ElicitResult +"""A client response to a single server-initiated input request (2026-07-28). + +`CreateMessageResultWithTools` is this SDK's array-content split of the schema's +single `CreateMessageResult` arm; the wire union has three arms. +""" + +InputResponses: TypeAlias = dict[str, InputResponse] +"""A map of client responses to server-initiated input requests (2026-07-28). + +Keys match those of the `InputRequests` map the server sent. Also used by the +tasks extension's `tasks/update` params. +""" + + +class InputRequiredResult(Result): + """The server needs additional input before the original request can complete (2026-07-28). + + Returned in place of the normal result of an interactive client request + (`tools/call`, `prompts/get`, `resources/read`). The client fulfills + `input_requests` and retries the original request, carrying the responses + and the echoed `request_state`. At least one of those two fields is + present on the wire (spec MUST; not enforced by the model). + """ + + result_type: Literal["input_required"] = "input_required" + """Discriminating tag for the dual-result response unions.""" + + input_requests: InputRequests | None = None + """Requests the client must complete before retrying. Keys are server-assigned.""" + + request_state: str | None = None + """Opaque state to pass back verbatim when the client retries the original request.""" + + +# Forward refs to InputResponses; rebuild at import time rather than first use. +InputResponseRequestParams.model_rebuild() +ReadResourceRequestParams.model_rebuild() +GetPromptRequestParams.model_rebuild() +CallToolRequestParams.model_rebuild() + +# Top-level message unions: superset across all supported protocol versions. +# Per-version validity is recorded in `mcp.types.methods`, not enforced here. + +ClientRequest = ( + PingRequest + | InitializeRequest + | CompleteRequest + | SetLevelRequest + | GetPromptRequest + | ListPromptsRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscribeRequest + | UnsubscribeRequest + | CallToolRequest + | ListToolsRequest + | DiscoverRequest + | SubscriptionsListenRequest +) +"""Union of client-to-server request payloads across all supported protocol versions. + +The 2025-11-25 task requests are deliberately excluded (types-only). +""" + +client_request_adapter = TypeAdapter[ClientRequest](ClientRequest) + + +ClientNotification = ( + CancelledNotification | ProgressNotification | InitializedNotification | RootsListChangedNotification +) +"""Notifications sent from the client to the server. + +`TaskStatusNotification` is deliberately excluded (types-only). +""" + +client_notification_adapter = TypeAdapter[ClientNotification](ClientNotification) + + ClientResult = EmptyResult | CreateMessageResult | CreateMessageResultWithTools | ListRootsResult | ElicitResult client_result_adapter = TypeAdapter[ClientResult](ClientResult) ServerRequest = PingRequest | CreateMessageRequest | ListRootsRequest | ElicitRequest +"""Union of standalone JSON-RPC requests a server can send to a client. + +Live through 2025-11-25 only: 2026-07-28 has no server-to-client JSON-RPC +requests (these payloads are embedded in `InputRequiredResult` instead). +""" + server_request_adapter = TypeAdapter[ServerRequest](ServerRequest) @@ -1463,13 +2121,20 @@ class ElicitationRequiredErrorData(MCPModel): | ToolListChangedNotification | PromptListChangedNotification | ElicitCompleteNotification + | SubscriptionsAcknowledgedNotification ) +"""Union of server-to-client notification payloads across all supported protocol versions. + +`TaskStatusNotification` is deliberately excluded (types-only). +""" + server_notification_adapter = TypeAdapter[ServerNotification](ServerNotification) ServerResult = ( EmptyResult | InitializeResult + | DiscoverResult | CompleteResult | GetPromptResult | ListPromptsResult @@ -1478,5 +2143,11 @@ class ElicitationRequiredErrorData(MCPModel): | ReadResourceResult | CallToolResult | ListToolsResult + | InputRequiredResult ) +"""Union of every result payload a server can return for a client request. + +`InputRequiredResult` is deliberately last: both of its fields are optional, +so an earlier position would shadow other members during union resolution. +""" server_result_adapter = TypeAdapter[ServerResult](ServerResult) diff --git a/src/mcp/types/_wire_base.py b/src/mcp/types/_wire_base.py new file mode 100644 index 000000000..d6a2e55f7 --- /dev/null +++ b/src/mcp/types/_wire_base.py @@ -0,0 +1,19 @@ +"""Shared pydantic bases for the `mcp.types.v*` wire-shape packages. + +No alias generator is configured: every wire name is an explicit +`Field(alias=...)` so each surface file shows exactly what goes on the wire. +""" + +from pydantic import BaseModel, ConfigDict + + +class WireModel(BaseModel): + """Base for surface-package models: unknown fields are accepted and dropped.""" + + model_config = ConfigDict(populate_by_name=True, extra="ignore") + + +class OpenWireModel(BaseModel): + """Base for `_meta` carrier models: unknown fields are retained for round-tripping.""" + + model_config = ConfigDict(populate_by_name=True, extra="allow") diff --git a/src/mcp/types/jsonrpc.py b/src/mcp/types/jsonrpc.py index 84304a37c..990973833 100644 --- a/src/mcp/types/jsonrpc.py +++ b/src/mcp/types/jsonrpc.py @@ -2,13 +2,16 @@ from __future__ import annotations -from typing import Annotated, Any, Literal +from typing import Annotated, Any, Final, Literal from pydantic import BaseModel, Field, TypeAdapter RequestId = Annotated[int, Field(strict=True)] | str """The ID of a JSON-RPC request.""" +JSONRPC_VERSION: Final[Literal["2.0"]] = "2.0" +"""The JSON-RPC version string carried by every MCP message envelope.""" + class JSONRPCRequest(BaseModel): """A JSON-RPC request that expects a response.""" @@ -27,9 +30,11 @@ class JSONRPCNotification(BaseModel): params: dict[str, Any] | None = None -# TODO(Marcelo): This is actually not correct. A JSONRPCResponse is the union of a successful response and an error. class JSONRPCResponse(BaseModel): - """A successful (non-error) response to a request.""" + """A successful (non-error) response to a request. + + Named `JSONRPCResultResponse` in the 2025-11-25+ schemas; the SDK keeps the original name. + """ jsonrpc: Literal["2.0"] id: RequestId @@ -38,18 +43,41 @@ class JSONRPCResponse(BaseModel): # MCP-specific error codes in the range [-32000, -32099] URL_ELICITATION_REQUIRED = -32042 -"""Error code indicating that a URL mode elicitation is required before the request can be processed.""" +"""A URL-mode elicitation is required before the request can be processed (protocol 2025-11-25 only).""" + +MISSING_REQUIRED_CLIENT_CAPABILITY = -32003 +"""The server requires a client capability the request did not declare (protocol 2026-07-28).""" -# SDK error codes +UNSUPPORTED_PROTOCOL_VERSION = -32004 +"""The request's protocol version is not supported by the server (protocol 2026-07-28).""" + +# SDK error codes: SDK-internal allocations in the JSON-RPC server-error range +# [-32000, -32099]; not defined by the MCP schema. New values must avoid codes +# the spec has allocated above. CONNECTION_CLOSED = -32000 +"""SDK-only: the connection closed before a response arrived; never emitted on the wire.""" + REQUEST_TIMEOUT = -32001 +"""SDK-only: a request timed out waiting for its response.""" # Standard JSON-RPC error codes PARSE_ERROR = -32700 +"""Standard JSON-RPC: invalid JSON was received.""" + INVALID_REQUEST = -32600 +"""Standard JSON-RPC: the message is not a valid request object.""" + METHOD_NOT_FOUND = -32601 +"""Standard JSON-RPC: the requested method does not exist or is not available.""" + INVALID_PARAMS = -32602 +"""Standard JSON-RPC: invalid method parameters.""" + INTERNAL_ERROR = -32603 +"""Standard JSON-RPC: an internal error occurred on the receiver. + +The SDK uses the generic `ErrorData` envelope; the schema's per-code wrapper types are not constructed. +""" class ErrorData(BaseModel): @@ -76,8 +104,15 @@ class JSONRPCError(BaseModel): jsonrpc: Literal["2.0"] id: RequestId | None + """The id of the request this error responds to. + + Required but nullable per JSON-RPC 2.0: `None` encodes `"id": null` (the id could not be determined). + """ + error: ErrorData JSONRPCMessage = JSONRPCRequest | JSONRPCNotification | JSONRPCResponse | JSONRPCError +"""Any JSON-RPC envelope that can be decoded off the wire or encoded to be sent.""" + jsonrpc_message_adapter: TypeAdapter[JSONRPCMessage] = TypeAdapter(JSONRPCMessage) diff --git a/src/mcp/types/methods.py b/src/mcp/types/methods.py new file mode 100644 index 000000000..f3431b84e --- /dev/null +++ b/src/mcp/types/methods.py @@ -0,0 +1,602 @@ +"""Per-version method maps and parse functions for inbound MCP traffic. + +Surface maps key `(method, version)` to schema-exact types (key absence is the +version gate); monolith maps key `method` to the version-free `mcp.types` models +user code receives. Session-layer wiring is a follow-up.""" + +from __future__ import annotations + +from collections.abc import Mapping +from functools import cache +from types import MappingProxyType, UnionType +from typing import Any, Final, TypeVar + +from pydantic import BaseModel, TypeAdapter + +import mcp.types as types +import mcp.types.v2025_11_25 as v2025 +import mcp.types.v2026_07_28 as v2026 +from mcp.shared.version import KNOWN_PROTOCOL_VERSIONS +from mcp.types._wire_base import WireModel + +__all__ = [ + "CLIENT_NOTIFICATIONS", + "CLIENT_REQUESTS", + "CLIENT_RESULTS", + "MONOLITH_NOTIFICATIONS", + "MONOLITH_REQUESTS", + "MONOLITH_RESULTS", + "SERVER_NOTIFICATIONS", + "SERVER_REQUESTS", + "SERVER_RESULTS", + "parse_client_notification", + "parse_client_request", + "parse_client_result", + "parse_server_notification", + "parse_server_request", + "parse_server_result", +] + + +# --- Surface maps: client-to-server --- + +CLIENT_REQUESTS: Final[Mapping[tuple[str, str], type[WireModel]]] = MappingProxyType( + { + # 2024-11-05 + ("completion/complete", "2024-11-05"): v2025.CompleteRequest, + ("initialize", "2024-11-05"): v2025.InitializeRequest, + ("logging/setLevel", "2024-11-05"): v2025.SetLevelRequest, + ("ping", "2024-11-05"): v2025.PingRequest, + ("prompts/get", "2024-11-05"): v2025.GetPromptRequest, + ("prompts/list", "2024-11-05"): v2025.ListPromptsRequest, + ("resources/list", "2024-11-05"): v2025.ListResourcesRequest, + ("resources/read", "2024-11-05"): v2025.ReadResourceRequest, + ("resources/subscribe", "2024-11-05"): v2025.SubscribeRequest, + ("resources/templates/list", "2024-11-05"): v2025.ListResourceTemplatesRequest, + ("resources/unsubscribe", "2024-11-05"): v2025.UnsubscribeRequest, + ("tools/call", "2024-11-05"): v2025.CallToolRequest, + ("tools/list", "2024-11-05"): v2025.ListToolsRequest, + # 2025-03-26 + ("completion/complete", "2025-03-26"): v2025.CompleteRequest, + ("initialize", "2025-03-26"): v2025.InitializeRequest, + ("logging/setLevel", "2025-03-26"): v2025.SetLevelRequest, + ("ping", "2025-03-26"): v2025.PingRequest, + ("prompts/get", "2025-03-26"): v2025.GetPromptRequest, + ("prompts/list", "2025-03-26"): v2025.ListPromptsRequest, + ("resources/list", "2025-03-26"): v2025.ListResourcesRequest, + ("resources/read", "2025-03-26"): v2025.ReadResourceRequest, + ("resources/subscribe", "2025-03-26"): v2025.SubscribeRequest, + ("resources/templates/list", "2025-03-26"): v2025.ListResourceTemplatesRequest, + ("resources/unsubscribe", "2025-03-26"): v2025.UnsubscribeRequest, + ("tools/call", "2025-03-26"): v2025.CallToolRequest, + ("tools/list", "2025-03-26"): v2025.ListToolsRequest, + # 2025-06-18 + ("completion/complete", "2025-06-18"): v2025.CompleteRequest, + ("initialize", "2025-06-18"): v2025.InitializeRequest, + ("logging/setLevel", "2025-06-18"): v2025.SetLevelRequest, + ("ping", "2025-06-18"): v2025.PingRequest, + ("prompts/get", "2025-06-18"): v2025.GetPromptRequest, + ("prompts/list", "2025-06-18"): v2025.ListPromptsRequest, + ("resources/list", "2025-06-18"): v2025.ListResourcesRequest, + ("resources/read", "2025-06-18"): v2025.ReadResourceRequest, + ("resources/subscribe", "2025-06-18"): v2025.SubscribeRequest, + ("resources/templates/list", "2025-06-18"): v2025.ListResourceTemplatesRequest, + ("resources/unsubscribe", "2025-06-18"): v2025.UnsubscribeRequest, + ("tools/call", "2025-06-18"): v2025.CallToolRequest, + ("tools/list", "2025-06-18"): v2025.ListToolsRequest, + # 2025-11-25 (tasks/* deliberately absent) + ("completion/complete", "2025-11-25"): v2025.CompleteRequest, + ("initialize", "2025-11-25"): v2025.InitializeRequest, + ("logging/setLevel", "2025-11-25"): v2025.SetLevelRequest, + ("ping", "2025-11-25"): v2025.PingRequest, + ("prompts/get", "2025-11-25"): v2025.GetPromptRequest, + ("prompts/list", "2025-11-25"): v2025.ListPromptsRequest, + ("resources/list", "2025-11-25"): v2025.ListResourcesRequest, + ("resources/read", "2025-11-25"): v2025.ReadResourceRequest, + ("resources/subscribe", "2025-11-25"): v2025.SubscribeRequest, + ("resources/templates/list", "2025-11-25"): v2025.ListResourceTemplatesRequest, + ("resources/unsubscribe", "2025-11-25"): v2025.UnsubscribeRequest, + ("tools/call", "2025-11-25"): v2025.CallToolRequest, + ("tools/list", "2025-11-25"): v2025.ListToolsRequest, + # 2026-07-28 (lifecycle, logging, subscribe pair removed; discover/listen added) + ("completion/complete", "2026-07-28"): v2026.CompleteRequest, + ("prompts/get", "2026-07-28"): v2026.GetPromptRequest, + ("prompts/list", "2026-07-28"): v2026.ListPromptsRequest, + ("resources/list", "2026-07-28"): v2026.ListResourcesRequest, + ("resources/read", "2026-07-28"): v2026.ReadResourceRequest, + ("resources/templates/list", "2026-07-28"): v2026.ListResourceTemplatesRequest, + ("server/discover", "2026-07-28"): v2026.DiscoverRequest, + ("subscriptions/listen", "2026-07-28"): v2026.SubscriptionsListenRequest, + ("tools/call", "2026-07-28"): v2026.CallToolRequest, + ("tools/list", "2026-07-28"): v2026.ListToolsRequest, + } +) + +CLIENT_NOTIFICATIONS: Final[Mapping[tuple[str, str], type[WireModel]]] = MappingProxyType( + { + # 2024-11-05 + ("notifications/cancelled", "2024-11-05"): v2025.CancelledNotification, + ("notifications/initialized", "2024-11-05"): v2025.InitializedNotification, + ("notifications/progress", "2024-11-05"): v2025.ProgressNotification, + ("notifications/roots/list_changed", "2024-11-05"): v2025.RootsListChangedNotification, + # 2025-03-26 + ("notifications/cancelled", "2025-03-26"): v2025.CancelledNotification, + ("notifications/initialized", "2025-03-26"): v2025.InitializedNotification, + ("notifications/progress", "2025-03-26"): v2025.ProgressNotification, + ("notifications/roots/list_changed", "2025-03-26"): v2025.RootsListChangedNotification, + # 2025-06-18 + ("notifications/cancelled", "2025-06-18"): v2025.CancelledNotification, + ("notifications/initialized", "2025-06-18"): v2025.InitializedNotification, + ("notifications/progress", "2025-06-18"): v2025.ProgressNotification, + ("notifications/roots/list_changed", "2025-06-18"): v2025.RootsListChangedNotification, + # 2025-11-25 (tasks/status deliberately absent) + ("notifications/cancelled", "2025-11-25"): v2025.CancelledNotification, + ("notifications/initialized", "2025-11-25"): v2025.InitializedNotification, + ("notifications/progress", "2025-11-25"): v2025.ProgressNotification, + ("notifications/roots/list_changed", "2025-11-25"): v2025.RootsListChangedNotification, + # 2026-07-28 (initialized and roots/list_changed removed) + ("notifications/cancelled", "2026-07-28"): v2026.CancelledNotification, + ("notifications/progress", "2026-07-28"): v2026.ProgressNotification, + } +) + + +# --- Surface maps: server-to-client --- + +SERVER_REQUESTS: Final[Mapping[tuple[str, str], type[WireModel]]] = MappingProxyType( + { + # 2024-11-05 + ("ping", "2024-11-05"): v2025.PingRequest, + ("roots/list", "2024-11-05"): v2025.ListRootsRequest, + ("sampling/createMessage", "2024-11-05"): v2025.CreateMessageRequest, + # 2025-03-26 + ("ping", "2025-03-26"): v2025.PingRequest, + ("roots/list", "2025-03-26"): v2025.ListRootsRequest, + ("sampling/createMessage", "2025-03-26"): v2025.CreateMessageRequest, + # 2025-06-18 (adds elicitation/create) + ("elicitation/create", "2025-06-18"): v2025.ElicitRequest, + ("ping", "2025-06-18"): v2025.PingRequest, + ("roots/list", "2025-06-18"): v2025.ListRootsRequest, + ("sampling/createMessage", "2025-06-18"): v2025.CreateMessageRequest, + # 2025-11-25 (tasks/* deliberately absent) + ("elicitation/create", "2025-11-25"): v2025.ElicitRequest, + ("ping", "2025-11-25"): v2025.PingRequest, + ("roots/list", "2025-11-25"): v2025.ListRootsRequest, + ("sampling/createMessage", "2025-11-25"): v2025.CreateMessageRequest, + # 2026-07-28: none (schema defines no ServerRequest union) + } +) + +SERVER_NOTIFICATIONS: Final[Mapping[tuple[str, str], type[WireModel]]] = MappingProxyType( + { + # 2024-11-05 + ("notifications/cancelled", "2024-11-05"): v2025.CancelledNotification, + ("notifications/message", "2024-11-05"): v2025.LoggingMessageNotification, + ("notifications/progress", "2024-11-05"): v2025.ProgressNotification, + ("notifications/prompts/list_changed", "2024-11-05"): v2025.PromptListChangedNotification, + ("notifications/resources/list_changed", "2024-11-05"): v2025.ResourceListChangedNotification, + ("notifications/resources/updated", "2024-11-05"): v2025.ResourceUpdatedNotification, + ("notifications/tools/list_changed", "2024-11-05"): v2025.ToolListChangedNotification, + # 2025-03-26 + ("notifications/cancelled", "2025-03-26"): v2025.CancelledNotification, + ("notifications/message", "2025-03-26"): v2025.LoggingMessageNotification, + ("notifications/progress", "2025-03-26"): v2025.ProgressNotification, + ("notifications/prompts/list_changed", "2025-03-26"): v2025.PromptListChangedNotification, + ("notifications/resources/list_changed", "2025-03-26"): v2025.ResourceListChangedNotification, + ("notifications/resources/updated", "2025-03-26"): v2025.ResourceUpdatedNotification, + ("notifications/tools/list_changed", "2025-03-26"): v2025.ToolListChangedNotification, + # 2025-06-18 + ("notifications/cancelled", "2025-06-18"): v2025.CancelledNotification, + ("notifications/message", "2025-06-18"): v2025.LoggingMessageNotification, + ("notifications/progress", "2025-06-18"): v2025.ProgressNotification, + ("notifications/prompts/list_changed", "2025-06-18"): v2025.PromptListChangedNotification, + ("notifications/resources/list_changed", "2025-06-18"): v2025.ResourceListChangedNotification, + ("notifications/resources/updated", "2025-06-18"): v2025.ResourceUpdatedNotification, + ("notifications/tools/list_changed", "2025-06-18"): v2025.ToolListChangedNotification, + # 2025-11-25 (adds elicitation/complete; tasks/status deliberately absent) + ("notifications/cancelled", "2025-11-25"): v2025.CancelledNotification, + ("notifications/elicitation/complete", "2025-11-25"): v2025.ElicitationCompleteNotification, + ("notifications/message", "2025-11-25"): v2025.LoggingMessageNotification, + ("notifications/progress", "2025-11-25"): v2025.ProgressNotification, + ("notifications/prompts/list_changed", "2025-11-25"): v2025.PromptListChangedNotification, + ("notifications/resources/list_changed", "2025-11-25"): v2025.ResourceListChangedNotification, + ("notifications/resources/updated", "2025-11-25"): v2025.ResourceUpdatedNotification, + ("notifications/tools/list_changed", "2025-11-25"): v2025.ToolListChangedNotification, + # 2026-07-28 (adds subscriptions/acknowledged) + ("notifications/cancelled", "2026-07-28"): v2026.CancelledNotification, + ("notifications/elicitation/complete", "2026-07-28"): v2026.ElicitationCompleteNotification, + ("notifications/message", "2026-07-28"): v2026.LoggingMessageNotification, + ("notifications/progress", "2026-07-28"): v2026.ProgressNotification, + ("notifications/prompts/list_changed", "2026-07-28"): v2026.PromptListChangedNotification, + ("notifications/resources/list_changed", "2026-07-28"): v2026.ResourceListChangedNotification, + ("notifications/resources/updated", "2026-07-28"): v2026.ResourceUpdatedNotification, + ("notifications/subscriptions/acknowledged", "2026-07-28"): v2026.SubscriptionsAcknowledgedNotification, + ("notifications/tools/list_changed", "2026-07-28"): v2026.ToolListChangedNotification, + } +) + + +# --- Surface maps: results --- + +SERVER_RESULTS: Final[Mapping[tuple[str, str], type[WireModel] | UnionType]] = MappingProxyType( + { + # 2024-11-05 + ("completion/complete", "2024-11-05"): v2025.CompleteResult, + ("initialize", "2024-11-05"): v2025.InitializeResult, + ("logging/setLevel", "2024-11-05"): v2025.EmptyResult, + ("ping", "2024-11-05"): v2025.EmptyResult, + ("prompts/get", "2024-11-05"): v2025.GetPromptResult, + ("prompts/list", "2024-11-05"): v2025.ListPromptsResult, + ("resources/list", "2024-11-05"): v2025.ListResourcesResult, + ("resources/read", "2024-11-05"): v2025.ReadResourceResult, + ("resources/subscribe", "2024-11-05"): v2025.EmptyResult, + ("resources/templates/list", "2024-11-05"): v2025.ListResourceTemplatesResult, + ("resources/unsubscribe", "2024-11-05"): v2025.EmptyResult, + ("tools/call", "2024-11-05"): v2025.CallToolResult, + ("tools/list", "2024-11-05"): v2025.ListToolsResult, + # 2025-03-26 + ("completion/complete", "2025-03-26"): v2025.CompleteResult, + ("initialize", "2025-03-26"): v2025.InitializeResult, + ("logging/setLevel", "2025-03-26"): v2025.EmptyResult, + ("ping", "2025-03-26"): v2025.EmptyResult, + ("prompts/get", "2025-03-26"): v2025.GetPromptResult, + ("prompts/list", "2025-03-26"): v2025.ListPromptsResult, + ("resources/list", "2025-03-26"): v2025.ListResourcesResult, + ("resources/read", "2025-03-26"): v2025.ReadResourceResult, + ("resources/subscribe", "2025-03-26"): v2025.EmptyResult, + ("resources/templates/list", "2025-03-26"): v2025.ListResourceTemplatesResult, + ("resources/unsubscribe", "2025-03-26"): v2025.EmptyResult, + ("tools/call", "2025-03-26"): v2025.CallToolResult, + ("tools/list", "2025-03-26"): v2025.ListToolsResult, + # 2025-06-18 + ("completion/complete", "2025-06-18"): v2025.CompleteResult, + ("initialize", "2025-06-18"): v2025.InitializeResult, + ("logging/setLevel", "2025-06-18"): v2025.EmptyResult, + ("ping", "2025-06-18"): v2025.EmptyResult, + ("prompts/get", "2025-06-18"): v2025.GetPromptResult, + ("prompts/list", "2025-06-18"): v2025.ListPromptsResult, + ("resources/list", "2025-06-18"): v2025.ListResourcesResult, + ("resources/read", "2025-06-18"): v2025.ReadResourceResult, + ("resources/subscribe", "2025-06-18"): v2025.EmptyResult, + ("resources/templates/list", "2025-06-18"): v2025.ListResourceTemplatesResult, + ("resources/unsubscribe", "2025-06-18"): v2025.EmptyResult, + ("tools/call", "2025-06-18"): v2025.CallToolResult, + ("tools/list", "2025-06-18"): v2025.ListToolsResult, + # 2025-11-25 + ("completion/complete", "2025-11-25"): v2025.CompleteResult, + ("initialize", "2025-11-25"): v2025.InitializeResult, + ("logging/setLevel", "2025-11-25"): v2025.EmptyResult, + ("ping", "2025-11-25"): v2025.EmptyResult, + ("prompts/get", "2025-11-25"): v2025.GetPromptResult, + ("prompts/list", "2025-11-25"): v2025.ListPromptsResult, + ("resources/list", "2025-11-25"): v2025.ListResourcesResult, + ("resources/read", "2025-11-25"): v2025.ReadResourceResult, + ("resources/subscribe", "2025-11-25"): v2025.EmptyResult, + ("resources/templates/list", "2025-11-25"): v2025.ListResourceTemplatesResult, + ("resources/unsubscribe", "2025-11-25"): v2025.EmptyResult, + ("tools/call", "2025-11-25"): v2025.CallToolResult, + ("tools/list", "2025-11-25"): v2025.ListToolsResult, + # 2026-07-28 (dual-result rows use the version's union aliases) + ("completion/complete", "2026-07-28"): v2026.CompleteResult, + ("prompts/get", "2026-07-28"): v2026.AnyGetPromptResult, + ("prompts/list", "2026-07-28"): v2026.ListPromptsResult, + ("resources/list", "2026-07-28"): v2026.ListResourcesResult, + ("resources/read", "2026-07-28"): v2026.AnyReadResourceResult, + ("resources/templates/list", "2026-07-28"): v2026.ListResourceTemplatesResult, + ("server/discover", "2026-07-28"): v2026.DiscoverResult, + ("subscriptions/listen", "2026-07-28"): v2026.EmptyResult, + ("tools/call", "2026-07-28"): v2026.AnyCallToolResult, + ("tools/list", "2026-07-28"): v2026.ListToolsResult, + } +) +"""Results servers send, keyed by the originating client request's (method, version).""" + +CLIENT_RESULTS: Final[Mapping[tuple[str, str], type[WireModel] | UnionType]] = MappingProxyType( + { + # 2024-11-05 + ("ping", "2024-11-05"): v2025.EmptyResult, + ("roots/list", "2024-11-05"): v2025.ListRootsResult, + ("sampling/createMessage", "2024-11-05"): v2025.CreateMessageResult, + # 2025-03-26 + ("ping", "2025-03-26"): v2025.EmptyResult, + ("roots/list", "2025-03-26"): v2025.ListRootsResult, + ("sampling/createMessage", "2025-03-26"): v2025.CreateMessageResult, + # 2025-06-18 + ("elicitation/create", "2025-06-18"): v2025.ElicitResult, + ("ping", "2025-06-18"): v2025.EmptyResult, + ("roots/list", "2025-06-18"): v2025.ListRootsResult, + ("sampling/createMessage", "2025-06-18"): v2025.CreateMessageResult, + # 2025-11-25 + ("elicitation/create", "2025-11-25"): v2025.ElicitResult, + ("ping", "2025-11-25"): v2025.EmptyResult, + ("roots/list", "2025-11-25"): v2025.ListRootsResult, + ("sampling/createMessage", "2025-11-25"): v2025.CreateMessageResult, + # 2026-07-28: none (no server-to-client requests at this version) + } +) +"""Results clients send, keyed by the originating server request's (method, version).""" + + +# --- Monolith maps --- + +MONOLITH_REQUESTS: Final[Mapping[str, type[types.Request[Any, Any]]]] = MappingProxyType( + { + "completion/complete": types.CompleteRequest, + "elicitation/create": types.ElicitRequest, + "initialize": types.InitializeRequest, + "logging/setLevel": types.SetLevelRequest, + "ping": types.PingRequest, + "prompts/get": types.GetPromptRequest, + "prompts/list": types.ListPromptsRequest, + "resources/list": types.ListResourcesRequest, + "resources/read": types.ReadResourceRequest, + "resources/subscribe": types.SubscribeRequest, + "resources/templates/list": types.ListResourceTemplatesRequest, + "resources/unsubscribe": types.UnsubscribeRequest, + "roots/list": types.ListRootsRequest, + "sampling/createMessage": types.CreateMessageRequest, + "server/discover": types.DiscoverRequest, + "subscriptions/listen": types.SubscriptionsListenRequest, + "tools/call": types.CallToolRequest, + "tools/list": types.ListToolsRequest, + } +) +"""Monolith request model per method, both directions.""" + +MONOLITH_NOTIFICATIONS: Final[Mapping[str, type[types.Notification[Any, Any]]]] = MappingProxyType( + { + "notifications/cancelled": types.CancelledNotification, + "notifications/elicitation/complete": types.ElicitCompleteNotification, + "notifications/initialized": types.InitializedNotification, + "notifications/message": types.LoggingMessageNotification, + "notifications/progress": types.ProgressNotification, + "notifications/prompts/list_changed": types.PromptListChangedNotification, + "notifications/resources/list_changed": types.ResourceListChangedNotification, + "notifications/resources/updated": types.ResourceUpdatedNotification, + "notifications/roots/list_changed": types.RootsListChangedNotification, + "notifications/subscriptions/acknowledged": types.SubscriptionsAcknowledgedNotification, + "notifications/tools/list_changed": types.ToolListChangedNotification, + } +) +"""Monolith notification model per method, both directions.""" + +MONOLITH_RESULTS: Final[Mapping[str, type[types.Result] | UnionType]] = MappingProxyType( + { + "completion/complete": types.CompleteResult, + "elicitation/create": types.ElicitResult, + "initialize": types.InitializeResult, + "logging/setLevel": types.EmptyResult, + "ping": types.EmptyResult, + "prompts/get": types.GetPromptResult | types.InputRequiredResult, + "prompts/list": types.ListPromptsResult, + "resources/list": types.ListResourcesResult, + "resources/read": types.ReadResourceResult | types.InputRequiredResult, + "resources/subscribe": types.EmptyResult, + "resources/templates/list": types.ListResourceTemplatesResult, + "resources/unsubscribe": types.EmptyResult, + "roots/list": types.ListRootsResult, + # Arm order load-bearing: a single-block body satisfies both arms and + # smart-union ties resolve leftmost. Pinned by tests/types/test_methods.py. + "sampling/createMessage": types.CreateMessageResult | types.CreateMessageResultWithTools, + "server/discover": types.DiscoverResult, + "subscriptions/listen": types.EmptyResult, + "tools/call": types.CallToolResult | types.InputRequiredResult, + "tools/list": types.ListToolsResult, + } +) +"""Monolith result model (or two-arm union) per request method.""" + + +# --- Parse functions --- + +# Envelope stubs merged into bodies for surface validation (surface classes are full frames). +_REQUEST_STUB: Final[Mapping[str, Any]] = MappingProxyType({"jsonrpc": "2.0", "id": 0}) +_NOTIFICATION_STUB: Final[Mapping[str, Any]] = MappingProxyType({"jsonrpc": "2.0"}) + + +def _check_known_version(version: str) -> None: + """Raise ValueError for unknown `version` so a typo cannot silently gate every method.""" + if version not in KNOWN_PROTOCOL_VERSIONS: + raise ValueError(f"version must be a known protocol version, got {version!r}") + + +def _body(method: str, params: Mapping[str, Any] | None) -> dict[str, Any]: + """Build a JSON-RPC body, omitting `params` when None.""" + body: dict[str, Any] = {"method": method} + if params is not None: + body["params"] = params + return body + + +@cache +def _adapter(target: type[BaseModel] | UnionType) -> TypeAdapter[Any]: + return TypeAdapter(target) + + +_MonolithT = TypeVar("_MonolithT") + + +def _monolith_row(monolith: Mapping[str, _MonolithT], method: str) -> _MonolithT: + """Look up `method` in `monolith`, raising RuntimeError on miss. + + Not KeyError: the surface row already matched, so a miss is inconsistent + extension maps and must not be caught by the session's `except KeyError` gate. + """ + try: + return monolith[method] + except KeyError: + raise RuntimeError(f"inconsistent extension maps: surface defines {method!r} but monolith does not") from None + + +def parse_client_request( + method: str, + version: str, + params: Mapping[str, Any] | None, + *, + surface: Mapping[tuple[str, str], type[WireModel]] = CLIENT_REQUESTS, + monolith: Mapping[str, type[types.Request[Any, Any]]] = MONOLITH_REQUESTS, +) -> types.Request[Any, Any]: + """Validate a client request against `surface`, then parse and return its `monolith` model. + + Args: + surface: `(method, version)` to schema-exact type map; the version-gate + lookup and shape check run against this. Pass an extended map to + admit custom methods. + monolith: `method` to version-free model map; the returned instance is + parsed from this row. Must cover every method `surface` admits. + + Raises: + ValueError: `version` is not a known protocol version. + KeyError: `(method, version)` is not in `surface` (the version gate). + pydantic.ValidationError: body fails surface or monolith validation. + RuntimeError: surface matched but `method` has no monolith row. + """ + _check_known_version(version) + surface_type = surface[(method, version)] + surface_type.model_validate({**_REQUEST_STUB, **_body(method, params)}, by_name=False) + return _monolith_row(monolith, method).model_validate(_body(method, params), by_name=False) + + +def parse_server_request( + method: str, + version: str, + params: Mapping[str, Any] | None, + *, + surface: Mapping[tuple[str, str], type[WireModel]] = SERVER_REQUESTS, + monolith: Mapping[str, type[types.Request[Any, Any]]] = MONOLITH_REQUESTS, +) -> types.Request[Any, Any]: + """Validate a server request against `surface`, then parse and return its `monolith` model. + + Args: + surface: `(method, version)` to schema-exact type map; the version-gate + lookup and shape check run against this. Pass an extended map to + admit custom methods. + monolith: `method` to version-free model map; the returned instance is + parsed from this row. Must cover every method `surface` admits. + + Raises: + ValueError: `version` is not a known protocol version. + KeyError: `(method, version)` is not in `surface` (the version gate). + pydantic.ValidationError: body fails surface or monolith validation. + RuntimeError: surface matched but `method` has no monolith row. + """ + _check_known_version(version) + surface_type = surface[(method, version)] + surface_type.model_validate({**_REQUEST_STUB, **_body(method, params)}, by_name=False) + return _monolith_row(monolith, method).model_validate(_body(method, params), by_name=False) + + +def parse_client_notification( + method: str, + version: str, + params: Mapping[str, Any] | None, + *, + surface: Mapping[tuple[str, str], type[WireModel]] = CLIENT_NOTIFICATIONS, + monolith: Mapping[str, type[types.Notification[Any, Any]]] = MONOLITH_NOTIFICATIONS, +) -> types.Notification[Any, Any]: + """Validate a client notification against `surface`, then parse and return its `monolith` model. + + Args: + surface: `(method, version)` to schema-exact type map; the version-gate + lookup and shape check run against this. Pass an extended map to + admit custom methods. + monolith: `method` to version-free model map; the returned instance is + parsed from this row. Must cover every method `surface` admits. + + Raises: + ValueError: `version` is not a known protocol version. + KeyError: `(method, version)` is not in `surface`. + pydantic.ValidationError: body fails surface or monolith validation. + RuntimeError: surface matched but `method` has no monolith row. + """ + _check_known_version(version) + surface_type = surface[(method, version)] + surface_type.model_validate({**_NOTIFICATION_STUB, **_body(method, params)}, by_name=False) + return _monolith_row(monolith, method).model_validate(_body(method, params), by_name=False) + + +def parse_server_notification( + method: str, + version: str, + params: Mapping[str, Any] | None, + *, + surface: Mapping[tuple[str, str], type[WireModel]] = SERVER_NOTIFICATIONS, + monolith: Mapping[str, type[types.Notification[Any, Any]]] = MONOLITH_NOTIFICATIONS, +) -> types.Notification[Any, Any]: + """Validate a server notification against `surface`, then parse and return its `monolith` model. + + Args: + surface: `(method, version)` to schema-exact type map; the version-gate + lookup and shape check run against this. Pass an extended map to + admit custom methods. + monolith: `method` to version-free model map; the returned instance is + parsed from this row. Must cover every method `surface` admits. + + Raises: + ValueError: `version` is not a known protocol version. + KeyError: `(method, version)` is not in `surface`. + pydantic.ValidationError: body fails surface or monolith validation. + RuntimeError: surface matched but `method` has no monolith row. + """ + _check_known_version(version) + surface_type = surface[(method, version)] + surface_type.model_validate({**_NOTIFICATION_STUB, **_body(method, params)}, by_name=False) + return _monolith_row(monolith, method).model_validate(_body(method, params), by_name=False) + + +def parse_server_result( + method: str, + version: str, + data: Mapping[str, Any], + *, + surface: Mapping[tuple[str, str], type[WireModel] | UnionType] = SERVER_RESULTS, + monolith: Mapping[str, type[types.Result] | UnionType] = MONOLITH_RESULTS, +) -> types.Result: + """Validate a server result against `surface`, then parse and return its `monolith` model. + + Args: + surface: `(method, version)` to schema-exact type map; the version-gate + lookup and shape check run against this. Pass an extended map to + admit custom methods. + monolith: `method` to version-free model map; the returned instance is + parsed from this row. Must cover every method `surface` admits. + + Raises: + ValueError: `version` is not a known protocol version. + KeyError: `(method, version)` is not in `surface`. + pydantic.ValidationError: result fails surface or monolith validation. + RuntimeError: surface matched but `method` has no monolith row. + """ + _check_known_version(version) + _adapter(surface[(method, version)]).validate_python(data, by_name=False) + result: types.Result = _adapter(_monolith_row(monolith, method)).validate_python(data, by_name=False) + return result + + +def parse_client_result( + method: str, + version: str, + data: Mapping[str, Any], + *, + surface: Mapping[tuple[str, str], type[WireModel] | UnionType] = CLIENT_RESULTS, + monolith: Mapping[str, type[types.Result] | UnionType] = MONOLITH_RESULTS, +) -> types.Result: + """Validate a client result against `surface`, then parse and return its `monolith` model. + + Args: + surface: `(method, version)` to schema-exact type map; the version-gate + lookup and shape check run against this. Pass an extended map to + admit custom methods. + monolith: `method` to version-free model map; the returned instance is + parsed from this row. Must cover every method `surface` admits. + + Raises: + ValueError: `version` is not a known protocol version. + KeyError: `(method, version)` is not in `surface`. + pydantic.ValidationError: result fails surface or monolith validation. + RuntimeError: surface matched but `method` has no monolith row. + """ + _check_known_version(version) + _adapter(surface[(method, version)]).validate_python(data, by_name=False) + result: types.Result = _adapter(_monolith_row(monolith, method)).validate_python(data, by_name=False) + return result diff --git a/src/mcp/types/v2025_11_25/__init__.py b/src/mcp/types/v2025_11_25/__init__.py new file mode 100644 index 000000000..17111f64e --- /dev/null +++ b/src/mcp/types/v2025_11_25/__init__.py @@ -0,0 +1,1484 @@ +"""Internal wire-shape models for protocol 2025-11-25. Not part of the public API. + +Serves inbound validation for every protocol version through 2025-11-25 (each +earlier schema is a strict subset of this one). Models default to +`extra="ignore"`; the few kept open are commented in place. See +`mcp.types._wire_base` and `mcp.types.methods`. +Pinned to schema/2025-11-25/schema.json @ 6d441518de8a9d5adbab0b10a76a667a63f90665. +""" + +from __future__ import annotations + +from typing import Annotated, Any, Literal, TypeAlias + +from pydantic import ConfigDict, Field + +from mcp.types._wire_base import OpenWireModel, WireModel + + +class BaseMetadata(WireModel): + """Base interface for metadata with name (identifier) and title (display name).""" + + name: str + """Programmatic identifier; also the display fallback when `title` is absent.""" + title: str | None = None + """Human-readable display name.""" + + +class BlobResourceContents(WireModel): + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + blob: str + """Base64-encoded binary data.""" + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + uri: str + + +class BooleanSchema(WireModel): + default: bool | None = None + description: str | None = None + title: str | None = None + type: Literal["boolean"] + + +class CancelTaskRequestParams(WireModel): + task_id: Annotated[str, Field(alias="taskId")] + + +class Elicitation(WireModel): + """Present if the client supports elicitation from the server.""" + + form: dict[str, Any] | None = None + url: dict[str, Any] | None = None + + +class Roots(WireModel): + """Present if the client supports listing roots.""" + + list_changed: Annotated[bool | None, Field(alias="listChanged")] = None + + +class Sampling(WireModel): + """Present if the client supports sampling from an LLM.""" + + context: dict[str, Any] | None = None + """Present if the client supports the `includeContext` parameter.""" + tools: dict[str, Any] | None = None + """Present if the client supports the `tools` and `toolChoice` parameters.""" + + +class Elicitation1(WireModel): + """Task support for elicitation-related requests.""" + + create: dict[str, Any] | None = None + + +class Sampling1(WireModel): + """Task support for sampling-related requests.""" + + create_message: Annotated[dict[str, Any] | None, Field(alias="createMessage")] = None + + +class Requests(WireModel): + """Specifies which request types can be augmented with tasks.""" + + elicitation: Elicitation1 | None = None + sampling: Sampling1 | None = None + + +class Tasks(WireModel): + """Present if the client supports task-augmented requests.""" + + cancel: dict[str, Any] | None = None + list: dict[str, Any] | None = None + requests: Requests | None = None + + +class ClientCapabilities(WireModel): + """Capabilities a client may support. Not a closed set.""" + + elicitation: Elicitation | None = None + experimental: dict[str, dict[str, Any]] | None = None + roots: Roots | None = None + sampling: Sampling | None = None + tasks: Tasks | None = None + + +class Argument(WireModel): + """The argument being completed.""" + + name: str + value: str + + +class Context(WireModel): + """Additional context for completions.""" + + arguments: dict[str, str] | None = None + """Already-resolved variables in a URI template or prompt.""" + + +class Completion(WireModel): + has_more: Annotated[bool | None, Field(alias="hasMore")] = None + total: int | None = None + values: list[str] + """Must not exceed 100 items.""" + + +class CompleteResult(WireModel): + """The server's response to a completion/complete request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + completion: Completion + + +Cursor: TypeAlias = str + + +class ElicitResult(WireModel): + """The client's response to an elicitation request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + action: Literal["accept", "cancel", "decline"] + # Deviation: schema.json renders the number arm as "integer" but schema.ts + # types it `number`, so fractional answers are legal. Follow schema.ts. + content: dict[str, list[str] | str | int | float | bool] | None = None + """Submitted form data; only present when action is "accept" and mode was "form".""" + + +class ElicitationCompleteNotificationParams(WireModel): + elicitation_id: Annotated[str, Field(alias="elicitationId")] + + +class ElicitationCompleteNotification(WireModel): + """Server-to-client notification that an out-of-band elicitation completed.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/elicitation/complete"] + params: ElicitationCompleteNotificationParams + + +class Error(WireModel): + code: int + data: Any | None = None + message: str + + +class GetTaskPayloadRequestParams(WireModel): + task_id: Annotated[str, Field(alias="taskId")] + + +class GetTaskPayloadResult(WireModel): + """Response to tasks/result; structure matches the original request's result type.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + + +class GetTaskRequestParams(WireModel): + task_id: Annotated[str, Field(alias="taskId")] + + +class Icon(WireModel): + """An optionally-sized icon that can be displayed in a user interface.""" + + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + sizes: list[str] | None = None + """Each entry is WxH (e.g. "48x48") or "any" for scalable formats.""" + src: str + """HTTP/HTTPS URL or `data:` URI.""" + theme: Literal["dark", "light"] | None = None + + +class Icons(WireModel): + """Base interface adding an `icons` property.""" + + icons: list[Icon] | None = None + + +class Implementation(WireModel): + """Describes the MCP implementation.""" + + description: str | None = None + icons: list[Icon] | None = None + name: str + title: str | None = None + version: str + website_url: Annotated[str | None, Field(alias="websiteUrl")] = None + + +class JSONRPCNotification(WireModel): + """A notification which does not expect a response.""" + + jsonrpc: Literal["2.0"] + method: str + params: dict[str, Any] | None = None + + +class LegacyTitledEnumSchema(WireModel): + """Use TitledSingleSelectEnumSchema instead.""" + + default: str | None = None + description: str | None = None + enum: list[str] + enum_names: Annotated[list[str] | None, Field(alias="enumNames")] = None + """Display names for enum values (legacy, non-standard JSON Schema).""" + title: str | None = None + type: Literal["string"] + + +LoggingLevel: TypeAlias = Literal["alert", "critical", "debug", "emergency", "error", "info", "notice", "warning"] + + +class LoggingMessageNotificationParams(WireModel): + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + data: Any + level: LoggingLevel + logger: str | None = None + + +class ModelHint(WireModel): + """Hints to use for model selection.""" + + name: str | None = None + """Substring of a model name; the client may map it to another provider's equivalent.""" + + +class ModelPreferences(WireModel): + """The server's advisory preferences for model selection during sampling.""" + + cost_priority: Annotated[float | None, Field(alias="costPriority", ge=0.0, le=1.0)] = None + hints: list[ModelHint] | None = None + """Evaluated in order; first match wins.""" + intelligence_priority: Annotated[float | None, Field(alias="intelligencePriority", ge=0.0, le=1.0)] = None + speed_priority: Annotated[float | None, Field(alias="speedPriority", ge=0.0, le=1.0)] = None + + +class Notification(WireModel): + method: str + params: dict[str, Any] | None = None + + +class NotificationParams(WireModel): + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + + +class NumberSchema(WireModel): + # Deviation: schema.json renders these as "integer" but schema.ts types + # them `number` (JSON Schema bounds are numbers). Follow schema.ts. + default: int | float | None = None + description: str | None = None + maximum: int | float | None = None + minimum: int | float | None = None + title: str | None = None + type: Literal["integer", "number"] + + +class PaginatedResult(WireModel): + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + next_cursor: Annotated[str | None, Field(alias="nextCursor")] = None + """Opaque pagination token; if present, more results may be available.""" + + +ProgressToken: TypeAlias = str | int + + +class PromptArgument(WireModel): + """Describes an argument that a prompt can accept.""" + + description: str | None = None + name: str + required: bool | None = None + title: str | None = None + + +class PromptListChangedNotification(WireModel): + """Server-to-client notification that the prompt list has changed.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/prompts/list_changed"] + params: NotificationParams | None = None + + +class PromptReference(WireModel): + """Identifies a prompt.""" + + name: str + title: str | None = None + type: Literal["ref/prompt"] + + +class Meta(OpenWireModel): + """Request `_meta` object.""" + + progress_token: Annotated[ProgressToken | None, Field(alias="progressToken")] = None + """If set, the caller wants `notifications/progress` for this request, tagged with this token.""" + + +class ReadResourceRequestParams(WireModel): + meta: Annotated[Meta | None, Field(alias="_meta")] = None + uri: str + + +class RelatedTaskMetadata(WireModel): + """Associates a message with a task via `_meta["io.modelcontextprotocol/related-task"]`.""" + + task_id: Annotated[str, Field(alias="taskId")] + + +class Request(WireModel): + method: str + params: dict[str, Any] | None = None + + +RequestId: TypeAlias = str | int + + +class RequestParams(WireModel): + meta: Annotated[Meta | None, Field(alias="_meta")] = None + + +class ResourceContents(WireModel): + """The contents of a specific resource or sub-resource.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + uri: str + + +class ResourceListChangedNotification(WireModel): + """Server-to-client notification that the resource list has changed.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/resources/list_changed"] + params: NotificationParams | None = None + + +class ResourceRequestParams(WireModel): + """Common parameters when working with resources.""" + + meta: Annotated[Meta | None, Field(alias="_meta")] = None + uri: str + + +class ResourceTemplateReference(WireModel): + """A reference to a resource or resource template definition.""" + + type: Literal["ref/resource"] + uri: str + """The URI or URI template of the resource.""" + + +class ResourceUpdatedNotificationParams(WireModel): + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + uri: str + """May be a sub-resource of the one the client subscribed to.""" + + +class Result(WireModel): + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + + +Role: TypeAlias = Literal["assistant", "user"] + + +class Root(WireModel): + """A root directory or file that the server can operate on.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + name: str | None = None + uri: str + """Must start with file:// for now.""" + + +class RootsListChangedNotification(WireModel): + """Client-to-server notification that the roots list has changed.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/roots/list_changed"] + params: NotificationParams | None = None + + +class Prompts(WireModel): + """Present if the server offers any prompt templates.""" + + list_changed: Annotated[bool | None, Field(alias="listChanged")] = None + + +class Resources(WireModel): + """Present if the server offers any resources to read.""" + + list_changed: Annotated[bool | None, Field(alias="listChanged")] = None + subscribe: bool | None = None + + +class Tools(WireModel): + """Task support for tool-related requests.""" + + call: dict[str, Any] | None = None + + +class Requests1(WireModel): + """Specifies which request types can be augmented with tasks.""" + + tools: Tools | None = None + + +class Tasks1(WireModel): + """Present if the server supports task-augmented requests.""" + + cancel: dict[str, Any] | None = None + list: dict[str, Any] | None = None + requests: Requests1 | None = None + + +class Tools1(WireModel): + """Present if the server offers any tools to call.""" + + list_changed: Annotated[bool | None, Field(alias="listChanged")] = None + + +class ServerCapabilities(WireModel): + """Capabilities a server may support. Not a closed set.""" + + completions: dict[str, Any] | None = None + experimental: dict[str, dict[str, Any]] | None = None + logging: dict[str, Any] | None = None + prompts: Prompts | None = None + resources: Resources | None = None + tasks: Tasks1 | None = None + tools: Tools1 | None = None + + +class SetLevelRequestParams(WireModel): + meta: Annotated[Meta | None, Field(alias="_meta")] = None + level: LoggingLevel + """Minimum severity to send to the client as notifications/message.""" + + +class StringSchema(WireModel): + default: str | None = None + description: str | None = None + format: Literal["date", "date-time", "email", "uri"] | None = None + max_length: Annotated[int | None, Field(alias="maxLength")] = None + min_length: Annotated[int | None, Field(alias="minLength")] = None + title: str | None = None + type: Literal["string"] + + +class SubscribeRequestParams(WireModel): + meta: Annotated[Meta | None, Field(alias="_meta")] = None + uri: str + + +class TaskMetadata(WireModel): + """Metadata for augmenting a request with task execution (the `task` param field).""" + + ttl: int | None = None + """Requested retention from creation, in milliseconds.""" + + +TaskStatus: TypeAlias = Literal["cancelled", "completed", "failed", "input_required", "working"] + + +class TextResourceContents(WireModel): + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + text: str + uri: str + + +class AnyOfItem(WireModel): + const: str + title: str + + +class Items(WireModel): + """Schema for array items with enum options and display labels.""" + + any_of: Annotated[list[AnyOfItem], Field(alias="anyOf")] + + +class TitledMultiSelectEnumSchema(WireModel): + """Multiple-selection enum with display titles for each option.""" + + default: list[str] | None = None + description: str | None = None + items: Items + max_items: Annotated[int | None, Field(alias="maxItems")] = None + min_items: Annotated[int | None, Field(alias="minItems")] = None + title: str | None = None + type: Literal["array"] + + +class OneOfItem(WireModel): + const: str + title: str + + +class TitledSingleSelectEnumSchema(WireModel): + """Single-selection enum with display titles for each option.""" + + default: str | None = None + description: str | None = None + one_of: Annotated[list[OneOfItem], Field(alias="oneOf")] + title: str | None = None + type: Literal["string"] + + +class InputSchema(WireModel): + """A JSON Schema object defining the expected parameters for the tool.""" + + # Kept open: arbitrary JSON Schema keywords ride extra fields. + model_config = ConfigDict( + extra="allow", + ) + schema_: Annotated[str | None, Field(alias="$schema")] = None + properties: dict[str, dict[str, Any]] | None = None + required: list[str] | None = None + type: Literal["object"] + + +class OutputSchema(WireModel): + """A JSON Schema object defining the structure of `CallToolResult.structuredContent`.""" + + # Kept open: arbitrary JSON Schema keywords ride extra fields. + model_config = ConfigDict( + extra="allow", + ) + schema_: Annotated[str | None, Field(alias="$schema")] = None + properties: dict[str, dict[str, Any]] | None = None + required: list[str] | None = None + type: Literal["object"] + + +class ToolAnnotations(WireModel): + """Untrusted hints describing a tool's behavior to clients.""" + + destructive_hint: Annotated[bool | None, Field(alias="destructiveHint")] = None + """Only meaningful when `readOnlyHint` is false. Default: true.""" + idempotent_hint: Annotated[bool | None, Field(alias="idempotentHint")] = None + """Only meaningful when `readOnlyHint` is false. Default: false.""" + open_world_hint: Annotated[bool | None, Field(alias="openWorldHint")] = None + """Default: true.""" + read_only_hint: Annotated[bool | None, Field(alias="readOnlyHint")] = None + """Default: false.""" + title: str | None = None + + +class ToolChoice(WireModel): + """Controls tool selection behavior for sampling requests.""" + + mode: Literal["auto", "none", "required"] | None = None + + +class ToolExecution(WireModel): + """Execution-related properties for a tool.""" + + task_support: Annotated[Literal["forbidden", "optional", "required"] | None, Field(alias="taskSupport")] = None + """Whether this tool supports task-augmented execution. Default: "forbidden".""" + + +class ToolListChangedNotification(WireModel): + """Server-to-client notification that the tool list has changed.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/tools/list_changed"] + params: NotificationParams | None = None + + +class ToolUseContent(WireModel): + """A request from the assistant to call a tool.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + id: str + """Unique identifier matched against `ToolResultContent.tool_use_id`.""" + input: dict[str, Any] + name: str + type: Literal["tool_use"] + + +class UnsubscribeRequestParams(WireModel): + meta: Annotated[Meta | None, Field(alias="_meta")] = None + uri: str + + +class Items1(WireModel): + """Schema for the array items.""" + + enum: list[str] + type: Literal["string"] + + +class UntitledMultiSelectEnumSchema(WireModel): + """Multiple-selection enum without per-option display titles.""" + + default: list[str] | None = None + description: str | None = None + items: Items1 + max_items: Annotated[int | None, Field(alias="maxItems")] = None + min_items: Annotated[int | None, Field(alias="minItems")] = None + title: str | None = None + type: Literal["array"] + + +class UntitledSingleSelectEnumSchema(WireModel): + """Single-selection enum without per-option display titles.""" + + default: str | None = None + description: str | None = None + enum: list[str] + title: str | None = None + type: Literal["string"] + + +class Annotations(WireModel): + """Optional annotations for the client.""" + + audience: list[Role] | None = None + last_modified: Annotated[str | None, Field(alias="lastModified")] = None + """ISO 8601 timestamp.""" + priority: Annotated[float | None, Field(ge=0.0, le=1.0)] = None + """1 means effectively required, 0 means entirely optional.""" + + +class AudioContent(WireModel): + """Audio provided to or from an LLM.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + data: str + """Base64-encoded audio data.""" + mime_type: Annotated[str, Field(alias="mimeType")] + type: Literal["audio"] + + +class CallToolRequestParams(WireModel): + meta: Annotated[Meta | None, Field(alias="_meta")] = None + arguments: dict[str, Any] | None = None + name: str + task: TaskMetadata | None = None + """If set, run as a task and return `CreateTaskResult` immediately.""" + + +class CancelTaskRequest(WireModel): + """A request to cancel a task.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["tasks/cancel"] + params: CancelTaskRequestParams + + +class CancelledNotificationParams(WireModel): + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + reason: str | None = None + request_id: Annotated[RequestId | None, Field(alias="requestId")] = None + """Required for non-task requests; MUST NOT be used for tasks (use `tasks/cancel`).""" + + +class CompleteRequestParams(WireModel): + meta: Annotated[Meta | None, Field(alias="_meta")] = None + argument: Argument + context: Context | None = None + ref: PromptReference | ResourceTemplateReference + + +class ElicitRequestURLParams(WireModel): + """Parameters for a URL-mode elicitation request.""" + + meta: Annotated[Meta | None, Field(alias="_meta")] = None + elicitation_id: Annotated[str, Field(alias="elicitationId")] + """Server-unique opaque ID.""" + message: str + mode: Literal["url"] + task: TaskMetadata | None = None + """If set, run as a task and return `CreateTaskResult` immediately.""" + url: str + + +class EmbeddedResource(WireModel): + """The contents of a resource, embedded into a prompt or tool call result.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + resource: TextResourceContents | BlobResourceContents + type: Literal["resource"] + + +EmptyResult: TypeAlias = Result + + +EnumSchema: TypeAlias = ( + UntitledSingleSelectEnumSchema + | TitledSingleSelectEnumSchema + | UntitledMultiSelectEnumSchema + | TitledMultiSelectEnumSchema + | LegacyTitledEnumSchema +) + + +class GetPromptRequestParams(WireModel): + meta: Annotated[Meta | None, Field(alias="_meta")] = None + arguments: dict[str, str] | None = None + name: str + + +class GetTaskPayloadRequest(WireModel): + """A request to retrieve the result of a completed task.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["tasks/result"] + params: GetTaskPayloadRequestParams + + +class GetTaskRequest(WireModel): + """A request to retrieve the state of a task.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["tasks/get"] + params: GetTaskRequestParams + + +class ImageContent(WireModel): + """An image provided to or from an LLM.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + data: str + """Base64-encoded image data.""" + mime_type: Annotated[str, Field(alias="mimeType")] + type: Literal["image"] + + +class InitializeRequestParams(WireModel): + meta: Annotated[Meta | None, Field(alias="_meta")] = None + capabilities: ClientCapabilities + client_info: Annotated[Implementation, Field(alias="clientInfo")] + protocol_version: Annotated[str, Field(alias="protocolVersion")] + """The latest protocol version the client supports.""" + + +class InitializeResult(WireModel): + """The server's response to an initialize request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + capabilities: ServerCapabilities + instructions: str | None = None + """Instructions describing how to use the server and its features.""" + protocol_version: Annotated[str, Field(alias="protocolVersion")] + """The protocol version the server wants to use; the client MUST disconnect if unsupported.""" + server_info: Annotated[Implementation, Field(alias="serverInfo")] + + +class InitializedNotification(WireModel): + """Sent from the client to the server after initialization has finished.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/initialized"] + params: NotificationParams | None = None + + +class JSONRPCErrorResponse(WireModel): + """A response to a request that indicates an error occurred.""" + + error: Error + id: RequestId | None = None + jsonrpc: Literal["2.0"] + + +class JSONRPCRequest(WireModel): + """A request that expects a response.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: str + params: dict[str, Any] | None = None + + +class JSONRPCResultResponse(WireModel): + """A successful (non-error) response to a request.""" + + id: RequestId + jsonrpc: Literal["2.0"] + result: Result + + +class ListRootsRequest(WireModel): + """Sent from the server to request a list of root URIs from the client.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["roots/list"] + params: RequestParams | None = None + + +class ListRootsResult(WireModel): + """The client's response to a roots/list request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + roots: list[Root] + + +class LoggingMessageNotification(WireModel): + """A log message passed from server to client.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/message"] + params: LoggingMessageNotificationParams + + +MultiSelectEnumSchema: TypeAlias = UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema + + +class PaginatedRequestParams(WireModel): + meta: Annotated[Meta | None, Field(alias="_meta")] = None + cursor: str | None = None + """Opaque pagination token; results start after this position.""" + + +class PingRequest(WireModel): + """A ping, issued by either side, to check that the other party is still alive.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["ping"] + params: RequestParams | None = None + + +PrimitiveSchemaDefinition: TypeAlias = ( + StringSchema + | NumberSchema + | BooleanSchema + | UntitledSingleSelectEnumSchema + | TitledSingleSelectEnumSchema + | UntitledMultiSelectEnumSchema + | TitledMultiSelectEnumSchema + | LegacyTitledEnumSchema +) + + +class ProgressNotificationParams(WireModel): + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + message: str | None = None + progress: float + """Monotonically increasing, even if `total` is unknown.""" + progress_token: Annotated[ProgressToken, Field(alias="progressToken")] + """The token from the originating request's `_meta.progressToken`.""" + total: float | None = None + + +class Prompt(WireModel): + """A prompt or prompt template that the server offers.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + arguments: list[PromptArgument] | None = None + description: str | None = None + icons: list[Icon] | None = None + name: str + title: str | None = None + + +class ReadResourceRequest(WireModel): + """Sent from the client to read a specific resource URI.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["resources/read"] + params: ReadResourceRequestParams + + +class ReadResourceResult(WireModel): + """The server's response to a resources/read request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + contents: list[TextResourceContents | BlobResourceContents] + + +class Resource(WireModel): + """A known resource that the server is capable of reading.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + description: str | None = None + icons: list[Icon] | None = None + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + name: str + size: int | None = None + """Raw content size in bytes (before base64 encoding), if known.""" + title: str | None = None + uri: str + + +class ResourceLink(WireModel): + """A resource link included in a prompt or tool call result.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + description: str | None = None + icons: list[Icon] | None = None + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + name: str + size: int | None = None + """Raw content size in bytes (before base64 encoding), if known.""" + title: str | None = None + type: Literal["resource_link"] + uri: str + + +class ResourceTemplate(WireModel): + """A template description for resources available on the server.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + description: str | None = None + icons: list[Icon] | None = None + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + """Only set if all resources matching this template share the same type.""" + name: str + title: str | None = None + uri_template: Annotated[str, Field(alias="uriTemplate")] + """RFC 6570 URI template.""" + + +class ResourceUpdatedNotification(WireModel): + """Server-to-client notification that a subscribed resource has changed.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/resources/updated"] + params: ResourceUpdatedNotificationParams + + +class SetLevelRequest(WireModel): + """A request from the client to enable or adjust logging.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["logging/setLevel"] + params: SetLevelRequestParams + + +SingleSelectEnumSchema: TypeAlias = UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema + + +class SubscribeRequest(WireModel): + """Sent from the client to request resources/updated notifications for a resource.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["resources/subscribe"] + params: SubscribeRequestParams + + +class Task(WireModel): + """Data associated with a task.""" + + created_at: Annotated[str, Field(alias="createdAt")] + """ISO 8601 timestamp.""" + last_updated_at: Annotated[str, Field(alias="lastUpdatedAt")] + """ISO 8601 timestamp.""" + poll_interval: Annotated[int | None, Field(alias="pollInterval")] = None + """Suggested polling interval in milliseconds.""" + status: TaskStatus + status_message: Annotated[str | None, Field(alias="statusMessage")] = None + task_id: Annotated[str, Field(alias="taskId")] + ttl: int | None + """Actual retention from creation in milliseconds; null means unlimited.""" + + +class TaskAugmentedRequestParams(WireModel): + """Common params for any task-augmented request.""" + + meta: Annotated[Meta | None, Field(alias="_meta")] = None + task: TaskMetadata | None = None + """If set, run as a task and return `CreateTaskResult` immediately.""" + + +class TaskStatusNotificationParams(WireModel): + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + created_at: Annotated[str, Field(alias="createdAt")] + """ISO 8601 timestamp.""" + last_updated_at: Annotated[str, Field(alias="lastUpdatedAt")] + """ISO 8601 timestamp.""" + poll_interval: Annotated[int | None, Field(alias="pollInterval")] = None + """Suggested polling interval in milliseconds.""" + status: TaskStatus + status_message: Annotated[str | None, Field(alias="statusMessage")] = None + task_id: Annotated[str, Field(alias="taskId")] + ttl: int | None + """Actual retention from creation in milliseconds; null means unlimited.""" + + +class TextContent(WireModel): + """Text provided to or from an LLM.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + text: str + type: Literal["text"] + + +class Tool(WireModel): + """Definition for a tool the client can call.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + annotations: ToolAnnotations | None = None + description: str | None = None + execution: ToolExecution | None = None + icons: list[Icon] | None = None + input_schema: Annotated[InputSchema, Field(alias="inputSchema")] + name: str + output_schema: Annotated[OutputSchema | None, Field(alias="outputSchema")] = None + title: str | None = None + + +class Data(WireModel): + """Error data carrying pending URL elicitations.""" + + elicitations: list[ElicitRequestURLParams] + + +class Error1(WireModel): + code: Literal[-32042] + data: Data + message: str + + +class URLElicitationRequiredError(WireModel): + """Error response indicating the server requires a URL-mode elicitation.""" + + error: Error1 + id: RequestId | None = None + jsonrpc: Literal["2.0"] + + +class UnsubscribeRequest(WireModel): + """Sent from the client to cancel resources/updated notifications for a resource.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["resources/unsubscribe"] + params: UnsubscribeRequestParams + + +class CallToolRequest(WireModel): + """Used by the client to invoke a tool provided by the server.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["tools/call"] + params: CallToolRequestParams + + +class CancelTaskResult(WireModel): + """The response to a tasks/cancel request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + created_at: Annotated[str, Field(alias="createdAt")] + """ISO 8601 timestamp.""" + last_updated_at: Annotated[str, Field(alias="lastUpdatedAt")] + """ISO 8601 timestamp.""" + poll_interval: Annotated[int | None, Field(alias="pollInterval")] = None + """Suggested polling interval in milliseconds.""" + status: TaskStatus + status_message: Annotated[str | None, Field(alias="statusMessage")] = None + task_id: Annotated[str, Field(alias="taskId")] + ttl: int | None + """Actual retention from creation in milliseconds; null means unlimited.""" + + +class CancelledNotification(WireModel): + """Sent by either side to cancel an in-flight request (not for tasks; use `tasks/cancel`).""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/cancelled"] + params: CancelledNotificationParams + + +class CompleteRequest(WireModel): + """A request from the client to ask for completion options.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["completion/complete"] + params: CompleteRequestParams + + +ContentBlock: TypeAlias = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource + + +class CreateTaskResult(WireModel): + """A response to a task-augmented request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + task: Task + + +class RequestedSchema(WireModel): + """A restricted JSON Schema subset: top-level properties only, no nesting.""" + + schema_: Annotated[str | None, Field(alias="$schema")] = None + properties: dict[str, PrimitiveSchemaDefinition] + required: list[str] | None = None + type: Literal["object"] + + +class ElicitRequestFormParams(WireModel): + """Parameters for a form-mode elicitation request.""" + + meta: Annotated[Meta | None, Field(alias="_meta")] = None + message: str + mode: Literal["form"] = "form" + requested_schema: Annotated[RequestedSchema, Field(alias="requestedSchema")] + task: TaskMetadata | None = None + """If set, run as a task and return `CreateTaskResult` immediately.""" + + +ElicitRequestParams: TypeAlias = ElicitRequestURLParams | ElicitRequestFormParams + + +class GetPromptRequest(WireModel): + """Used by the client to get a prompt provided by the server.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["prompts/get"] + params: GetPromptRequestParams + + +class GetTaskResult(WireModel): + """The response to a tasks/get request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + created_at: Annotated[str, Field(alias="createdAt")] + """ISO 8601 timestamp.""" + last_updated_at: Annotated[str, Field(alias="lastUpdatedAt")] + """ISO 8601 timestamp.""" + poll_interval: Annotated[int | None, Field(alias="pollInterval")] = None + """Suggested polling interval in milliseconds.""" + status: TaskStatus + status_message: Annotated[str | None, Field(alias="statusMessage")] = None + task_id: Annotated[str, Field(alias="taskId")] + ttl: int | None + """Actual retention from creation in milliseconds; null means unlimited.""" + + +class InitializeRequest(WireModel): + """Sent from the client to the server when it first connects.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["initialize"] + params: InitializeRequestParams + + +JSONRPCMessage: TypeAlias = JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse + + +JSONRPCResponse: TypeAlias = JSONRPCResultResponse | JSONRPCErrorResponse + + +class ListPromptsRequest(WireModel): + """Sent from the client to request a list of prompts and prompt templates.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["prompts/list"] + params: PaginatedRequestParams | None = None + + +class ListPromptsResult(WireModel): + """The server's response to a prompts/list request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + next_cursor: Annotated[str | None, Field(alias="nextCursor")] = None + prompts: list[Prompt] + + +class ListResourceTemplatesRequest(WireModel): + """Sent from the client to request a list of resource templates.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["resources/templates/list"] + params: PaginatedRequestParams | None = None + + +class ListResourceTemplatesResult(WireModel): + """The server's response to a resources/templates/list request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + next_cursor: Annotated[str | None, Field(alias="nextCursor")] = None + resource_templates: Annotated[list[ResourceTemplate], Field(alias="resourceTemplates")] + + +class ListResourcesRequest(WireModel): + """Sent from the client to request a list of resources.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["resources/list"] + params: PaginatedRequestParams | None = None + + +class ListResourcesResult(WireModel): + """The server's response to a resources/list request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + next_cursor: Annotated[str | None, Field(alias="nextCursor")] = None + resources: list[Resource] + + +class ListTasksRequest(WireModel): + """A request to retrieve a list of tasks.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["tasks/list"] + params: PaginatedRequestParams | None = None + + +class ListTasksResult(WireModel): + """The response to a tasks/list request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + next_cursor: Annotated[str | None, Field(alias="nextCursor")] = None + tasks: list[Task] + + +class ListToolsRequest(WireModel): + """Sent from the client to request a list of tools.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["tools/list"] + params: PaginatedRequestParams | None = None + + +class ListToolsResult(WireModel): + """The server's response to a tools/list request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + next_cursor: Annotated[str | None, Field(alias="nextCursor")] = None + tools: list[Tool] + + +class PaginatedRequest(WireModel): + id: RequestId + jsonrpc: Literal["2.0"] + method: str + params: PaginatedRequestParams | None = None + + +class ProgressNotification(WireModel): + """An out-of-band progress update for a long-running request.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/progress"] + params: ProgressNotificationParams + + +class PromptMessage(WireModel): + """A message returned as part of a prompt; like `SamplingMessage` but allows embedded resources.""" + + content: ContentBlock + role: Role + + +class TaskStatusNotification(WireModel): + """An optional notification that a task's status has changed.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/tasks/status"] + params: TaskStatusNotificationParams + + +class ToolResultContent(WireModel): + """The result of a tool use, provided by the user back to the assistant.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + content: list[ContentBlock] + is_error: Annotated[bool | None, Field(alias="isError")] = None + """Default: false.""" + structured_content: Annotated[Any | None, Field(alias="structuredContent")] = None + tool_use_id: Annotated[str, Field(alias="toolUseId")] + """Must match the `id` of an earlier `ToolUseContent`.""" + type: Literal["tool_result"] + + +class CallToolResult(WireModel): + """The server's response to a tool call.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + content: list[ContentBlock] + is_error: Annotated[bool | None, Field(alias="isError")] = None + """Tool errors should set this true (not raise a protocol error). Default: false.""" + structured_content: Annotated[Any | None, Field(alias="structuredContent")] = None + + +ClientNotification: TypeAlias = ( + CancelledNotification + | InitializedNotification + | ProgressNotification + | TaskStatusNotification + | RootsListChangedNotification +) + + +ClientRequest: TypeAlias = ( + InitializeRequest + | PingRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscribeRequest + | UnsubscribeRequest + | ListPromptsRequest + | GetPromptRequest + | ListToolsRequest + | CallToolRequest + | GetTaskRequest + | GetTaskPayloadRequest + | CancelTaskRequest + | ListTasksRequest + | SetLevelRequest + | CompleteRequest +) + + +class ElicitRequest(WireModel): + """A request from the server to elicit additional information from the user.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["elicitation/create"] + params: ElicitRequestParams + + +class GetPromptResult(WireModel): + """The server's response to a prompts/get request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + description: str | None = None + messages: list[PromptMessage] + + +SamplingMessageContentBlock: TypeAlias = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent + + +ServerNotification: TypeAlias = ( + CancelledNotification + | ProgressNotification + | ResourceListChangedNotification + | ResourceUpdatedNotification + | PromptListChangedNotification + | ToolListChangedNotification + | TaskStatusNotification + | LoggingMessageNotification + | ElicitationCompleteNotification +) + + +ServerResult: TypeAlias = ( + Result + | InitializeResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | ListPromptsResult + | GetPromptResult + | ListToolsResult + | CallToolResult + | GetTaskResult + | GetTaskPayloadResult + | CancelTaskResult + | ListTasksResult + | CompleteResult +) + + +class CreateMessageResult(WireModel): + """The client's response to a sampling/createMessage request.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + content: ( + TextContent + | ImageContent + | AudioContent + | ToolUseContent + | ToolResultContent + | list[SamplingMessageContentBlock] + ) + model: str + role: Role + stop_reason: Annotated[str | None, Field(alias="stopReason")] = None + """Standard values: "endTurn", "stopSequence", "maxTokens", "toolUse"; open string.""" + + +class SamplingMessage(WireModel): + """A message issued to or received from an LLM API.""" + + meta: Annotated[dict[str, Any] | None, Field(alias="_meta")] = None + content: ( + TextContent + | ImageContent + | AudioContent + | ToolUseContent + | ToolResultContent + | list[SamplingMessageContentBlock] + ) + role: Role + + +ClientResult: TypeAlias = ( + Result + | GetTaskResult + | GetTaskPayloadResult + | CancelTaskResult + | ListTasksResult + | CreateMessageResult + | ListRootsResult + | ElicitResult +) + + +class CreateMessageRequestParams(WireModel): + meta: Annotated[Meta | None, Field(alias="_meta")] = None + include_context: Annotated[ + Literal["allServers", "none", "thisServer"] | None, + Field(alias="includeContext"), + ] = None + """Default "none"; "thisServer"/"allServers" are soft-deprecated.""" + max_tokens: Annotated[int, Field(alias="maxTokens")] + messages: list[SamplingMessage] + metadata: dict[str, Any] | None = None + """Provider-specific passthrough.""" + model_preferences: Annotated[ModelPreferences | None, Field(alias="modelPreferences")] = None + stop_sequences: Annotated[list[str] | None, Field(alias="stopSequences")] = None + system_prompt: Annotated[str | None, Field(alias="systemPrompt")] = None + task: TaskMetadata | None = None + """If set, run as a task and return `CreateTaskResult` immediately.""" + temperature: float | None = None + tool_choice: Annotated[ToolChoice | None, Field(alias="toolChoice")] = None + tools: list[Tool] | None = None + + +class CreateMessageRequest(WireModel): + """A request from the server to sample an LLM via the client.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["sampling/createMessage"] + params: CreateMessageRequestParams + + +ServerRequest: TypeAlias = ( + PingRequest + | GetTaskRequest + | GetTaskPayloadRequest + | CancelTaskRequest + | ListTasksRequest + | CreateMessageRequest + | ListRootsRequest + | ElicitRequest +) diff --git a/src/mcp/types/v2026_07_28/__init__.py b/src/mcp/types/v2026_07_28/__init__.py new file mode 100644 index 000000000..8a970b4f2 --- /dev/null +++ b/src/mcp/types/v2026_07_28/__init__.py @@ -0,0 +1,1492 @@ +"""Internal wire-shape models for protocol 2026-07-28. Not part of the public API. + +Schema-exact validators that the wire-method maps in `mcp.types.methods` point +inbound 2026-07-28 validation at. Generated from schema/draft/schema.json +@ 6d441518de8a9d5adbab0b10a76a667a63f90665 and hand-maintained against that +revision. Models use `extra="ignore"` (unknown keys accepted and dropped) unless +commented otherwise; see `mcp.types._wire_base`. +""" + +from __future__ import annotations + +from typing import Annotated, Any, Literal, TypeAlias + +from pydantic import ConfigDict, Field +from typing_extensions import TypeAliasType + +from mcp.types._wire_base import OpenWireModel, WireModel + +# Deviates from schema.json (renders only string|integer|boolean); follows +# schema.ts, which defines all six JSON types, so floats and null validate. +JSONValue = TypeAliasType("JSONValue", "JSONObject | list[JSONValue] | str | int | float | bool | None") + + +JSONObject = TypeAliasType("JSONObject", dict[str, "JSONValue"]) + + +class BaseMetadata(WireModel): + """Base interface for metadata with name (identifier) and title (display name).""" + + name: str + """Programmatic identifier; also the display fallback when `title` is absent.""" + title: str | None = None + """Human-readable display name.""" + + +class BooleanSchema(WireModel): + default: bool | None = None + description: str | None = None + title: str | None = None + type: Literal["boolean"] + + +class Argument(WireModel): + """The argument being completed.""" + + name: str + value: str + """Value to use for completion matching.""" + + +class Context(WireModel): + """Additional context for completions.""" + + arguments: dict[str, str] | None = None + """Already-resolved variables in a URI template or prompt.""" + + +class Completion(WireModel): + has_more: Annotated[bool | None, Field(alias="hasMore")] = None + """Whether more options exist beyond this response, even if `total` is unknown.""" + total: int | None = None + """Total options available; can exceed `len(values)`.""" + values: Annotated[list[str], Field(max_length=100)] + """Completion values; at most 100 items.""" + + +Cursor: TypeAlias = str + + +class ElicitRequestURLParams(WireModel): + """Parameters for a URL-mode `elicitation/create` request.""" + + elicitation_id: Annotated[str, Field(alias="elicitationId")] + """Server-unique opaque ID for this elicitation.""" + message: str + mode: Literal["url"] + url: str + """URL the user should navigate to.""" + + +class ElicitResult(WireModel): + """Client's result for an `elicitation/create` request.""" + + action: Literal["accept", "cancel", "decline"] + """`accept` = submitted, `decline` = explicit no, `cancel` = dismissed.""" + # Deviates from schema.json (renders number arm as integer); follows + # schema.ts (string|number|boolean|string[]) so float answers validate. + content: dict[str, list[str] | str | int | float | bool] | None = None + """Submitted form data; only present when `action == "accept"` and mode was `"form"`.""" + + +class ElicitationCompleteNotificationParams(WireModel): + elicitation_id: Annotated[str, Field(alias="elicitationId")] + """ID of the elicitation that completed.""" + + +class ElicitationCompleteNotification(WireModel): + """Server-to-client: an out-of-band elicitation has completed.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/elicitation/complete"] + params: ElicitationCompleteNotificationParams + + +class Error(WireModel): + code: int + data: Any | None = None + message: str + + +class Icon(WireModel): + """An optionally-sized icon for display in a UI.""" + + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + sizes: list[str] | None = None + """Sizes the icon supports, each as `"WxH"` or `"any"`; absent means any size.""" + src: str + """HTTP(S) or `data:` URI; consumers should vet origin and sandbox SVG.""" + theme: Literal["dark", "light"] | None = None + """Background theme this icon is designed for; absent means any theme.""" + + +class Icons(WireModel): + """Mixin adding the `icons` property.""" + + icons: list[Icon] | None = None + + +class Implementation(WireModel): + """Describes an MCP implementation.""" + + description: str | None = None + icons: list[Icon] | None = None + name: str + title: str | None = None + version: str + website_url: Annotated[str | None, Field(alias="websiteUrl")] = None + + +class InternalError(WireModel): + """JSON-RPC: internal error on the receiver.""" + + code: Literal[-32603] + data: Any | None = None + message: str + + +class InvalidParamsError(WireModel): + """JSON-RPC: method parameters are invalid or malformed.""" + + code: Literal[-32602] + data: Any | None = None + message: str + + +class InvalidRequestError(WireModel): + """JSON-RPC: request object does not conform to JSON-RPC 2.0.""" + + code: Literal[-32600] + data: Any | None = None + message: str + + +class JSONRPCNotification(WireModel): + """A notification, which does not expect a response.""" + + jsonrpc: Literal["2.0"] + method: str + params: dict[str, Any] | None = None + + +class LegacyTitledEnumSchema(WireModel): + """Deprecated; use `TitledSingleSelectEnumSchema`.""" + + default: str | None = None + description: str | None = None + enum: list[str] + enum_names: Annotated[list[str] | None, Field(alias="enumNames")] = None + """Display names for enum values (non-standard for JSON Schema 2020-12).""" + title: str | None = None + type: Literal["string"] + + +LoggingLevel: TypeAlias = Literal["alert", "critical", "debug", "emergency", "error", "info", "notice", "warning"] + + +class MetaObject(OpenWireModel): + """Contents of a `_meta` field; see the schema for key naming and reservation rules.""" + + +class MethodNotFoundError(WireModel): + """JSON-RPC: requested method does not exist or is not available.""" + + code: Literal[-32601] + data: Any | None = None + message: str + + +class ModelHint(WireModel): + """Hints for model selection; undeclared keys are client-defined.""" + + name: str | None = None + """Model-name substring hint; the client may also map it to an equivalent from another provider.""" + + +class ModelPreferences(WireModel): + """Server's advisory preferences for model selection during sampling.""" + + cost_priority: Annotated[float | None, Field(alias="costPriority", ge=0.0, le=1.0)] = None + hints: list[ModelHint] | None = None + """Evaluated in order (first match wins); should outweigh the numeric priorities.""" + intelligence_priority: Annotated[float | None, Field(alias="intelligencePriority", ge=0.0, le=1.0)] = None + speed_priority: Annotated[float | None, Field(alias="speedPriority", ge=0.0, le=1.0)] = None + + +class Notification(WireModel): + method: str + params: dict[str, Any] | None = None + + +class NotificationParams(WireModel): + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + + +class NumberSchema(WireModel): + default: float | None = None + description: str | None = None + maximum: float | None = None + minimum: float | None = None + title: str | None = None + type: Literal["integer", "number"] + + +class PaginatedResult(WireModel): + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + next_cursor: Annotated[str | None, Field(alias="nextCursor")] = None + """Opaque pagination position; if present, more results may be available.""" + result_type: Annotated[str, Field(alias="resultType")] + """Result-type discriminator; treat absence (pre-2026-07-28 peer) as `"complete"`.""" + + +class ParseError(WireModel): + """JSON-RPC: invalid JSON received.""" + + code: Literal[-32700] + data: Any | None = None + message: str + + +ProgressToken: TypeAlias = str | int + + +class PromptArgument(WireModel): + """Describes an argument that a prompt can accept.""" + + description: str | None = None + name: str + required: bool | None = None + title: str | None = None + + +class PromptListChangedNotification(WireModel): + """Server-to-client: the prompt list has changed.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/prompts/list_changed"] + params: NotificationParams | None = None + + +class PromptReference(WireModel): + """Identifies a prompt.""" + + name: str + title: str | None = None + type: Literal["ref/prompt"] + + +class Request(WireModel): + method: str + params: dict[str, Any] | None = None + + +RequestId: TypeAlias = str | int + + +class ResourceContents(WireModel): + """The contents of a specific resource or sub-resource.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + uri: str + + +class ResourceListChangedNotification(WireModel): + """Server-to-client: the resource list has changed.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/resources/list_changed"] + params: NotificationParams | None = None + + +class ResourceTemplateReference(WireModel): + """A reference to a resource or resource template definition.""" + + type: Literal["ref/resource"] + uri: str + """URI or URI template of the resource.""" + + +class ResourceUpdatedNotificationParams(WireModel): + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + uri: str + """URI of the updated resource; may be a sub-resource of the subscription URI.""" + + +class Result(WireModel): + """Common result fields.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + result_type: Annotated[str, Field(alias="resultType")] + """Result-type discriminator; treat absence (pre-2026-07-28 peer) as `"complete"`.""" + + +ResultType: TypeAlias = str + + +Role: TypeAlias = Literal["assistant", "user"] + + +class Root(WireModel): + """A root directory or file the server can operate on.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + name: str | None = None + uri: str + """Must start with `file://` for now.""" + + +class Prompts(WireModel): + """Present if the server offers any prompt templates.""" + + list_changed: Annotated[bool | None, Field(alias="listChanged")] = None + """Whether the server supports prompt-list-changed notifications.""" + + +class Resources(WireModel): + """Present if the server offers any resources to read.""" + + list_changed: Annotated[bool | None, Field(alias="listChanged")] = None + """Whether the server supports resource-list-changed notifications.""" + subscribe: bool | None = None + """Whether the server supports subscribing to resource updates.""" + + +class Tools(WireModel): + """Present if the server offers any tools to call.""" + + list_changed: Annotated[bool | None, Field(alias="listChanged")] = None + """Whether the server supports tool-list-changed notifications.""" + + +class StringSchema(WireModel): + default: str | None = None + description: str | None = None + format: Literal["date", "date-time", "email", "uri"] | None = None + max_length: Annotated[int | None, Field(alias="maxLength")] = None + min_length: Annotated[int | None, Field(alias="minLength")] = None + title: str | None = None + type: Literal["string"] + + +class SubscriptionFilter(WireModel): + """Notification types a client opts in to on `subscriptions/listen`; each is opt-in.""" + + # Stays open: filter contents are extensible on the wire. + model_config = ConfigDict( + extra="allow", + ) + prompts_list_changed: Annotated[bool | None, Field(alias="promptsListChanged")] = None + """Receive `notifications/prompts/list_changed`.""" + resource_subscriptions: Annotated[list[str] | None, Field(alias="resourceSubscriptions")] = None + """Receive `notifications/resources/updated` for these resource URIs.""" + resources_list_changed: Annotated[bool | None, Field(alias="resourcesListChanged")] = None + """Receive `notifications/resources/list_changed`.""" + tools_list_changed: Annotated[bool | None, Field(alias="toolsListChanged")] = None + """Receive `notifications/tools/list_changed`.""" + + +class SubscriptionsAcknowledgedNotificationParams(WireModel): + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + notifications: SubscriptionFilter + """Subset of requested notification types the server agreed to honor.""" + + +class TextResourceContents(WireModel): + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + text: str + uri: str + + +class AnyOfItem(WireModel): + const: str + title: str + + +class Items(WireModel): + """Array-item schema with enum options and display labels.""" + + any_of: Annotated[list[AnyOfItem], Field(alias="anyOf")] + + +class TitledMultiSelectEnumSchema(WireModel): + """Multi-select enum schema with per-option display titles.""" + + default: list[str] | None = None + description: str | None = None + items: Items + max_items: Annotated[int | None, Field(alias="maxItems")] = None + min_items: Annotated[int | None, Field(alias="minItems")] = None + title: str | None = None + type: Literal["array"] + + +class OneOfItem(WireModel): + const: str + title: str + + +class TitledSingleSelectEnumSchema(WireModel): + """Single-select enum schema with per-option display titles.""" + + default: str | None = None + description: str | None = None + one_of: Annotated[list[OneOfItem], Field(alias="oneOf")] + title: str | None = None + type: Literal["string"] + + +class InputSchema(WireModel): + """JSON Schema for tool parameters; root must be `type: "object"`, defaulting to 2020-12.""" + + # Stays open: arbitrary JSON Schema keywords ride extra fields. + model_config = ConfigDict( + extra="allow", + ) + schema_: Annotated[str | None, Field(alias="$schema")] = None + type: Literal["object"] + + +class OutputSchema(WireModel): + """JSON Schema for a tool's `structuredContent` output; defaults to 2020-12.""" + + # Stays open: arbitrary JSON Schema keywords ride extra fields. + model_config = ConfigDict( + extra="allow", + ) + schema_: Annotated[str | None, Field(alias="$schema")] = None + + +class ToolAnnotations(WireModel): + """Tool hints; not guaranteed faithful, never trust from untrusted servers.""" + + destructive_hint: Annotated[bool | None, Field(alias="destructiveHint")] = None + """May perform destructive updates (only meaningful when not read-only); default true.""" + idempotent_hint: Annotated[bool | None, Field(alias="idempotentHint")] = None + """Repeat calls with same args have no additional effect; default false.""" + open_world_hint: Annotated[bool | None, Field(alias="openWorldHint")] = None + """Interacts with an open world of external entities; default true.""" + read_only_hint: Annotated[bool | None, Field(alias="readOnlyHint")] = None + """Does not modify its environment; default false.""" + title: str | None = None + + +class ToolChoice(WireModel): + """Controls tool-selection behavior for sampling requests.""" + + mode: Literal["auto", "none", "required"] | None = None + """`auto` (default) = model decides, `required` = must use a tool, `none` = must not.""" + + +class ToolListChangedNotification(WireModel): + """Server-to-client: the tool list has changed.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/tools/list_changed"] + params: NotificationParams | None = None + + +class ToolUseContent(WireModel): + """A request from the assistant to call a tool.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + id: str + """Unique ID matching this tool use to its result.""" + input: dict[str, Any] + """Arguments conforming to the tool's input schema.""" + name: str + type: Literal["tool_use"] + + +class Data1(WireModel): + requested: str + """Protocol version the client requested.""" + supported: list[str] + """Protocol versions the server supports; the client should retry with one of these.""" + + +class Error2(WireModel): + code: Literal[-32004] + data: Data1 + message: str + + +class UnsupportedProtocolVersionError(WireModel): + """The requested protocol version is unknown or unsupported (HTTP: `400 Bad Request`).""" + + error: Error2 + id: RequestId | None = None + jsonrpc: Literal["2.0"] + + +class Items1(WireModel): + """Array-item schema.""" + + enum: list[str] + type: Literal["string"] + + +class UntitledMultiSelectEnumSchema(WireModel): + """Multi-select enum schema without per-option display titles.""" + + default: list[str] | None = None + description: str | None = None + items: Items1 + max_items: Annotated[int | None, Field(alias="maxItems")] = None + min_items: Annotated[int | None, Field(alias="minItems")] = None + title: str | None = None + type: Literal["array"] + + +class UntitledSingleSelectEnumSchema(WireModel): + """Single-select enum schema without per-option display titles.""" + + default: str | None = None + description: str | None = None + enum: list[str] + title: str | None = None + type: Literal["string"] + + +class Annotations(WireModel): + """Client-facing annotations informing how objects are used or displayed.""" + + audience: list[Role] | None = None + """Intended audience(s) of this object or data.""" + last_modified: Annotated[str | None, Field(alias="lastModified")] = None + """ISO 8601 timestamp of last modification.""" + priority: Annotated[float | None, Field(ge=0.0, le=1.0)] = None + """Importance for operating the server: 1 = effectively required, 0 = entirely optional.""" + + +class AudioContent(WireModel): + """Audio provided to or from an LLM.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + data: str + """Base64-encoded audio data.""" + mime_type: Annotated[str, Field(alias="mimeType")] + type: Literal["audio"] + + +class BlobResourceContents(WireModel): + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + blob: str + """Base64-encoded binary data.""" + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + uri: str + + +class CacheableResult(WireModel): + """A result carrying a TTL hint for client-side caching.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + cache_scope: Annotated[Literal["private", "public"], Field(alias="cacheScope")] + """`"public"` = shareable across users/intermediaries; `"private"` = requesting user only.""" + result_type: Annotated[str, Field(alias="resultType")] + """Result-type discriminator; treat absence (pre-2026-07-28 peer) as `"complete"`.""" + ttl_ms: Annotated[int, Field(alias="ttlMs", ge=0)] + """Cache freshness hint in ms (HTTP max-age semantics; 0 = immediately stale).""" + + +class CancelledNotificationParams(WireModel): + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + reason: str | None = None + request_id: Annotated[RequestId | None, Field(alias="requestId")] = None + """ID of a request previously issued in the same direction.""" + + +ClientResult: TypeAlias = Result + + +class CompleteResult(WireModel): + """Server's result for a `completion/complete` request.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + completion: Completion + result_type: Annotated[str, Field(alias="resultType")] + """Result-type discriminator; treat absence (pre-2026-07-28 peer) as `"complete"`.""" + + +class CompleteResultResponse(WireModel): + """Successful response to a `completion/complete` request.""" + + id: RequestId + jsonrpc: Literal["2.0"] + result: CompleteResult + + +class EmbeddedResource(WireModel): + """Resource contents embedded in a prompt or tool-call result.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + resource: TextResourceContents | BlobResourceContents + type: Literal["resource"] + + +EmptyResult: TypeAlias = Result + + +EnumSchema: TypeAlias = ( + UntitledSingleSelectEnumSchema + | TitledSingleSelectEnumSchema + | UntitledMultiSelectEnumSchema + | TitledMultiSelectEnumSchema + | LegacyTitledEnumSchema +) + + +class ImageContent(WireModel): + """An image provided to or from an LLM.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + data: str + """Base64-encoded image data.""" + mime_type: Annotated[str, Field(alias="mimeType")] + type: Literal["image"] + + +class JSONRPCErrorResponse(WireModel): + """An error response to a request.""" + + error: Error + id: RequestId | None = None + jsonrpc: Literal["2.0"] + + +class JSONRPCRequest(WireModel): + """A request that expects a response.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: str + params: dict[str, Any] | None = None + + +class JSONRPCResultResponse(WireModel): + """A successful (non-error) response to a request.""" + + id: RequestId + jsonrpc: Literal["2.0"] + result: Result + + +class ListRootsResult(WireModel): + """Client's result for a `roots/list` request.""" + + roots: list[Root] + + +class LoggingMessageNotificationParams(WireModel): + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + data: Any + """Any JSON-serializable log payload.""" + level: LoggingLevel + logger: str | None = None + + +MultiSelectEnumSchema: TypeAlias = UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema + + +PrimitiveSchemaDefinition: TypeAlias = ( + StringSchema + | NumberSchema + | BooleanSchema + | UntitledSingleSelectEnumSchema + | TitledSingleSelectEnumSchema + | UntitledMultiSelectEnumSchema + | TitledMultiSelectEnumSchema + | LegacyTitledEnumSchema +) + + +class ProgressNotificationParams(WireModel): + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + message: str | None = None + progress: float + """Monotonically increasing progress value; `total` may be unknown.""" + progress_token: Annotated[ProgressToken, Field(alias="progressToken")] + """Token from the originating request, associating this notification with it.""" + total: float | None = None + + +class Prompt(WireModel): + """A prompt or prompt template the server offers.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + arguments: list[PromptArgument] | None = None + description: str | None = None + icons: list[Icon] | None = None + name: str + title: str | None = None + + +class ReadResourceResult(WireModel): + """Server's result for a `resources/read` request.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + cache_scope: Annotated[Literal["private", "public"], Field(alias="cacheScope")] + """`"public"` = shareable across users/intermediaries; `"private"` = requesting user only.""" + contents: list[TextResourceContents | BlobResourceContents] + result_type: Annotated[str, Field(alias="resultType")] + """Result-type discriminator; treat absence (pre-2026-07-28 peer) as `"complete"`.""" + ttl_ms: Annotated[int, Field(alias="ttlMs", ge=0)] + """Cache freshness hint in ms (HTTP max-age semantics; 0 = immediately stale).""" + + +class Resource(WireModel): + """A known resource the server is capable of reading.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + description: str | None = None + icons: list[Icon] | None = None + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + name: str + size: int | None = None + """Raw content size in bytes (before base64), if known.""" + title: str | None = None + uri: str + + +class ResourceLink(WireModel): + """A resource reference in a prompt or tool-call result; not guaranteed to appear in `resources/list`.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + description: str | None = None + icons: list[Icon] | None = None + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + name: str + size: int | None = None + """Raw content size in bytes (before base64), if known.""" + title: str | None = None + type: Literal["resource_link"] + uri: str + + +class ResourceTemplate(WireModel): + """A template description for resources available on the server.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + description: str | None = None + icons: list[Icon] | None = None + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + """MIME type for all matching resources; include only if uniform across matches.""" + name: str + title: str | None = None + uri_template: Annotated[str, Field(alias="uriTemplate")] + """RFC 6570 URI template.""" + + +class ResourceUpdatedNotification(WireModel): + """Server-to-client: a resource the client subscribed to via `subscriptions/listen` has changed.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/resources/updated"] + params: ResourceUpdatedNotificationParams + + +SingleSelectEnumSchema: TypeAlias = UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema + + +class SubscriptionsAcknowledgedNotification(WireModel): + """First message on a `subscriptions/listen` stream, reporting which notifications the server honors.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/subscriptions/acknowledged"] + params: SubscriptionsAcknowledgedNotificationParams + + +class TextContent(WireModel): + """Text provided to or from an LLM.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + annotations: Annotations | None = None + text: str + type: Literal["text"] + + +class Tool(WireModel): + """Definition for a tool the client can call.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + annotations: ToolAnnotations | None = None + """Display-name precedence: `title`, `annotations.title`, then `name`.""" + description: str | None = None + icons: list[Icon] | None = None + input_schema: Annotated[InputSchema, Field(alias="inputSchema")] + name: str + output_schema: Annotated[OutputSchema | None, Field(alias="outputSchema")] = None + title: str | None = None + + +class CancelledNotification(WireModel): + """Either side cancelling a previously-issued request; the result will be unused.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/cancelled"] + params: CancelledNotificationParams + + +ContentBlock: TypeAlias = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource + + +class RequestedSchema(WireModel): + """Restricted JSON Schema subset: top-level properties only, no nesting.""" + + schema_: Annotated[str | None, Field(alias="$schema")] = None + properties: dict[str, PrimitiveSchemaDefinition] + required: list[str] | None = None + type: Literal["object"] + + +class ElicitRequestFormParams(WireModel): + """Parameters for a form-mode `elicitation/create` request.""" + + message: str + mode: Literal["form"] = "form" + requested_schema: Annotated[RequestedSchema, Field(alias="requestedSchema")] + + +ElicitRequestParams: TypeAlias = ElicitRequestFormParams | ElicitRequestURLParams + + +JSONRPCMessage: TypeAlias = JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse + + +JSONRPCResponse: TypeAlias = JSONRPCResultResponse | JSONRPCErrorResponse + + +class ListPromptsResult(WireModel): + """Server's result for a `prompts/list` request.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + cache_scope: Annotated[Literal["private", "public"], Field(alias="cacheScope")] + """`"public"` = shareable across users/intermediaries; `"private"` = requesting user only.""" + next_cursor: Annotated[str | None, Field(alias="nextCursor")] = None + """Opaque pagination position; if present, more results may be available.""" + prompts: list[Prompt] + result_type: Annotated[str, Field(alias="resultType")] + """Result-type discriminator; treat absence (pre-2026-07-28 peer) as `"complete"`.""" + ttl_ms: Annotated[int, Field(alias="ttlMs", ge=0)] + """Cache freshness hint in ms (HTTP max-age semantics; 0 = immediately stale).""" + + +class ListPromptsResultResponse(WireModel): + """Successful response to a `prompts/list` request.""" + + id: RequestId + jsonrpc: Literal["2.0"] + result: ListPromptsResult + + +class ListResourceTemplatesResult(WireModel): + """Server's result for a `resources/templates/list` request.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + cache_scope: Annotated[Literal["private", "public"], Field(alias="cacheScope")] + """`"public"` = shareable across users/intermediaries; `"private"` = requesting user only.""" + next_cursor: Annotated[str | None, Field(alias="nextCursor")] = None + """Opaque pagination position; if present, more results may be available.""" + resource_templates: Annotated[list[ResourceTemplate], Field(alias="resourceTemplates")] + result_type: Annotated[str, Field(alias="resultType")] + """Result-type discriminator; treat absence (pre-2026-07-28 peer) as `"complete"`.""" + ttl_ms: Annotated[int, Field(alias="ttlMs", ge=0)] + """Cache freshness hint in ms (HTTP max-age semantics; 0 = immediately stale).""" + + +class ListResourceTemplatesResultResponse(WireModel): + """Successful response to a `resources/templates/list` request.""" + + id: RequestId + jsonrpc: Literal["2.0"] + result: ListResourceTemplatesResult + + +class ListResourcesResult(WireModel): + """Server's result for a `resources/list` request.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + cache_scope: Annotated[Literal["private", "public"], Field(alias="cacheScope")] + """`"public"` = shareable across users/intermediaries; `"private"` = requesting user only.""" + next_cursor: Annotated[str | None, Field(alias="nextCursor")] = None + """Opaque pagination position; if present, more results may be available.""" + resources: list[Resource] + result_type: Annotated[str, Field(alias="resultType")] + """Result-type discriminator; treat absence (pre-2026-07-28 peer) as `"complete"`.""" + ttl_ms: Annotated[int, Field(alias="ttlMs", ge=0)] + """Cache freshness hint in ms (HTTP max-age semantics; 0 = immediately stale).""" + + +class ListResourcesResultResponse(WireModel): + """Successful response to a `resources/list` request.""" + + id: RequestId + jsonrpc: Literal["2.0"] + result: ListResourcesResult + + +class ListToolsResult(WireModel): + """Server's result for a `tools/list` request.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + cache_scope: Annotated[Literal["private", "public"], Field(alias="cacheScope")] + """`"public"` = shareable across users/intermediaries; `"private"` = requesting user only.""" + next_cursor: Annotated[str | None, Field(alias="nextCursor")] = None + """Opaque pagination position; if present, more results may be available.""" + result_type: Annotated[str, Field(alias="resultType")] + """Result-type discriminator; treat absence (pre-2026-07-28 peer) as `"complete"`.""" + tools: list[Tool] + ttl_ms: Annotated[int, Field(alias="ttlMs", ge=0)] + """Cache freshness hint in ms (HTTP max-age semantics; 0 = immediately stale).""" + + +class ListToolsResultResponse(WireModel): + """Successful response to a `tools/list` request.""" + + id: RequestId + jsonrpc: Literal["2.0"] + result: ListToolsResult + + +class LoggingMessageNotification(WireModel): + """Server-to-client log message; opted in via `io.modelcontextprotocol/logLevel` in request `_meta`.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/message"] + params: LoggingMessageNotificationParams + + +class ProgressNotification(WireModel): + """Out-of-band progress update for a long-running request.""" + + jsonrpc: Literal["2.0"] + method: Literal["notifications/progress"] + params: ProgressNotificationParams + + +class PromptMessage(WireModel): + """A message returned as part of a prompt; like `SamplingMessage` but supports embedded resources.""" + + content: ContentBlock + role: Role + + +ServerNotification: TypeAlias = ( + CancelledNotification + | ProgressNotification + | ResourceListChangedNotification + | SubscriptionsAcknowledgedNotification + | ResourceUpdatedNotification + | PromptListChangedNotification + | ToolListChangedNotification + | LoggingMessageNotification + | ElicitationCompleteNotification +) + + +class ToolResultContent(WireModel): + """The result of a tool use, provided by the user back to the assistant.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + content: list[ContentBlock] + """Unstructured result; same shape as `CallToolResult.content`.""" + is_error: Annotated[bool | None, Field(alias="isError")] = None + """Default false.""" + structured_content: Annotated[Any | None, Field(alias="structuredContent")] = None + """Any JSON value; should conform to the tool's `outputSchema` if defined.""" + tool_use_id: Annotated[str, Field(alias="toolUseId")] + """ID of the corresponding `ToolUseContent`.""" + type: Literal["tool_result"] + + +class CallToolResult(WireModel): + """Server's result for a `tools/call` request.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + content: list[ContentBlock] + """Unstructured result of the tool call.""" + is_error: Annotated[bool | None, Field(alias="isError")] = None + """Default false; tool-level errors go here, not as protocol-level errors, so the LLM can see them.""" + result_type: Annotated[str, Field(alias="resultType")] + """Result-type discriminator; treat absence (pre-2026-07-28 peer) as `"complete"`.""" + structured_content: Annotated[Any | None, Field(alias="structuredContent")] = None + """Any JSON value conforming to the tool's `outputSchema` if defined.""" + + +ClientNotification: TypeAlias = CancelledNotification | ProgressNotification + + +class ElicitRequest(WireModel): + """Server request to elicit additional information from the user via the client.""" + + method: Literal["elicitation/create"] + params: ElicitRequestParams + + +class GetPromptResult(WireModel): + """Server's result for a `prompts/get` request.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + description: str | None = None + messages: list[PromptMessage] + result_type: Annotated[str, Field(alias="resultType")] + """Result-type discriminator; treat absence (pre-2026-07-28 peer) as `"complete"`.""" + + +SamplingMessageContentBlock: TypeAlias = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent + + +class CreateMessageResult(WireModel): + """Client's result for a `sampling/createMessage` request.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + content: ( + TextContent + | ImageContent + | AudioContent + | ToolUseContent + | ToolResultContent + | list[SamplingMessageContentBlock] + ) + model: str + """Name of the model that generated the message.""" + role: Role + stop_reason: Annotated[str | None, Field(alias="stopReason")] = None + """Open string; standard values are `"endTurn"`, `"stopSequence"`, `"maxTokens"`, `"toolUse"`.""" + + +InputResponse: TypeAlias = CreateMessageResult | ListRootsResult | ElicitResult + + +InputResponses: TypeAlias = dict[str, InputResponse] +"""Client responses to server-initiated requests, keyed by the matching `InputRequests` key.""" + + +class SamplingMessage(WireModel): + """A message issued to or received from an LLM API.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + content: ( + TextContent + | ImageContent + | AudioContent + | ToolUseContent + | ToolResultContent + | list[SamplingMessageContentBlock] + ) + role: Role + + +class CallToolRequest(WireModel): + """Client request to invoke a tool provided by the server.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["tools/call"] + params: CallToolRequestParams + + +class CallToolRequestParams(WireModel): + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + arguments: dict[str, Any] | None = None + input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None + name: str + request_state: Annotated[str | None, Field(alias="requestState")] = None + + +class CallToolResultResponse(WireModel): + """Successful response to a `tools/call` request.""" + + id: RequestId + jsonrpc: Literal["2.0"] + result: InputRequiredResult | CallToolResult + + +class Elicitation(WireModel): + """Present if the client supports elicitation from the server.""" + + form: JSONObject | None = None + url: JSONObject | None = None + + +class Sampling(WireModel): + """Present if the client supports sampling from an LLM.""" + + context: JSONObject | None = None + """Declares support for context inclusion via `includeContext`.""" + tools: JSONObject | None = None + """Declares support for `tools` and `toolChoice`.""" + + +class ClientCapabilities(WireModel): + """Capabilities a client may support; not a closed set.""" + + elicitation: Elicitation | None = None + experimental: dict[str, JSONObject] | None = None + extensions: dict[str, JSONObject] | None = None + """Supported MCP extensions, keyed by extension identifier.""" + roots: dict[str, Any] | None = None + sampling: Sampling | None = None + + +class CompleteRequest(WireModel): + """Client request for completion options.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["completion/complete"] + params: CompleteRequestParams + + +class CompleteRequestParams(WireModel): + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + argument: Argument + context: Context | None = None + ref: PromptReference | ResourceTemplateReference + + +class CreateMessageRequest(WireModel): + """Server request for the client to sample an LLM (with human-in-the-loop approval).""" + + method: Literal["sampling/createMessage"] + params: CreateMessageRequestParams + + +class CreateMessageRequestParams(WireModel): + include_context: Annotated[ + Literal["allServers", "none", "thisServer"] | None, + Field(alias="includeContext"), + ] = None + """Default `"none"`; `"thisServer"`/`"allServers"` are deprecated (SEP-2596).""" + max_tokens: Annotated[int, Field(alias="maxTokens")] + """Requested cap; the client may sample fewer.""" + messages: list[SamplingMessage] + metadata: JSONObject | None = None + """Provider-specific passthrough metadata.""" + model_preferences: Annotated[ModelPreferences | None, Field(alias="modelPreferences")] = None + stop_sequences: Annotated[list[str] | None, Field(alias="stopSequences")] = None + system_prompt: Annotated[str | None, Field(alias="systemPrompt")] = None + temperature: float | None = None + tool_choice: Annotated[ToolChoice | None, Field(alias="toolChoice")] = None + """Error if set without `ClientCapabilities.sampling.tools`; default `{mode: "auto"}`.""" + tools: list[Tool] | None = None + """Error if set without `ClientCapabilities.sampling.tools`.""" + + +class DiscoverRequest(WireModel): + """Client request for the server's supported versions, capabilities, and metadata; servers must implement.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["server/discover"] + params: RequestParams + + +class DiscoverResult(WireModel): + """Server's result for a `server/discover` request.""" + + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + cache_scope: Annotated[Literal["private", "public"], Field(alias="cacheScope")] + """`"public"` = shareable across users/intermediaries; `"private"` = requesting user only.""" + capabilities: ServerCapabilities + instructions: str | None = None + """Natural-language guidance for the LLM; should not duplicate tool descriptions.""" + result_type: Annotated[str, Field(alias="resultType")] + """Result-type discriminator; treat absence (pre-2026-07-28 peer) as `"complete"`.""" + server_info: Annotated[Implementation, Field(alias="serverInfo")] + supported_versions: Annotated[list[str], Field(alias="supportedVersions")] + """Protocol versions this server supports.""" + ttl_ms: Annotated[int, Field(alias="ttlMs", ge=0)] + """Cache freshness hint in ms (HTTP max-age semantics; 0 = immediately stale).""" + + +class DiscoverResultResponse(WireModel): + """Successful response to a `server/discover` request.""" + + id: RequestId + jsonrpc: Literal["2.0"] + result: DiscoverResult + + +class GetPromptRequest(WireModel): + """Client request for a prompt provided by the server.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["prompts/get"] + params: GetPromptRequestParams + + +class GetPromptRequestParams(WireModel): + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + arguments: dict[str, str] | None = None + input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None + name: str + request_state: Annotated[str | None, Field(alias="requestState")] = None + + +class GetPromptResultResponse(WireModel): + """Successful response to a `prompts/get` request.""" + + id: RequestId + jsonrpc: Literal["2.0"] + result: InputRequiredResult | GetPromptResult + + +class InputRequiredResult(WireModel): + """Server signals that more input is needed; at least one of `inputRequests` or `requestState` must be present.""" + + meta: Annotated[MetaObject | 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")] + """Result-type discriminator; treat absence (pre-2026-07-28 peer) as `"complete"`.""" + + +class InputResponseRequestParams(WireModel): + 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): + """Client request for the server's prompts and prompt templates.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["prompts/list"] + params: PaginatedRequestParams + + +class ListResourceTemplatesRequest(WireModel): + """Client request for the server's resource templates.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["resources/templates/list"] + params: PaginatedRequestParams + + +class ListResourcesRequest(WireModel): + """Client request for the server's resources.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["resources/list"] + params: PaginatedRequestParams + + +class ListRootsRequest(WireModel): + """Server request for the client's root URIs.""" + + method: Literal["roots/list"] + params: RequestParams | None = None + + +class ListToolsRequest(WireModel): + """Client request for the server's tools.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["tools/list"] + params: PaginatedRequestParams + + +class Data(WireModel): + required_capabilities: Annotated[ClientCapabilities, Field(alias="requiredCapabilities")] + """Capabilities the server requires from the client to process the request.""" + + +class Error1(WireModel): + code: Literal[-32003] + data: Data + message: str + + +class MissingRequiredClientCapabilityError(WireModel): + """The request requires a client capability not declared in `clientCapabilities` (HTTP: `400 Bad Request`).""" + + error: Error1 + id: RequestId | None = None + jsonrpc: Literal["2.0"] + + +class PaginatedRequest(WireModel): + id: RequestId + jsonrpc: Literal["2.0"] + method: str + params: PaginatedRequestParams + + +class PaginatedRequestParams(WireModel): + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + cursor: str | None = None + """Opaque pagination position; results start after this cursor.""" + + +class ReadResourceRequest(WireModel): + """Client request to read a specific resource URI.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["resources/read"] + params: ReadResourceRequestParams + + +class ReadResourceRequestParams(WireModel): + 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 + """Any URI scheme; interpretation is server-defined.""" + + +class ReadResourceResultResponse(WireModel): + """Successful response to a `resources/read` request.""" + + id: RequestId + jsonrpc: Literal["2.0"] + result: InputRequiredResult | ReadResourceResult + + +class RequestMetaObject(OpenWireModel): + """Extends `MetaObject` with request-specific reserved keys; same key naming rules apply.""" + + io_modelcontextprotocol_client_capabilities: Annotated[ + ClientCapabilities, Field(alias="io.modelcontextprotocol/clientCapabilities") + ] + """Per-request client capabilities; servers must not infer from prior requests.""" + io_modelcontextprotocol_client_info: Annotated[Implementation, Field(alias="io.modelcontextprotocol/clientInfo")] + """Identifies the client software making the request.""" + io_modelcontextprotocol_log_level: Annotated[ + LoggingLevel | None, Field(alias="io.modelcontextprotocol/logLevel") + ] = None + """Log level for this request; absent means no `notifications/message` may be sent.""" + io_modelcontextprotocol_protocol_version: Annotated[str, Field(alias="io.modelcontextprotocol/protocolVersion")] + """Protocol version for this request; over HTTP, must match the `MCP-Protocol-Version` header.""" + progress_token: Annotated[ProgressToken | None, Field(alias="progressToken")] = None + """Opaque token opting in to `notifications/progress` for this request.""" + + +class RequestParams(WireModel): + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + + +class ResourceRequestParams(WireModel): + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + uri: str + """Any URI scheme; interpretation is server-defined.""" + + +class ServerCapabilities(WireModel): + """Capabilities a server may support; not a closed set.""" + + completions: JSONObject | None = None + experimental: dict[str, JSONObject] | None = None + extensions: dict[str, JSONObject] | None = None + """Supported MCP extensions, keyed by extension identifier.""" + logging: JSONObject | None = None + prompts: Prompts | None = None + resources: Resources | None = None + tools: Tools | None = None + + +class SubscriptionsListenRequest(WireModel): + """Client request to open a long-lived channel for receiving notifications outside any specific request.""" + + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["subscriptions/listen"] + params: SubscriptionsListenRequestParams + + +class SubscriptionsListenRequestParams(WireModel): + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + notifications: SubscriptionFilter + """Notification types the client opts in to on this stream.""" + + +AnyCallToolResult: TypeAlias = CallToolResult | InputRequiredResult +"""Named alias for `CallToolResultResponse.result` so the wire-method maps can reference it as a value.""" + +AnyGetPromptResult: TypeAlias = GetPromptResult | InputRequiredResult +"""Everything a `prompts/get` response's `result` may be at this version.""" + +AnyReadResourceResult: TypeAlias = ReadResourceResult | InputRequiredResult +"""Everything a `resources/read` response's `result` may be at this version.""" + + +ServerResult: TypeAlias = ( + Result + | InputRequiredResult + | DiscoverResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | ListPromptsResult + | GetPromptResult + | ListToolsResult + | CallToolResult + | CompleteResult +) + + +InputRequest: TypeAlias = CreateMessageRequest | ListRootsRequest | ElicitRequest + + +ClientRequest: TypeAlias = ( + DiscoverRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscriptionsListenRequest + | ListPromptsRequest + | GetPromptRequest + | ListToolsRequest + | CallToolRequest + | CompleteRequest +) + + +InputRequests: TypeAlias = dict[str, InputRequest] +"""Server-initiated requests the client must fulfill, keyed by server-assigned identifier.""" + + +JSONArray: TypeAlias = list["JSONValue"] + + +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() +ListRootsRequest.model_rebuild() +ListToolsRequest.model_rebuild() +PaginatedRequest.model_rebuild() +PaginatedRequestParams.model_rebuild() +ReadResourceRequest.model_rebuild() +ReadResourceRequestParams.model_rebuild() +ServerCapabilities.model_rebuild() +SubscriptionsListenRequest.model_rebuild() diff --git a/tests/interaction/transports/test_hosting_resume.py b/tests/interaction/transports/test_hosting_resume.py index c7945d56c..b835c7802 100644 --- a/tests/interaction/transports/test_hosting_resume.py +++ b/tests/interaction/transports/test_hosting_resume.py @@ -108,6 +108,7 @@ async def test_a_post_sse_stream_begins_with_a_priming_event_and_stamps_every_ev "content": [{"type": "text", "text": "counted to 2"}], "structuredContent": {"result": "counted to 2"}, "isError": False, + "resultType": "complete", }, ) ) diff --git a/tests/server/test_session.py b/tests/server/test_session.py index 86ec50757..44aab6b33 100644 --- a/tests/server/test_session.py +++ b/tests/server/test_session.py @@ -219,6 +219,6 @@ async def test_protocol_version_is_none_on_stateless_connection(): seen: list[str | None] = [] async with connected_runner(_runner_server(seen), initialized=False, stateless=True) as (client, runner): result = await client.send_raw_request("tools/list", None) - assert result == {"tools": []} + assert result == {"tools": [], "resultType": "complete", "ttlMs": 0, "cacheScope": "private"} assert seen == [None] assert runner.session.protocol_version is None diff --git a/tests/test_types.py b/tests/test_types.py index f424efdbf..d604ef1bb 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,20 +1,34 @@ from typing import Any import pytest +from inline_snapshot import snapshot from mcp.types import ( LATEST_PROTOCOL_VERSION, + CallToolResult, ClientCapabilities, + CompleteResult, + Completion, CreateMessageRequestParams, CreateMessageResult, CreateMessageResultWithTools, + DiscoverResult, + EmptyResult, + GetPromptResult, Implementation, InitializeRequest, InitializeRequestParams, + InputRequiredResult, JSONRPCRequest, + ListPromptsResult, + ListResourcesResult, + ListResourceTemplatesResult, ListToolsResult, + ReadResourceResult, + Result, SamplingCapability, SamplingMessage, + ServerCapabilities, TextContent, Tool, ToolChoice, @@ -360,3 +374,61 @@ def test_list_tools_result_preserves_json_schema_2020_12_fields(): assert tool.input_schema["$schema"] == "https://json-schema.org/draft/2020-12/schema" assert "$defs" in tool.input_schema assert tool.input_schema["additionalProperties"] is False + + +def _wire_dump(result: Result) -> dict[str, Any]: + return result.model_dump(by_alias=True, mode="json", exclude_none=True) + + +def test_concrete_wire_results_always_dump_result_type_complete(): + """Required by 2026-07-28; older peers tolerate the extra key.""" + carriers: list[Result] = [ + CompleteResult(completion=Completion(values=[])), + GetPromptResult(messages=[]), + CallToolResult(content=[]), + ReadResourceResult(contents=[]), + ListPromptsResult(prompts=[]), + ListResourcesResult(resources=[]), + ListResourceTemplatesResult(resource_templates=[]), + ListToolsResult(tools=[]), + DiscoverResult( + supported_versions=["2026-07-28"], + capabilities=ServerCapabilities(), + server_info=Implementation(name="server", version="1.0"), + ), + ] + for result in carriers: + assert _wire_dump(result)["resultType"] == "complete", type(result).__name__ + + +def test_cacheable_results_always_dump_their_caching_directives(): + """Required by 2026-07-28; older peers tolerate the extra keys.""" + cacheable: list[Result] = [ + ReadResourceResult(contents=[]), + ListPromptsResult(prompts=[]), + ListResourceTemplatesResult(resource_templates=[]), + ListResourcesResult(resources=[]), + ListToolsResult(tools=[]), + DiscoverResult( + supported_versions=["2026-07-28"], + capabilities=ServerCapabilities(), + server_info=Implementation(name="server", version="1.0"), + ), + ] + for result in cacheable: + dumped = _wire_dump(result) + assert dumped["ttlMs"] == 0, type(result).__name__ + assert dumped["cacheScope"] == "private", type(result).__name__ + + +def test_empty_result_dumps_no_fields_by_default(): + """Deployed peers reject extra keys on empty results, so resultType is never volunteered.""" + assert _wire_dump(EmptyResult()) == snapshot({}) + + +def test_empty_result_dumps_result_type_only_when_explicitly_tagged(): + assert _wire_dump(EmptyResult(result_type="complete")) == snapshot({"resultType": "complete"}) + + +def test_input_required_result_dumps_its_discriminating_tag(): + assert _wire_dump(InputRequiredResult()) == snapshot({"resultType": "input_required"}) diff --git a/tests/types/__init__.py b/tests/types/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/types/test_methods.py b/tests/types/test_methods.py new file mode 100644 index 000000000..423bafaf4 --- /dev/null +++ b/tests/types/test_methods.py @@ -0,0 +1,742 @@ +"""Tests for the wire-method maps and two-step parse functions in `mcp.types.methods`.""" + +import importlib.util +from collections.abc import Mapping +from types import MappingProxyType, UnionType +from typing import Any, get_args + +import pydantic +import pytest + +import mcp.types as types +import mcp.types.v2025_11_25 as v2025 +import mcp.types.v2026_07_28 as v2026 +from mcp.shared.version import KNOWN_PROTOCOL_VERSIONS +from mcp.types import methods +from mcp.types._wire_base import WireModel + +# Transcribed from each schema's ClientRequest/ServerRequest/ClientNotification/ +# ServerNotification unions, minus the tasks/* family (extensions register those). +EXPECTED_METHODS: dict[str, dict[str, frozenset[str]]] = { + "2024-11-05": { + "CLIENT_REQUESTS": frozenset( + { + "completion/complete", + "initialize", + "logging/setLevel", + "ping", + "prompts/get", + "prompts/list", + "resources/list", + "resources/read", + "resources/subscribe", + "resources/templates/list", + "resources/unsubscribe", + "tools/call", + "tools/list", + } + ), + "CLIENT_NOTIFICATIONS": frozenset( + { + "notifications/cancelled", + "notifications/initialized", + "notifications/progress", + "notifications/roots/list_changed", + } + ), + "SERVER_REQUESTS": frozenset({"ping", "roots/list", "sampling/createMessage"}), + "SERVER_NOTIFICATIONS": frozenset( + { + "notifications/cancelled", + "notifications/message", + "notifications/progress", + "notifications/prompts/list_changed", + "notifications/resources/list_changed", + "notifications/resources/updated", + "notifications/tools/list_changed", + } + ), + }, + "2025-03-26": { + "CLIENT_REQUESTS": frozenset( + { + "completion/complete", + "initialize", + "logging/setLevel", + "ping", + "prompts/get", + "prompts/list", + "resources/list", + "resources/read", + "resources/subscribe", + "resources/templates/list", + "resources/unsubscribe", + "tools/call", + "tools/list", + } + ), + "CLIENT_NOTIFICATIONS": frozenset( + { + "notifications/cancelled", + "notifications/initialized", + "notifications/progress", + "notifications/roots/list_changed", + } + ), + "SERVER_REQUESTS": frozenset({"ping", "roots/list", "sampling/createMessage"}), + "SERVER_NOTIFICATIONS": frozenset( + { + "notifications/cancelled", + "notifications/message", + "notifications/progress", + "notifications/prompts/list_changed", + "notifications/resources/list_changed", + "notifications/resources/updated", + "notifications/tools/list_changed", + } + ), + }, + "2025-06-18": { + "CLIENT_REQUESTS": frozenset( + { + "completion/complete", + "initialize", + "logging/setLevel", + "ping", + "prompts/get", + "prompts/list", + "resources/list", + "resources/read", + "resources/subscribe", + "resources/templates/list", + "resources/unsubscribe", + "tools/call", + "tools/list", + } + ), + "CLIENT_NOTIFICATIONS": frozenset( + { + "notifications/cancelled", + "notifications/initialized", + "notifications/progress", + "notifications/roots/list_changed", + } + ), + "SERVER_REQUESTS": frozenset({"elicitation/create", "ping", "roots/list", "sampling/createMessage"}), + "SERVER_NOTIFICATIONS": frozenset( + { + "notifications/cancelled", + "notifications/message", + "notifications/progress", + "notifications/prompts/list_changed", + "notifications/resources/list_changed", + "notifications/resources/updated", + "notifications/tools/list_changed", + } + ), + }, + "2025-11-25": { + "CLIENT_REQUESTS": frozenset( + { + "completion/complete", + "initialize", + "logging/setLevel", + "ping", + "prompts/get", + "prompts/list", + "resources/list", + "resources/read", + "resources/subscribe", + "resources/templates/list", + "resources/unsubscribe", + "tools/call", + "tools/list", + } + ), + "CLIENT_NOTIFICATIONS": frozenset( + { + "notifications/cancelled", + "notifications/initialized", + "notifications/progress", + "notifications/roots/list_changed", + } + ), + "SERVER_REQUESTS": frozenset({"elicitation/create", "ping", "roots/list", "sampling/createMessage"}), + "SERVER_NOTIFICATIONS": frozenset( + { + "notifications/cancelled", + "notifications/elicitation/complete", + "notifications/message", + "notifications/progress", + "notifications/prompts/list_changed", + "notifications/resources/list_changed", + "notifications/resources/updated", + "notifications/tools/list_changed", + } + ), + }, + "2026-07-28": { + "CLIENT_REQUESTS": frozenset( + { + "completion/complete", + "prompts/get", + "prompts/list", + "resources/list", + "resources/read", + "resources/templates/list", + "server/discover", + "subscriptions/listen", + "tools/call", + "tools/list", + } + ), + "CLIENT_NOTIFICATIONS": frozenset({"notifications/cancelled", "notifications/progress"}), + # No standalone server-to-client request channel at this version. + "SERVER_REQUESTS": frozenset(), + "SERVER_NOTIFICATIONS": frozenset( + { + "notifications/cancelled", + "notifications/elicitation/complete", + "notifications/message", + "notifications/progress", + "notifications/prompts/list_changed", + "notifications/resources/list_changed", + "notifications/resources/updated", + "notifications/subscriptions/acknowledged", + "notifications/tools/list_changed", + } + ), + }, +} + +# Pinned per (method, version): class identity, or exact arm tuple for unions. +EXPECTED_SERVER_RESULTS: dict[tuple[str, str], type[WireModel] | tuple[type[WireModel], ...]] = { + ("completion/complete", "2024-11-05"): v2025.CompleteResult, + ("initialize", "2024-11-05"): v2025.InitializeResult, + ("logging/setLevel", "2024-11-05"): v2025.EmptyResult, + ("ping", "2024-11-05"): v2025.EmptyResult, + ("prompts/get", "2024-11-05"): v2025.GetPromptResult, + ("prompts/list", "2024-11-05"): v2025.ListPromptsResult, + ("resources/list", "2024-11-05"): v2025.ListResourcesResult, + ("resources/read", "2024-11-05"): v2025.ReadResourceResult, + ("resources/subscribe", "2024-11-05"): v2025.EmptyResult, + ("resources/templates/list", "2024-11-05"): v2025.ListResourceTemplatesResult, + ("resources/unsubscribe", "2024-11-05"): v2025.EmptyResult, + ("tools/call", "2024-11-05"): v2025.CallToolResult, + ("tools/list", "2024-11-05"): v2025.ListToolsResult, + ("completion/complete", "2025-03-26"): v2025.CompleteResult, + ("initialize", "2025-03-26"): v2025.InitializeResult, + ("logging/setLevel", "2025-03-26"): v2025.EmptyResult, + ("ping", "2025-03-26"): v2025.EmptyResult, + ("prompts/get", "2025-03-26"): v2025.GetPromptResult, + ("prompts/list", "2025-03-26"): v2025.ListPromptsResult, + ("resources/list", "2025-03-26"): v2025.ListResourcesResult, + ("resources/read", "2025-03-26"): v2025.ReadResourceResult, + ("resources/subscribe", "2025-03-26"): v2025.EmptyResult, + ("resources/templates/list", "2025-03-26"): v2025.ListResourceTemplatesResult, + ("resources/unsubscribe", "2025-03-26"): v2025.EmptyResult, + ("tools/call", "2025-03-26"): v2025.CallToolResult, + ("tools/list", "2025-03-26"): v2025.ListToolsResult, + ("completion/complete", "2025-06-18"): v2025.CompleteResult, + ("initialize", "2025-06-18"): v2025.InitializeResult, + ("logging/setLevel", "2025-06-18"): v2025.EmptyResult, + ("ping", "2025-06-18"): v2025.EmptyResult, + ("prompts/get", "2025-06-18"): v2025.GetPromptResult, + ("prompts/list", "2025-06-18"): v2025.ListPromptsResult, + ("resources/list", "2025-06-18"): v2025.ListResourcesResult, + ("resources/read", "2025-06-18"): v2025.ReadResourceResult, + ("resources/subscribe", "2025-06-18"): v2025.EmptyResult, + ("resources/templates/list", "2025-06-18"): v2025.ListResourceTemplatesResult, + ("resources/unsubscribe", "2025-06-18"): v2025.EmptyResult, + ("tools/call", "2025-06-18"): v2025.CallToolResult, + ("tools/list", "2025-06-18"): v2025.ListToolsResult, + ("completion/complete", "2025-11-25"): v2025.CompleteResult, + ("initialize", "2025-11-25"): v2025.InitializeResult, + ("logging/setLevel", "2025-11-25"): v2025.EmptyResult, + ("ping", "2025-11-25"): v2025.EmptyResult, + ("prompts/get", "2025-11-25"): v2025.GetPromptResult, + ("prompts/list", "2025-11-25"): v2025.ListPromptsResult, + ("resources/list", "2025-11-25"): v2025.ListResourcesResult, + ("resources/read", "2025-11-25"): v2025.ReadResourceResult, + ("resources/subscribe", "2025-11-25"): v2025.EmptyResult, + ("resources/templates/list", "2025-11-25"): v2025.ListResourceTemplatesResult, + ("resources/unsubscribe", "2025-11-25"): v2025.EmptyResult, + ("tools/call", "2025-11-25"): v2025.CallToolResult, + ("tools/list", "2025-11-25"): v2025.ListToolsResult, + ("completion/complete", "2026-07-28"): v2026.CompleteResult, + ("prompts/get", "2026-07-28"): (v2026.GetPromptResult, v2026.InputRequiredResult), + ("prompts/list", "2026-07-28"): v2026.ListPromptsResult, + ("resources/list", "2026-07-28"): v2026.ListResourcesResult, + ("resources/read", "2026-07-28"): (v2026.ReadResourceResult, v2026.InputRequiredResult), + ("resources/templates/list", "2026-07-28"): v2026.ListResourceTemplatesResult, + ("server/discover", "2026-07-28"): v2026.DiscoverResult, + ("subscriptions/listen", "2026-07-28"): v2026.EmptyResult, + ("tools/call", "2026-07-28"): (v2026.CallToolResult, v2026.InputRequiredResult), + ("tools/list", "2026-07-28"): v2026.ListToolsResult, +} + +EXPECTED_CLIENT_RESULTS: dict[tuple[str, str], type[WireModel] | tuple[type[WireModel], ...]] = { + ("ping", "2024-11-05"): v2025.EmptyResult, + ("roots/list", "2024-11-05"): v2025.ListRootsResult, + ("sampling/createMessage", "2024-11-05"): v2025.CreateMessageResult, + ("ping", "2025-03-26"): v2025.EmptyResult, + ("roots/list", "2025-03-26"): v2025.ListRootsResult, + ("sampling/createMessage", "2025-03-26"): v2025.CreateMessageResult, + ("elicitation/create", "2025-06-18"): v2025.ElicitResult, + ("ping", "2025-06-18"): v2025.EmptyResult, + ("roots/list", "2025-06-18"): v2025.ListRootsResult, + ("sampling/createMessage", "2025-06-18"): v2025.CreateMessageResult, + ("elicitation/create", "2025-11-25"): v2025.ElicitResult, + ("ping", "2025-11-25"): v2025.EmptyResult, + ("roots/list", "2025-11-25"): v2025.ListRootsResult, + ("sampling/createMessage", "2025-11-25"): v2025.CreateMessageResult, +} + +EMPTY_SERVER_RESPONSE_METHODS = frozenset( + {"logging/setLevel", "ping", "resources/subscribe", "resources/unsubscribe", "subscriptions/listen"} +) +EMPTY_CLIENT_RESPONSE_METHODS = frozenset({"ping"}) + +# Pre-2026 versions share the 2025-11-25 surface package. +PACKAGE_BY_VERSION = { + "2024-11-05": "mcp.types.v2025_11_25", + "2025-03-26": "mcp.types.v2025_11_25", + "2025-06-18": "mcp.types.v2025_11_25", + "2025-11-25": "mcp.types.v2025_11_25", + "2026-07-28": "mcp.types.v2026_07_28", +} + +# The three reserved `params._meta` entries the 2026 surface requires on every request. +META_TRIPLE: dict[str, Any] = { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": {"name": "client", "version": "1.0"}, + "io.modelcontextprotocol/clientCapabilities": {}, +} + +# One minimal valid params mapping per surface request class. +REQUEST_PARAMS_FIXTURES: dict[type[WireModel], dict[str, Any] | None] = { + v2025.CallToolRequest: {"name": "echo"}, + v2025.CompleteRequest: {"ref": {"type": "ref/prompt", "name": "p"}, "argument": {"name": "a", "value": "v"}}, + v2025.CreateMessageRequest: { + "messages": [{"role": "user", "content": {"type": "text", "text": "hi"}}], + "maxTokens": 100, + }, + v2025.ElicitRequest: {"message": "m", "requestedSchema": {"type": "object", "properties": {}}}, + v2025.GetPromptRequest: {"name": "greeting"}, + v2025.InitializeRequest: { + "protocolVersion": "2025-11-25", + "capabilities": {}, + "clientInfo": {"name": "client", "version": "1.0"}, + }, + v2025.ListPromptsRequest: None, + v2025.ListResourcesRequest: None, + v2025.ListResourceTemplatesRequest: None, + v2025.ListRootsRequest: None, + v2025.ListToolsRequest: None, + v2025.PingRequest: None, + v2025.ReadResourceRequest: {"uri": "https://example.com/resource"}, + v2025.SetLevelRequest: {"level": "info"}, + v2025.SubscribeRequest: {"uri": "https://example.com/resource"}, + v2025.UnsubscribeRequest: {"uri": "https://example.com/resource"}, + v2026.CallToolRequest: {"_meta": META_TRIPLE, "name": "echo"}, + v2026.CompleteRequest: { + "_meta": META_TRIPLE, + "ref": {"type": "ref/prompt", "name": "p"}, + "argument": {"name": "a", "value": "v"}, + }, + v2026.DiscoverRequest: {"_meta": META_TRIPLE}, + v2026.GetPromptRequest: {"_meta": META_TRIPLE, "name": "greeting"}, + v2026.ListPromptsRequest: {"_meta": META_TRIPLE}, + v2026.ListResourcesRequest: {"_meta": META_TRIPLE}, + v2026.ListResourceTemplatesRequest: {"_meta": META_TRIPLE}, + v2026.ListToolsRequest: {"_meta": META_TRIPLE}, + v2026.ReadResourceRequest: {"_meta": META_TRIPLE, "uri": "https://example.com/resource"}, + v2026.SubscriptionsListenRequest: {"_meta": META_TRIPLE, "notifications": {}}, +} + +NOTIFICATION_PARAMS_FIXTURES: dict[type[WireModel], dict[str, Any] | None] = { + v2025.CancelledNotification: {"requestId": 1}, + v2025.ElicitationCompleteNotification: {"elicitationId": "e1"}, + v2025.InitializedNotification: None, + v2025.LoggingMessageNotification: {"level": "info", "data": "x"}, + v2025.ProgressNotification: {"progressToken": 1, "progress": 0.5}, + v2025.PromptListChangedNotification: None, + v2025.ResourceListChangedNotification: None, + v2025.ResourceUpdatedNotification: {"uri": "https://example.com/resource"}, + v2025.RootsListChangedNotification: None, + v2025.ToolListChangedNotification: None, + v2026.CancelledNotification: {"requestId": 1}, + v2026.ElicitationCompleteNotification: {"elicitationId": "e1"}, + v2026.LoggingMessageNotification: {"level": "info", "data": "x"}, + v2026.ProgressNotification: {"progressToken": 1, "progress": 0.5}, + v2026.PromptListChangedNotification: None, + v2026.ResourceListChangedNotification: None, + v2026.ResourceUpdatedNotification: {"uri": "https://example.com/resource"}, + v2026.SubscriptionsAcknowledgedNotification: {"notifications": {}}, + v2026.ToolListChangedNotification: None, +} + +# One minimal valid result body per response row value (class or union alias). +RESULT_BODY_FIXTURES: dict[type[WireModel] | UnionType, dict[str, Any]] = { + v2025.CallToolResult: {"content": []}, + v2025.CompleteResult: {"completion": {"values": []}}, + v2025.CreateMessageResult: {"role": "assistant", "content": {"type": "text", "text": "hi"}, "model": "m"}, + v2025.ElicitResult: {"action": "accept"}, + v2025.EmptyResult: {}, + v2025.GetPromptResult: {"messages": []}, + v2025.InitializeResult: { + "protocolVersion": "2025-11-25", + "capabilities": {}, + "serverInfo": {"name": "server", "version": "1.0"}, + }, + v2025.ListPromptsResult: {"prompts": []}, + v2025.ListResourcesResult: {"resources": []}, + v2025.ListResourceTemplatesResult: {"resourceTemplates": []}, + v2025.ListRootsResult: {"roots": [{"uri": "file:///workspace"}]}, + v2025.ListToolsResult: {"tools": []}, + v2025.ReadResourceResult: {"contents": []}, + v2026.AnyCallToolResult: {"content": [], "resultType": "complete"}, + v2026.AnyGetPromptResult: {"messages": [], "resultType": "complete"}, + v2026.AnyReadResourceResult: {"contents": [], "resultType": "complete", "ttlMs": 0, "cacheScope": "private"}, + v2026.CompleteResult: {"completion": {"values": []}, "resultType": "complete"}, + v2026.DiscoverResult: { + "supportedVersions": ["2026-07-28"], + "capabilities": {}, + "serverInfo": {"name": "server", "version": "1.0"}, + "resultType": "complete", + "ttlMs": 0, + "cacheScope": "private", + }, + v2026.EmptyResult: {"resultType": "complete"}, + v2026.ListPromptsResult: {"prompts": [], "resultType": "complete", "ttlMs": 0, "cacheScope": "private"}, + v2026.ListResourcesResult: {"resources": [], "resultType": "complete", "ttlMs": 0, "cacheScope": "private"}, + v2026.ListResourceTemplatesResult: { + "resourceTemplates": [], + "resultType": "complete", + "ttlMs": 0, + "cacheScope": "private", + }, + v2026.ListToolsResult: {"tools": [], "resultType": "complete", "ttlMs": 0, "cacheScope": "private"}, +} + + +def test_maps_define_exactly_the_expected_methods_for_every_known_version(): + # Derive the version axis from KNOWN_PROTOCOL_VERSIONS so a new version + # without map rows fails here rather than gating every method at runtime. + assert set(EXPECTED_METHODS) == set(KNOWN_PROTOCOL_VERSIONS) + surface_maps: dict[str, Mapping[tuple[str, str], object]] = { + "CLIENT_REQUESTS": methods.CLIENT_REQUESTS, + "CLIENT_NOTIFICATIONS": methods.CLIENT_NOTIFICATIONS, + "SERVER_REQUESTS": methods.SERVER_REQUESTS, + "SERVER_NOTIFICATIONS": methods.SERVER_NOTIFICATIONS, + } + for version in KNOWN_PROTOCOL_VERSIONS: + for map_name, surface_map in surface_maps.items(): + derived = {method for (method, row_version) in surface_map if row_version == version} + assert derived == EXPECTED_METHODS[version][map_name], f"{map_name} at {version}" + + +def test_response_map_keys_mirror_the_request_map_keys(): + assert set(methods.SERVER_RESULTS) == set(methods.CLIENT_REQUESTS) + assert set(methods.CLIENT_RESULTS) == set(methods.SERVER_REQUESTS) + + +def test_response_row_values_match_the_pinned_classes_and_unions(): + """Only the known empty-response methods may be valued by the bare `EmptyResult`.""" + assert set(EXPECTED_SERVER_RESULTS) == set(methods.SERVER_RESULTS) + assert set(EXPECTED_CLIENT_RESULTS) == set(methods.CLIENT_RESULTS) + pinned = [ + (methods.SERVER_RESULTS, EXPECTED_SERVER_RESULTS, EMPTY_SERVER_RESPONSE_METHODS), + (methods.CLIENT_RESULTS, EXPECTED_CLIENT_RESULTS, EMPTY_CLIENT_RESPONSE_METHODS), + ] + for response_map, expected_rows, empty_methods in pinned: + for (method, version), expected in expected_rows.items(): + actual = response_map[(method, version)] + if isinstance(expected, tuple): + assert get_args(actual) == expected, f"{method} at {version}" + else: + assert actual is expected, f"{method} at {version}" + if method not in empty_methods: + assert actual is not v2025.EmptyResult, f"{method} at {version}" + assert actual is not v2026.EmptyResult, f"{method} at {version}" + + +def test_surface_keys_agree_with_their_classes_and_the_monolith_maps(): + """Each surface key's method matches its class's method literal, its monolith row, and its version's package.""" + request_maps: list[Mapping[tuple[str, str], type[WireModel]]] = [ + methods.CLIENT_REQUESTS, + methods.SERVER_REQUESTS, + ] + notification_maps: list[Mapping[tuple[str, str], type[WireModel]]] = [ + methods.CLIENT_NOTIFICATIONS, + methods.SERVER_NOTIFICATIONS, + ] + for surface_maps, monolith_map in ( + (request_maps, methods.MONOLITH_REQUESTS), + (notification_maps, methods.MONOLITH_NOTIFICATIONS), + ): + for surface_map in surface_maps: + for (method, version), surface_type in surface_map.items(): + assert method in monolith_map, f"{method} has no monolith row" + assert get_args(surface_type.model_fields["method"].annotation) == (method,) + assert get_args(monolith_map[method].model_fields["method"].annotation) == (method,) + assert surface_type.__module__ == PACKAGE_BY_VERSION[version], f"{method} at {version}" + for response_map in (methods.SERVER_RESULTS, methods.CLIENT_RESULTS): + for (method, version), row in response_map.items(): + assert method in methods.MONOLITH_RESULTS, f"{method} has no monolith row" + for arm in get_args(row) or (row,): + assert arm.__module__ == PACKAGE_BY_VERSION[version], f"{method} at {version}" + + +def _assign_item(mapping: Any) -> None: + mapping["new-key"] = None + + +def test_built_in_maps_are_immutable(): + map_names = [ + "CLIENT_NOTIFICATIONS", + "CLIENT_REQUESTS", + "CLIENT_RESULTS", + "MONOLITH_NOTIFICATIONS", + "MONOLITH_REQUESTS", + "MONOLITH_RESULTS", + "SERVER_NOTIFICATIONS", + "SERVER_REQUESTS", + "SERVER_RESULTS", + ] + for map_name in map_names: + built_in = getattr(methods, map_name) + assert isinstance(built_in, MappingProxyType), map_name + with pytest.raises(TypeError): + _assign_item(built_in) + + +def test_minimal_request_bodies_parse_through_every_request_row(): + for (method, version), surface_type in methods.CLIENT_REQUESTS.items(): + parsed = methods.parse_client_request(method, version, REQUEST_PARAMS_FIXTURES[surface_type]) + assert isinstance(parsed, types.Request), f"{method} at {version}" + for (method, version), surface_type in methods.SERVER_REQUESTS.items(): + parsed = methods.parse_server_request(method, version, REQUEST_PARAMS_FIXTURES[surface_type]) + assert isinstance(parsed, types.Request), f"{method} at {version}" + + +def test_minimal_notification_bodies_parse_through_every_notification_row(): + for (method, version), surface_type in methods.CLIENT_NOTIFICATIONS.items(): + parsed = methods.parse_client_notification(method, version, NOTIFICATION_PARAMS_FIXTURES[surface_type]) + assert isinstance(parsed, types.Notification), f"{method} at {version}" + for (method, version), surface_type in methods.SERVER_NOTIFICATIONS.items(): + parsed = methods.parse_server_notification(method, version, NOTIFICATION_PARAMS_FIXTURES[surface_type]) + assert isinstance(parsed, types.Notification), f"{method} at {version}" + + +def test_minimal_result_bodies_parse_through_every_result_row(): + for (method, version), row in methods.SERVER_RESULTS.items(): + parsed = methods.parse_server_result(method, version, RESULT_BODY_FIXTURES[row]) + assert isinstance(parsed, types.Result), f"{method} at {version}" + for (method, version), row in methods.CLIENT_RESULTS.items(): + parsed = methods.parse_client_result(method, version, RESULT_BODY_FIXTURES[row]) + assert isinstance(parsed, types.Result), f"{method} at {version}" + + +def test_non_file_root_uri_passes_the_surface_step_and_rejects_at_the_monolith_step(): + """The monolith's `Root.uri` is file-scheme only; the surfaces declare a plain string.""" + non_file_roots = {"roots": [{"uri": "https://example.com/x"}]} + # Surface step admits the body, so the two-step parse fails at the monolith step. + pydantic.TypeAdapter(v2025.ListRootsResult).validate_python(non_file_roots) + with pytest.raises(pydantic.ValidationError): + methods.parse_client_result("roots/list", "2025-11-25", non_file_roots) + + # Same divergence on the 2026 path that embeds a roots response. + retry_params = {"_meta": META_TRIPLE, "name": "echo", "inputResponses": {"r1": non_file_roots}} + frame = {"jsonrpc": "2.0", "id": 0, "method": "tools/call", "params": retry_params} + v2026.CallToolRequest.model_validate(frame, by_name=False) + with pytest.raises(pydantic.ValidationError): + methods.parse_client_request("tools/call", "2026-07-28", retry_params) + + file_roots = {"roots": [{"uri": "file:///workspace"}]} + assert isinstance(methods.parse_client_result("roots/list", "2025-11-25", file_roots), types.ListRootsResult) + retried = methods.parse_client_request( + "tools/call", "2026-07-28", {"_meta": META_TRIPLE, "name": "echo", "inputResponses": {"r1": file_roots}} + ) + assert isinstance(retried, types.CallToolRequest) + + +def test_absent_map_keys_raise_key_error_for_every_gate_shape(): + """Key absence is the version gate; the session layer maps it to `METHOD_NOT_FOUND`.""" + gated = [ + ("resources/subscribe", "2026-07-28"), # removed at this version + ("server/discover", "2025-11-25"), # not yet at this version + ("tasks/get", "2025-11-25"), # never built-in + ("sampling/createMessage", "2025-11-25"), # wrong direction + ] + for method, version in gated: + with pytest.raises(KeyError): + methods.parse_client_request(method, version, None) + with pytest.raises(KeyError): + methods.parse_server_request("ping", "2026-07-28", None) + + +def test_unknown_version_strings_raise_value_error_on_every_parse_function(): + body_parsers = [ + methods.parse_client_request, + methods.parse_server_request, + methods.parse_client_notification, + methods.parse_server_notification, + ] + for body_parser in body_parsers: + with pytest.raises(ValueError) as excinfo: + body_parser("ping", "2099-01-01", None) + assert "2099-01-01" in str(excinfo.value) + result_parsers = [methods.parse_server_result, methods.parse_client_result] + for result_parser in result_parsers: + with pytest.raises(ValueError) as excinfo: + result_parser("ping", "2099-01-01", {}) + assert "2099-01-01" in str(excinfo.value) + + +def test_2026_07_28_requests_missing_a_reserved_meta_entry_reject_as_missing(): + for absent_key in META_TRIPLE: + partial_meta = {key: value for key, value in META_TRIPLE.items() if key != absent_key} + with pytest.raises(pydantic.ValidationError) as excinfo: + methods.parse_client_request("tools/list", "2026-07-28", {"_meta": partial_meta}) + assert [error["loc"] for error in excinfo.value.errors() if error["type"] == "missing"] == [ + ("params", "_meta", absent_key) + ] + + +def test_2026_07_28_results_require_result_type(): + with pytest.raises(pydantic.ValidationError): + methods.parse_server_result("tools/call", "2026-07-28", {"content": []}) + with pytest.raises(pydantic.ValidationError): + methods.parse_server_result("subscriptions/listen", "2026-07-28", {}) + + +def test_empty_result_body_parses_at_versions_that_define_it(): + parsed = methods.parse_server_result("ping", "2025-11-25", {}) + assert isinstance(parsed, types.EmptyResult) + + +def test_2026_07_28_shaped_result_extras_pass_at_earlier_versions(): + """The earlier surface ignores unknown keys; the monolith preserves them on fields it declares.""" + parsed = methods.parse_server_result( + "tools/list", "2025-11-25", {"tools": [], "resultType": "complete", "ttlMs": 5, "cacheScope": "public"} + ) + assert isinstance(parsed, types.ListToolsResult) + assert parsed.result_type == "complete" + assert parsed.ttl_ms == 5 + assert parsed.cache_scope == "public" + + +def test_embedded_input_request_entries_without_method_reject_at_the_surface_step(): + """The monolith's embedded request classes default `method`, so only the surface step rejects this.""" + body = {"resultType": "input_required", "inputRequests": {"r1": {"params": None}}} + monolith_row = methods.MONOLITH_RESULTS["tools/call"] + monolith_only: types.Result = pydantic.TypeAdapter[Any](monolith_row).validate_python(body) + assert isinstance(monolith_only, types.InputRequiredResult) + with pytest.raises(pydantic.ValidationError): + methods.parse_server_result("tools/call", "2026-07-28", body) + + +def test_none_params_omit_the_key_so_required_params_reject(): + with pytest.raises(pydantic.ValidationError) as excinfo: + methods.parse_client_request("tools/call", "2025-11-25", None) + assert [error["loc"] for error in excinfo.value.errors() if error["type"] == "missing"] == [("params",)] + assert isinstance(methods.parse_client_request("ping", "2025-11-25", None), types.PingRequest) + + +def test_snake_case_spellings_of_required_aliased_fields_reject_as_missing(): + """Wire parsing is alias-only (`by_name=False`), at both the surface and monolith steps.""" + snake_params = {"messages": [{"role": "user", "content": {"type": "text", "text": "hi"}}], "max_tokens": 100} + with pytest.raises(pydantic.ValidationError) as excinfo: + methods.parse_server_request("sampling/createMessage", "2025-11-25", snake_params) + assert [error["loc"] for error in excinfo.value.errors() if error["type"] == "missing"] == [("params", "maxTokens")] + with pytest.raises(pydantic.ValidationError): + types.CreateMessageRequest.model_validate( + {"method": "sampling/createMessage", "params": snake_params}, by_name=False + ) + + +def test_extension_map_rows_parse_through_the_same_functions(): + extended_surface = {**methods.CLIENT_REQUESTS, ("tasks/get", "2025-11-25"): v2025.GetTaskRequest} + extended_monolith = {**methods.MONOLITH_REQUESTS, "tasks/get": types.GetTaskRequest} + parsed = methods.parse_client_request( + "tasks/get", "2025-11-25", {"taskId": "t1"}, surface=extended_surface, monolith=extended_monolith + ) + assert isinstance(parsed, types.GetTaskRequest) + assert parsed.params.task_id == "t1" + + +def test_inconsistent_extension_maps_raise_runtime_error_after_the_surface_hit(): + """Must not raise `KeyError`: the session layer treats that as the version gate.""" + extended_surface = {**methods.CLIENT_REQUESTS, ("tasks/get", "2025-11-25"): v2025.GetTaskRequest} + with pytest.raises(RuntimeError, match="inconsistent extension maps"): + methods.parse_client_request("tasks/get", "2025-11-25", {"taskId": "t1"}, surface=extended_surface) + + +def test_input_required_unions_discriminate_identically_in_both_arm_orders(): + complete_bodies: dict[str, dict[str, Any]] = { + "tools/call": {"content": []}, + "prompts/get": {"messages": []}, + "resources/read": {"contents": []}, + } + shared_bodies: list[dict[str, Any]] = [ + {"resultType": "input_required", "inputRequests": {"r1": {"method": "roots/list"}}}, + {"resultType": "input_required", "requestState": "blob"}, + ] + for method, complete_body in complete_bodies.items(): + row = methods.MONOLITH_RESULTS[method] + complete_arm, input_required_arm = get_args(row) + assert input_required_arm is types.InputRequiredResult + bodies: list[dict[str, Any]] = [ + complete_body, + {**complete_body, "resultType": "complete"}, + *shared_bodies, + {**complete_body, "resultType": "task"}, # open tag is preserved + {**complete_body, "resultType": "input_required"}, # complete shape plus the tag + ] + for body in bodies: + forward = pydantic.TypeAdapter[Any](complete_arm | input_required_arm).validate_python(body) + reversed_order = pydantic.TypeAdapter[Any](input_required_arm | complete_arm).validate_python(body) + assert type(forward) is type(reversed_order), f"{method}: {body}" + assert forward.result_type == reversed_order.result_type + through_row = pydantic.TypeAdapter[Any](row).validate_python(complete_body) + assert isinstance(through_row, complete_arm) + open_tagged = pydantic.TypeAdapter[Any](row).validate_python({**complete_body, "resultType": "task"}) + assert open_tagged.result_type == "task" + + +def test_sampling_union_keeps_the_complete_arm_first_because_order_is_load_bearing(): + """A single-block body satisfies both arms; smart-union ties resolve leftmost.""" + assert get_args(methods.MONOLITH_RESULTS["sampling/createMessage"]) == ( + types.CreateMessageResult, + types.CreateMessageResultWithTools, + ) + single_block: dict[str, Any] = {"role": "assistant", "content": {"type": "text", "text": "hi"}, "model": "m"} + through_row = methods.parse_client_result("sampling/createMessage", "2025-11-25", single_block) + assert type(through_row) is types.CreateMessageResult + reversed_union = pydantic.TypeAdapter[Any](types.CreateMessageResultWithTools | types.CreateMessageResult) + assert type(reversed_union.validate_python(single_block)) is types.CreateMessageResultWithTools + + array_body: dict[str, Any] = {"role": "assistant", "content": [{"type": "text", "text": "hi"}], "model": "m"} + tool_use_body: dict[str, Any] = { + "role": "assistant", + "content": {"type": "tool_use", "name": "t", "id": "c1", "input": {}}, + "model": "m", + } + for body in (array_body, tool_use_body): + parsed = methods.parse_client_result("sampling/createMessage", "2025-11-25", body) + assert type(parsed) is types.CreateMessageResultWithTools + + +def test_importing_the_module_builds_no_adapters_and_identical_rows_share_one(): + # Execute a fresh copy so the cache assertion is order-independent. + spec = importlib.util.find_spec("mcp.types.methods") + assert spec is not None and spec.loader is not None + fresh = importlib.util.module_from_spec(spec) + spec.loader.exec_module(fresh) + assert fresh._adapter.cache_info().currsize == 0 + fresh.parse_server_result("ping", "2025-11-25", {}) + assert fresh._adapter.cache_info().currsize == 2 + # Identical row values at another version: no new adapters. + fresh.parse_server_result("ping", "2024-11-05", {}) + assert fresh._adapter.cache_info().currsize == 2 diff --git a/tests/types/test_wire_frames.py b/tests/types/test_wire_frames.py new file mode 100644 index 000000000..4badda6c3 --- /dev/null +++ b/tests/types/test_wire_frames.py @@ -0,0 +1,87 @@ +"""Snapshot pins for outbound JSON-RPC frames; a diff is a wire-visible change needing a deliberate decision.""" + +from typing import Any + +from inline_snapshot import snapshot +from pydantic import BaseModel + +from mcp.types import ( + METHOD_NOT_FOUND, + CallToolRequest, + CallToolRequestParams, + CallToolResult, + EmptyResult, + ErrorData, + InputRequiredResult, + JSONRPCError, + JSONRPCNotification, + JSONRPCRequest, + JSONRPCResponse, + ListRootsRequest, + ListToolsResult, + ProgressNotification, + ProgressNotificationParams, + TextContent, + Tool, +) + + +def _body(model: BaseModel) -> dict[str, Any]: + """Mirror the session layer's outbound payload dump.""" + return model.model_dump(by_alias=True, mode="json", exclude_none=True) + + +def _frame(envelope: BaseModel) -> str: + """Mirror the transports' frame serialization.""" + return envelope.model_dump_json(by_alias=True, exclude_unset=True) + + +def test_request_frame_carries_the_envelope_and_the_dumped_request_body(): + request = CallToolRequest(params=CallToolRequestParams(name="echo", arguments={"text": "hi"})) + frame = JSONRPCRequest(jsonrpc="2.0", id=1, **_body(request)) + assert _frame(frame) == snapshot( + '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}' + ) + + +def test_notification_frame_has_no_id_and_carries_the_dumped_params(): + notification = ProgressNotification(params=ProgressNotificationParams(progress_token="t1", progress=0.5)) + frame = JSONRPCNotification(jsonrpc="2.0", **_body(notification)) + assert _frame(frame) == snapshot( + '{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"t1","progress":0.5}}' + ) + + +def test_non_empty_result_frame_always_dumps_result_type_complete(): + result = CallToolResult(content=[TextContent(text="ok")]) + frame = JSONRPCResponse(jsonrpc="2.0", id=1, result=_body(result)) + assert _frame(frame) == snapshot( + '{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"ok"}],"isError":false,"resultType":"complete"}}' + ) + + +def test_cacheable_list_result_frame_always_dumps_its_caching_directives(): + result = ListToolsResult(tools=[Tool(name="echo", input_schema={"type": "object"})]) + frame = JSONRPCResponse(jsonrpc="2.0", id=2, result=_body(result)) + assert _frame(frame) == snapshot( + '{"jsonrpc":"2.0","id":2,"result":{"ttlMs":0,"cacheScope":"private","tools":[{"name":"echo","inputSchema":{"type":"object"}}],"resultType":"complete"}}' + ) + + +def test_empty_result_frame_dumps_an_empty_result_object(): + """Deployed peers reject extra keys on empty results, so the SDK omits resultType here.""" + frame = JSONRPCResponse(jsonrpc="2.0", id=3, result=_body(EmptyResult())) + assert _frame(frame) == snapshot('{"jsonrpc":"2.0","id":3,"result":{}}') + + +def test_input_required_result_frame_carries_the_tag_and_the_embedded_requests(): + result = InputRequiredResult(input_requests={"r1": ListRootsRequest()}, request_state="s1") + frame = JSONRPCResponse(jsonrpc="2.0", id=4, result=_body(result)) + assert _frame(frame) == snapshot( + '{"jsonrpc":"2.0","id":4,"result":{"resultType":"input_required","inputRequests":{"r1":{"method":"roots/list"}},"requestState":"s1"}}' + ) + + +def test_error_frame_wraps_error_data_in_the_jsonrpc_envelope(): + frame = JSONRPCError(jsonrpc="2.0", id=5, error=ErrorData(code=METHOD_NOT_FOUND, message="Method not found")) + assert _frame(frame) == snapshot('{"jsonrpc":"2.0","id":5,"error":{"code":-32601,"message":"Method not found"}}')