# MCP servers

Connect external capabilities to the tool registry.

`MCPManager` connects to local or remote Model Context Protocol servers and discovers their tools. Pass the manager to `Agent(mcp=...)`: the discovered tools are bridged into the agent's registry at construction, so the execution engine validates and authorizes them like any local tool.

## Connect a server

Install the MCP extra first. A stdio server's executable must be installed and available to the Python process.

~~~bash
python -m pip install "harnessx[mcp]"
~~~

~~~python
import asyncio
from harnessx import Agent, CliPermissionManager, MCPManager, MCPServerConfig

async def main():
    async with MCPManager() as mcp:
        await mcp.connect(MCPServerConfig.stdio(
            "files", "npx", args=["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
        ))
        async with Agent(mcp=mcp, permissions=CliPermissionManager()) as agent:
            print("MCP tools:", agent.mcp_tools)
            result = await agent.run("List the files in /tmp.")
            result.raise_for_status()
            print(result.output)

asyncio.run(main())
~~~

A remote server is the same code with a different config:

~~~python
import os

await mcp.connect(MCPServerConfig.http(
    "remote", "http://localhost:8000/mcp",
    headers={"Authorization": f"Bearer {os.environ['MCP_TOKEN']}"},
))
~~~

`MCPServerConfig.stdio(name, command, args=, env=, permission=)` describes a subprocess server and `MCPServerConfig.http(name, url, headers=, permission=)` a remote one, so the transport is chosen by constructor. `connect()` also accepts the keyword form, `connect("files", command="npx", args=[...])` or `connect("remote", url=...)`, which builds the same config. A config with both or neither of `command` and `url` raises `ConfigurationError`. The name is yours to choose; it keys the connection and prefixes every bridged tool.

Pass the endpoint the server documents: modern servers expose streamable HTTP, commonly at `/mcp`; older ones expose SSE, commonly at `/sse`. The manager attempts streamable HTTP and falls back to SSE on the same URL; `list_servers()` reports which one connected. Compatibility depends on the installed MCP client and server; qualify the endpoint you plan to use.

`async with MCPManager() as mcp:` disconnects every server on exit. Connect servers **before** constructing the agent: `Agent(mcp=mcp)` bridges the tools discovered at that moment and lists their registered names on `agent.mcp_tools`. Connecting a server name twice raises `ValueError`.

Several servers connect in one call with `connect_all`, concurrently by default; it returns the discovered tools by server name. Servers that connected stay connected when others fail, and the failures are raised together as an `ExceptionGroup`:

~~~python
tools = await mcp.connect_all([
    MCPServerConfig.http("tavily", "https://mcp.tavily.com/mcp", permission="allow"),
    MCPServerConfig.http("revenue", "https://datagol.example/mcp", permission="allow"),
])
print({name: len(found) for name, found in tools.items()})
~~~

Keep credentials on the server. Connecting a subprocess authorizes launching that program; tool approval applies when its discovered tools are invoked.

## Tool names and permissions

Tools are registered with server-qualified, sanitized names such as `files_read_file`. The MCP tool policy defaults to ASK, and a direct ASK call without an approval callback is denied, so the examples above supply a CLI approval manager. Use `agent.mcp_tools` when your application needs per-tool overrides:

~~~python
from harnessx import PermissionLevel

def require_approval(agent):
    for name in agent.mcp_tools:
        agent.permissions.set_permission(name, PermissionLevel.ASK)
    return agent.mcp_tools
~~~

`permission=PermissionLevel.ALLOW` on the server config, or on `connect()`, sets a per-server default instead. External tools remain subject to the same execution-path checks as local tools. `manager.register_tools(registry)` bridges tools into a `ToolRegistry` that was not constructed with the manager.

## Retry policies for MCP tools

`MCPServerConfig.http()` and `.stdio()` accept `replay_policy` and `retry` as server-wide defaults. They default to `"manual"` and `None`, and `manual` prevents automatic retries. `retry=None` uses `ToolRetry(attempts=3, backoff_seconds=1.0, retry_error_results=True)`, with the default 30-second cap on one wait. Choose `safe` only for a server whose operations are safe to repeat, and `idempotent` when the integration enforces deduplication. See [Retries & fallback models](../retries-and-fallbacks/#tool-and-mcp-retries) for the execution rules.

## Lifecycle

MCP connections are caller-owned, like `provider=` and `memory=`. `Agent.aclose()` does not disconnect the manager; leave the `async with MCPManager()` block, or call `await mcp.disconnect_all()`, during application shutdown and handle transport failures.
