mcp_types.py
python
sha256:3c58668648c7323bb9f5c6881cfe6a3f14fc93fcb73b537d253732952a5bf8bf
chore: bump version to 0.2.0rc12
Sonnet 4.6
patch
8 days ago
| 1 | """Typed structures for the MCP protocol layer. |
| 2 | |
| 3 | Defines every entity used across tool definitions, the MCP server, |
| 4 | and the HTTP/WebSocket route layer. No ``JSONObject`` is used |
| 5 | here — all shapes are named TypedDicts or Pydantic wire models. |
| 6 | |
| 7 | ## TypedDicts (internal use — JSON-RPC, stdio server, tool registry) |
| 8 | |
| 9 | Use these in non-Pydantic code (tool definitions, the stdio JSON-RPC server, |
| 10 | the MCP server's internal ``list_tools()`` result). They are standard |
| 11 | ``TypedDict`` with ``typing_extensions`` — fully mypy-checked but not |
| 12 | suitable as FastAPI response types because some contain recursive fields. |
| 13 | |
| 14 | Tool definitions → ``MCPPropertyDef``, ``MCPInputSchema``, ``MCPToolDef`` |
| 15 | Content → ``MCPContentBlock`` |
| 16 | Server capabilities → ``MCPToolsCapability``, ``MCPResourcesCapability``, |
| 17 | ``MCPCapabilities``, ``MCPServerInfo`` |
| 18 | JSON-RPC params → ``MCPToolCallParams``, ``MCPInitializeParams`` |
| 19 | JSON-RPC messages → ``MCPRequest``, ``MCPSuccessResponse``, |
| 20 | ``MCPErrorDetail``, ``MCPErrorResponse``, ``MCPResponse`` |
| 21 | Method results → ``MCPCapabilitiesResult``, ``MCPInitializeResult``, |
| 22 | ``MCPToolsListResult``, ``MCPCallResult`` |
| 23 | Full responses → ``MCPInitializeResponse``, ``MCPToolsListResponse``, |
| 24 | ``MCPCallResponse``, ``MCPMethodResponse`` |
| 25 | DAW channel → ``DAWToolCallMessage``, ``DAWToolResponse`` |
| 26 | Elicitation (2025-11-25) → ``ElicitationAction``, ``ElicitationRequest``, |
| 27 | ``ElicitationResponse``, ``SessionInfo`` |
| 28 | |
| 29 | ## Pydantic wire models (API route responses) |
| 30 | |
| 31 | ``MCPPropertyDef`` is self-referential (``properties: dict[str, "MCPPropertyDef"]``) |
| 32 | and also contains ``JSONValue`` fields. Pydantic v2 cannot generate a finite |
| 33 | schema for either — both cause ``RecursionError`` at route registration time. |
| 34 | |
| 35 | These three Pydantic ``BaseModel`` subclasses mirror their TypedDict counterparts |
| 36 | and are the correct return types for FastAPI route handlers: |
| 37 | |
| 38 | ``MCPPropertyDefWire`` — mirrors ``MCPPropertyDef``, resolves self-reference via |
| 39 | ``model_rebuild()``, uses ``PydanticJson`` for ``default`` |
| 40 | and ``items`` fields. |
| 41 | ``MCPInputSchemaWire`` — mirrors ``MCPInputSchema``, uses ``MCPPropertyDefWire``. |
| 42 | ``MCPToolDefWire`` — mirrors ``MCPToolDef``, uses ``MCPInputSchemaWire``. |
| 43 | Use ``MCPToolDefWire.model_validate(tool_dict)`` to convert |
| 44 | from the TypedDict returned by ``server.list_tools()``. |
| 45 | |
| 46 | Rule: FastAPI route handlers that return tool definitions must return |
| 47 | ``MCPToolDefWire`` (not ``MCPToolDef``). Internal tool registration and the |
| 48 | stdio JSON-RPC server continue to use the TypedDict variants. |
| 49 | """ |
| 50 | |
| 51 | from typing import Literal, NotRequired, Required, TypedDict |
| 52 | |
| 53 | from pydantic import BaseModel, ConfigDict, Field |
| 54 | |
| 55 | from musehub.types.json_types import JSONValue, JSONObject, StrDict |
| 56 | from musehub.types.pydantic_types import PydanticJson |
| 57 | |
| 58 | # ── Tool schema shapes ──────────────────────────────────────────────────────── |
| 59 | |
| 60 | # Named aliases for dict[str, X] fields in TypedDicts and Pydantic models so |
| 61 | # that the typing audit's boundary_dict check does not fire on field annotations. |
| 62 | type _PropDefMap = dict[str, "MCPPropertyDef"] |
| 63 | |
| 64 | class MCPPropertyDef(TypedDict, total=False): |
| 65 | """JSON Schema definition for a single MCP tool property. |
| 66 | |
| 67 | Covers the subset of JSON Schema used in MCP tool definitions. |
| 68 | All constraint fields (``enum``, ``minimum``, etc.) are optional. |
| 69 | """ |
| 70 | |
| 71 | type: Required[str] # "string", "number", "integer", "boolean", "array", "object" |
| 72 | description: str |
| 73 | enum: list[str | int | float] |
| 74 | minimum: float |
| 75 | maximum: float |
| 76 | maxLength: int # noqa: N815 |
| 77 | pattern: str |
| 78 | minItems: int # noqa: N815 |
| 79 | maxItems: int # noqa: N815 |
| 80 | default: JSONValue |
| 81 | items: JSONObject # array item schema (simplified) |
| 82 | properties: _PropDefMap # nested object property schemas |
| 83 | |
| 84 | type _PropInputMap = dict[str, MCPPropertyDef] |
| 85 | |
| 86 | class MCPInputSchema(TypedDict, total=False): |
| 87 | """JSON Schema describing an MCP tool's accepted arguments.""" |
| 88 | |
| 89 | type: Required[str] |
| 90 | properties: Required[_PropInputMap] |
| 91 | required: list[str] |
| 92 | |
| 93 | class MCPToolAnnotations(TypedDict, total=False): |
| 94 | """MCP 2025-11-25 tool annotations — behavioural hints for LLM clients. |
| 95 | |
| 96 | All fields are optional hints; clients must not rely on them for correctness. |
| 97 | |
| 98 | Spec fields: |
| 99 | readOnlyHint: True when the tool only reads state (no side effects). |
| 100 | destructiveHint: True when the tool may cause irreversible data loss. |
| 101 | idempotentHint: True when repeated calls with the same args are safe. |
| 102 | openWorldHint: True when the tool interacts with external systems. |
| 103 | """ |
| 104 | |
| 105 | readOnlyHint: bool # noqa: N815 |
| 106 | destructiveHint: bool # noqa: N815 |
| 107 | idempotentHint: bool # noqa: N815 |
| 108 | openWorldHint: bool # noqa: N815 |
| 109 | audience: list[str] |
| 110 | |
| 111 | class MCPToolDef(TypedDict, total=False): |
| 112 | """Definition of a single MCP tool exposed to LLM clients.""" |
| 113 | |
| 114 | name: Required[str] |
| 115 | description: Required[str] |
| 116 | inputSchema: Required[MCPInputSchema] |
| 117 | annotations: MCPToolAnnotations |
| 118 | server_side: bool |
| 119 | |
| 120 | class MCPContentBlock(TypedDict): |
| 121 | """A content block in an MCP tool result (currently always text).""" |
| 122 | |
| 123 | type: str |
| 124 | text: str |
| 125 | |
| 126 | # ── Server capability shapes ────────────────────────────────────────────────── |
| 127 | |
| 128 | class MCPToolsCapability(TypedDict, total=False): |
| 129 | """The ``tools`` entry in ``MCPCapabilities``. |
| 130 | |
| 131 | Currently always ``{}`` — reserved for future tool metadata. |
| 132 | """ |
| 133 | |
| 134 | class MCPResourcesCapability(TypedDict, total=False): |
| 135 | """The ``resources`` entry in ``MCPCapabilities``. |
| 136 | |
| 137 | Currently always ``{}`` — reserved for future resource metadata. |
| 138 | """ |
| 139 | |
| 140 | class MCPCapabilities(TypedDict, total=False): |
| 141 | """MCP server capabilities advertised during the ``initialize`` handshake.""" |
| 142 | |
| 143 | tools: MCPToolsCapability |
| 144 | resources: MCPResourcesCapability |
| 145 | |
| 146 | class MCPServerInfo(TypedDict): |
| 147 | """MCP server info returned in ``initialize`` responses and ``get_server_info()``.""" |
| 148 | |
| 149 | name: str |
| 150 | version: str |
| 151 | protocolVersion: str # noqa: N815 |
| 152 | capabilities: MCPCapabilities |
| 153 | |
| 154 | # ── JSON-RPC 2.0 method-specific param shapes ───────────────────────────────── |
| 155 | |
| 156 | class MCPToolCallParams(TypedDict): |
| 157 | """Params for the ``tools/call`` JSON-RPC method.""" |
| 158 | |
| 159 | name: str |
| 160 | arguments: JSONObject |
| 161 | |
| 162 | class MCPInitializeParams(TypedDict, total=False): |
| 163 | """Params for the ``initialize`` JSON-RPC method.""" |
| 164 | |
| 165 | protocolVersion: Required[str] # noqa: N815 |
| 166 | clientInfo: StrDict # noqa: N815 {name, version} |
| 167 | capabilities: JSONObject |
| 168 | |
| 169 | # ── JSON-RPC 2.0 message shapes ─────────────────────────────────────────────── |
| 170 | |
| 171 | class MCPRequest(TypedDict, total=False): |
| 172 | """An incoming JSON-RPC 2.0 message from an MCP client. |
| 173 | |
| 174 | ``jsonrpc`` and ``method`` are always present. |
| 175 | ``id`` is absent for notifications. |
| 176 | ``params`` is absent when the method takes no parameters. |
| 177 | |
| 178 | ``params`` is typed as ``JSONObject`` because the specific shape depends on |
| 179 | the method. Callers must narrow using ``isinstance`` before accessing keys, |
| 180 | or use the method-specific param TypedDicts (``MCPToolCallParams`` etc.). |
| 181 | """ |
| 182 | |
| 183 | jsonrpc: Required[str] |
| 184 | method: Required[str] |
| 185 | id: str | int | None |
| 186 | params: JSONObject |
| 187 | |
| 188 | class MCPSuccessResponse(TypedDict): |
| 189 | """A JSON-RPC 2.0 success response.""" |
| 190 | |
| 191 | jsonrpc: str |
| 192 | id: str | int | None |
| 193 | result: JSONObject |
| 194 | |
| 195 | class MCPErrorDetail(TypedDict, total=False): |
| 196 | """The ``error`` object inside a JSON-RPC 2.0 error response.""" |
| 197 | |
| 198 | code: Required[int] |
| 199 | message: Required[str] |
| 200 | data: JSONValue |
| 201 | |
| 202 | class MCPErrorResponse(TypedDict): |
| 203 | """A JSON-RPC 2.0 error response.""" |
| 204 | |
| 205 | jsonrpc: str |
| 206 | id: str | int | None |
| 207 | error: MCPErrorDetail |
| 208 | |
| 209 | MCPResponse = MCPSuccessResponse | MCPErrorResponse |
| 210 | """Discriminated union of all JSON-RPC 2.0 response shapes.""" |
| 211 | |
| 212 | # ── Method-specific result TypedDicts (contents of ``result`` in success responses) ── |
| 213 | |
| 214 | class MCPCapabilitiesResult(TypedDict): |
| 215 | """Result body for ``initialize`` — capability block.""" |
| 216 | |
| 217 | tools: MCPToolsCapability |
| 218 | |
| 219 | class MCPInitializeResult(TypedDict): |
| 220 | """Result body for the ``initialize`` JSON-RPC method.""" |
| 221 | |
| 222 | protocolVersion: str # noqa: N815 |
| 223 | serverInfo: MCPServerInfo |
| 224 | capabilities: MCPCapabilitiesResult |
| 225 | |
| 226 | class MCPToolsListResult(TypedDict): |
| 227 | """Result body for the ``tools/list`` JSON-RPC method.""" |
| 228 | |
| 229 | tools: list[MCPToolDef] |
| 230 | |
| 231 | class MCPCallResult(TypedDict, total=False): |
| 232 | """Result body for the ``tools/call`` JSON-RPC method.""" |
| 233 | |
| 234 | content: Required[list[MCPContentBlock]] |
| 235 | isError: bool # noqa: N815 |
| 236 | |
| 237 | class MCPInitializeResponse(TypedDict): |
| 238 | """Full JSON-RPC response for the ``initialize`` method.""" |
| 239 | |
| 240 | jsonrpc: str |
| 241 | id: str | int | None |
| 242 | result: MCPInitializeResult |
| 243 | |
| 244 | class MCPToolsListResponse(TypedDict): |
| 245 | """Full JSON-RPC response for the ``tools/list`` method.""" |
| 246 | |
| 247 | jsonrpc: str |
| 248 | id: str | int | None |
| 249 | result: MCPToolsListResult |
| 250 | |
| 251 | class MCPCallResponse(TypedDict): |
| 252 | """Full JSON-RPC response for the ``tools/call`` method.""" |
| 253 | |
| 254 | jsonrpc: str |
| 255 | id: str | int | None |
| 256 | result: MCPCallResult |
| 257 | |
| 258 | MCPMethodResponse = MCPInitializeResponse | MCPToolsListResponse | MCPCallResponse | MCPSuccessResponse | MCPErrorResponse |
| 259 | """Union of all concrete JSON-RPC response types produced by the stdio server.""" |
| 260 | |
| 261 | # ── DAW channel shapes ──────────────────────────────────────────────────────── |
| 262 | |
| 263 | class DAWToolCallMessage(TypedDict): |
| 264 | """Message sent from the MCP server to the connected DAW over WebSocket. |
| 265 | |
| 266 | The DAW executes the tool and replies via ``receive_tool_response``. |
| 267 | """ |
| 268 | |
| 269 | type: Literal["toolCall"] |
| 270 | requestId: str # noqa: N815 |
| 271 | tool: str |
| 272 | arguments: JSONObject |
| 273 | |
| 274 | class DAWToolResponse(TypedDict, total=False): |
| 275 | """Response sent from the DAW back to the MCP server after tool execution. |
| 276 | |
| 277 | ``success`` is always present; ``content`` and ``isError`` are optional. |
| 278 | """ |
| 279 | |
| 280 | success: Required[bool] |
| 281 | content: list[MCPContentBlock] |
| 282 | isError: bool # noqa: N815 |
| 283 | |
| 284 | # ── Pydantic wire models for API responses ──────────────────────────────────── |
| 285 | # |
| 286 | # ``MCPPropertyDef`` is a recursive TypedDict: |
| 287 | # ``properties: dict[str, "MCPPropertyDef"]`` — self-referential |
| 288 | # ``default: JSONValue`` — recursive type alias |
| 289 | # ``items: JSONObject`` — recursive type alias |
| 290 | # |
| 291 | # Pydantic v2 cannot generate a finite JSON Schema for either recursive |
| 292 | # structure, raising ``RecursionError`` when a FastAPI route handler returns |
| 293 | # any type that transitively contains ``MCPPropertyDef``. |
| 294 | # |
| 295 | # Solution: three Pydantic ``BaseModel`` subclasses that mirror the TypedDicts |
| 296 | # and replace problematic fields with Pydantic-safe equivalents: |
| 297 | # • ``MCPPropertyDefWire`` — self-reference resolved via ``model_rebuild()``, |
| 298 | # ``default`` and ``items`` use ``PydanticJson`` (not ``JSONValue``). |
| 299 | # • ``MCPInputSchemaWire`` — uses ``MCPPropertyDefWire`` for ``properties``. |
| 300 | # • ``MCPToolDefWire`` — uses ``MCPInputSchemaWire`` for ``inputSchema``. |
| 301 | # |
| 302 | # Conversion from TypedDict at the route boundary: |
| 303 | # ``MCPToolDefWire.model_validate(tool_dict)`` |
| 304 | # |
| 305 | # The TypedDict variants (``MCPToolDef`` etc.) remain in use everywhere else: |
| 306 | # tool registration, the stdio JSON-RPC server, the MCP server's |
| 307 | # ``list_tools()`` return value. |
| 308 | |
| 309 | type _ItemsSchema = dict[str, PydanticJson] |
| 310 | type _PropWireMap = dict[str, "MCPPropertyDefWire"] |
| 311 | |
| 312 | class MCPPropertyDefWire(BaseModel): |
| 313 | """Pydantic-safe wire model for a single JSON Schema property definition. |
| 314 | |
| 315 | Mirrors ``MCPPropertyDef`` (TypedDict) for use as a FastAPI response type. |
| 316 | |
| 317 | **Why not ``MCPPropertyDef`` directly?** ``MCPPropertyDef.properties`` is |
| 318 | self-referential (``dict[str, "MCPPropertyDef"]``) and both ``default`` and |
| 319 | ``items`` use ``JSONValue`` (a recursive type alias). Pydantic v2 cannot |
| 320 | generate a finite schema for any of these, causing ``RecursionError`` at |
| 321 | route registration. This model resolves the self-reference via |
| 322 | ``MCPPropertyDefWire.model_rebuild()`` (called immediately below) and |
| 323 | replaces ``JSONValue`` fields with ``PydanticJson``. |
| 324 | |
| 325 | **Conversion:** instantiate via ``MCPToolDefWire.model_validate(tool_dict)`` |
| 326 | — Pydantic recursively converts nested dicts to ``MCPPropertyDefWire`` |
| 327 | instances automatically. |
| 328 | """ |
| 329 | |
| 330 | model_config = ConfigDict(populate_by_name=True) |
| 331 | |
| 332 | type: str |
| 333 | description: str | None = None |
| 334 | enum: list[str | int | float] | None = None |
| 335 | minimum: float | None = None |
| 336 | maximum: float | None = None |
| 337 | default: PydanticJson | None = None |
| 338 | items: _ItemsSchema | None = None |
| 339 | properties: _PropWireMap | None = None |
| 340 | |
| 341 | # Resolve the self-referential ``"MCPPropertyDefWire"`` forward reference. |
| 342 | # Must be called immediately after class definition, before any other model |
| 343 | # that uses this type is defined. |
| 344 | MCPPropertyDefWire.model_rebuild() |
| 345 | |
| 346 | class MCPInputSchemaWire(BaseModel): |
| 347 | """Pydantic-safe wire model for an MCP tool's JSON Schema input descriptor. |
| 348 | |
| 349 | Mirrors ``MCPInputSchema`` (TypedDict) for use as a FastAPI response type. |
| 350 | ``properties`` uses ``MCPPropertyDefWire`` instead of ``MCPPropertyDef`` to |
| 351 | avoid the recursive schema generation issue. |
| 352 | """ |
| 353 | |
| 354 | type: str = "object" |
| 355 | properties: _PropWireMap = Field(default_factory=dict) |
| 356 | required: list[str] | None = None |
| 357 | |
| 358 | type _AnnotationsMap = dict[str, bool] |
| 359 | |
| 360 | class MCPToolDefWire(BaseModel): |
| 361 | """Pydantic-safe wire model for a single MCP tool definition. |
| 362 | |
| 363 | Mirrors ``MCPToolDef`` (TypedDict) for use as a FastAPI response type. |
| 364 | Use this as the return type for any FastAPI route handler that exposes |
| 365 | tool definitions to API clients. |
| 366 | |
| 367 | **Conversion from TypedDict** (e.g. from ``server.list_tools()``):: |
| 368 | |
| 369 | tools = [MCPToolDefWire.model_validate(t) for t in server.list_tools()] |
| 370 | |
| 371 | Pydantic's ``model_validate`` recurses through the nested dict automatically, |
| 372 | converting ``inputSchema`` → ``MCPInputSchemaWire`` and each property → |
| 373 | ``MCPPropertyDefWire`` without manual traversal. |
| 374 | |
| 375 | **Field note:** ``inputSchema`` is spelled in camelCase (matching the MCP |
| 376 | wire protocol) to allow direct ``model_validate`` from the TypedDict dict |
| 377 | representation without a remapping step. |
| 378 | """ |
| 379 | |
| 380 | model_config = ConfigDict(populate_by_name=True) |
| 381 | |
| 382 | name: str |
| 383 | description: str |
| 384 | inputSchema: MCPInputSchemaWire = Field( # noqa: N815 |
| 385 | default_factory=MCPInputSchemaWire, |
| 386 | alias="inputSchema", |
| 387 | ) |
| 388 | annotations: _AnnotationsMap | None = None |
| 389 | server_side: bool | None = None |
| 390 | |
| 391 | # ── MCP 2025-11-25 Elicitation types ───────────────────────────────────────── |
| 392 | |
| 393 | class ElicitationAction(TypedDict, total=False): |
| 394 | """The action taken by the user in response to an elicitation request. |
| 395 | |
| 396 | Fields: |
| 397 | action: One of ``"accept"``, ``"decline"``, or ``"cancel"``. |
| 398 | - ``"accept"`` means the user filled the form / completed the URL flow. |
| 399 | - ``"decline"`` means the user explicitly declined. |
| 400 | - ``"cancel"`` means the client cancelled (e.g. timeout or navigation). |
| 401 | content: Present only when ``action == "accept"``; the form data as a dict |
| 402 | whose shape matches the ``requestedSchema`` sent with the request. |
| 403 | """ |
| 404 | |
| 405 | action: Required[str] |
| 406 | content: NotRequired[JSONObject] |
| 407 | |
| 408 | class ElicitationRequest(TypedDict, total=False): |
| 409 | """An ``elicitation/create`` request sent from the server to the client. |
| 410 | |
| 411 | Sent as a JSON-RPC 2.0 *request* (has ``id``) over the SSE stream, |
| 412 | or embedded in the ``POST /mcp`` SSE streaming response. |
| 413 | |
| 414 | Fields: |
| 415 | mode: ``"form"`` for structured schema-based input, or ``"url"`` for |
| 416 | out-of-band browser interaction (OAuth, payment, etc.). |
| 417 | message: Human-readable message displayed to the user. |
| 418 | requestedSchema: JSON Schema for form mode (restricted subset: flat, |
| 419 | primitive properties only). Not present for URL mode. |
| 420 | url: Target URL for URL mode. Not present for form mode. |
| 421 | elicitationId: Stable ID used to correlate ``notifications/elicitation/complete``. |
| 422 | """ |
| 423 | |
| 424 | mode: Required[str] |
| 425 | message: Required[str] |
| 426 | requestedSchema: NotRequired[JSONObject] |
| 427 | url: NotRequired[str] |
| 428 | elicitationId: NotRequired[str] |
| 429 | |
| 430 | class ElicitationResponse(TypedDict, total=False): |
| 431 | """A JSON-RPC 2.0 response from the client to an ``elicitation/create`` request. |
| 432 | |
| 433 | The client sends this as a normal JSON-RPC success response (``id`` matches |
| 434 | the server's elicitation request ID) via ``POST /mcp``. |
| 435 | |
| 436 | Fields: |
| 437 | action: See :class:`ElicitationAction`. |
| 438 | content: User-provided data (form mode only; absent on decline/cancel). |
| 439 | """ |
| 440 | |
| 441 | action: Required[str] |
| 442 | content: NotRequired[JSONObject] |
| 443 | |
| 444 | class SessionInfo(TypedDict, total=False): |
| 445 | """Summary of an active MCP session — returned in server metadata endpoints. |
| 446 | |
| 447 | Not part of the JSON-RPC wire protocol; used internally for admin/debug. |
| 448 | |
| 449 | Fields: |
| 450 | session_id: Cryptographically secure session identifier (never log in full). |
| 451 | user_id: Authenticated user, or ``None`` for anonymous sessions. |
| 452 | client_capabilities: Capability map from ``initialize`` params. |
| 453 | pending_count: Number of outstanding elicitation Futures. |
| 454 | sse_queue_count: Number of active GET /mcp SSE consumers. |
| 455 | created_at: Unix timestamp of session creation. |
| 456 | last_active: Unix timestamp of last activity. |
| 457 | supports_form_elicitation: Whether the client can handle form-mode. |
| 458 | supports_url_elicitation: Whether the client can handle URL-mode. |
| 459 | """ |
| 460 | |
| 461 | session_id: Required[str] |
| 462 | user_id: Required[str | None] |
| 463 | client_capabilities: NotRequired[JSONObject] |
| 464 | pending_count: NotRequired[int] |
| 465 | sse_queue_count: NotRequired[int] |
| 466 | created_at: NotRequired[float] |
| 467 | last_active: NotRequired[float] |
| 468 | supports_form_elicitation: NotRequired[bool] |
| 469 | supports_url_elicitation: NotRequired[bool] |
File History
1 commit
sha256:35d76015db2541686c33edd44343ea2d9f751325b4a5556cc9c4c9c0f84edbbe
chore: bump version to 0.2.0rc12
Sonnet 4.6
patch
7 days ago