Skip to content

Library: MCP and the harness registry

import { diagnoseMcp, goldenSetPath, harnessRegistry, listMcpServers } from 'buddy-agent-harness'

These exports are the data and the readers behind init, doctor, and mcp. harnessRegistry is the one table of harnesses: where each is detected, where its skills are projected, what stands between it and AGENTS.md, and where it keeps its MCP servers. listMcpServers and diagnoseMcp read MCP configuration through that table. The first lists every configured server, redacted. The second compares a golden MCP server set against each harness’s copy and returns doctor’s mcp-* findings.

None of them write. listMcpServers and diagnoseMcp read files and, for diagnoseMcp, git history; nothing here creates, moves, or repairs anything.

const harnessRegistry: readonly Harness[]

Every harness the package knows, one entry each. The order is claude-code, cursor, codex, copilot-cli, gemini-cli, devin-desktop, windsurf.

Skills directories come from cyber-agentkit. The two harnesses it does not know, devin-desktop and windsurf, are filled in by hand.

import { harnessRegistry } from 'buddy-agent-harness'
const mcpFiles = harnessRegistry.flatMap((harness) =>
harness.project.mcpConfig ? [`${harness.name}: ${harness.project.mcpConfig.path}`] : [],
)
// ['claude-code: .mcp.json', 'cursor: .cursor/mcp.json', 'codex: .codex/config.toml', 'gemini-cli: .gemini/settings.json']
FieldTypeMeaning
nameHarnessNameThe harness.
projectHarnessScopeWhat the harness does inside a repository. init and doctor act only here.
userHarnessScope, optionalWhat it does outside any repository. Absent when no user-scope path is documented, as for devin-desktop and windsurf. Diagnosable, never written.
deprecatedHarnessName, optionalSet when the name has been superseded; the value is the name that replaces it. windsurf carries 'devin-desktop'.

What one harness does at one scope. Every path is relative to that scope’s root: the repository for project, the home directory for user.

FieldTypeMeaning
detectstringThe directory whose presence means this harness is configured at this scope, such as .claude or .github/skills.
skillsDirectorystring, optionalThe projection target for .agents/skills. Absent when the harness reads .agents/skills natively.
instructionBridgeInstructionBridge, optionalWhat the harness needs at this scope to read AGENTS.md. Diagnosed and gated, never written by init.
shadowedByreadonly string[], optionalFiles whose presence beside AGENTS.md stops the harness reading it. For Claude Code: CLAUDE.md, .claude/CLAUDE.md, and CLAUDE.local.md.
mcpConfigMcpConfig, optionalWhere this harness keeps its own MCP servers. doctor compares against it; mcp project writes into it.
nonstandardarray, optionalWhat this harness reads that no other can, such as .cursorrules or .claude/commands. Each item has a path, a shape ('file' or 'directory'), and a kind ('instructions', 'rule', 'command', 'skill', or 'subagent'). doctor reports them so they can be converted to canonical form.
type HarnessName =
| 'claude-code'
| 'cursor'
| 'codex'
| 'copilot-cli'
| 'gemini-cli'
| 'devin-desktop'
| 'windsurf'
| 'vscode'
| 'opencode'
| 'zed'

The first seven have a harnessRegistry entry. vscode, opencode, and zed have none: only listMcpServers recognizes them, so McpServerEntry.harness can name them.

type HarnessScopeName = 'project' | 'user'

The two scopes a harness reads configuration at. Their roots differ; their shape does not.

Where one harness keeps its MCP servers at project scope, as its vendor documents it.

FieldTypeMeaning
pathstringRepository-relative path of the file.
keystringThe key the servers sit under. It differs per harness — mcpServers, or mcp_servers for Codex — and the wrong key reads as an unconfigured repository.
format'json' | 'toml'The file’s format.
dialect'claude-code' | 'cursor' | 'codex' | 'gemini-cli'Which harness’s entry shape the file holds; the same key can hold different fields. See dialects.
sharedtrue, optionalThe file holds more than MCP configuration, so only key is touched, never the whole file. Set for .codex/config.toml and .gemini/settings.json.

A harness with no mcpConfig is never read or written. Devin Desktop documents no project-scope file, and Copilot CLI’s is not chosen yet.

