-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add FastMCPToolset #2784
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add FastMCPToolset #2784
Conversation
f68660d
to
54367c8
Compare
54367c8
to
27592c7
Compare
@DouweM this is ready for review when you have a chance |
@@ -88,6 +88,8 @@ cli = [ | |||
] | |||
# MCP | |||
mcp = ["mcp>=1.12.3"] | |||
# FastMCP | |||
fastmcp = ["fastmcp>=2.12.0"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please also add it to the main pyproject.toml
, and as it says above "if you add optional groups, please update docs/install.md"
async def my_tool(a: int, b: int) -> int: | ||
return a + b | ||
|
||
toolset = FastMCPToolset.from_fastmcp_server(fastmcp_server) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How common is this compared to creating it from a FastMCP client? In a hypothetical world where there's only Pydantic AI, this code snippet would just register the tool directly on the agent. I suppose this is useful in a monorepo? I think it'd be good to explain a bit more clearly what specific scenarios this functionality is meant for. And to have an example of using a FastMCP client.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The most common will be providing a FastMCP Client for sure. Ultimately this is just a helper that wraps the server in a client so we can remove it and just document how can you make a client for an in memory fastmcp server
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having talked about this with Jeremiah, I wouldn't mind eventually making this the officially supported way to use MCP with Pydantic AI and handing over the responsibility to keep up with the MCP spec to them. That'd be a v2 thing, but in the meantime we can at least recommend people try this out if they want features we don't have on MCPServer
yet, like MCP resources, prompts, progress...
So what do you think about making it easier to build an FastMCPToolset
from a URL + optional headers, or command + args + env, similar to how the various MCPServer
subclasses are constructed? That'd be a simple wrapper method around from_mcp_config
, which then could be the primary use case documented here, with the FastMCP client, server, and mcp.json options secondary. We can document FastMCP as an alternative Pydantic AI + MCP implementation with broader feature support, rather than something specific to FastMCP servers.
async def my_tool(a: int, b: int) -> int: | ||
return a + b | ||
|
||
toolset = FastMCPToolset.from_fastmcp_server(fastmcp_server) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having talked about this with Jeremiah, I wouldn't mind eventually making this the officially supported way to use MCP with Pydantic AI and handing over the responsibility to keep up with the MCP spec to them. That'd be a v2 thing, but in the meantime we can at least recommend people try this out if they want features we don't have on MCPServer
yet, like MCP resources, prompts, progress...
So what do you think about making it easier to build an FastMCPToolset
from a URL + optional headers, or command + args + env, similar to how the various MCPServer
subclasses are constructed? That'd be a simple wrapper method around from_mcp_config
, which then could be the primary use case documented here, with the FastMCP client, server, and mcp.json options secondary. We can document FastMCP as an alternative Pydantic AI + MCP implementation with broader feature support, rather than something specific to FastMCP servers.
ToolErrorBehavior = Literal['model_retry', 'error'] | ||
|
||
|
||
class FastMCPToolset(AbstractToolset[AgentDepsT]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this a @dataclass
?
tool_error_behavior: Literal['model_retry', 'error'] | ||
fastmcp_client: Client[Any] | ||
|
||
max_retries: int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once this is a dataclass
, we can reorder this like this and may not need __init__
anymore. We can add the argument descriptions as field docstrings, and move things like setting the enter lock to a new __post_init__
.
tool_error_behavior: Literal['model_retry', 'error'] | |
fastmcp_client: Client[Any] | |
max_retries: int | |
fastmcp_client: Client[Any] | |
_: KW_ONLY | |
max_retries: int | |
tool_error_behavior: Literal['model_retry', 'error'] |
return _map_fastmcp_tool_results(parts=call_tool_result.content) | ||
|
||
# Otherwise, if we have structured content, return that | ||
if call_tool_result.structured_content: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@strawgate Hmm, we also have a PR open to have our MCPServer
prefer structuredContent
over regular unstructured content
: #2854, to address #2742, which quotes MCP Specification section 5.2.6 Structured Content:
For backwards compatibility, a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block.
That suggests to me that structuredContent
should always be preferred, but from what you're saying there are MCP servers in the wild that wouldn't work well with that... Your logic here, to only prefer content
if it has non-TextPart
s seems reasonable though -- I'll ask for the same to be implemented in that PR.
# If any of the results are not text content, let's map them to Pydantic AI binary message parts | ||
if any(not isinstance(part, TextContent) for part in call_tool_result.content): | ||
return _map_fastmcp_tool_results(parts=call_tool_result.content) | ||
|
||
# Otherwise, if we have structured content, return that | ||
if call_tool_result.structured_content: | ||
return call_tool_result.structured_content | ||
|
||
return _map_fastmcp_tool_results(parts=call_tool_result.content) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this code structure slightly better, but this'll need a comment explaining why we can't always just return the structured content: (basically as you did in your PR comment)
# If any of the results are not text content, let's map them to Pydantic AI binary message parts | |
if any(not isinstance(part, TextContent) for part in call_tool_result.content): | |
return _map_fastmcp_tool_results(parts=call_tool_result.content) | |
# Otherwise, if we have structured content, return that | |
if call_tool_result.structured_content: | |
return call_tool_result.structured_content | |
return _map_fastmcp_tool_results(parts=call_tool_result.content) | |
if call_tool_result.structured_content and not any(not isinstance(part, TextContent) for part in call_tool_result.content): | |
return call_tool_result.structured_content | |
return _map_fastmcp_tool_results(parts=call_tool_result.content) |
if isinstance(part, ImageContent | AudioContent): | ||
return messages.BinaryContent(data=base64.b64decode(part.data), media_type=part.mimeType) | ||
|
||
msg = f'Unsupported/Unknown content block type: {type(part)}' # pragma: no cover |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the other types?
In mcp.py
, we have to specifically account for embedded resources or resource links (which we're possibly doing incorrectly: #2288)
Fixes: #2406
Introduces a FastMCPToolset which can take a FastMCP Client, a FastMCP Server, a config for an MCP Server, or an MCP JSON config.
This enables running FastMCP Servers in-memory, running Stdio servers via FastMCP, connecting to remote MCP Servers, transforming tools, etc.
Todo: