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.
python -m pip install "harnessx[mcp]"
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:
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:
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:
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 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.