Skip to content

Commit b8bc17d

Browse files
committed
Defer building the generated wire models and emit them in dependency order
The generated _v2025_11_25 and _v2026_07_28 packages spent ~100-115 ms each at import building pydantic core schemas, validators and serializers for ~175 models per version, of which a connection touches only a handful. Both generated bases now carry defer_build=True: WireModel, and a new generic WireRootModel[T] the RootModel aliases derive from (deferring only the object models made things slower, as the eager union roll-ups then regenerated every deferred model's schema inline). The generator drops codegen's trailing model_rebuild() epilogue and emits classes in dependency order - failing if any class names a class defined later - so every non-recursive generated class still exposes complete field metadata at import. Building each model's validator now happens once, on first use.
1 parent 695b1ab commit b8bc17d

6 files changed

Lines changed: 549 additions & 366 deletions

File tree

‎pyproject.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ max-complexity = 24 # Default is 10
221221
"src/mcp/types/*.py" = ["F403"]
222222
# Generated by scripts/gen_surface_types.py: raw datamodel-codegen output (TID251 lifts the repo-wide RootModel ban for these generated validators).
223223
"src/mcp-types/mcp_types/_v*/__init__.py" = ["D212", "E501", "I001", "TID251", "UP007", "UP037"]
224+
# The deferred RootModel base the generated validators derive from.
225+
"src/mcp-types/mcp_types/_wire_base.py" = ["TID251"]
224226
"tests/server/mcpserver/test_func_metadata.py" = ["E501"]
225227
"tests/shared/test_progress_notifications.py" = ["PLW0603"]
226228

‎scripts/gen_surface_types.py‎

Lines changed: 138 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from __future__ import annotations
1313

1414
import argparse
15+
import ast
1516
import difflib
1617
import hashlib
1718
import json
@@ -196,6 +197,139 @@ def run_codegen(schema_path: Path, output_path: Path) -> None:
196197
raise SystemExit(f"datamodel-codegen failed:\n{result.stderr}")
197198

198199

200+
def use_deferred_bases(source: str) -> str:
201+
"""Route generated `RootModel[T]` classes through the deferred `WireRootModel[T]` base.
202+
203+
Object models already derive from `WireModel` via codegen's `--base-class`;
204+
RootModel classes get no such hook, so rewrite them here. Both bases carry
205+
`defer_build=True` (see `mcp_types/_wire_base.py`), which is only a win if
206+
EVERY class in the package is deferred: an eagerly-built union rollup would
207+
regenerate the schemas of every deferred model it references.
208+
"""
209+
source = source.replace("RootModel[", "WireRootModel[")
210+
source = source.replace(
211+
"from mcp_types._wire_base import WireModel", "from mcp_types._wire_base import WireModel, WireRootModel"
212+
)
213+
source = re.sub(r"^from pydantic import (.*), RootModel$", r"from pydantic import \1", source, flags=re.MULTILINE)
214+
assert "RootModel[" not in source.replace("WireRootModel[", ""), "unrewritten RootModel base"
215+
return source
216+
217+
218+
def strip_rebuild_epilogue(source: str) -> str:
219+
"""Drop codegen's trailing `X.model_rebuild()` calls.
220+
221+
codegen emits them to force-resolve forward references eagerly; with the
222+
deferred bases each model is built once on first use, when its whole module
223+
namespace already exists, so the calls would only re-introduce the eager
224+
build cost the deferral removes.
225+
"""
226+
return re.sub(r"^\w+\.model_rebuild\(\)\n", "", source, flags=re.MULTILINE)
227+
228+
229+
def define_before_use(source: str) -> str:
230+
"""Reorder the generated classes so every referenced class is defined first.
231+
232+
codegen's ordering leaves whole subtrees referring to not-yet-defined
233+
classes (it relied on the eager `model_rebuild()` epilogue to patch them
234+
up). A deferred model whose annotations name an undefined class collects
235+
incomplete `model_fields` (an `Annotated[...]` alias trapped in an
236+
unevaluated string) until its first-use build; defining dependencies first
237+
keeps field metadata complete at import for every class. Only genuine
238+
recursion cannot be linearized (the mutually-recursive JSONValue trio in
239+
2026-07-28); such a cycle is kept together in codegen's own order, its
240+
string self-references resolving on first use.
241+
"""
242+
tree = ast.parse(source)
243+
lines = source.splitlines(keepends=True)
244+
classes = [node for node in tree.body if isinstance(node, ast.ClassDef)]
245+
if not classes:
246+
return source
247+
names = {node.name for node in classes}
248+
index = {node.name: i for i, node in enumerate(classes)}
249+
250+
def referenced(node: ast.ClassDef, *, quoted: bool) -> set[str]:
251+
refs: set[str] = set()
252+
for sub in ast.walk(node):
253+
if isinstance(sub, ast.Name) and sub.id in names:
254+
refs.add(sub.id)
255+
elif quoted and isinstance(sub, ast.Constant) and isinstance(sub.value, str) and sub.value in names:
256+
refs.add(sub.value) # quoted forward reference
257+
refs.discard(node.name)
258+
return refs
259+
260+
deps = {node.name: referenced(node, quoted=True) for node in classes}
261+
262+
# Tarjan's SCC: a cycle (real recursion) is placed as one unit.
263+
counter: list[int] = [0]
264+
stack: list[str] = []
265+
on_stack: set[str] = set()
266+
ids: dict[str, int] = {}
267+
low: dict[str, int] = {}
268+
comps: list[list[str]] = []
269+
270+
def strongconnect(v: str) -> None:
271+
ids[v] = low[v] = counter[0]
272+
counter[0] += 1
273+
stack.append(v)
274+
on_stack.add(v)
275+
for w in sorted(deps[v], key=index.__getitem__):
276+
if w not in ids:
277+
strongconnect(w)
278+
low[v] = min(low[v], low[w])
279+
elif w in on_stack:
280+
low[v] = min(low[v], ids[w])
281+
if low[v] == ids[v]:
282+
comp: list[str] = []
283+
while True:
284+
w = stack.pop()
285+
on_stack.discard(w)
286+
comp.append(w)
287+
if w == v:
288+
break
289+
comps.append(sorted(comp, key=index.__getitem__))
290+
291+
for name in sorted(names, key=index.__getitem__):
292+
if name not in ids:
293+
strongconnect(name)
294+
295+
# Emit each SCC once, dependencies first, stable on codegen's original order.
296+
comp_of = {name: i for i, comp in enumerate(comps) for name in comp}
297+
emitted: list[str] = []
298+
done: set[int] = set()
299+
300+
def emit(ci: int) -> None:
301+
if ci in done:
302+
return
303+
done.add(ci)
304+
for name in comps[ci]:
305+
for dep in sorted(deps[name], key=index.__getitem__):
306+
if comp_of[dep] != ci:
307+
emit(comp_of[dep])
308+
emitted.extend(comps[ci])
309+
310+
for name in sorted(names, key=index.__getitem__):
311+
emit(comp_of[name])
312+
assert set(emitted) == names
313+
# Invariant: every class named by a bare identifier is defined earlier;
314+
# only quoted references inside a genuine recursive cycle may still point
315+
# forward. A regression here silently reintroduces incomplete
316+
# `model_fields` at import, so fail generation instead.
317+
position = {name: i for i, name in enumerate(emitted)}
318+
unresolved = {
319+
(node.name, ref)
320+
for node in classes
321+
for ref in referenced(node, quoted=False)
322+
if position[ref] > position[node.name]
323+
}
324+
assert not unresolved, f"classes still name a later-defined class: {sorted(unresolved)}"
325+
326+
node_of = {node.name: node for node in classes}
327+
blocks = ["".join(lines[node_of[name].lineno - 1 : node_of[name].end_lineno]) for name in emitted]
328+
head = "".join(lines[: classes[0].lineno - 1])
329+
tail = "".join(lines[max(node.end_lineno or 0 for node in classes) :])
330+
return head + "\n\n\n".join(block.strip("\n") + "\n" for block in blocks) + tail
331+
332+
199333
def allow_open_class_extras(source: str, open_classes: frozenset[str]) -> str:
200334
"""Restore `extra="allow"` on `open_classes` only.
201335
@@ -243,12 +377,11 @@ def build(entry: dict[str, str]) -> str:
243377
# strict mkdocs link validation.
244378
source = source.replace("](/", "](https://modelcontextprotocol.io/")
245379
source = allow_open_class_extras(source, OPEN_CLASSES[version])
380+
source = use_deferred_bases(source)
381+
source = strip_rebuild_epilogue(source)
382+
source = define_before_use(source)
246383
if epilogue := EPILOGUES.get(version, ""):
247-
# Insert before the trailing model_rebuild() block: pyright's evaluation
248-
# order for the recursive RootModel block is sensitive to placement.
249-
match = re.search(r"^\w+\.model_rebuild\(\)$", source, flags=re.MULTILINE)
250-
cut = match.start() if match else len(source)
251-
source = f"{source[:cut]}{epilogue}\n\n{source[cut:]}"
384+
source = f"{source.rstrip()}\n\n\n{epilogue}"
252385
source = HEADER.format(version=version, sha=entry["sha256"]) + source
253386

254387
staging = TYPES_DIR / f"_staging_{version}.py"

‎src/mcp-types/mcp_types/_v2025_11_25/__init__.py‎

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
from typing import Annotated, Any, Literal
1010

11-
from mcp_types._wire_base import WireModel
12-
from pydantic import ConfigDict, Field, RootModel
11+
from mcp_types._wire_base import WireModel, WireRootModel
12+
from pydantic import ConfigDict, Field
1313

1414

1515
class BaseMetadata(WireModel):
@@ -285,7 +285,7 @@ class CompleteResult(WireModel):
285285
completion: Completion
286286

287287

288-
class Cursor(RootModel[str]):
288+
class Cursor(WireRootModel[str]):
289289
root: str
290290
"""
291291
An opaque token used to represent a cursor for pagination.
@@ -557,7 +557,7 @@ class LegacyTitledEnumSchema(WireModel):
557557

558558

559559
class LoggingLevel(
560-
RootModel[
560+
WireRootModel[
561561
Literal[
562562
"alert",
563563
"critical",
@@ -723,7 +723,7 @@ class PaginatedResult(WireModel):
723723
"""
724724

725725

726-
class ProgressToken(RootModel[str | int]):
726+
class ProgressToken(WireRootModel[str | int]):
727727
root: str | int
728728
"""
729729
A progress token, used to associate progress notifications with the original request.
@@ -853,7 +853,7 @@ class Request(WireModel):
853853
params: dict[str, Any] | None = None
854854

855855

856-
class RequestId(RootModel[str | int]):
856+
class RequestId(WireRootModel[str | int]):
857857
root: str | int
858858
"""
859859
A uniquely identifying ID for a request in JSON-RPC.
@@ -970,7 +970,7 @@ class Result(WireModel):
970970
"""
971971

972972

973-
class Role(RootModel[Literal["assistant", "user"]]):
973+
class Role(WireRootModel[Literal["assistant", "user"]]):
974974
root: Literal["assistant", "user"]
975975
"""
976976
The sender or recipient of messages and data in a conversation.
@@ -1216,7 +1216,7 @@ class TaskMetadata(WireModel):
12161216
"""
12171217

12181218

1219-
class TaskStatus(RootModel[Literal["cancelled", "completed", "failed", "input_required", "working"]]):
1219+
class TaskStatus(WireRootModel[Literal["cancelled", "completed", "failed", "input_required", "working"]]):
12201220
root: Literal["cancelled", "completed", "failed", "input_required", "working"]
12211221
"""
12221222
The status of a task.
@@ -1867,12 +1867,12 @@ class EmbeddedResource(WireModel):
18671867
type: Literal["resource"]
18681868

18691869

1870-
class EmptyResult(RootModel[Result]):
1870+
class EmptyResult(WireRootModel[Result]):
18711871
root: Result
18721872

18731873

18741874
class EnumSchema(
1875-
RootModel[
1875+
WireRootModel[
18761876
UntitledSingleSelectEnumSchema
18771877
| TitledSingleSelectEnumSchema
18781878
| UntitledMultiSelectEnumSchema
@@ -2115,7 +2115,7 @@ class LoggingMessageNotification(WireModel):
21152115
params: LoggingMessageNotificationParams
21162116

21172117

2118-
class MultiSelectEnumSchema(RootModel[UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema]):
2118+
class MultiSelectEnumSchema(WireRootModel[UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema]):
21192119
root: UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema
21202120

21212121

@@ -2153,7 +2153,7 @@ class PingRequest(WireModel):
21532153

21542154

21552155
class PrimitiveSchemaDefinition(
2156-
RootModel[
2156+
WireRootModel[
21572157
StringSchema
21582158
| NumberSchema
21592159
| BooleanSchema
@@ -2499,7 +2499,7 @@ class SetLevelRequest(WireModel):
24992499
params: SetLevelRequestParams
25002500

25012501

2502-
class SingleSelectEnumSchema(RootModel[UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema]):
2502+
class SingleSelectEnumSchema(WireRootModel[UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema]):
25032503
root: UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema
25042504

25052505

@@ -2793,7 +2793,7 @@ class CompleteRequest(WireModel):
27932793
params: CompleteRequestParams
27942794

27952795

2796-
class ContentBlock(RootModel[TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource]):
2796+
class ContentBlock(WireRootModel[TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource]):
27972797
root: TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource
27982798

27992799

@@ -2812,7 +2812,7 @@ class CreateTaskResult(WireModel):
28122812
task: Task
28132813

28142814

2815-
class ElicitRequestParams(RootModel[ElicitRequestURLParams | ElicitRequestFormParams]):
2815+
class ElicitRequestParams(WireRootModel[ElicitRequestURLParams | ElicitRequestFormParams]):
28162816
root: ElicitRequestURLParams | ElicitRequestFormParams
28172817
"""
28182818
The parameters for a request to elicit additional information from the user via the client.
@@ -2857,14 +2857,16 @@ class InitializeRequest(WireModel):
28572857
params: InitializeRequestParams
28582858

28592859

2860-
class JSONRPCMessage(RootModel[JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse]):
2860+
class JSONRPCMessage(
2861+
WireRootModel[JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse]
2862+
):
28612863
root: JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse
28622864
"""
28632865
Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent.
28642866
"""
28652867

28662868

2867-
class JSONRPCResponse(RootModel[JSONRPCResultResponse | JSONRPCErrorResponse]):
2869+
class JSONRPCResponse(WireRootModel[JSONRPCResultResponse | JSONRPCErrorResponse]):
28682870
root: JSONRPCResultResponse | JSONRPCErrorResponse
28692871
"""
28702872
A response to a request, containing either the result or error.
@@ -3174,7 +3176,7 @@ class CallToolResult(WireModel):
31743176

31753177

31763178
class ClientNotification(
3177-
RootModel[
3179+
WireRootModel[
31783180
CancelledNotification
31793181
| InitializedNotification
31803182
| ProgressNotification
@@ -3192,7 +3194,7 @@ class ClientNotification(
31923194

31933195

31943196
class ClientRequest(
3195-
RootModel[
3197+
WireRootModel[
31963198
InitializeRequest
31973199
| PingRequest
31983200
| ListResourcesRequest
@@ -3267,13 +3269,13 @@ class GetPromptResult(WireModel):
32673269

32683270

32693271
class SamplingMessageContentBlock(
3270-
RootModel[TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent]
3272+
WireRootModel[TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent]
32713273
):
32723274
root: TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent
32733275

32743276

32753277
class ServerNotification(
3276-
RootModel[
3278+
WireRootModel[
32773279
CancelledNotification
32783280
| ProgressNotification
32793281
| ResourceListChangedNotification
@@ -3299,7 +3301,7 @@ class ServerNotification(
32993301

33003302

33013303
class ServerResult(
3302-
RootModel[
3304+
WireRootModel[
33033305
Result
33043306
| InitializeResult
33053307
| ListResourcesResult
@@ -3399,7 +3401,7 @@ class SamplingMessage(WireModel):
33993401

34003402

34013403
class ClientResult(
3402-
RootModel[
3404+
WireRootModel[
34033405
Result
34043406
| GetTaskResult
34053407
| GetTaskPayloadResult
@@ -3503,7 +3505,7 @@ class CreateMessageRequest(WireModel):
35033505

35043506

35053507
class ServerRequest(
3506-
RootModel[
3508+
WireRootModel[
35073509
PingRequest
35083510
| GetTaskRequest
35093511
| GetTaskPayloadRequest

0 commit comments

Comments
 (0)