Guide

MCP servers

Read every harness's MCP list in one shape, edit one entry, or reset them all to a master file.

Every dialect, one shape

Each harness keeps its MCP servers in its own file and its own shape. Claude, Codex, Gemini, Grok, OMP and most others use the { command, args, env, url } family, in JSON or TOML. OpenCode nests them differently, Copilot reads the VS Code file, Antigravity has its own, and Prime Agent takes that family but only accepts env values as {"env": "NAME"} references. Mastra Code takes the family too but ignores type, so its dialect reads the transport from command or url alone. Pi takes it without sse. Its own /mcp saves fields like exposure into the entry, and a replace that keeps the transport keeps them. The registry records the file, the format, the object key and the dialect per harness; the library reads and writes through that:

ts
import { getHarness, listMcpServers } from "@agntn/harnesses";

for (const listing of listMcpServers(getHarness("codex"))) {
  listing.path; // "/home/dev/.codex/config.toml"
  listing.exists; // false when the file is not there yet
  listing.servers; // McpServerConfig[]: { name, transport, command?, args?, env?, url?, headers?, enabled? }
  listing.error; // set when the file exists but would not parse
}

transport is stdio, http or sse, inferred from command or url when the file doesn't say. enabled is present only for harnesses that track an on/off state per server. Unparseable files are reported, not thrown, so one broken TOML doesn't take every other harness down with it.

Add and remove

ts
import { addMcpServer, removeMcpServer } from "@agntn/harnesses";

addMcpServer(getHarness("omp"), {
  name: "probe",
  transport: "stdio",
  command: "node",
  args: ["srv.mjs", "mcp"],
});
// { path: "/home/dev/.omp/agent/mcp.json", replaced: false }

removeMcpServer(getHarness("omp"), "probe", "user");
// { path: "...", removed: true }

Scope is user by default, project targets the project file. JSON files are rewritten whole, atomically. TOML files get a surgical edit: the server's table is inserted, replaced or removed and everything else, comments included, stays byte for byte. A harness without a config file for that scope throws before touching anything.

Sync from one file

~/.config/agntn/mcp.jsonc is the master list. $XDG_CONFIG_HOME is honoured. JSONC, so comments are fine.

jsonc
{
  "excludes": ["grok"],
  "keep": { "codex": ["node_repl"] },
  "mcpServers": {
    "harnesses": { "command": "npx", "args": ["-y", "@agntn/harnesses", "mcp"] },
    "wiki": { "command": "~/.local/bin/wiki-mcp", "env": { "WIKI_ROOT": "${WIKI_ROOT}" } },
    "remote": { "url": "https://example.test/mcp", "headers": { "Authorization": "Bearer …" }, "excludes": [] }
  }
}
ts
import { getAllHarnesses, syncMcpServers } from "@agntn/harnesses";

const report = syncMcpServers(getAllHarnesses());
report.targets[0]; // { id, path, results: [{ name, action: "added" | "replaced" | "removed" | "kept" | "unchanged" | "skipped", reason? }] }

The rule is exactly this list. For every harness with a user scope config file, servers on the master list are added or replaced, and servers not on it are removed. A harness under excludes keeps its own servers, but names on the master list are withdrawn from it, so a name means the same thing everywhere. The one way back in is a server's own excludes, more on that below. Harnesses with no known config file are reported as skipped.

Got a server only one harness can run? Codex ships node_repl, and nobody else can start it. Put it on the master list and sync pushes it everywhere. Leave it off and sync deletes it as an extra. keep is the way out: it names servers per harness, and sync leaves them alone and reports them as kept. excludes would save it too, but it also pulls every master server out of codex. keep is the scalpel, excludes the axe. A kept name that's also on the master list just syncs like any master server. The CLI and the tools refuse an unknown harness id in keep or excludes before they write anything.

So the axe took out a harness, and now it needs one server back? Give that server its own excludes. It replaces the global array for that server and nothing else. [] lets it into every harness, ["grok"] keeps out Grok alone. That's why remote above reaches Grok while harnesses and wiki don't. Every server without the field follows the global list. Take the field away later and the next sync pulls the server back out. It's sync bookkeeping, so no harness config ever sees it.

~ and ${HOME} in command, args and env values expand to absolute paths at sync time. Harnesses spawn MCP servers without a shell, so a tilde in the written config is a server that never starts, and the error message on the harness side rarely says so.

An env value that is exactly ${NAME} is a reference to the variable NAME of the harness environment. Prime Agent keeps it as {"env": "NAME"}, because its kernel refuses literal values and resolves the reference itself when the server starts. Every other harness gets the value from the environment of the process running the sync, the same moment ${HOME} expands. A server the target cannot hold, a literal for Prime Agent or a reference to a variable that is not set for the others, is reported as skipped with the reason and the rest of the list still syncs. addMcpServer applies the same rule and throws McpEnvError instead. An sse server bound for Pi goes the same way, with McpTransportError.

In the CLI

shell
harnesses mcp-servers list                 # every harness, every file
harnesses mcp-servers list codex --json
harnesses mcp-servers add omp probe --command node --args "srv.mjs mcp"
harnesses mcp-servers add pi remote --url https://example.test/mcp --scope project
harnesses mcp-servers remove omp probe
harnesses mcp-servers sync                 # all harnesses
harnesses mcp-servers sync gemini          # one

list in the CLI and the harnesses_mcp_list tool replace every env and headers value with <redacted>; a token in an MCP config is still a token. The library's listMcpServers keeps them, because an edit has to write them back.