Skip to content

pydantic_ai.mcp

MCPServer

Bases: ABC

Base class for attaching agents to MCP servers.

See https://modelcontextprotocol.io for more information.

Source code in pydantic_ai_slim/pydantic_ai/mcp.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
class MCPServer(ABC):
    """Base class for attaching agents to MCP servers.

    See <https://modelcontextprotocol.io> for more information.
    """

    # these three fields should be re-defined by dataclass subclasses so they appear as fields
    tool_prefix: str | None = None
    log_level: mcp_types.LoggingLevel | None = None
    log_handler: LoggingFnT | None = None
    init_timeout: float = 5

    _running_count: int = 0
    _client: ClientSession
    _read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
    _write_stream: MemoryObjectSendStream[SessionMessage]
    _exit_stack: AsyncExitStack
    sampling_model: models.Model | None = None

    @abstractmethod
    @asynccontextmanager
    async def client_streams(
        self,
    ) -> AsyncIterator[
        tuple[
            MemoryObjectReceiveStream[SessionMessage | Exception],
            MemoryObjectSendStream[SessionMessage],
        ]
    ]:
        """Create the streams for the MCP server."""
        raise NotImplementedError('MCP Server subclasses must implement this method.')
        yield

    def get_prefixed_tool_name(self, tool_name: str) -> str:
        """Get the tool name with prefix if `tool_prefix` is set."""
        return f'{self.tool_prefix}_{tool_name}' if self.tool_prefix else tool_name

    def get_unprefixed_tool_name(self, tool_name: str) -> str:
        """Get original tool name without prefix for calling tools."""
        return tool_name.removeprefix(f'{self.tool_prefix}_') if self.tool_prefix else tool_name

    @property
    def is_running(self) -> bool:
        """Check if the MCP server is running."""
        return bool(self._running_count)

    async def list_tools(self) -> list[tools.ToolDefinition]:
        """Retrieve tools that are currently active on the server.

        Note:
        - We don't cache tools as they might change.
        - We also don't subscribe to the server to avoid complexity.
        """
        mcp_tools = await self._client.list_tools()
        return [
            tools.ToolDefinition(
                name=self.get_prefixed_tool_name(tool.name),
                description=tool.description or '',
                parameters_json_schema=tool.inputSchema,
            )
            for tool in mcp_tools.tools
        ]

    async def call_tool(
        self, tool_name: str, arguments: dict[str, Any]
    ) -> (
        str
        | messages.BinaryContent
        | dict[str, Any]
        | list[Any]
        | Sequence[str | messages.BinaryContent | dict[str, Any] | list[Any]]
    ):
        """Call a tool on the server.

        Args:
            tool_name: The name of the tool to call.
            arguments: The arguments to pass to the tool.

        Returns:
            The result of the tool call.

        Raises:
            ModelRetry: If the tool call fails.
        """
        result = await self._client.call_tool(self.get_unprefixed_tool_name(tool_name), arguments)

        content = [self._map_tool_result_part(part) for part in result.content]

        if result.isError:
            text = '\n'.join(str(part) for part in content)
            raise exceptions.ModelRetry(text)
        else:
            return content[0] if len(content) == 1 else content

    async def __aenter__(self) -> Self:
        if self._running_count == 0:
            self._exit_stack = AsyncExitStack()

            self._read_stream, self._write_stream = await self._exit_stack.enter_async_context(self.client_streams())
            client = ClientSession(
                read_stream=self._read_stream,
                write_stream=self._write_stream,
                sampling_callback=self._sampling_callback,
                logging_callback=self.log_handler,
            )
            self._client = await self._exit_stack.enter_async_context(client)

            with anyio.fail_after(self.init_timeout):
                await self._client.initialize()

                if log_level := self.log_level:
                    await self._client.set_logging_level(log_level)
        self._running_count += 1
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_value: BaseException | None,
        traceback: TracebackType | None,
    ) -> bool | None:
        self._running_count -= 1
        if self._running_count <= 0:
            await self._exit_stack.aclose()

    async def _sampling_callback(
        self, context: RequestContext[ClientSession, Any], params: mcp_types.CreateMessageRequestParams
    ) -> mcp_types.CreateMessageResult | mcp_types.ErrorData:
        """MCP sampling callback."""
        if self.sampling_model is None:
            raise ValueError('Sampling model is not set')

        pai_messages = _mcp.map_from_mcp_params(params)
        model_settings = models.ModelSettings()
        if max_tokens := params.maxTokens:
            model_settings['max_tokens'] = max_tokens
        if temperature := params.temperature:
            model_settings['temperature'] = temperature
        if stop_sequences := params.stopSequences:
            model_settings['stop_sequences'] = stop_sequences

        model_response = await self.sampling_model.request(
            pai_messages,
            model_settings,
            models.ModelRequestParameters(),
        )
        return mcp_types.CreateMessageResult(
            role='assistant',
            content=_mcp.map_from_model_response(model_response),
            model=self.sampling_model.model_name,
        )

    def _map_tool_result_part(
        self, part: mcp_types.Content
    ) -> str | messages.BinaryContent | dict[str, Any] | list[Any]:
        # See https://github.com/jlowin/fastmcp/blob/main/docs/servers/tools.mdx#return-values

        if isinstance(part, mcp_types.TextContent):
            text = part.text
            if text.startswith(('[', '{')):
                try:
                    return pydantic_core.from_json(text)
                except ValueError:
                    pass
            return text
        elif isinstance(part, mcp_types.ImageContent):
            return messages.BinaryContent(data=base64.b64decode(part.data), media_type=part.mimeType)
        elif isinstance(part, mcp_types.AudioContent):
            # NOTE: The FastMCP server doesn't support audio content.
            # See <https://github.com/modelcontextprotocol/python-sdk/issues/952> for more details.
            return messages.BinaryContent(
                data=base64.b64decode(part.data), media_type=part.mimeType
            )  # pragma: no cover
        elif isinstance(part, mcp_types.EmbeddedResource):
            resource = part.resource
            if isinstance(resource, mcp_types.TextResourceContents):
                return resource.text
            elif isinstance(resource, mcp_types.BlobResourceContents):
                return messages.BinaryContent(
                    data=base64.b64decode(resource.blob),
                    media_type=resource.mimeType or 'application/octet-stream',
                )
            else:
                assert_never(resource)
        else:
            assert_never(part)

