gabriel / musehub public
mcp_types.py python
469 lines 18.3 KB
Raw
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