JuliusBrussee/caveman · error
mcpServers must be a JSON object; refusing to overwrite it
Error message
${mcpPath} mcpServers must be a JSON object; refusing to overwrite it What it means
`claudeNativeMutations()` writes the Caveman MCP server entry into Claude's global config (`claudeGlobalConfigPath()`). It validates that the existing `mcpServers` key is a plain JSON object; if it is any other type (string, array, null), the CLI aborts rather than overwrite data it cannot safely merge. The file must be repaired by hand.
Solutions
- Open the config file in the message and convert `mcpServers` to a JSON object keyed by server name
- Back up the file before editing (cp file file.bak)
- If mcpServers is an array, transform each entry into an object keyed by its name field
- Re-run `caveman setup` once the shape is corrected
Example fix
// before: ~/.claude.json
{ "mcpServers": [ { "caveman": { "command": "caveman-mcp" } } ] }
// after
{ "mcpServers": { "caveman": { "type": "stdio", "command": "caveman-mcp", "args": [], "env": {} } } } Defensive patterns
Strategy: type-guard
Validate before calling
// Pre-flight check on Claude global config:
const cfg = JSON.parse(fs.readFileSync(claudeConfigPath, 'utf8'));
if (cfg.mcpServers !== undefined && (typeof cfg.mcpServers !== 'object' || cfg.mcpServers === null || Array.isArray(cfg.mcpServers))) {
console.error(`Fix ${claudeConfigPath}: mcpServers must be an object`);
} Type guard
function isMcpServerMap(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null && !Array.isArray(v);
} Try / catch
try {
await cavemanSetup(['claude']);
} catch (e) {
if (String(e.message).includes('mcpServers must be a JSON object')) {
// repair mcpServers shape in the reported file, then retry
}
throw e;
} Prevention
- Always model mcpServers as { name: { command, args, env } }, never an array
- Use Claude's own `claude mcp add` command instead of hand-editing the file
- Back up ~/.claude.json before bulk edits or migrations
- Lint the config with a schema before running setup tools
When it happens
Trigger: Running native Claude setup when the parsed global config has `mcpServers` defined but with a non-object value (`typeof mcpRoot.mcpServers !== "object" || null || Array.isArray`).
Common situations: Manually edited ~/.claude.json where mcpServers became an array of servers instead of a keyed object; a sync tool that flattened mcpServers; corrupted config after an editor mishap; null placeholder from a generated template.
Related errors
- mcp must be a JSON object; refusing to overwrite it
- could not install caveman-cloud MCP for
- could not remove caveman-cloud MCP for
- env must be a JSON object; refusing to overwrite it
- mcpServers must be a JSON object; refusing to overwrite it
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/a2dc47a2c8ca588f.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:7058
// user already carries in settings.
for (const [key, value] of Object.entries(gatewayNoProxyEnv(gw))) {
if (env[key] === undefined) env[key] = value;
}
// Claude Code turns tool search (progressive MCP tool disclosure) OFF as soon
// as ANTHROPIC_BASE_URL is not a first-party Anthropic host, so pointing it at
// caveman would otherwise force every MCP tool schema inline on every request
// — measured at ~48k extra prompt tokens on a 4-server setup. The proxy
// forwards tool_reference blocks byte-identically, which is the condition
// Claude Code names for the override. Never clobber an explicit user value.
if (env.ENABLE_TOOL_SEARCH === undefined) env.ENABLE_TOOL_SEARCH = TOOL_SEARCH_DEFAULT;
settings.env = env;
const withHooks = nativeHooksDocument("claude", nativeShrinkEnabled(), settings);
const mcpPath = claudeGlobalConfigPath();
const mcpBefore = fileBytes(mcpPath);
const mcpRoot = parseJsonFileObject(mcpPath, mcpBefore);
if (mcpRoot.mcpServers !== undefined && (typeof mcpRoot.mcpServers !== "object" || mcpRoot.mcpServers === null || Array.isArray(mcpRoot.mcpServers))) {
throw new Error(`${mcpPath} mcpServers must be a JSON object; refusing to overwrite it`);
}
const servers = mcpRoot.mcpServers && typeof mcpRoot.mcpServers === "object" && !Array.isArray(mcpRoot.mcpServers)
? mcpRoot.mcpServers as Record<string, unknown>
: {};
const previousMcp = servers.caveman;
const installedMcp = { type: "stdio", command: mcpBinary, args: [], env: {} };
servers.caveman = installedMcp;
mcpRoot.mcpServers = servers;
return [
{
file: settingsPath,
before: settingsBefore,
after: Buffer.from(JSON.stringify(withHooks, null, 2) + "\n"),
kind: "claude-settings",
owned: { route, previous_route: previousRoute ?? null, assume_first_party: assumeFirstParty ? "1" : null },
},
{View on GitHub (pinned to 3ee70a1026)