{"record":{"id":"a2dc47a2c8ca588f","repo":"JuliusBrussee/caveman","slug":"mcppath-mcpservers-must-be-a-json-object-refus","errorCode":null,"errorMessage":"${mcpPath} mcpServers must be a JSON object; refusing to overwrite it","messagePattern":"(.+?) mcpServers must be a JSON object; refusing to overwrite it","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":7058,"sourceCode":"  // user already carries in settings.\n  for (const [key, value] of Object.entries(gatewayNoProxyEnv(gw))) {\n    if (env[key] === undefined) env[key] = value;\n  }\n  // Claude Code turns tool search (progressive MCP tool disclosure) OFF as soon\n  // as ANTHROPIC_BASE_URL is not a first-party Anthropic host, so pointing it at\n  // caveman would otherwise force every MCP tool schema inline on every request\n  // — measured at ~48k extra prompt tokens on a 4-server setup. The proxy\n  // forwards tool_reference blocks byte-identically, which is the condition\n  // Claude Code names for the override. Never clobber an explicit user value.\n  if (env.ENABLE_TOOL_SEARCH === undefined) env.ENABLE_TOOL_SEARCH = TOOL_SEARCH_DEFAULT;\n  settings.env = env;\n  const withHooks = nativeHooksDocument(\"claude\", nativeShrinkEnabled(), settings);\n\n  const mcpPath = claudeGlobalConfigPath();\n  const mcpBefore = fileBytes(mcpPath);\n  const mcpRoot = parseJsonFileObject(mcpPath, mcpBefore);\n  if (mcpRoot.mcpServers !== undefined && (typeof mcpRoot.mcpServers !== \"object\" || mcpRoot.mcpServers === null || Array.isArray(mcpRoot.mcpServers))) {\n    throw new Error(`${mcpPath} mcpServers must be a JSON object; refusing to overwrite it`);\n  }\n  const servers = mcpRoot.mcpServers && typeof mcpRoot.mcpServers === \"object\" && !Array.isArray(mcpRoot.mcpServers)\n    ? mcpRoot.mcpServers as Record<string, unknown>\n    : {};\n  const previousMcp = servers.caveman;\n  const installedMcp = { type: \"stdio\", command: mcpBinary, args: [], env: {} };\n  servers.caveman = installedMcp;\n  mcpRoot.mcpServers = servers;\n\n  return [\n    {\n      file: settingsPath,\n      before: settingsBefore,\n      after: Buffer.from(JSON.stringify(withHooks, null, 2) + \"\\n\"),\n      kind: \"claude-settings\",\n      owned: { route, previous_route: previousRoute ?? null, assume_first_party: assumeFirstParty ? \"1\" : null },\n    },\n    {","sourceCodeStart":7040,"sourceCodeEnd":7076,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L7040-L7076","documentation":"`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.","triggerScenarios":"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`).","commonSituations":"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.","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"],"exampleFix":"// before: ~/.claude.json\n{ \"mcpServers\": [ { \"caveman\": { \"command\": \"caveman-mcp\" } } ] }\n// after\n{ \"mcpServers\": { \"caveman\": { \"type\": \"stdio\", \"command\": \"caveman-mcp\", \"args\": [], \"env\": {} } } }","handlingStrategy":"type-guard","validationCode":"// Pre-flight check on Claude global config:\nconst cfg = JSON.parse(fs.readFileSync(claudeConfigPath, 'utf8'));\nif (cfg.mcpServers !== undefined && (typeof cfg.mcpServers !== 'object' || cfg.mcpServers === null || Array.isArray(cfg.mcpServers))) {\n  console.error(`Fix ${claudeConfigPath}: mcpServers must be an object`);\n}","typeGuard":"function isMcpServerMap(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v);\n}","tryCatchPattern":"try {\n  await cavemanSetup(['claude']);\n} catch (e) {\n  if (String(e.message).includes('mcpServers must be a JSON object')) {\n    // repair mcpServers shape in the reported file, then retry\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["config","json-shape","claude","mcp"],"backgroundTag":"config-type-mismatch","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}