Skip to content

Commit 5fdd48a

Browse files
authored
Completely drop RootModel from types module (#1910)
1 parent f4672c5 commit 5fdd48a

36 files changed

+640
-800
lines changed

docs/migration.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,60 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app(json_response=Tru
179179

180180
**Note:** DNS rebinding protection is automatically enabled when `host` is `127.0.0.1`, `localhost`, or `::1`. This now happens in `sse_app()` and `streamable_http_app()` instead of the constructor.
181181

182+
### Replace `RootModel` by union types with `TypeAdapter` validation
183+
184+
The following union types are no longer `RootModel` subclasses:
185+
186+
- `ClientRequest`
187+
- `ServerRequest`
188+
- `ClientNotification`
189+
- `ServerNotification`
190+
- `ClientResult`
191+
- `ServerResult`
192+
- `JSONRPCMessage`
193+
194+
This means you can no longer access `.root` on these types or use `model_validate()` directly on them. Instead, use the provided `TypeAdapter` instances for validation.
195+
196+
**Before (v1):**
197+
198+
```python
199+
from mcp.types import ClientRequest, ServerNotification
200+
201+
# Using RootModel.model_validate()
202+
request = ClientRequest.model_validate(data)
203+
actual_request = request.root # Accessing the wrapped value
204+
205+
notification = ServerNotification.model_validate(data)
206+
actual_notification = notification.root
207+
```
208+
209+
**After (v2):**
210+
211+
```python
212+
from mcp.types import client_request_adapter, server_notification_adapter
213+
214+
# Using TypeAdapter.validate_python()
215+
request = client_request_adapter.validate_python(data)
216+
# No .root access needed - request is the actual type
217+
218+
notification = server_notification_adapter.validate_python(data)
219+
# No .root access needed - notification is the actual type
220+
```
221+
222+
**Available adapters:**
223+
224+
| Union Type | Adapter |
225+
|------------|---------|
226+
| `ClientRequest` | `client_request_adapter` |
227+
| `ServerRequest` | `server_request_adapter` |
228+
| `ClientNotification` | `client_notification_adapter` |
229+
| `ServerNotification` | `server_notification_adapter` |
230+
| `ClientResult` | `client_result_adapter` |
231+
| `ServerResult` | `server_result_adapter` |
232+
| `JSONRPCMessage` | `jsonrpc_message_adapter` |
233+
234+
All adapters are exported from `mcp.types`.
235+
182236
### Resource URI type changed from `AnyUrl` to `str`
183237

184238
The `uri` field on resource-related types now uses `str` instead of Pydantic's `AnyUrl`. This aligns with the [MCP specification schema](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) which defines URIs as plain strings (`uri: string`) without strict URL validation. This change allows relative paths like `users/me` that were previously rejected.

pyproject.toml

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,23 @@ extend-exclude = ["README.md"]
125125

126126
[tool.ruff.lint]
127127
select = [
128-
"C4", # flake8-comprehensions
129-
"C90", # mccabe
130-
"D212", # pydocstyle: multi-line docstring summary should start at the first line
131-
"E", # pycodestyle
132-
"F", # pyflakes
133-
"I", # isort
134-
"PERF", # Perflint
135-
"PL", # Pylint
136-
"UP", # pyupgrade
128+
"C4", # flake8-comprehensions
129+
"C90", # mccabe
130+
"D212", # pydocstyle: multi-line docstring summary should start at the first line
131+
"E", # pycodestyle
132+
"F", # pyflakes
133+
"I", # isort
134+
"PERF", # Perflint
135+
"PL", # Pylint
136+
"UP", # pyupgrade
137+
"TID251", # https://docs.astral.sh/ruff/rules/banned-api/
137138
]
138139
ignore = ["PERF203", "PLC0415", "PLR0402"]
139140

141+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
142+
"pydantic.RootModel".msg = "Use `pydantic.TypeAdapter` instead."
143+
144+
140145
[tool.ruff.lint.mccabe]
141146
max-complexity = 24 # Default is 10
142147

src/mcp/client/experimental/task_handlers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ def build_capability(self) -> types.ClientTasksCapability | None:
242242
def handles_request(request: types.ServerRequest) -> bool:
243243
"""Check if this handler handles the given request type."""
244244
return isinstance(
245-
request.root,
245+
request,
246246
types.GetTaskRequest | types.GetTaskPayloadRequest | types.ListTasksRequest | types.CancelTaskRequest,
247247
)
248248

@@ -259,7 +259,7 @@ async def handle_request(
259259
types.ClientResult | types.ErrorData
260260
)
261261

262-
match responder.request.root:
262+
match responder.request:
263263
case types.GetTaskRequest(params=params):
264264
response = await self.get_task(ctx, params)
265265
client_response = client_response_type.validate_python(response)
@@ -281,7 +281,7 @@ async def handle_request(
281281
await responder.respond(client_response)
282282

283283
case _: # pragma: no cover
284-
raise ValueError(f"Unhandled request type: {type(responder.request.root)}")
284+
raise ValueError(f"Unhandled request type: {type(responder.request)}")
285285

286286

287287
# Backwards compatibility aliases

src/mcp/client/experimental/tasks.py

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,13 @@ async def call_tool_as_task(
9292
_meta = types.RequestParams.Meta(**meta)
9393

9494
return await self._session.send_request(
95-
types.ClientRequest(
96-
types.CallToolRequest(
97-
params=types.CallToolRequestParams(
98-
name=name,
99-
arguments=arguments,
100-
task=types.TaskMetadata(ttl=ttl),
101-
_meta=_meta,
102-
),
103-
)
95+
types.CallToolRequest(
96+
params=types.CallToolRequestParams(
97+
name=name,
98+
arguments=arguments,
99+
task=types.TaskMetadata(ttl=ttl),
100+
_meta=_meta,
101+
),
104102
),
105103
types.CreateTaskResult,
106104
)
@@ -115,10 +113,8 @@ async def get_task(self, task_id: str) -> types.GetTaskResult:
115113
GetTaskResult containing the task status and metadata
116114
"""
117115
return await self._session.send_request(
118-
types.ClientRequest(
119-
types.GetTaskRequest(
120-
params=types.GetTaskRequestParams(task_id=task_id),
121-
)
116+
types.GetTaskRequest(
117+
params=types.GetTaskRequestParams(task_id=task_id),
122118
),
123119
types.GetTaskResult,
124120
)
@@ -142,10 +138,8 @@ async def get_task_result(
142138
The task result, validated against result_type
143139
"""
144140
return await self._session.send_request(
145-
types.ClientRequest(
146-
types.GetTaskPayloadRequest(
147-
params=types.GetTaskPayloadRequestParams(task_id=task_id),
148-
)
141+
types.GetTaskPayloadRequest(
142+
params=types.GetTaskPayloadRequestParams(task_id=task_id),
149143
),
150144
result_type,
151145
)
@@ -164,9 +158,7 @@ async def list_tasks(
164158
"""
165159
params = types.PaginatedRequestParams(cursor=cursor) if cursor else None
166160
return await self._session.send_request(
167-
types.ClientRequest(
168-
types.ListTasksRequest(params=params),
169-
),
161+
types.ListTasksRequest(params=params),
170162
types.ListTasksResult,
171163
)
172164

@@ -180,10 +172,8 @@ async def cancel_task(self, task_id: str) -> types.CancelTaskResult:
180172
CancelTaskResult with the updated task state
181173
"""
182174
return await self._session.send_request(
183-
types.ClientRequest(
184-
types.CancelTaskRequest(
185-
params=types.CancelTaskRequestParams(task_id=task_id),
186-
)
175+
types.CancelTaskRequest(
176+
params=types.CancelTaskRequestParams(task_id=task_id),
187177
),
188178
types.CancelTaskResult,
189179
)

0 commit comments

Comments
 (0)