client_streams abstractmethod async

client_streams() -> AsyncIterator[
    tuple[
        MemoryObjectReceiveStream[
            SessionMessage | Exception
        ],
        MemoryObjectSendStream[SessionMessage],
    ]
]

Create the streams for the MCP server.

Source code in pydantic_ai_slim/pydantic_ai/mcp.py
58
59
60
61
62
63
64
65
66
67
68
69
70
@abstractmethod
@asynccontextmanager
async def client_streams(
    self,
) -> AsyncIterator[
    tuple[
        MemoryObjectReceiveStream[SessionMessage | Exception],
        MemoryObjectSendStream[SessionMessage],
    ]
]:
    """Create the streams for the MCP server."""
    raise NotImplementedError('MCP Server subclasses must implement this method.')
    yield

get_prefixed_tool_name

get_prefixed_tool_name(tool_name: str) -> str

Get the tool name with prefix if tool_prefix is set.

Source code in pydantic_ai_slim/pydantic_ai/mcp.py
72
73
74
def get_prefixed_tool_name(self, tool_name: str) -> str:
    """Get the tool name with prefix if `tool_prefix` is set."""
    return f'{self.tool_prefix}_{tool_name}' if self.tool_prefix else tool_name

get_unprefixed_tool_name

get_unprefixed_tool_name(tool_name: str) -> str

Get original tool name without prefix for calling tools.

Source code in pydantic_ai_slim/pydantic_ai/mcp.py
76
77
78
def get_unprefixed_tool_name(self, tool_name: str) -> str:
    """Get original tool name without prefix for calling tools."""
    return tool_name.removeprefix(f'{self.tool_prefix}_') if self.tool_prefix else tool_name

is_running property

is_running: bool

Check if the MCP server is running.

list_tools async

list_tools() -> list[ToolDefinition]

Retrieve tools that are currently active on the server.

