Skip to content

Commit 6da7ba4

Browse files
committed
Spell warm()'s every-version flag as all_versions and return a frozen dataclass
`warm(everything=True)` warmed every protocol version's routing surface - not "everything the SDK could ever build": the mcp package's own model roots (mcpserver settings, tool/prompt argument models) belong to the mcp layer that mcp-types cannot see, and they build during server construction and tool registration anyway, which is startup work rather than first-request work. Naming the flag `all_versions` says what it does; the recommended host recipe is unchanged (`warm(version)` for the version the transport negotiates). WarmReport becomes a frozen dataclass instead of a NamedTuple, so a later field addition does not disturb code that reads the existing fields, and `mcp.warm` stays the very same function as `mcp_types.methods.warm` (pinned by a test).
1 parent b592f09 commit 6da7ba4

3 files changed

Lines changed: 45 additions & 18 deletions

File tree

‎src/mcp-types/mcp_types/methods.py‎

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717

1818
import time
1919
from collections.abc import Iterable, Iterator, Mapping
20+
from dataclasses import dataclass
2021
from functools import cache
2122
from importlib import import_module
2223
from types import MappingProxyType, ModuleType, UnionType
23-
from typing import TYPE_CHECKING, Any, Final, Literal, NamedTuple, TypeGuard, TypeVar, cast, get_args
24+
from typing import TYPE_CHECKING, Any, Final, Literal, TypeGuard, TypeVar, cast, get_args
2425

2526
from pydantic import BaseModel, TypeAdapter
2627

