Skip to content

Commit 45bb6c9

Browse files
committed
Defer building the SDK's own eager pydantic models and adapters
Every SDK-defined model that used to be built at import - the auth and OAuth handler models, mcpserver's Resource/ResourceTemplate/Prompt/Tool/ Settings/Context/FuncMetadata, the client's parameter models, transport security, apps and elicitation results - now sets defer_build=True, and the remaining module-level TypeAdapters (message_validator, token_request_adapter) build on first use as well. Each model or validator is built once, at its first construction or validation, instead of adding its cost to the import of the module that defines it. Models that derive from BaseModel directly opt into the lazy __signature__ with @deferred_model, so inspect.signature() on a never-used model keeps reporting its fields (and mcpserver Settings, whose signature could not resolve at all before, now does).
1 parent 08f4385 commit 45bb6c9

23 files changed

Lines changed: 223 additions & 27 deletions

File tree

‎src/mcp/client/auth/extensions/client_credentials.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
import httpx2
1515
import jwt
16-
from pydantic import BaseModel, Field
16+
from mcp_types._deferred import deferred_model
17+
from pydantic import BaseModel, ConfigDict, Field
1718

1819
from mcp.client.auth import OAuthClientProvider, OAuthFlowError, TokenStorage
1920
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata
@@ -134,6 +135,7 @@ async def provider(audience: str) -> str:
134135
return provider
135136

136137

138+
@deferred_model
137139
class SignedJWTParameters(BaseModel):
138140
"""Parameters for creating SDK-signed JWT assertions.
139141
@@ -156,6 +158,9 @@ class SignedJWTParameters(BaseModel):
156158
```
157159
"""
158160

161+
# defer_build: build the validator on first use rather than at import (import-time cost).
162+
model_config = ConfigDict(defer_build=True)
163+
159164
issuer: str = Field(description="Issuer for JWT assertions (typically client_id).")
160165
subject: str = Field(description="Subject identifier for JWT assertions (typically client_id).")
161166
signing_key: str = Field(description="Private key for JWT signing (PEM format).")

‎src/mcp/client/auth/oauth2.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616

1717
import anyio
1818
import httpx2
19+
from mcp_types._deferred import deferred_model
1920
from mcp_types.version import is_version_at_least
20-
from pydantic import BaseModel, Field, ValidationError
21+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
2122

2223
from mcp.client.auth.exceptions import OAuthFlowError, OAuthRegistrationError, OAuthTokenError
2324
from mcp.client.auth.utils import (
@@ -109,9 +110,13 @@ def check_registration_usable(client_info: OAuthClientInformationFull) -> None:
109110
)
110111

111112

113+
@deferred_model
112114
class PKCEParameters(BaseModel):
113115
"""PKCE (Proof Key for Code Exchange) parameters."""
114116

117+
# defer_build: build the validator on first use rather than at import (import-time cost).
118+
model_config = ConfigDict(defer_build=True)
119+
115120
code_verifier: str = Field(..., min_length=43, max_length=128)
116121
code_challenge: str = Field(..., min_length=43, max_length=128)
117122

‎src/mcp/client/session_group.py‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
import anyio
1717
import httpx2
1818
import mcp_types as types
19-
from pydantic import BaseModel, Field
19+
from mcp_types._deferred import deferred_model
20+
from pydantic import BaseModel, ConfigDict, Field
2021
from typing_extensions import Self
2122

2223
import mcp
@@ -28,10 +29,16 @@
2829
from mcp.shared.dispatcher import ProgressFnT
2930
from mcp.shared.exceptions import MCPError
3031

32+
# `defer_build=True` below: pydantic builds each validator on first use rather than at
33+
# import (import-time cost); the build is transparent at first construction/validation.
3134

35+
36+
@deferred_model
3237
class SseServerParameters(BaseModel):
3338
"""Parameters for initializing an sse_client."""
3439

40+
model_config = ConfigDict(defer_build=True)
41+
3542
# The endpoint URL.
3643
url: str
3744

@@ -45,9 +52,12 @@ class SseServerParameters(BaseModel):
4552
sse_read_timeout: float = 300.0
4653

