Skip to main content

Consuming External MCP Servers

The Model Context Protocol (MCP) is a JSON-RPC 2.0 standard for connecting AI models to tools and data sources. harnessd is an MCP client: when you point it at external MCP servers, their tools appear alongside the built-in harness tools in the agent's tool list. The agent can then call those tools without any special plumbing — it just sees more tools.

This is useful for giving your agent access to domain-specific capabilities you do not want to reimplement: a filesystem server, a database query server, a custom API wrapper, or any existing MCP-compatible tool.

This page covers harnessd as an MCP consumer (client). For the reverse direction — exposing harnessd itself as an MCP server — see Exposing harnessd as an MCP Server.


Four ways to configure servers

You can register MCP servers at four distinct points in the lifecycle. Choose the one (or combination) that fits your deployment:

MethodWhen it appliesScope
HARNESS_MCP_SERVERS env varProcess startupAll runs
[mcp_servers] TOML sectionProcess startupAll runs
Profile TOML (~/.harness/profiles/<name>.toml)Run startupRuns using that profile
mcp_servers in POST /v1/runs bodyPer-requestSingle run

1 — HARNESS_MCP_SERVERS environment variable

Set this variable to a JSON array of server config objects before starting harnessd. Each object describes one server.

# stdio server — command is the MCP server subprocess
export HARNESS_MCP_SERVERS='[
{"name": "filesystem", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]},
{"name": "fetch", "command": "uvx", "args": ["mcp-server-fetch"]}
]'

# HTTP server — url points at a running MCP endpoint
export HARNESS_MCP_SERVERS='[
{"name": "my-server", "url": "http://localhost:3001/mcp"}
]'

Config object fields:

FieldTypeRequiredDescription
namestringyesUnique logical name for this server
transportstringno (inferred)"stdio" or "http"
commandstringone of command / urlSubprocess path (stdio transport)
argsarray of stringsnoSubprocess arguments
urlstringone of command / urlHTTP endpoint URL (http or https only)

Transport inference: If you omit transport, the harness infers it. A config with command and no urlstdio. A config with url and no commandhttp.

2 — TOML config ([mcp_servers])

Add a [mcp_servers.<name>] section to any of the TOML config files (~/.harness/config.toml or <workspace>/.harness/config.toml):

[mcp_servers.my-tool]
transport = "stdio"
command = "/usr/local/bin/my-mcp-server"
args = ["--verbose"]

[mcp_servers.remote-tool]
transport = "http"
url = "http://localhost:3001/mcp"

TOML layers merge additively: entries from a higher-priority layer overwrite same-named entries from lower-priority layers.

3 — Profile TOML

A profile TOML file (~/.harness/profiles/<name>.toml) can contain its own [mcp_servers] section. These servers are scoped to individual runs that activate the profile — they are not added to the global server pool. This lets you give specific task profiles their own specialized toolsets.

# ~/.harness/profiles/db-analyst.toml
[mcp_servers.sqlite]
transport = "stdio"
command = "uvx"
args = ["mcp-server-sqlite", "--db-path", "/data/analytics.db"]

Profile files are loaded from ~/.harness/profiles/<name>.toml. A missing profile file is non-fatal (treated as no extra servers). An invalid profile name (empty, or containing path-traversal characters) is an error.

4 — Per-run request body

Pass mcp_servers in the POST /v1/runs JSON body to attach a server to a single run. The server is created at run start and torn down when the run completes.

{
"prompt": "Query the database and summarize sales for Q2",
"mcp_servers": [
{
"name": "sqlite",
"command": "uvx",
"args": ["mcp-server-sqlite", "--db-path", "/tmp/sales.db"]
}
]
}

Using curl against a locally running harnessd:

curl -X POST http://localhost:8080/v1/runs \
-H "Content-Type: application/json" \
-d '{
"prompt": "query /tmp/my.db",
"mcp_servers": [
{"name": "sqlite", "command": "uvx", "args": ["mcp-server-sqlite", "--db-path", "/tmp/my.db"]}
]
}'

Name collision rules for per-run servers: A per-run server name that collides with a globally registered server name is rejected with an error. Profile servers can shadow global names for the duration of that run.


Registration priority (TOML wins)

At harnessd startup, servers from the TOML config are registered first and take precedence. Servers from HARNESS_MCP_SERVERS are registered next; any entry whose name was already registered from TOML is logged and skipped.

In short: TOML config beats the env var when names collide.

TOML config (highest precedence for globals)

HARNESS_MCP_SERVERS env var (duplicate names skipped)

Profile servers (scoped to matching runs, can shadow globals)