Note: - We don't cache tools as they might change. - We also don't subscribe to the server to avoid complexity.

Source code in pydantic_ai_slim/pydantic_ai/mcp.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
async def list_tools(self) -> list[tools.ToolDefinition]:
    """Retrieve tools that are currently active on the server.

    Note:
    - We don't cache tools as they might change.
    - We also don't subscribe to the server to avoid complexity.
    """
    mcp_tools = await self._client.list_tools()
    return [
        tools.ToolDefinition(
            name=self.get_prefixed_tool_name(tool.name),
            description=tool.description or '',
            parameters_json_schema=tool.inputSchema,
        )
        for tool in mcp_tools.tools
    ]

call_tool async

call_tool(
    tool_name: str, arguments: dict[str, Any]
) -> (
    str
    | BinaryContent
    | dict[str, Any]
    | list[Any]
    | Sequence[
        str | BinaryContent | dict[str, Any] | list[Any]
    ]
)

Call a tool on the server.

Parameters:

Name Type Description Default
tool_name str

The name of the tool to call.

required
arguments dict[str, Any]

The arguments to pass to the tool.

required

Returns:

Type Description
str | BinaryContent | dict[str, Any] | list[Any] | Sequence[str | BinaryContent | dict[str, Any] | list[Any]]

The result of the tool call.

Raises:

Type Description
ModelRetry

If the tool call fails.

Source code in pydantic_ai_slim/pydantic_ai/mcp.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
async def call_tool(
    self, tool_name: str, arguments: dict[str, Any]
) -> (
    str
    | messages.BinaryContent
    | dict[str, Any]
    | list[Any]
    | Sequence[str | messages.BinaryContent | dict[str, Any] | list[Any]]
):
    """Call a tool on the server.

    Args:
        tool_name: The name of the tool to call.
        arguments: The arguments to pass to the tool.

    Returns:
        The result of the tool call.

    Raises:
        ModelRetry: If the tool call fails.
    """
    result = await self._client.call_tool(self.get_unprefixed_tool_name(tool_name), arguments)

    content = [self._map_tool_result_part(part) for part in result.content]

    if result.isError:
        text = '\n'.join(str(part) for part in content)
        raise exceptions.ModelRetry(text)
    else:
        return content[0] if len(content) == 1 else content

MCPServerStdio dataclass

Bases: MCPServer

Runs an MCP server in a subprocess and communicates with it over stdin/stdout.

This class implements the stdio transport from the MCP specification. See https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio for more information.

Note

Using this class as an async context manager will start the server as a subprocess when entering the context, and stop it when exiting the context.

Example:

from pydantic_ai import Agent
from pydantic_ai.mcp import MCPServerStdio

server = MCPServerStdio(  # (1)!
    'deno',
    args=[
        'run',
        '-N',
        '-R=node_modules',
        '-W=node_modules',
        '--node-modules-dir=auto',
        'jsr:@pydantic/mcp-run-python',
        'stdio',
    ]
)
agent = Agent('openai:gpt-4o', mcp_servers=[server])

async def main():
    async with agent.run_mcp_servers():  # (2)!
        ...

  1. See MCP Run Python for more information.
  2. This will start the server as a subprocess and connect to it.