4754

55+
@deferred_model
4856
class StreamableHttpParameters(BaseModel):
4957
"""Parameters for initializing a streamable_http_client."""
5058

59+
model_config = ConfigDict(defer_build=True)
60+
5161
# The endpoint URL.
5262
url: str
5363

@@ -104,6 +114,8 @@ class ClientSessionGroup:
104114
class _ComponentNames(BaseModel):
105115
"""Used for reverse index to find components."""
106116

117+
model_config = ConfigDict(defer_build=True)
118+
107119
prompts: set[str] = Field(default_factory=set)
108120
resources: set[str] = Field(default_factory=set)
109121
tools: set[str] = Field(default_factory=set)

‎src/mcp/client/stdio.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
import mcp_types as types
2222
from anyio.abc import AsyncResource, Process
2323
from anyio.streams.text import TextReceiveStream
24-
from pydantic import BaseModel, Field
24+
from mcp_types._deferred import deferred_model
25+
from pydantic import BaseModel, ConfigDict, Field
2526

2627
from mcp.client._transport import TransportStreams
2728
from mcp.os.posix.utilities import terminate_posix_process_tree
@@ -90,7 +91,11 @@ def get_default_environment() -> dict[str, str]:
9091
return env
9192

9293

94+
@deferred_model
9395
class StdioServerParameters(BaseModel):
96+
# defer_build: build the validator on first use rather than at import (import-time cost).
97+
model_config = ConfigDict(defer_build=True)
98+
9499
command: str
95100
"""The executable to run to start the server."""
96101

‎src/mcp/server/apps.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def get_time(ctx: Context) -> str:
3232
from collections.abc import Callable, Sequence
3333
from typing import Any, Literal, TypeVar
3434

35+
from mcp_types._deferred import deferred_model
3536
from pydantic import BaseModel, ConfigDict
3637
from pydantic.alias_generators import to_camel
3738

@@ -52,21 +53,24 @@ def get_time(ctx: Context) -> str:
5253
_CallableT = TypeVar("_CallableT", bound=Callable[..., Any])
5354

5455

56+
@deferred_model
5557
class ResourcePermissions(BaseModel):
5658
"""Iframe permissions a `ui://` resource requests (`_meta.ui.permissions`)."""
5759

58-
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
60+
# defer_build: build the validator on first use rather than at import (import-time cost).
61+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, defer_build=True)
5962

6063
camera: dict[str, Any] | None = None
6164
microphone: dict[str, Any] | None = None
6265
geolocation: dict[str, Any] | None = None
6366
clipboard_write: dict[str, Any] | None = None
6467

6568

69+
@deferred_model
6670
class ResourceCsp(BaseModel):
6771
"""Content-Security-Policy domains for a `ui://` resource (`_meta.ui.csp`)."""
6872

69-
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
73+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, defer_build=True)
7074

7175
connect_domains: list[str] | None = None
7276
resource_domains: list[str] | None = None

‎src/mcp/server/auth/handlers/authorize.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from typing import Any, Literal
44

55
# TODO(Marcelo): We should drop the `RootModel`.
6-
from pydantic import AnyUrl, BaseModel, Field, RootModel, ValidationError # noqa: TID251
6+
from mcp_types._deferred import deferred_model
7+
from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel, ValidationError # noqa: TID251
78
from starlette.datastructures import FormData, QueryParams
89
from starlette.requests import Request
910
from starlette.responses import RedirectResponse, Response
@@ -22,7 +23,14 @@
2223
logger = logging.getLogger(__name__)
2324

2425

26+
# `defer_build=True` below: pydantic builds each validator on first use rather than at
27+
# import (import-time cost); the build is transparent at first construction/validation.
28+
29+
30+
@deferred_model
2531
class AuthorizationRequest(BaseModel):
32+
model_config = ConfigDict(defer_build=True)
33+
2634
# See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
2735
client_id: str = Field(..., description="The client ID")
2836
redirect_uri: AnyUrl | None = Field(None, description="URL to redirect to after authorization")
@@ -42,7 +50,10 @@ class AuthorizationRequest(BaseModel):
4250
)
4351