@@ -871,8 +872,13 @@ def parse_client_result(
871872
"""Every monolith map, for `warm()`."""
872873

873874

874-
class WarmReport(NamedTuple):
875-
"""What one `warm()` call built (already-built work is skipped and not counted)."""
875+
@dataclass(frozen=True)
876+
class WarmReport:
877+
"""What one `warm()` call built (already-built work is skipped and not counted).
878+
879+
A frozen dataclass rather than a tuple, so a later field addition does not
880+
change how existing fields are read.
881+
"""
876882

877883
models: int
878884
"""Deferred model classes whose validators this call built."""
@@ -910,7 +916,7 @@ def _warm_adapter(adapter: TypeAdapter[Any]) -> int:
910916
return 0 if adapter.rebuild() is None else 1
911917

912918

913-
def warm(version: str | None = None, *, everything: bool = False) -> WarmReport:
919+
def warm(version: str | None = None, *, all_versions: bool = False) -> WarmReport:
914920
"""Build the SDK's deferred validators up front, before the first message needs them.
915921
916922
Importing the SDK stays fast because pydantic validators build on first use
@@ -919,21 +925,26 @@ def warm(version: str | None = None, *, everything: bool = False) -> WarmReport:
919925
startup, calls this from a startup hook; nothing in the SDK calls it, and
920926
steady-state behaviour is identical either way.
921927
922-
* `warm()` builds the version-independent set: the monolith `mcp_types`
923-
models a session routes through, the JSON-RPC envelopes and the
924-
module-level union adapters.
925-
* `warm(version)` additionally imports that protocol version's wire
926-
package and builds its routing surface (the wire models and the cached
927-
adapters a connection at that version routes through).
928-
* `warm(everything=True)` does that for every known protocol version.
928+
* `warm(version)` is what a host wants: it builds the version-independent
929+
set (below) and imports that protocol version's wire package and builds
930+
its routing surface (the wire models and the cached adapters a
931+
connection at that version routes through), so the first connection at
932+
that version builds nothing.
933+
* `warm()` builds only the version-independent set: the monolith
934+
`mcp_types` models a session routes through, the JSON-RPC envelopes and
935+
the module-level union adapters. The first connection still imports and
936+
builds its version's routing surface.
937+
* `warm(all_versions=True)` warms every known protocol version's routing
938+
surface, for a proxy or gateway that serves clients of both eras.
929939
930940
Model classes are completed before the adapters that reference them are
931941
built, so no schema is generated twice; anything already built is skipped,
932942
so the call is idempotent and repeat calls are cheap.
933943
934944
Args:
935945
version: The protocol version whose routing surface to build too.
936-
everything: Warm every known protocol version (benchmarks, tests).
946+
all_versions: Warm every known protocol version's routing surface
947+
(a proxy or gateway serving both eras; benchmarks and tests).
937948
938949
Returns:
939950
Counts of what this call built and the milliseconds it took.
@@ -943,7 +954,7 @@ def warm(version: str | None = None, *, everything: bool = False) -> WarmReport:
943954
"""
944955
started = time.perf_counter()
945956
versions: list[str] = []
946-
if everything:
957+
if all_versions:
947958
versions = sorted(KNOWN_PROTOCOL_VERSIONS)
948959
elif version is not None:
949960
_check_known_version(version)

‎tests/test_init.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import pytest
8+
from mcp_types import methods
89

910
import mcp
1011
import mcp.server.session
@@ -27,6 +28,11 @@ def test_client_and_server_reexports_are_the_defining_objects():
2728
assert mcp.ClientSessionGroup is ClientSessionGroup
2829

2930

31+
def test_warm_is_the_types_layer_prewarm_helper_itself():
32+
"""SDK-defined: `mcp.warm` is `mcp_types.methods.warm` - one helper, two spellings."""
33+
assert mcp.warm is methods.warm
34+
35+
3036
def test_dir_lists_every_export_and_the_bound_submodules():
3137
"""SDK-defined: `dir(mcp)` reports all of `__all__` plus the submodules a bare import binds."""
3238
listing = set(dir(mcp))

‎tests/types/test_methods.py‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for the wire-method maps and two-step parse functions in `mcp_types.methods`."""
22

3+
import dataclasses
34
import importlib.util
45
import json
56
import os
@@ -1034,7 +1035,7 @@ def test_importing_the_module_builds_no_adapters_and_identical_rows_share_one():
10341035
# --- warm(): the opt-in prewarm helper ---
10351036

10361037
_WARM_PROBE = """
1037-
import json, sys
1038+
import dataclasses, json, sys
10381039
import pydantic
10391040
import mcp_types
10401041
from mcp_types import methods
@@ -1069,7 +1070,7 @@ def counting_init(self, *a, **k):
10691070
{{"tools": [], "cacheScope": "private", "ttlMs": 0, "resultType": "complete"}},
10701071
)
10711072
mcp_types.jsonrpc_message_adapter.validate_python({{"jsonrpc": "2.0", "id": 1, "method": "ping"}})
1072-
print(json.dumps({{"report": report._asdict() if report else None, "wire": loaded_wire, "built": built}}))
1073+
print(json.dumps({{"report": dataclasses.asdict(report) if report else None, "wire": loaded_wire, "built": built}}))
10731074
"""
10741075

10751076

@@ -1115,10 +1116,19 @@ def test_warm_is_idempotent_and_gates_unknown_versions():
11151116
"""SDK-defined: repeating a `warm()` builds nothing more; an unknown version is a ValueError."""
11161117
baseline = methods.warm() # the version-independent set (this process may already have built it)
11171118
assert baseline.elapsed_ms >= 0
1118-
methods.warm(everything=True)
1119-
again = methods.warm(everything=True)
1119+
methods.warm(all_versions=True)
1120+
again = methods.warm(all_versions=True)
11201121
assert (again.models, again.adapters) == (0, 0)
11211122
assert again.elapsed_ms >= 0
11221123
with pytest.raises(ValueError) as excinfo:
11231124
methods.warm("2099-01-01")
1124-
assert "2099-01-01" in str(excinfo.value)
1125+
assert str(excinfo.value) == snapshot("version must be a known protocol version, got '2099-01-01'")
1126+
1127+
1128+
def test_warm_report_is_an_immutable_dataclass():
1129+
"""SDK-defined: the report is a frozen dataclass (fields read by name, arity free to grow),
1130+
not a tuple."""
1131+
report = methods.warm()
1132+
assert dataclasses.is_dataclass(report)
1133+
with pytest.raises(dataclasses.FrozenInstanceError):
1134+
setattr(report, "models", -1)

0 commit comments

Comments
 (0)