Guide

MCP servers

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

Four dialects, one shape

Each harness keeps its MCP servers in its own file and its own shape. Claude, Codex, Gemini, Grok, Pi's cousins 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. The registry records the file, the format, the object key and the dialect per harness; the library reads and writes through that:

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 hide the other eleven harnesses.

Add and remove

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.

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

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

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. Harnesses with no known config file are reported as skipped.

~ 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.

In the CLI

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.

@agntn/harnesses·MIT license· Paths follow the upstream CLIs and carry their evidence level. This site reads nothing from your machine.