4452

53+
@deferred_model
4554
class AuthorizationErrorResponse(BaseModel):
55+
model_config = ConfigDict(defer_build=True)
56+
4657
error: AuthorizationErrorCode
4758
error_description: str | None
4859
error_uri: AnyUrl | None = None
@@ -59,7 +70,10 @@ def best_effort_extract_string(key: str, params: None | FormData | QueryParams)
5970
return None
6071

6172

73+
@deferred_model
6274
class AnyUrlModel(RootModel[AnyUrl]):
75+
model_config = ConfigDict(defer_build=True)
76+
6377
root: AnyUrl
6478

6579

‎src/mcp/server/auth/handlers/register.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from typing import Any
55
from uuid import uuid4
66

7-
from pydantic import BaseModel, ValidationError
7+
from mcp_types._deferred import deferred_model
8+
from pydantic import BaseModel, ConfigDict, ValidationError
89
from starlette.requests import Request
910
from starlette.responses import Response
1011

@@ -19,7 +20,11 @@
1920
RegistrationRequest = OAuthClientMetadata
2021

2122

23+
@deferred_model
2224
class RegistrationErrorResponse(BaseModel):
25+
# defer_build: build the validator on first use rather than at import (import-time cost).
26+
model_config = ConfigDict(defer_build=True)
27+
2328
error: RegistrationErrorCode
2429
error_description: str | None
2530

‎src/mcp/server/auth/handlers/revoke.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from functools import partial
33
from typing import Any, Literal
44

5-
from pydantic import BaseModel, ValidationError
5+
from mcp_types._deferred import deferred_model
6+
from pydantic import BaseModel, ConfigDict, ValidationError
67
from starlette.requests import Request
78
from starlette.responses import Response
89

@@ -13,17 +14,26 @@
1314
from mcp.server.auth.middleware.client_auth import AuthenticationError, ClientAuthenticator
1415
from mcp.server.auth.provider import AccessToken, OAuthAuthorizationServerProvider, RefreshToken
1516

17+
# `defer_build=True` below: pydantic builds each validator on first use rather than at
18+
# import (import-time cost); the build is transparent at first construction/validation.
1619

20+
21+
@deferred_model
1722
class RevocationRequest(BaseModel):
1823
"""See https://datatracker.ietf.org/doc/html/rfc7009#section-2.1"""
1924

25+
model_config = ConfigDict(defer_build=True)
26+
2027
token: str
2128
token_type_hint: Literal["access_token", "refresh_token"] | None = None
2229
client_id: str
2330
client_secret: str | None
2431

2532

33+
@deferred_model
2634
class RevocationErrorResponse(BaseModel):
35+
model_config = ConfigDict(defer_build=True)
36+
2737
error: Literal["invalid_request", "unauthorized_client"]
2838
error_description: str | None = None
2939

‎src/mcp/server/auth/handlers/token.py‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from dataclasses import dataclass
55
from typing import Annotated, Any, Literal
66

7-
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, TypeAdapter, ValidationError
7+
from mcp_types._deferred import deferred_model
8+
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, ConfigDict, Field, TypeAdapter, ValidationError
89
from starlette.requests import Request
910

1011
from mcp.server.auth.errors import stringify_pydantic_error
@@ -18,8 +19,14 @@
1819
)
1920
from mcp.shared.auth import OAuthToken
2021

22+
# `defer_build=True` below: pydantic builds each validator on first use rather than at
23+
# import (import-time cost); the build is transparent at first construction/validation.
2124

25+
26+
@deferred_model
2227
class AuthorizationCodeRequest(BaseModel):
28+
model_config = ConfigDict(defer_build=True)
29+
2330
# See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
2431
grant_type: Literal["authorization_code"]
2532
code: str = Field(..., description="The authorization code")
@@ -33,7 +40,10 @@ class AuthorizationCodeRequest(BaseModel):
3340
resource: str | None = Field(None, description="Resource indicator for the token")
3441

3542

