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 local server
Install the MCP extra first. The command'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())
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.
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.
The MCP tool policy defaults to ASK. This terminal example deliberately supplies a CLI approval manager. Direct ASK calls without an approval callback are denied.
Connect a remote server
import asyncio
import os
from harnessx import Agent, CliPermissionManager, MCPManager, MCPServerConfig
async def main():
async with MCPManager() as mcp:
await mcp.connect(MCPServerConfig.http(
"remote",
"http://localhost:8000/mcp",
headers={"Authorization": f"Bearer {os.environ['MCP_TOKEN']}"},
))
async with Agent(mcp=mcp, permissions=CliPermissionManager()) as agent:
print("MCP tools:", agent.mcp_tools)
result = await agent.run("What tools do you have, and what does the first one do?")
result.raise_for_status()
print(result.output)
asyncio.run(main())
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.
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. 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.
Do not follow Agent(mcp=mcp) with an explicit mcp.register_tools(agent.tools): for the same manager it is a no-op, and a name already held by another tool raises ValueError. manager.register_tools(registry) remains available for a ToolRegistry that was not constructed with the manager.
Lifecycle and web setup
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.
In harness-web, General chat → Conversation setup supports local commands and remote URLs, discovered tool lists, and per-server Ask/Allow/Deny selection. Connections belong to that conversation and disappear on server restart.