Source code in pydantic_ai_slim/pydantic_ai/mcp.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
@dataclass
class MCPServerStdio(MCPServer):
    """Runs an MCP server in a subprocess and communicates with it over stdin/stdout.

    This class implements the stdio transport from the MCP specification.
    See <https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio> for more information.

    !!! note
        Using this class as an async context manager will start the server as a subprocess when entering the context,
        and stop it when exiting the context.

    Example:
    ```python {py="3.10"}
    from pydantic_ai import Agent
    from pydantic_ai.mcp import MCPServerStdio

    server = MCPServerStdio(  # (1)!
        'deno',
        args=[
            'run',
            '-N',
            '-R=node_modules',
            '-W=node_modules',
            '--node-modules-dir=auto',
            'jsr:@pydantic/mcp-run-python',
            'stdio',
        ]
    )
    agent = Agent('openai:gpt-4o', mcp_servers=[server])

    async def main():
        async with agent.run_mcp_servers():  # (2)!
            ...
    ```

    1. See [MCP Run Python](../mcp/run-python.md) for more information.
    2. This will start the server as a subprocess and connect to it.
    """

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

    args: Sequence[str]
    """The arguments to pass to the command."""

    env: dict[str, str] | None = None
    """The environment variables the CLI server will have access to.

    By default the subprocess will not inherit any environment variables from the parent process.
    If you want to inherit the environment variables from the parent process, use `env=os.environ`.
    """

    cwd: str | Path | None = None
    """The working directory to use when spawning the process."""

    # last fields are re-defined from the parent class so they appear as fields
    tool_prefix: str | None = None
    """A prefix to add to all tools that are registered with the server.

    If not empty, will include a trailing underscore(`_`).

    e.g. if `tool_prefix='foo'`, then a tool named `bar` will be registered as `foo_bar`
    """

    log_level: mcp_types.LoggingLevel | None = None
    """The log level to set when connecting to the server, if any.

    See <https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/logging#logging> for more details.

    If `None`, no log level will be set.
    """
    log_handler: LoggingFnT | None = None
    """A handler for logging messages from the server."""

    init_timeout: float = 5
    """The timeout in seconds to wait for the client to initialize."""

    @asynccontextmanager
    async def client_streams(
        self,
    ) -> AsyncIterator[
        tuple[
            MemoryObjectReceiveStream[SessionMessage | Exception],
            MemoryObjectSendStream[SessionMessage],
        ]
    ]:
        server = StdioServerParameters(command=self.command, args=list(self.args), env=self.env, cwd=self.cwd)
        async with stdio_client(server=server) as (read_stream, write_stream):
            yield read_stream, write_stream

    def __repr__(self) -> str:
        return f'MCPServerStdio(command={self.command!r}, args={self.args!r}, tool_prefix={self.tool_prefix!r})'

command instance-attribute

command: str

The command to run.

args instance-attribute

args: Sequence[str]

The arguments to pass to the command.

env class-attribute instance-attribute

env: dict[str, str] | None = None

The environment variables the CLI server will have access to.

By default the subprocess will not inherit any environment variables from the parent process. If you want to inherit the environment variables from the parent process, use env=os.environ.

cwd class-attribute instance-attribute

cwd: str | Path | None = None

The working directory to use when spawning the process.

tool_prefix class-attribute instance-attribute

tool_prefix: str | None = None

A prefix to add to all tools that are registered with the server.

If not empty, will include a trailing underscore(_).

e.g. if tool_prefix='foo', then a tool named bar will be registered as foo_bar

log_level class-attribute instance-attribute

log_level: LoggingLevel | None = None

The log level to set when connecting to the server, if any.

See https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/logging#logging for more details.

If None, no log level will be set.

log_handler class-attribute instance-attribute

log_handler: LoggingFnT | None = None

A handler for logging messages from the server.

init_timeout class-attribute instance-attribute

init_timeout: float = 5

The timeout in seconds to wait for the client to initialize.

MCPServerSSE dataclass

Bases: _MCPServerHTTP

An MCP server that connects over streamable HTTP connections.

This class implements the SSE transport from the MCP specification. See https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse for more information.

Note

Using this class as an async context manager will create a new pool of HTTP connections to connect to a server which should already be running.

Example:

from pydantic_ai import Agent
from pydantic_ai.mcp import MCPServerSSE

server = MCPServerSSE('http://localhost:3001/sse')  # (1)!
agent = Agent('openai:gpt-4o', mcp_servers=[server])

async def main():
    async with agent.run_mcp_servers():  # (2)!
        ...

  1. E.g. you might be connecting to a server run with mcp-run-python.
  2. This will connect to a server running on localhost:3001.
