Skip to content

Add support for MCP servers #1100

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

Merged
merged 42 commits into from
Mar 17, 2025
Merged

Add support for MCP servers #1100

merged 42 commits into from
Mar 17, 2025

Conversation

Kludex
Copy link
Member

@Kludex Kludex commented Mar 11, 2025

No description provided.

Copy link
Contributor

hyperlint-ai bot commented Mar 11, 2025

PR Change Summary

Added support for MCP (Model Control Protocol) servers, enhancing integration capabilities for external services.

  • Introduced documentation for MCP servers integration.
  • Provided installation instructions for MCP support.
  • Included usage examples for HTTP/SSE and stdio-based servers.
  • Demonstrated how to connect to multiple MCP servers simultaneously.

Added Files

  • docs/api/mcp.md
  • docs/mcp_servers.md

How can I customize these reviews?

Check out the Hyperlint AI Reviewer docs for more information on how to customize the review.

If you just want to ignore it on this PR, you can add the hyperlint-ignore label to the PR. Future changes won't trigger a Hyperlint review.

Note specifically for link checks, we only check the first 30 links in a file and we cache the results for several hours (for instance, if you just added a page, you might experience this). Our recommendation is to add hyperlint-ignore to the PR to ignore the link check for this PR.

What is Hyperlint?

Hyperlint is an AI agent that helps you write, edit, and maintain your documentation.

Learn more about the Hyperlint AI reviewer and the checks that we can run on your documentation.

