Skip to content

Commit 25d360c

Browse files
Allow custom clientInfo when connecting to MCP servers (#3572)
1 parent 5ba691d commit 25d360c

File tree

4 files changed

+89
-2
lines changed

4 files changed

+89
-2
lines changed

docs/mcp/client.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,31 @@ async def main():
473473
request. Anything supported by **httpx** (`verify`, `cert`, custom
474474
proxies, timeouts, etc.) therefore applies to all MCP traffic.
475475

476+
## Client Identification
477+
478+
When connecting to an MCP server, you can optionally specify an [Implementation](https://modelcontextprotocol.io/specification/2025-11-25/schema#implementation) object as client information that will be sent to the server during initialization. This is useful for:
479+
480+
- Identifying your application in server logs
481+
- Allowing servers to provide custom behavior based on the client
482+
- Debugging and monitoring MCP connections
483+
- Version-specific feature negotiation
484+
485+
All MCP client classes ([`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio], [`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP], and [`MCPServerSSE`][pydantic_ai.mcp.MCPServerSSE]) support the `client_info` parameter:
486+
487+
```python {title="mcp_client_with_name.py"}
488+
from mcp import types as mcp_types
489+
490+
from pydantic_ai.mcp import MCPServerSSE
491+
492+
server = MCPServerSSE(
493+
'http://localhost:3001/sse',
494+
client_info=mcp_types.Implementation(
495+
name='MyApplication',
496+
version='2.1.0',
497+
),
498+
)
499+
```
500+
476501
## MCP Sampling
477502

478503
!!! info "What is MCP Sampling?"

pydantic_ai_slim/pydantic_ai/mcp.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ def __init__(
387387
cache_resources: bool = True,
388388
*,
389389
id: str | None = None,
390+
client_info: mcp_types.Implementation | None = None,
390391
):
391392
self.tool_prefix = tool_prefix
392393
self.log_level = log_level
@@ -400,6 +401,7 @@ def __init__(
400401
self.elicitation_callback = elicitation_callback
401402
self.cache_tools = cache_tools
402403
self.cache_resources = cache_resources
404+
self.client_info = client_info
403405

404406
self._id = id or tool_prefix
405407

@@ -685,6 +687,7 @@ async def __aenter__(self) -> Self:
685687
if self._running_count == 0:
686688
async with AsyncExitStack() as exit_stack:
687689
self._read_stream, self._write_stream = await exit_stack.enter_async_context(self.client_streams())
690+
688691
client = ClientSession(
689692
read_stream=self._read_stream,
690693
write_stream=self._write_stream,
@@ -693,6 +696,7 @@ async def __aenter__(self) -> Self:
693696
logging_callback=self.log_handler,
694697
read_timeout_seconds=timedelta(seconds=self.read_timeout),
695698
message_handler=self._handle_notification,
699+
client_info=self.client_info,
696700
)
697701
self._client = await exit_stack.enter_async_context(client)
698702

@@ -879,6 +883,7 @@ def __init__(
879883
cache_tools: bool = True,
880884
cache_resources: bool = True,
881885
id: str | None = None,
886+
client_info: mcp_types.Implementation | None = None,
882887
):
883888
"""Build a new MCP server.
884889
@@ -902,6 +907,7 @@ def __init__(
902907
cache_resources: Whether to cache the list of resources.
903908
See [`MCPServer.cache_resources`][pydantic_ai.mcp.MCPServer.cache_resources].
904909
id: An optional unique ID for the MCP server. An MCP server needs to have an ID in order to be used in a durable execution environment like Temporal, in which case the ID will be used to identify the server's activities within the workflow.
910+
client_info: Information describing the MCP client implementation.
905911
"""
906912
self.command = command
907913
self.args = args
@@ -922,6 +928,7 @@ def __init__(
922928
cache_tools,
923929
cache_resources,
924930
id=id,
931+
client_info=client_info,
925932
)
926933

927934
@classmethod
@@ -1042,6 +1049,7 @@ def __init__(
10421049
elicitation_callback: ElicitationFnT | None = None,
10431050
cache_tools: bool = True,
10441051
cache_resources: bool = True,
1052+
client_info: mcp_types.Implementation | None = None,
10451053
**_deprecated_kwargs: Any,
10461054
):
10471055
"""Build a new MCP server.
@@ -1065,6 +1073,7 @@ def __init__(
10651073
See [`MCPServer.cache_tools`][pydantic_ai.mcp.MCPServer.cache_tools].
10661074
cache_resources: Whether to cache the list of resources.
10671075
See [`MCPServer.cache_resources`][pydantic_ai.mcp.MCPServer.cache_resources].
1076+
client_info: Information describing the MCP client implementation.
10681077
"""
10691078
if 'sse_read_timeout' in _deprecated_kwargs:
10701079
if read_timeout is not None:
@@ -1098,6 +1107,7 @@ def __init__(
10981107
cache_tools,
10991108
cache_resources,
11001109
id=id,
1110+
client_info=client_info,
11011111
)
11021112

11031113
@property

tests/mcp_server.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,25 @@ class UserResponse(BaseModel):
223223
response: str
224224

225225

226+
@mcp.tool()
227+
async def get_client_info(ctx: Context[ServerSession, None]) -> dict[str, Any] | None:
228+
"""Get information about the connected MCP client.
229+
230+
Returns:
231+
Dictionary with client info (name, version, etc.) or None if not available.
232+
"""
233+
client_params = ctx.session.client_params
234+
if client_params is None:
235+
return None
236+
client_info = client_params.clientInfo
237+
return {
238+
'name': client_info.name,
239+
'version': client_info.version,
240+
'title': getattr(client_info, 'title', None),
241+
'websiteUrl': getattr(client_info, 'websiteUrl', None),
242+
}
243+
244+
226245
@mcp.tool()
227246
async def use_elicitation(ctx: Context[ServerSession, None], question: str) -> str:
228247
"""Use elicitation callback to ask the user a question."""

tests/test_mcp.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
ElicitRequestParams,
5555
ElicitResult,
5656
ImageContent,
57+
Implementation,
5758
TextContent,
5859
)
5960

@@ -95,7 +96,7 @@ async def test_stdio_server(run_context: RunContext[int]):
9596
server = MCPServerStdio('python', ['-m', 'tests.mcp_server'])
9697
async with server:
9798
tools = [tool.tool_def for tool in (await server.get_tools(run_context)).values()]
98-
assert len(tools) == snapshot(19)
99+
assert len(tools) == snapshot(20)
99100
assert tools[0].name == 'celsius_to_fahrenheit'
100101
assert isinstance(tools[0].description, str)
101102
assert tools[0].description.startswith('Convert Celsius to Fahrenheit.')
@@ -156,7 +157,7 @@ async def test_stdio_server_with_cwd(run_context: RunContext[int]):
156157
server = MCPServerStdio('python', ['mcp_server.py'], cwd=test_dir)
157158
async with server:
158159
tools = await server.get_tools(run_context)
159-
assert len(tools) == snapshot(19)
160+
assert len(tools) == snapshot(20)
160161

161162

162163
async def test_process_tool_call(run_context: RunContext[int]) -> int:
@@ -2053,6 +2054,38 @@ async def test_instructions(mcp_server: MCPServerStdio) -> None:
20532054
assert mcp_server.instructions == 'Be a helpful assistant.'
20542055

20552056

2057+
async def test_client_info_passed_to_session() -> None:
2058+
"""Test that provided client_info is passed unchanged to ClientSession."""
2059+
implementation = Implementation(
2060+
name='MyCustomClient',
2061+
version='2.5.3',
2062+
title='Custom MCP client',
2063+
websiteUrl='https://example.com/client',
2064+
)
2065+
server = MCPServerStdio('python', ['-m', 'tests.mcp_server'], client_info=implementation)
2066+
2067+
async with server:
2068+
result = await server.direct_call_tool('get_client_info', {})
2069+
assert result == {
2070+
'name': 'MyCustomClient',
2071+
'version': '2.5.3',
2072+
'title': 'Custom MCP client',
2073+
'websiteUrl': 'https://example.com/client',
2074+
}
2075+
2076+
2077+
async def test_client_info_not_set() -> None:
2078+
"""Test that when client_info is not set, the default MCP client info is used."""
2079+
server = MCPServerStdio('python', ['-m', 'tests.mcp_server'])
2080+
2081+
async with server:
2082+
result = await server.direct_call_tool('get_client_info', {})
2083+
# When client_info is not set, the MCP library provides default client info
2084+
assert result is not None
2085+
assert isinstance(result, dict)
2086+
assert result['name'] == 'mcp'
2087+
2088+
20562089
async def test_agent_run_stream_with_mcp_server_http(allow_model_requests: None, model: Model):
20572090
server = MCPServerStreamableHTTP(url='https://mcp.deepwiki.com/mcp', timeout=30)
20582091
agent = Agent(model, toolsets=[server], instructions='Be concise.')

0 commit comments

Comments
 (0)