43+
@deferred_model
3644
class RefreshTokenRequest(BaseModel):
45+
model_config = ConfigDict(defer_build=True)
46+
3747
# See https://datatracker.ietf.org/doc/html/rfc6749#section-6
3848
grant_type: Literal["refresh_token"]
3949
refresh_token: str = Field(..., description="The refresh token")
@@ -45,7 +55,10 @@ class RefreshTokenRequest(BaseModel):
4555
resource: str | None = Field(None, description="Resource indicator for the token")
4656

4757

58+
@deferred_model
4859
class JwtBearerRequest(BaseModel):
60+
model_config = ConfigDict(defer_build=True)
61+
4962
# RFC 7523 §2.1 JWT bearer authorization grant. SEP-990 leg 2: the client presents the
5063
# enterprise IdP-issued ID-JAG to the MCP authorization server as the `assertion`.
5164
grant_type: Literal["urn:ietf:params:oauth:grant-type:jwt-bearer"]
@@ -63,12 +76,15 @@ class JwtBearerRequest(BaseModel):
6376
AuthorizationCodeRequest | RefreshTokenRequest | JwtBearerRequest,
6477
Field(discriminator="grant_type"),
6578
]
66-
token_request_adapter = TypeAdapter[TokenRequest](TokenRequest)
79+
token_request_adapter = TypeAdapter[TokenRequest](TokenRequest, config=ConfigDict(defer_build=True))
6780

6881

82+
@deferred_model
6983
class TokenErrorResponse(BaseModel):
7084
"""See https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"""
7185

86+
model_config = ConfigDict(defer_build=True)
87+
7288
error: TokenErrorCode
7389
error_description: str | None = None
7490
error_uri: AnyHttpUrl | None = None

‎src/mcp/server/auth/provider.py‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22
from typing import Any, Generic, Literal, Protocol, TypeVar
33
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
44

5-
from pydantic import AnyUrl, BaseModel
5+
from mcp_types._deferred import deferred_model
6+
from pydantic import AnyUrl, BaseModel, ConfigDict
67

78
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
89

10+
# `defer_build=True` on the models below: pydantic builds each validator on first use
11+
# rather than at import (import-time cost); the build is transparent at first construction.
912

13+
14+
@deferred_model
1015
class AuthorizationParams(BaseModel):
16+
model_config = ConfigDict(defer_build=True)
17+
1118
state: str | None
1219
scopes: list[str] | None
1320
code_challenge: str
@@ -16,6 +23,7 @@ class AuthorizationParams(BaseModel):
1623
resource: str | None = None # RFC 8707 resource indicator
1724

1825

26+
@deferred_model
1927
class IdentityAssertionParams(BaseModel):
2028
"""Validated parameters of a SEP-990 identity-assertion (RFC 7523 jwt-bearer) request.
2129
@@ -24,12 +32,17 @@ class IdentityAssertionParams(BaseModel):
2432
RFC 7523 §3 and the SEP-990 §5.1 processing rules before issuing an access token.
2533
"""
2634

35+
model_config = ConfigDict(defer_build=True)
36+
2737
assertion: str # RFC 7523 §2.1: the JWT (ID-JAG) presented as the authorization grant
2838
scopes: list[str] | None = None
2939
resource: str | None = None # RFC 8707 resource indicator from the token request
3040

3141

42+
@deferred_model
3243
class AuthorizationCode(BaseModel):
44+
model_config = ConfigDict(defer_build=True)
45+
3346
code: str
3447
scopes: list[str]
3548
expires_at: float
@@ -41,15 +54,21 @@ class AuthorizationCode(BaseModel):
4154
subject: str | None = None # resource owner; propagate to the issued AccessToken
4255

4356

57+
@deferred_model
4458
class RefreshToken(BaseModel):
59+
model_config = ConfigDict(defer_build=True)
60+
4561
token: str
4662
client_id: str
4763
scopes: list[str]
4864
expires_at: int | None = None
4965
subject: str | None = None # resource owner; propagate to refreshed AccessTokens
5066

5167

68+
@deferred_model
5269
class AccessToken(BaseModel):
70+
model_config = ConfigDict(defer_build=True)
71+
5372
token: str
5473
client_id: str
5574
scopes: list[str]

0 commit comments

Comments
 (0)