Skip to content

Commit b0a61fb

Browse files
authored
Merge branch 'main' into dmontagu/add-experiment-metadata
2 parents 140ddf3 + bfbf2ca commit b0a61fb

21 files changed

+1718
-51
lines changed

docs/api/toolsets.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@
1414
- PreparedToolset
1515
- WrapperToolset
1616
- ToolsetFunc
17+
18+
::: pydantic_ai.toolsets.fastmcp

docs/install.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pip/uv-add "pydantic-ai-slim[openai]"
5555
* `tavily` - installs `tavily-python` [PyPI ↗](https://pypi.org/project/tavily-python){:target="_blank"}
5656
* `cli` - installs `rich` [PyPI ↗](https://pypi.org/project/rich){:target="_blank"}, `prompt-toolkit` [PyPI ↗](https://pypi.org/project/prompt-toolkit){:target="_blank"}, and `argcomplete` [PyPI ↗](https://pypi.org/project/argcomplete){:target="_blank"}
5757
* `mcp` - installs `mcp` [PyPI ↗](https://pypi.org/project/mcp){:target="_blank"}
58+
* `fastmcp` - installs `fastmcp` [PyPI ↗](https://pypi.org/project/fastmcp){:target="_blank"}
5859
* `a2a` - installs `fasta2a` [PyPI ↗](https://pypi.org/project/fasta2a){:target="_blank"}
5960
* `ag-ui` - installs `ag-ui-protocol` [PyPI ↗](https://pypi.org/project/ag-ui-protocol){:target="_blank"} and `starlette` [PyPI ↗](https://pypi.org/project/starlette){:target="_blank"}
6061
* `dbos` - installs [`dbos`](durable_execution/dbos.md) [PyPI ↗](https://pypi.org/project/dbos){:target="_blank"}

docs/mcp/client.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ to use their tools.
55

66
## Install
77

8-
You need to either install [`pydantic-ai`](../install.md), or[`pydantic-ai-slim`](../install.md#slim-install) with the `mcp` optional group:
8+
You need to either install [`pydantic-ai`](../install.md), or [`pydantic-ai-slim`](../install.md#slim-install) with the `mcp` optional group:
99

1010
```bash
1111
pip/uv-add "pydantic-ai-slim[mcp]"

docs/mcp/fastmcp-client.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# FastMCP Client
2+
3+
[FastMCP](https://gofastmcp.com/) is a higher-level MCP framework that bills itself as "The fast, Pythonic way to build MCP servers and clients." It supports additional capabilities on top of the MCP specification like [Tool Transformation](https://gofastmcp.com/patterns/tool-transformation), [OAuth](https://gofastmcp.com/clients/auth/oauth), and more.
4+
5+
As an alternative to Pydantic AI's standard [`MCPServer` MCP client](client.md) built on the [MCP SDK](https://github.com/modelcontextprotocol/python-sdk), you can use the [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset] [toolset](../toolsets.md) that leverages the [FastMCP Client](https://gofastmcp.com/clients/) to connect to local and remote MCP servers, whether or not they're built using [FastMCP Server](https://gofastmcp.com/servers/).
6+
7+
Note that it does not yet support integration elicitation or sampling, which are supported by the [standard `MCPServer` client](client.md).
8+
9+
## Install
10+
11+
To use the `FastMCPToolset`, you will need to install [`pydantic-ai-slim`](../install.md#slim-install) with the `fastmcp` optional group:
12+
13+
```bash
14+
pip/uv-add "pydantic-ai-slim[fastmcp]"
15+
```
16+
17+
## Usage
18+
19+
A `FastMCPToolset` can then be created from:
20+
21+
- A FastMCP Server: `#!python FastMCPToolset(fastmcp.FastMCP('my_server'))`
22+
- A FastMCP Client: `#!python FastMCPToolset(fastmcp.Client(...))`
23+
- A FastMCP Transport: `#!python FastMCPToolset(fastmcp.StdioTransport(command='uvx', args=['mcp-run-python', 'stdio']))`
24+
- A Streamable HTTP URL: `#!python FastMCPToolset('http://localhost:8000/mcp')`
25+
- An HTTP SSE URL: `#!python FastMCPToolset('http://localhost:8000/sse')`
26+
- A Python Script: `#!python FastMCPToolset('my_server.py')`
27+
- A Node.js Script: `#!python FastMCPToolset('my_server.js')`
28+
- A JSON MCP Configuration: `#!python FastMCPToolset({'mcpServers': {'my_server': {'command': 'uvx', 'args': ['mcp-run-python', 'stdio']}}})`
29+
30+
If you already have a [FastMCP Server](https://gofastmcp.com/servers) in the same codebase as your Pydantic AI agent, you can create a `FastMCPToolset` directly from it and save agent a network round trip:
31+
32+
```python
33+
from fastmcp import FastMCP
34+
35+
from pydantic_ai import Agent
36+
from pydantic_ai.toolsets.fastmcp import FastMCPToolset
37+
38+
fastmcp_server = FastMCP('my_server')
39+
@fastmcp_server.tool()
40+
async def add(a: int, b: int) -> int:
41+
return a + b
42+
43+
toolset = FastMCPToolset(fastmcp_server)
44+
45+
agent = Agent('openai:gpt-5', toolsets=[toolset])
46+
47+
async def main():
48+
result = await agent.run('What is 7 plus 5?')
49+
print(result.output)
50+
#> The answer is 12.
51+
```
52+
53+
_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_
54+
55+
Connecting your agent to a Streamable HTTP MCP Server is as simple as:
56+
57+
```python
58+
from pydantic_ai import Agent
59+
from pydantic_ai.toolsets.fastmcp import FastMCPToolset
60+
61+
toolset = FastMCPToolset('http://localhost:8000/mcp')
62+
63+
agent = Agent('openai:gpt-5', toolsets=[toolset])
64+
```
65+
66+
_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_
67+
68+
You can also create a `FastMCPToolset` from a JSON MCP Configuration:
69+
70+
```python
71+
from pydantic_ai import Agent
72+
from pydantic_ai.toolsets.fastmcp import FastMCPToolset
73+
74+
mcp_config = {
75+
'mcpServers': {
76+
'time_mcp_server': {
77+
'command': 'uvx',
78+
'args': ['mcp-run-python', 'stdio']
79+
}
80+
}
81+
}
82+
83+
toolset = FastMCPToolset(mcp_config)
84+
85+
agent = Agent('openai:gpt-5', toolsets=[toolset])
86+
```
87+
88+
_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_

docs/mcp/overview.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# Model Context Protocol (MCP)
22

3-
Pydantic AI supports [Model Context Protocol (MCP)](https://modelcontextprotocol.io) in two ways:
3+
Pydantic AI supports [Model Context Protocol (MCP)](https://modelcontextprotocol.io) in multiple ways:
44

5-
1. [Agents](../agents.md) can connect to MCP servers and user their tools
6-
1. Pydantic AI can act as an MCP client and connect directly to local and remote MCP servers, [learn more …](client.md)
7-
2. Some model providers can themselves connect to remote MCP servers, [learn more …](../builtin-tools.md#mcp-server-tool)
8-
2. Agents can be used within MCP servers, [learn more …](server.md)
5+
1. [Agents](../agents.md) can connect to MCP servers and use their tools using three different methods:
6+
1. Pydantic AI can act as an MCP client and connect directly to local and remote MCP servers. [Learn more](client.md) about [`MCPServer`][pydantic_ai.mcp.MCPServer].
7+
2. Pydantic AI can use the [FastMCP Client](https://gofastmcp.com/clients/client/) to connect to local and remote MCP servers, whether or not they're built using [FastMCP Server](https://gofastmcp.com/servers). [Learn more](fastmcp-client.md) about [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset].
8+
3. Some model providers can themselves connect to remote MCP servers using a "built-in tool". [Learn more](../builtin-tools.md#mcp-server-tool) about [`MCPServerTool`][pydantic_ai.builtin_tools.MCPServerTool].
9+
2. Agents can be used within MCP servers. [Learn more](server.md)
910

1011
## What is MCP?
1112

docs/toolsets.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,10 @@ If you want to reuse a network connection or session across tool listings and ca
661661

662662
### MCP Servers
663663

664-
See the [MCP Client](./mcp/client.md) documentation for how to use MCP servers with Pydantic AI.
664+
Pydantic AI provides two toolsets that allow an agent to connect to and call tools on local and remote MCP Servers:
665+
666+
1. `MCPServer`: the [MCP SDK-based Client](./mcp/client.md) which offers more direct control by leveraging the MCP SDK directly
667+
2. `FastMCPToolset`: the [FastMCP-based Client](./mcp/fastmcp-client.md) which offers additional capabilities like Tool Transformation, simpler OAuth configuration, and more.
665668

666669
### LangChain Tools {#langchain-tools}
667670

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ nav:
4949
- MCP:
5050
- Overview: mcp/overview.md
5151
- mcp/client.md
52+
- mcp/fastmcp-client.md
5253
- mcp/server.md
5354
- Multi-Agent Patterns: multi-agent-applications.md
5455
- Testing: testing.md

pydantic_ai_slim/pydantic_ai/_output.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import inspect
44
import json
5+
import re
56
from abc import ABC, abstractmethod
67
from collections.abc import Awaitable, Callable, Sequence
78
from dataclasses import dataclass, field
@@ -70,6 +71,7 @@
7071

7172
DEFAULT_OUTPUT_TOOL_NAME = 'final_result'
7273
DEFAULT_OUTPUT_TOOL_DESCRIPTION = 'The final response which ends this conversation'
74+
OUTPUT_TOOL_NAME_SANITIZER = re.compile(r'[^a-zA-Z0-9-_]')
7375

7476

7577
async def execute_traced_output_function(
@@ -997,7 +999,9 @@ def build(
997999
if name is None:
9981000
name = default_name
9991001
if multiple:
1000-
name += f'_{object_def.name}'
1002+
# strip unsupported characters like "[" and "]" from generic class names
1003+
safe_name = OUTPUT_TOOL_NAME_SANITIZER.sub('', object_def.name or '')
1004+
name += f'_{safe_name}'
10011005

10021006
i = 1
10031007
original_name = name

pydantic_ai_slim/pydantic_ai/models/openai.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,8 @@ async def _map_messages( # noqa: C901
14311431
call_id=call_id,
14321432
type='function_call',
14331433
)
1434+
if profile.openai_responses_requires_function_call_status_none:
1435+
param['status'] = None # type: ignore[reportGeneralTypeIssues]
14341436
if id and send_item_ids: # pragma: no branch
14351437
param['id'] = id
14361438
openai_messages.append(param)

pydantic_ai_slim/pydantic_ai/models/test.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,14 @@ class _WrappedTextOutput:
4444
value: str | None
4545

4646

47-
@dataclass
47+
@dataclass(init=False)
4848
class _WrappedToolOutput:
4949
"""A wrapper class to tag an output that came from the custom_output_args field."""
5050

51-
value: Any | None
51+
value: dict[str, Any] | None
52+
53+
def __init__(self, value: Any | None):
54+
self.value = pydantic_core.to_jsonable_python(value)
5255

5356

5457
@dataclass(init=False)
@@ -364,7 +367,7 @@ def __init__(self, schema: _utils.ObjectJsonSchema, seed: int = 0):
364367
self.defs = schema.get('$defs', {})
365368
self.seed = seed
366369

367-
def generate(self) -> Any:
370+
def generate(self) -> dict[str, Any]:
368371
"""Generate data for the JSON schema."""
369372
return self._gen_any(self.schema)
370373

0 commit comments

Comments
 (0)