Skip to content

Commit f0ab53e

Browse files
authored
Add meta to Client methods (#1923)
1 parent d77292f commit f0ab53e

File tree

27 files changed

+221
-156
lines changed

27 files changed

+221
-156
lines changed

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2119,8 +2119,6 @@ uv run client
21192119
import asyncio
21202120
import os
21212121

2122-
from pydantic import AnyUrl
2123-
21242122
from mcp import ClientSession, StdioServerParameters, types
21252123
from mcp.client.stdio import stdio_client
21262124
from mcp.shared.context import RequestContext
@@ -2173,7 +2171,7 @@ async def run():
21732171
print(f"Available tools: {[t.name for t in tools.tools]}")
21742172

21752173
# Read a resource (greeting resource from fastmcp_quickstart)
2176-
resource_content = await session.read_resource(AnyUrl("greeting://World"))
2174+
resource_content = await session.read_resource("greeting://World")
21772175
content_block = resource_content.contents[0]
21782176
if isinstance(content_block, types.TextContent):
21792177
print(f"Resource content: {content_block.text}")

docs/migration.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,35 @@ notification = server_notification_adapter.validate_python(data)
233233

234234
All adapters are exported from `mcp.types`.
235235

236+
### `RequestParams.Meta` replaced with `RequestParamsMeta` TypedDict
237+
238+
The nested `RequestParams.Meta` Pydantic model class has been replaced with a top-level `RequestParamsMeta` TypedDict. This affects the `ctx.meta` field in request handlers and any code that imports or references this type.
239+
240+
**Key changes:**
241+
242+
- `RequestParams.Meta` (Pydantic model) → `RequestParamsMeta` (TypedDict)
243+
- Attribute access (`meta.progress_token`) → Dictionary access (`meta.get("progress_token")`)
244+
- `progress_token` field changed from `ProgressToken | None = None` to `NotRequired[ProgressToken]`
245+
`
246+
247+
**In request context handlers:**
248+
249+
```python
250+
# Before (v1)
251+
@server.call_tool()
252+
async def handle_tool(name: str, arguments: dict) -> list[TextContent]:
253+
ctx = server.request_context
254+
if ctx.meta and ctx.meta.progress_token:
255+
await ctx.session.send_progress_notification(ctx.meta.progress_token, 0.5, 100)
256+
257+
# After (v2)
258+
@server.call_tool()
259+
async def handle_tool(name: str, arguments: dict) -> list[TextContent]:
260+
ctx = server.request_context
261+
if ctx.meta and "progress_token" in ctx.meta:
262+
await ctx.session.send_progress_notification(ctx.meta["progress_token"], 0.5, 100)
263+
```
264+
236265
### Resource URI type changed from `AnyUrl` to `str`
237266

238267
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.
@@ -274,7 +303,19 @@ Affected types:
274303
- `UnsubscribeRequestParams.uri`
275304
- `ResourceUpdatedNotificationParams.uri`
276305

277-
The `ClientSession.read_resource()`, `subscribe_resource()`, and `unsubscribe_resource()` methods now accept both `str` and `AnyUrl` for backwards compatibility.
306+
The `Client` and `ClientSession` methods `read_resource()`, `subscribe_resource()`, and `unsubscribe_resource()` now only accept `str` for the `uri` parameter. If you were passing `AnyUrl` objects, convert them to strings:
307+
308+
```python
309+
# Before (v1)
310+
from pydantic import AnyUrl
311+
312+
await client.read_resource(AnyUrl("test://resource"))
313+
314+
# After (v2)
315+
await client.read_resource("test://resource")
316+
# Or if you have an AnyUrl from elsewhere:
317+
await client.read_resource(str(my_any_url))
318+
```
278319

279320
## Deprecations
280321

examples/servers/everything-server/mcp_everything_server/server.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,9 @@ async def test_tool_with_progress(ctx: Context[ServerSession, None]) -> str:
161161
await ctx.report_progress(progress=100, total=100, message="Completed step 100 of 100")
162162

163163
# Return progress token as string
164-
progress_token = ctx.request_context.meta.progress_token if ctx.request_context and ctx.request_context.meta else 0
164+
progress_token = (
165+
ctx.request_context.meta.get("progress_token") if ctx.request_context and ctx.request_context.meta else 0
166+
)
165167
return str(progress_token)
166168

167169

examples/snippets/clients/stdio_client.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
import asyncio
66
import os
77

8-
from pydantic import AnyUrl
9-
108
from mcp import ClientSession, StdioServerParameters, types
119
from mcp.client.stdio import stdio_client
1210
from mcp.shared.context import RequestContext
@@ -59,7 +57,7 @@ async def run():
5957
print(f"Available tools: {[t.name for t in tools.tools]}")
6058

6159
# Read a resource (greeting resource from fastmcp_quickstart)
62-
resource_content = await session.read_resource(AnyUrl("greeting://World"))
60+
resource_content = await session.read_resource("greeting://World")
6361
content_block = resource_content.contents[0]
6462
if isinstance(content_block, types.TextContent):
6563
print(f"Resource content: {content_block.text}")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ dependencies = [
3737
"jsonschema>=4.20.0",
3838
"pywin32>=311; sys_platform == 'win32'",
3939
"pyjwt[crypto]>=2.10.1",
40-
"typing-extensions>=4.9.0",
40+
"typing-extensions>=4.13.0",
4141
"typing-inspection>=0.4.1",
4242
]
4343

src/mcp/client/client.py

Lines changed: 69 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,29 @@
55
from contextlib import AsyncExitStack
66
from typing import Any
77

8-
from pydantic import AnyUrl
9-
10-
import mcp.types as types
118
from mcp.client._memory import InMemoryTransport
12-
from mcp.client.session import (
13-
ClientSession,
14-
ElicitationFnT,
15-
ListRootsFnT,
16-
LoggingFnT,
17-
MessageHandlerFnT,
18-
SamplingFnT,
19-
)
9+
from mcp.client.session import ClientSession, ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
2010
from mcp.server import Server
2111
from mcp.server.fastmcp import FastMCP
2212
from mcp.shared.session import ProgressFnT
13+
from mcp.types import (
14+
CallToolResult,
15+
CompleteResult,
16+
EmptyResult,
17+
GetPromptResult,
18+
Implementation,
19+
ListPromptsResult,
20+
ListResourcesResult,
21+
ListResourceTemplatesResult,
22+
ListToolsResult,
23+
LoggingLevel,
24+
PaginatedRequestParams,
25+
PromptReference,
26+
ReadResourceResult,
27+
RequestParamsMeta,
28+
ResourceTemplateReference,
29+
ServerCapabilities,
30+
)
2331

2432

2533
class Client:
@@ -39,8 +47,11 @@ class Client:
3947
def add(a: int, b: int) -> int:
4048
return a + b
4149
42-
async with Client(server) as client:
43-
result = await client.call_tool("add", {"a": 1, "b": 2})
50+
async def main():
51+
async with Client(server) as client:
52+
result = await client.call_tool("add", {"a": 1, "b": 2})
53+
54+
asyncio.run(main())
4455
```
4556
"""
4657

@@ -62,7 +73,7 @@ def __init__(
6273
list_roots_callback: ListRootsFnT | None = None,
6374
logging_callback: LoggingFnT | None = None,
6475
message_handler: MessageHandlerFnT | None = None,
65-
client_info: types.Implementation | None = None,
76+
client_info: Implementation | None = None,
6677
elicitation_callback: ElicitationFnT | None = None,
6778
) -> None:
6879
"""Initialize the client with a server.
@@ -143,13 +154,13 @@ def session(self) -> ClientSession:
143154
return self._session
144155

145156
@property
146-
def server_capabilities(self) -> types.ServerCapabilities | None:
157+
def server_capabilities(self) -> ServerCapabilities | None:
147158
"""The server capabilities received during initialization, or None if not yet initialized."""
148159
return self.session.get_server_capabilities()
149160

150-
async def send_ping(self) -> types.EmptyResult:
161+
async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
151162
"""Send a ping request to the server."""
152-
return await self.session.send_ping()
163+
return await self.session.send_ping(meta=meta)
153164

154165
async def send_progress_notification(
155166
self,
@@ -166,36 +177,47 @@ async def send_progress_notification(
166177
message=message,
167178
)
168179

169-
async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResult:
180+
async def set_logging_level(self, level: LoggingLevel, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
170181
"""Set the logging level on the server."""
171-
return await self.session.set_logging_level(level)
182+
return await self.session.set_logging_level(level=level, meta=meta)
172183

173-
async def list_resources(self, *, cursor: str | None = None) -> types.ListResourcesResult:
184+
async def list_resources(
185+
self,
186+
*,
187+
cursor: str | None = None,
188+
meta: RequestParamsMeta | None = None,
189+
) -> ListResourcesResult:
174190
"""List available resources from the server."""
175-
return await self.session.list_resources(params=types.PaginatedRequestParams(cursor=cursor))
191+
return await self.session.list_resources(params=PaginatedRequestParams(cursor=cursor, _meta=meta))
176192

177-
async def list_resource_templates(self, *, cursor: str | None = None) -> types.ListResourceTemplatesResult:
193+
async def list_resource_templates(
194+
self,
195+
*,
196+
cursor: str | None = None,
197+
meta: RequestParamsMeta | None = None,
198+
) -> ListResourceTemplatesResult:
178199
"""List available resource templates from the server."""
179-
return await self.session.list_resource_templates(params=types.PaginatedRequestParams(cursor=cursor))
200+
return await self.session.list_resource_templates(params=PaginatedRequestParams(cursor=cursor, _meta=meta))
180201

181-
async def read_resource(self, uri: str | AnyUrl) -> types.ReadResourceResult:
202+
async def read_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> ReadResourceResult:
182203
"""Read a resource from the server.
183204
184205
Args:
185206
uri: The URI of the resource to read.
207+
meta: Additional metadata for the request
186208
187209
Returns:
188210
The resource content.
189211
"""
190-
return await self.session.read_resource(uri)
212+
return await self.session.read_resource(uri, meta=meta)
191213

192-
async def subscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult:
214+
async def subscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
193215
"""Subscribe to resource updates."""
194-
return await self.session.subscribe_resource(uri)
216+
return await self.session.subscribe_resource(uri, meta=meta)
195217

196-
async def unsubscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult:
218+
async def unsubscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
197219
"""Unsubscribe from resource updates."""
198-
return await self.session.unsubscribe_resource(uri)
220+
return await self.session.unsubscribe_resource(uri, meta=meta)
199221

200222
async def call_tool(
201223
self,
@@ -204,8 +226,8 @@ async def call_tool(
204226
read_timeout_seconds: float | None = None,
205227
progress_callback: ProgressFnT | None = None,
206228
*,
207-
meta: dict[str, Any] | None = None,
208-
) -> types.CallToolResult:
229+
meta: RequestParamsMeta | None = None,
230+
) -> CallToolResult:
209231
"""Call a tool on the server.
210232
211233
Args:
@@ -226,28 +248,36 @@ async def call_tool(
226248
meta=meta,
227249
)
228250

229-
async def list_prompts(self, *, cursor: str | None = None) -> types.ListPromptsResult:
251+
async def list_prompts(
252+
self,
253+
*,
254+
cursor: str | None = None,
255+
meta: RequestParamsMeta | None = None,
256+
) -> ListPromptsResult:
230257
"""List available prompts from the server."""
231-
return await self.session.list_prompts(params=types.PaginatedRequestParams(cursor=cursor))
258+
return await self.session.list_prompts(params=PaginatedRequestParams(cursor=cursor, _meta=meta))
232259

233-
async def get_prompt(self, name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult:
260+
async def get_prompt(
261+
self, name: str, arguments: dict[str, str] | None = None, *, meta: RequestParamsMeta | None = None
262+
) -> GetPromptResult:
234263
"""Get a prompt from the server.
235264
236265
Args:
237266
name: The name of the prompt
238267
arguments: Arguments to pass to the prompt
268+
meta: Additional metadata for the request
239269
240270
Returns:
241271
The prompt content.
242272
"""
243-
return await self.session.get_prompt(name=name, arguments=arguments)
273+
return await self.session.get_prompt(name=name, arguments=arguments, meta=meta)
244274

245275
async def complete(
246276
self,
247-
ref: types.ResourceTemplateReference | types.PromptReference,
277+
ref: ResourceTemplateReference | PromptReference,
248278
argument: dict[str, str],
249279
context_arguments: dict[str, str] | None = None,
250-
) -> types.CompleteResult:
280+
) -> CompleteResult:
251281
"""Get completions for a prompt or resource template argument.
252282
253283
Args:
@@ -260,9 +290,9 @@ async def complete(
260290
"""
261291
return await self.session.complete(ref=ref, argument=argument, context_arguments=context_arguments)
262292

263-
async def list_tools(self, *, cursor: str | None = None) -> types.ListToolsResult:
293+
async def list_tools(self, *, cursor: str | None = None, meta: RequestParamsMeta | None = None) -> ListToolsResult:
264294
"""List available tools from the server."""
265-
return await self.session.list_tools(params=types.PaginatedRequestParams(cursor=cursor))
295+
return await self.session.list_tools(params=PaginatedRequestParams(cursor=cursor, _meta=meta))
266296

267297
async def send_roots_list_changed(self) -> None:
268298
"""Send a notification that the roots list has changed."""

src/mcp/client/experimental/tasks.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import mcp.types as types
3030
from mcp.shared.experimental.tasks.polling import poll_until_terminal
31+
from mcp.types._types import RequestParamsMeta
3132

3233
if TYPE_CHECKING:
3334
from mcp.client.session import ClientSession
@@ -53,7 +54,7 @@ async def call_tool_as_task(
5354
arguments: dict[str, Any] | None = None,
5455
*,
5556
ttl: int = 60000,
56-
meta: dict[str, Any] | None = None,
57+
meta: RequestParamsMeta | None = None,
5758
) -> types.CreateTaskResult:
5859
"""Call a tool as a task, returning a CreateTaskResult for polling.
5960
@@ -87,17 +88,13 @@ async def call_tool_as_task(
8788
# Get result
8889
final = await session.experimental.get_task_result(task_id, CallToolResult)
8990
"""
90-
_meta: types.RequestParams.Meta | None = None
91-
if meta is not None:
92-
_meta = types.RequestParams.Meta(**meta)
93-
9491
return await self._session.send_request(
9592
types.CallToolRequest(
9693
params=types.CallToolRequestParams(
9794
name=name,
9895
arguments=arguments,
9996
task=types.TaskMetadata(ttl=ttl),
100-
_meta=_meta,
97+
_meta=meta,
10198
),
10299
),
103100
types.CreateTaskResult,
@@ -113,9 +110,7 @@ async def get_task(self, task_id: str) -> types.GetTaskResult:
113110
GetTaskResult containing the task status and metadata
114111
"""
115112
return await self._session.send_request(
116-
types.GetTaskRequest(
117-
params=types.GetTaskRequestParams(task_id=task_id),
118-
),
113+
types.GetTaskRequest(params=types.GetTaskRequestParams(task_id=task_id)),
119114
types.GetTaskResult,
120115
)
121116

0 commit comments

Comments
 (0)