Harnesspathkeyformat
claude-code.mcp.jsonmcpServersjson
cursor.cursor/mcp.jsonmcpServersjson
codex.codex/config.tomlmcp_serverstoml
gemini-cli.gemini/settings.jsonmcpServersjson

What a harness needs to read AGENTS.md at one scope. At project scope only Gemini CLI has one. At user scope it is what loads ~/.agents/AGENTS.md, and every harness with a user-scope instruction file has one.

kind is the union’s tag, and the other fields depend on it. path is relative to the scope’s root: the repository, or the home directory.

kindOther fieldsMeaning
'settings-entry'path, keyAn AGENTS.md entry in the array at the dotted key of a JSON settings file. Gemini CLI at project scope: .gemini/settings.json, context.fileName.
'import'path, lineA line in the harness’s own instruction file that pulls the canonical file in. Claude Code at user scope: .claude/CLAUDE.md, @~/.agents/AGENTS.md.
'symlink'pathThe harness’s own instruction file as a symlink to the canonical file. Codex, Copilot CLI, and Gemini CLI at user scope.
function listMcpServers(options: ListMcpServersOptions): McpServerEntry[]

Lists every configured MCP server across the supported harnesses and scopes, redacted of anything that could carry a credential. It is an inventory, not a diagnosis: a missing or unparseable file contributes no entries and raises nothing. For drift and literal secrets, use diagnoseMcp.

import { listMcpServers } from 'buddy-agent-harness'
const servers = listMcpServers({ projectDir: process.cwd() })
// [{ harness: 'claude-code', scope: 'project', configFile: '/repo/.mcp.json', name: 'linear',
// transport: 'stdio', command: 'npx', args: ['-y', 'linear-mcp'], url: undefined, enabled: true }]
Sourceharnessscope
Each registry harness’s mcpConfig, under projectDirthat harnessproject
~/.claude.json, top-level mcpServersclaude-codeuser
~/.claude.json, projects[<projectDir>].mcpServersclaude-codelocal
.mcp.json of each installed Claude Code plugin, from ~/.claude/plugins/installed_plugins.jsonclaude-codeplugin
~/.cursor/mcp.jsoncursoruser
config.toml in $CODEX_HOME, or ~/.codexcodexuser
mcp-config.json in $COPILOT_HOME, or ~/.copilotcopilot-cliuser
~/.gemini/settings.jsongemini-cliuser
~/.codeium/windsurf/mcp_config.jsondevin-desktopuser
VS Code’s user mcp.json, and .vscode/mcp.jsonvscodeuser, project
opencode.json under $XDG_CONFIG_HOME/opencode (or ~/.config/opencode), and in projectDiropencodeuser, project
settings.json under $XDG_CONFIG_HOME/zed (or ~/.config/zed), and .zed/settings.jsonzeduser, project

windsurf is skipped as deprecated; its legacy file is read under its new name, devin-desktop. A plugin installed for a different project is skipped. A plugin server’s enabled follows the plugin’s own enabled state in enabledPlugins, merged from ~/.claude/settings.json, then .claude/settings.json, then .claude/settings.local.json, with the last write winning. A plugin not enabled there lists its servers as disabled.

OpenCode’s command array, Zed’s command object, and Windsurf’s serverUrl are reshaped into the shared McpServer fields before an entry is built.

FieldTypeMeaning
projectDirstringThe project directory to read project-scope configuration from.
homeDirstring, optionalOverrides os.homedir(), for a test that must not touch the real one.
envRecord<string, string | undefined>, optionalOverrides process.env. Read only for CODEX_HOME, COPILOT_HOME, XDG_CONFIG_HOME, and, on Windows, APPDATA.
platformNodeJS.Platform, optionalOverrides os.platform(), which decides where VS Code’s user directory is.

One configured server, redacted.

FieldTypeMeaning
harnessHarnessNameThe harness whose file declares it.
scope'project' | 'user' | 'local' | 'plugin'local is Claude Code’s per-project entry inside ~/.claude.json; plugin is a server an installed Claude Code plugin ships.
configFilestringThe absolute path of the file the entry was read from.
namestringThe name the server is declared under.
transportMcpTransport | undefinedStated, or inferred: url means http, command means stdio.
commandstring | undefinedThe command’s basename, never its full path.
argsreadonly string[]The arguments, with any credential-bearing --flag=value pair dropped.
urlstring | undefinedThe URL’s origin and path, never its query string.
enabledbooleanfalse only when the entry says so, or when its plugin is not enabled.
pluginstring, optionalThe plugin id the server came from. Set only when scope is 'plugin'.

