Skip to content

Conversation

strawgate
Copy link
Contributor

@strawgate strawgate commented Sep 3, 2025

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:

  • Add tests
  • Update docs
  • Coverage

@strawgate strawgate changed the title Add FastMCP Toolset Add FastMCPToolset Sep 3, 2025
@strawgate strawgate force-pushed the fastmcp-toolset branch 3 times, most recently from f68660d to 54367c8 Compare September 4, 2025 22:13
@strawgate
Copy link
Contributor Author

@DouweM this is ready for review when you have a chance

@DouweM DouweM self-assigned this Sep 8, 2025
@@ -88,6 +88,8 @@ cli = [
]
# MCP
mcp = ["mcp>=1.12.3"]
# FastMCP
fastmcp = ["fastmcp>=2.12.0"]
Copy link
Collaborator

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)
Copy link
Collaborator

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.

Copy link
Contributor Author

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

Copy link
Collaborator

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)
Copy link
Collaborator

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]):
Copy link
Collaborator

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?

Comment on lines +54 to +57
tool_error_behavior: Literal['model_retry', 'error']
fastmcp_client: Client[Any]

max_retries: int
Copy link
Collaborator

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__.

Suggested change
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:
Copy link
Collaborator

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-TextParts seems reasonable though -- I'll ask for the same to be implemented in that PR.

Comment on lines +136 to +144
# 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)
Copy link
Collaborator

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)

Suggested change
# 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
Copy link
Collaborator

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Contributing FastMCP Toolset
2 participants