Per-run mcp_servers (scoped to one run, must not collide with globals)

Transports

stdio

The harness launches the MCP server as a subprocess using the command and args you provide. Communication happens over the subprocess's stdin/stdout as newline-delimited JSON-RPC 2.0. The scanner buffer is 4 MB per line, which is large enough for all normal MCP payloads. Concurrent requests within one session are multiplexed by request ID.

When the run or server registration is torn down, the harness drains pending requests, closes the subprocess's stdin, and waits for the read loop to exit. This shutdown is bounded and idempotent.

HTTP

The harness sends JSON-RPC 2.0 requests to the URL you specify. The URL scheme must be http or https. Requests include Accept: application/json, text/event-stream; the response may be plain JSON or an SSE stream — both are handled. The HTTP client timeout is 30 seconds and is not currently configurable per-server.

Protocol handshake and fallback

For both transports, the harness attempts the protocol version "2025-11-25" first. If the server returns JSON-RPC error code -32602 or -32600 (invalid/unsupported request), the harness retries with "2024-11-05". This means the harness works with both new and older MCP server implementations without manual configuration.

Connection timing: Global MCP servers (configured via HARNESS_MCP_SERVERS, TOML, or a profile) are connected and initialized eagerly at harnessd startup so their tools can be enumerated for the tool catalog. Servers added mid-session via connect_mcp or supplied in the per-run mcp_servers field are connected on first use — their discovery is deferred until the agent calls connect_mcp or until run startup, respectively.


How tools appear to the agent

Naming convention: mcp_<server>_<tool>

Every tool from an external MCP server is added to the agent's tool list under the name:

mcp_<server>_<tool>

Both <server> and <tool> are sanitized: lowercased, with -, , /, and . replaced by _. There is a single underscore between the prefix, server name, and tool name.

Example: The read_file tool from the filesystem server becomes mcp_filesystem_read_file.

An older runbook (docs/runbooks/mcp.md) describes the naming format as {server_name}__{tool_name} (double underscore, no prefix). This is incorrect. The code produces mcp_{server}_{tool} — single underscore, with the mcp_ prefix. Rely on the format shown here, not the runbook.

MCP tools are always registered at the deferred tier, meaning they are hidden from the LLM's initial tool list and activated on demand via find_tool. This keeps the context window from being flooded when many MCP tools are registered. Global MCP tools (discovered at startup from HARNESS_MCP_SERVERS, TOML, or profile servers) are tagged mcp, integration, external. Tools registered dynamically via connect_mcp or per-run mcp_servers additionally carry dynamic and mcp_server:<serverName>.

Listing tools at runtime

After harnessd is running, you can inspect which MCP servers and tools are registered:

# List connected MCP servers and their tool lists (requires runs:read scope when auth is enabled)
curl http://localhost:8080/v1/mcp/servers

list_mcp_resources and read_mcp_resource

Two additional deferred tools let the agent work with MCP resources (data objects exposed by a server, as opposed to callable tools):

ToolParametersDescription
list_mcp_resourcesmcp_nameList all resources exposed by the named server
read_mcp_resourcemcp_name, uriRead a resource by its URI

In the current production implementation, list_mcp_resources returns an empty list and read_mcp_resource returns an error. MCP resource support is defined in the interface but not yet implemented in the production clientManagerRegistry. Do not rely on these tools returning meaningful data in the current release.

connect_mcp — connecting mid-session

The connect_mcp deferred tool lets the agent connect to a new HTTP/SSE MCP server at any point during a run, without restarting harnessd. Once connected, the server's tools are immediately available as mcp_<server>_<tool> names in the same run.

Required parameter: url — the HTTP/SSE endpoint of the MCP server (e.g. http://localhost:3000/mcp).

Optional parameter: server_name — a display name for the server. If omitted, a name is derived from the URL hostname. Must contain only alphanumeric characters, hyphens, and underscores.

The agent activates connect_mcp via find_tool and can then call it:

find_tool({"query": "select:connect_mcp"})
connect_mcp({"url": "http://localhost:3001/mcp", "server_name": "my-tool"})

After the call completes, mcp_my_tool_<tool> names appear in the tool list for the rest of the run (hyphens in server_name are replaced with underscores in the registered tool prefix).


Quick reference

Configuration summary

GoalHow
Add a server for all runsHARNESS_MCP_SERVERS env var or [mcp_servers] TOML
Add a server for a specific agent profile[mcp_servers] in the profile TOML
Add a server for one run onlymcp_servers field in the POST /v1/runs body
Connect a server after a run has startedAgent calls connect_mcp tool
Inspect registered servers at runtimeGET /v1/mcp/servers

Next steps