env and headers are not carried at all.

function diagnoseMcp(options: DiagnoseMcpOptions): ConfigurationFinding[]

Returns the mcp-* findings doctor reports: the golden set at goldenSetPath compared against the MCP file of each enabled harness. A harness is enabled by the rule doctor uses: Claude Code and Cursor always, and any other whose detect directory exists.

  1. Each target file is read first, so one that does not parse is reported as mcp-target-unreadable even with no golden set. A target that does not exist yet is skipped: nothing has drifted.
  2. Every literal credential in a target is reported, as mcp-committed-secret when git tracks the file and mcp-literal-secret when it does not.
  3. With no golden set, it stops there. A golden set that does not parse is mcp-golden-unreadable, with its line and column when the parser gave them.
  4. Otherwise, per target: a golden server the target does not carry is mcp-unprojected; a target server the golden set does not declare is mcp-undeclared; and each field that differs is mcp-diverged-target, mcp-diverged-golden, mcp-diverged-both, or mcp-diverged-unknown, naming which side moved.

Only the fields the target’s dialect can hold are compared, and a field the golden set leaves unset is never drift. Which side moved is read from the last-projected record that mcp project --write leaves at .agents/buddy-agent-harness/mcp.projected.json; failing that, from the newest of the last 200 commits where the two files agreed on that field. Every finding and its repair is listed on MCP Servers.

A finding never carries a secret’s value. Its path is a locator such as .cursor/mcp.json#servers.linear.headers.Authorization, or .agents/buddy-agent-harness/mcp.toml#L3:7 for a file that does not parse.

FieldTypeMeaning
rootstringThe repository or package directory.
gitGitBridgeStateAnswers which files git tracks, which commits touched a path, and a file’s content at a commit. Constructed as new GitBridgeState(root); every method degrades to “cannot tell” outside a repository. The class is not exported from the package entry.
clistringHow to name this tool in the repair commands, such as buddy-agent-harness.

Each finding has the shape doctor prints in its findings section:

FieldTypeMeaning
pathstringThe locator: a file, a server in it (#servers.<name>), a field of that server (#servers.<name>.<field>), or a position (#L<line>:<column>).
problemMcpProblemOne of the ten mcp-* names above.
detailstringWhat is wrong, in prose.
repair{ command: string; instruction: string }What repairs it. command is a shell invocation, or empty when none does; instruction is always present and complete on its own.
const goldenSetPath: '.agents/buddy-agent-harness/mcp.toml'

The repository-relative path of the golden MCP server set. It is namespaced under .agents/buddy-agent-harness/ rather than sitting at .agents/mcp.json; Where it lives says why. The servers sit under the file’s servers table.

The shared model every MCP file is read into: the golden set, each harness’s copy, and listMcpServers’s sources. It is a superset of every harness’s fields. It carries what the user wrote and never invents a value.

FieldType
transportMcpTransport, optional
commandstring, optional
argsreadonly string[], optional
envReadonly<Record<string, string>>, optional
urlstring, optional
headersReadonly<Record<string, string>>, optional
descriptionstring, optional
enabledboolean, optional
timeoutnumber, optional
sourcestring, optional

A field of the wrong type is dropped on the way in rather than carried through, so a timeout written as a string is never reported as a divergence. An unstated transport is inferred: url means http, command means stdio. env and headers keep only their string values.

type McpField =
| 'transport'
| 'command'
| 'args'
| 'env'
| 'url'
| 'headers'
| 'description'
| 'enabled'
| 'timeout'
| 'source'

One field of McpServer, in the order a finding reports them. args compares in order, as a command line. env and headers compare per name: only the names the golden set declares count, so a name only the harness has is not drift.

type McpTransport = 'stdio' | 'http' | 'sse'
type McpDirection = 'target' | 'golden' | 'both' | 'unknown'

Which side of a diverged field moved since the golden set and the harness’s copy last agreed. unknown means no baseline could say. Each value names one finding: mcp-diverged-target, mcp-diverged-golden, mcp-diverged-both, and mcp-diverged-unknown. See which side moved.