Source code in pydantic_ai_slim/pydantic_ai/mcp.py
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
@dataclass
class MCPServerSSE(_MCPServerHTTP):
    """An MCP server that connects over streamable HTTP connections.

    This class implements the SSE transport from the MCP specification.
    See <https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse> for more information.

    !!! note
        Using this class as an async context manager will create a new pool of HTTP connections to connect
        to a server which should already be running.

    Example:
    ```python {py="3.10"}
    from pydantic_ai import Agent
    from pydantic_ai.mcp import MCPServerSSE

    server = MCPServerSSE('http://localhost:3001/sse')  # (1)!
    agent = Agent('openai:gpt-4o', mcp_servers=[server])

    async def main():
        async with agent.run_mcp_servers():  # (2)!
            ...
    ```

    1. E.g. you might be connecting to a server run with [`mcp-run-python`](../mcp/run-python.md).
    2. This will connect to a server running on `localhost:3001`.
    """

    @property
    def _transport_client(self):
        return sse_client  # pragma: no cover

MCPServerHTTP dataclass

Bases: MCPServerSSE

An MCP server that connects over HTTP using the old SSE transport.

This class implements the SSE transport from the MCP specification. See https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse for more information.

Note

Using this class as an async context manager will create a new pool of HTTP connections to connect to a server which should already be running.

Example:

from pydantic_ai import Agent
from pydantic_ai.mcp import MCPServerHTTP

server = MCPServerHTTP('http://localhost:3001/sse')  # (1)!
agent = Agent('openai:gpt-4o', mcp_servers=[server])

async def main():
    async with agent.run_mcp_servers():  # (2)!
        ...

  1. E.g. you might be connecting to a server run with mcp-run-python.
  2. This will connect to a server running on localhost:3001.
Source code in pydantic_ai_slim/pydantic_ai/mcp.py
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
@deprecated('The `MCPServerHTTP` class is deprecated, use `MCPServerSSE` instead.')
@dataclass
class MCPServerHTTP(MCPServerSSE):
    """An MCP server that connects over HTTP using the old SSE transport.

    This class implements the SSE transport from the MCP specification.
    See <https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse> for more information.

    !!! note
        Using this class as an async context manager will create a new pool of HTTP connections to connect
        to a server which should already be running.

    Example:
    ```python {py="3.10" test="skip"}
    from pydantic_ai import Agent
    from pydantic_ai.mcp import MCPServerHTTP

    server = MCPServerHTTP('http://localhost:3001/sse')  # (1)!
    agent = Agent('openai:gpt-4o', mcp_servers=[server])

    async def main():
        async with agent.run_mcp_servers():  # (2)!
            ...
    ```

    1. E.g. you might be connecting to a server run with [`mcp-run-python`](../mcp/run-python.md).
    2. This will connect to a server running on `localhost:3001`.
    """

MCPServerStreamableHTTP dataclass

Bases: _MCPServerHTTP

An MCP server that connects over HTTP using the Streamable HTTP transport.

This class implements the Streamable HTTP transport from the MCP specification. See https://modelcontextprotocol.io/introduction#streamable-http for more information.

Note

Using this class as an async context manager will create a new pool of HTTP connections to connect to a server which should already be running.

Example:

from pydantic_ai import Agent
from pydantic_ai.mcp import MCPServerStreamableHTTP

server = MCPServerStreamableHTTP('http://localhost:8000/mcp')  # (1)!
agent = Agent('openai:gpt-4o', mcp_servers=[server])

async def main():
    async with agent.run_mcp_servers():  # (2)!
        ...

Source code in pydantic_ai_slim/pydantic_ai/mcp.py
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
@dataclass
class MCPServerStreamableHTTP(_MCPServerHTTP):
    """An MCP server that connects over HTTP using the Streamable HTTP transport.

    This class implements the Streamable HTTP transport from the MCP specification.
    See <https://modelcontextprotocol.io/introduction#streamable-http> for more information.

    !!! note
        Using this class as an async context manager will create a new pool of HTTP connections to connect
        to a server which should already be running.

    Example:
    ```python {py="3.10"}
    from pydantic_ai import Agent
    from pydantic_ai.mcp import MCPServerStreamableHTTP

    server = MCPServerStreamableHTTP('http://localhost:8000/mcp')  # (1)!
    agent = Agent('openai:gpt-4o', mcp_servers=[server])

    async def main():
        async with agent.run_mcp_servers():  # (2)!
            ...
    ```
    """

    @property
    def _transport_client(self):
        return streamablehttp_client  # pragma: no cover