@@ -148,6 +150,7 @@ def __init__(
result_tool_description: str | None = None,
result_retries: int | None = None,
tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (),
mcp_servers: dict[str, MCPServer] | None = None,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I use the name of the server... Maybe it can be a list...

Copy link

github-actions bot commented Mar 11, 2025

Docs Preview

commit: 46cf989
Preview URL: https://0cf3982a-pydantic-ai-previews.pydantic.workers.dev


for server in ctx.deps.mcp_servers:
tools = await server.list_tools()
if tool_name in {tool.name for tool in tools}:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to do anything to ensure there aren’t naming conflicts between servers?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should I do? Error? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess so, or namespace the tools by server or something

Comment on lines 48 to 93
async def main():
async with MCPServer.stdio('python', ['-m', 'pydantic_ai.mcp']) as server:
agent = Agent('openai:gpt-4o', mcp_servers=[server])
result = await agent.run('Can you convert 30 degrees celsius to fahrenheit?')
print(result.data)
#> 30 degrees Celsius is equal to 86 degrees Fahrenheit.
Copy link
Contributor

@dmontagu dmontagu Mar 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this using the server in any way? It would be nice if the example did do that. Or at least looked more like it did that lol.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see that it is. I think you should make it more clear that this is using the example MCP server present in the pydantic_ai.mcp module.

I think it's also worth printing out the full message history here if it shows that a tool call related to the MCP server was made.

Copy link
Member

@samuelcolvin samuelcolvin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to change this so we have separate types for:

  • stdio servers where pydantic-ai is responsible for running the server as a subprocess
  • SSE where pydantic-ai is just connecting to a server running in a different process (maybe remotely)

Here's a rough sketch of running both:

from pydantic_ai import Agent
from pydantic_ai.mcp import MCPSubprocessServer, MCPRemoteServer

agent = Agent(
    'openai:gpt-4o',
    mcp_servers=[
        MCPSubprocessServer('python', ('-m', 'my_mcp_server')),
        MCPRemoteServer('http://localhost:8000/sse')
    ],
)

async def main():
    async for agent.run_mcp_servers():
        ...

The advantage of this distinction are:

  • more type safe
  • easier to document the difference and clearer to the user what's going
  • no need to use run_mcp_servers for MCPRemoteServer which can use the existing http connection from cached_async_http_client
  • I also thing "subprocess" vs "remote" is a clearer distinction for most developers than "stdio" vs "sse" although of course we should document what transport they're using under the hood

@@ -0,0 +1,72 @@
# MCP Servers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be called "MCP Client" since pydantic-ai is acting as a client

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using the same notation everybody is tho. Claude Desktop, Cursor, and Cline use the "MCP Server" terminology even on the STDIO.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, but we're building an MCP client, so we should call it that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the chapter is about MCP Servers. It far better for SEO. No one uses the term client, even when you are configuring the client: Cursor, Claude Desktop, Cline.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rephrase the introduction phrase as something like:

PydanticAI supports integration with MCP Servers and act as a MCP client...

maybe link with https://modelcontextprotocol.io/introduction#general-architecture.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should have a new section in docs:

  • mcp/index.md - a general introduction saying PydanticAI can be used as an MCP client or to build servers, and comes with some servers
  • mcp/client.md - this page - describing how to use PydanticAI as an MCP client
  • mcp/run-python.md - docs for MCP server to run Python code in a sandbox #1140
  • ... more


async def main():
async with MCPServer.sse(url='http://localhost:8000/sse') as mcp_server:
Agent('your-model', mcp_servers=[mcp_server])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we were providing a way to set or initialise the server on an agent created in the global scope?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, surely with and SSE server, we're just using an HTTP connection, so we should be able to use the existing http connection and we then don't need a context manager.

proper initialization and cleanup of resources. You can use either an HTTP/SSE server or a
stdio-based server.

### HTTP/SSE Server
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need some explanation on what's going on here.

@Kludex
Copy link
Member Author

Kludex commented Mar 13, 2025

@samuelcolvin What happens if the user didn't use the following, and they have set a MCPSubprocessServer?

async with agent.run_mcp_servers():
    ...

Should we error? Or... ? Creating and recreating the server can be too resource intensive.

@samuelcolvin
Copy link
Member

error.

@Wh1isper
Copy link
Contributor

We have to drop python 3.9 support if using mcp official lib, while it's ok for me

@Kludex
Copy link
Member Author

Kludex commented Mar 13, 2025

We have to drop python 3.9 support if using mcp official lib, while it's ok for me

Nop. I rather support 3.9 on their side. Not much work for it.

@Kludex
Copy link
Member Author

Kludex commented Mar 14, 2025

At this point, since the user has to do run_mcp_servers, and we have a global HTTP client, I think it would make sense to make Agent an async context manager.

async with Agent() as agent:  # This creates the HTTP client and runs the MCP servers.
    await agent.run('What is the capital of France?')

Honestly, just the HTTP client already justifies to make the Agent an async context manager.

@Kludex Kludex requested a review from samuelcolvin March 14, 2025 10:27
Copy link
Member

@samuelcolvin samuelcolvin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

otherwise looking good.

@@ -0,0 +1,72 @@
# MCP Servers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, but we're building an MCP client, so we should call it that.

You can have a MCP server running on a remote server. In this case, you'd use the
[`MCPRemoteServer`][pydantic_ai.mcp.MCPRemoteServer] class:

```python {title="basic_mcp_setup.py" test="skip"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
```python {title="basic_mcp_setup.py" test="skip"}
```python {title="mcp_remote_server.py" test="skip"}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, these examples need to be run!

Copy link
Member Author

@Kludex Kludex Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you suggest how? It's a remote server. I've enabled the other one.

@@ -0,0 +1,72 @@
# MCP Servers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rephrase the introduction phrase as something like:

PydanticAI supports integration with MCP Servers and act as a MCP client...

maybe link with https://modelcontextprotocol.io/introduction#general-architecture.

Comment on lines 84 to 87
PydanticAI comes with two ways to connect to MCP servers:

- [`MCPRemoteServer`][pydantic_ai.mcp.MCPRemoteServer] which connects to an MCP server using the [HTTP SSE](https://modelcontextprotocol.io/docs/concepts/transports#server-sent-events-sse) transport
- [`MCPSubprocessServer`][pydantic_ai.mcp.MCPSubprocessServer] which runs the server as a subprocess and connects to it using the [stdio](https://modelcontextprotocol.io/docs/concepts/transports#standard-input%2Foutput-stdio) transport
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it a bit confusing that the MCP*Server classes are actually clients. Can't we name them MCP*Client?

Copy link
Member Author

@Kludex Kludex Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should see the object as a representation of the server.

command: str
"""The command to run."""

args: Sequence[str]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlikely but this will allow MCPSubprocessServer('python', '-m pydantic_ai_examples.mcp_server') which will not work as expected.

Maybe enforce list[str], or list[str] | tuple[str, ...]? Feel free to leave it as is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how shlex behaves on non Unix shells, and I think it's best to enforce a proper sequence of str instead of trying to parse str inputs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So either we leave it as is if you think passing a bare string will not be a common mistake, or change the type hint to list[str] | tuple[str, ...]?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone get this mistake, I'll fix it.

Copy link
Member

@samuelcolvin samuelcolvin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do some work on the docs tomorrow.

@@ -0,0 +1,72 @@
# MCP Servers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should have a new section in docs:

  • mcp/index.md - a general introduction saying PydanticAI can be used as an MCP client or to build servers, and comes with some servers
  • mcp/client.md - this page - describing how to use PydanticAI as an MCP client
  • mcp/run-python.md - docs for MCP server to run Python code in a sandbox #1140
  • ... more



@dataclass
class MCPSubprocessServer(MCPServer):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I was wrong about this name, we should use the names MCP users which are clearly "stdio" and "sse", my mistake ✋ .

I guess we should call this MCPServerStdio, and the other one MCPServerSSE.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I was wrong about this name, we should use the names MCP users which are clearly "stdio" and "sse", my mistake ✋ .

I guess we should call this MCPServerStdio, and the other one MCPServerSSE.

...

@Kludex Kludex enabled auto-merge (squash) March 17, 2025 15:02
@Viicos Viicos disabled auto-merge March 17, 2025 15:04
Kludex and others added 2 commits March 17, 2025 16:42
Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>
@Kludex Kludex merged commit 444f5d0 into main Mar 17, 2025
16 checks passed
@Kludex Kludex deleted the mcp-support branch March 17, 2025 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants