Skip to content

MCP Capability

MCP is a provider-adaptive capability and the primary entry point for MCP in Pydantic AI. It runs the MCP server locally by default — keeping credentials, hooks, and tracing under your control — and supports both URL-based servers and direct client / toolset / transport inputs.

Backed by MCPServerTool on the native side (see MCP Server Tool for provider support and configuration) — pass native=MCPServerTool(...) directly when you need full control (e.g. a different id, authorization_token, or description than the capability would derive). On the local side, local= accepts any MCPToolset input (URL, fastmcp.Client, transport, in-process FastMCP server, script path, …) — non-toolset inputs are wrapped in MCPToolset automatically.

Without an explicit id=, the capability uses the server URL’s hostname as its id, which also names its local toolset and durable operations. Only the hostname is used, because a URL’s path or query can carry a credential. Two MCP capabilities for servers on the same host would get the same id, so the agent rejects them and asks for an explicit id= on each.

mcp.py
from pydantic_ai.capabilities import MCP
from pydantic_ai.native_tools import MCPServerTool

# URL-based MCP server, running locally (requires `pydantic-ai-slim[mcp]`)
MCP('https://mcp.example.com/api')

# Local client without a URL — pass any `MCPToolset` input
# (URL, `fastmcp.Client`, transport, in-process `FastMCP` server, script path, etc.)
MCP(local=my_fastmcp_client)

# Native preferred; URL-based local fallback
MCP('https://mcp.example.com/api', native=True)

# Strict native-only (no local — does not require the `mcp` extra)
MCP('https://mcp.example.com/api', native=True, local=False)

# Explicit native + explicit local — independent configuration on each side
# (e.g. provider-relay URL for native, direct connection for local)
MCP(
    native=MCPServerTool(
        id='public-mcp',
        url='https://relay.example.com/mcp',
        authorization_token='relay-token',
    ),
    local=my_fastmcp_client,
)

Tool names

By default, the model sees the server’s tools under the names the server gives them. Generic names like search can collide with the tools of another server or with your own, so pass tool_prefix to prepend a prefix to each name, joined with _:

mcp_capability_tool_prefix.py
from pydantic_ai import Agent
from pydantic_ai.capabilities import MCP

agent = Agent(
    'openai:gpt-6.1-sol',
    capabilities=[
        MCP('https://docs.example.com/mcp', id='docs', tool_prefix=True),  # `docs_search`
        MCP('https://tickets.example.com/mcp', tool_prefix='tickets'),  # `tickets_search`
    ],
)

tool_prefix=True uses the id= you pass as the prefix, and raises a UserError without one: the id derived from the URL’s hostname is never used, so the tool names the model sees are always ones you chose. allowed_tools keeps matching the server’s own tool names, and the capability’s id and its toolset’s id don’t change, so durable execution and defer_loading work as before. With native MCP, the model provider names the server’s tools, so the prefix applies to the local tools only.

Per-user credentials

In an app where each user has their own credentials for the MCP server, one agent serves all of them, so authorization_token and headers can’t be fixed when the capability is created. Pass a function instead: it’s called with the run context at the start of each run, so it can read the current user’s token from the run’s dependencies:

mcp_capability_per_user.py
from dataclasses import dataclass

from pydantic_ai import Agent, RunContext
from pydantic_ai.capabilities import MCP


@dataclass
class Deps:
    mcp_token: str


def mcp_token(ctx: RunContext[Deps]) -> str:
    return f'Bearer {ctx.deps.mcp_token}'


agent = Agent(
    'openai:gpt-6.1-sol',
    deps_type=Deps,
    capabilities=[MCP('https://mcp.example.com/api', authorization_token=mcp_token)],
)


async def main():
    result = await agent.run('What is 7 plus 5?', deps=Deps(mcp_token='<token>'))
    print(result.output)
    #> The answer is 12.

(To run this example, ensure asyncio is imported and add asyncio.run(main()); no other changes are needed.)

The value the function returns is used for that run on both sides: the native MCPServerTool is built with it, and the local toolset connects in an MCP session of its own, so concurrent runs never share a session between users. Returning None sends no Authorization header. headers accepts a function the same way. See per-user authentication for why each user needs a session of their own.

For lower-level access — managing the MCPToolset lifecycle directly, advanced transport / client configuration, or using MCP servers without going through a capability — see the MCP documentation.