{"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":6056,"sourceCode":"  const settings = parseJsonFileObject(settingsPath, settingsBefore);\n  if (settings.env !== undefined && (typeof settings.env !== \"object\" || settings.env === null || Array.isArray(settings.env))) {\n    throw new Error(`${settingsPath} env must be a JSON object; refusing to overwrite it`);\n  }\n  assertNativeHooksShape(settingsPath, settings, \"claude\");\n  const env = settings.env && typeof settings.env === \"object\" && !Array.isArray(settings.env)\n    ? settings.env as Record<string, unknown>\n    : {};\n  const route = appendUrlPath(gw, \"/w/claude\");\n  const previousRoute = env.ANTHROPIC_BASE_URL;\n  env.ANTHROPIC_BASE_URL = route;\n  settings.env = env;\n  const withHooks = nativeHooksDocument(\"claude\", true, settings);\n\n  const mcpPath = join(homedir(), \".claude.json\");\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 },\n    },\n    {","sourceCodeStart":6038,"sourceCodeEnd":6074,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/cli/src/index.ts#L6038-L6074","documentation":"claudeNativeMutations() merges a `caveman` entry into the mcpServers map inside ~/.claude.json (Claude Code's MCP registry). If an `mcpServers` key exists but is not a JSON object (array, string, number, or null), it throws instead of overwriting, because that key holds user-defined server configs.","triggerScenarios":"Native Claude install when ~/.claude.json contains \"mcpServers\": [...] or \"mcpServers\": \"none\" or \"mcpServers\": null.","commonSituations":"User manually disabled MCP servers by replacing the object with a string/null; a different MCP manager tool wrote an array; hand-editing ~/.claude.json introduced a shape change.","solutions":["Edit ~/.claude.json so mcpServers is an object mapping server names to configs, or remove the key entirely","Back up ~/.claude.json first (it also holds Claude Code state), then re-run the caveman native install","Verify with `jq '.mcpServers | type' ~/.claude.json` that it reports \"object\" or is absent"],"exampleFix":"// before — ~/.claude.json\n{ \"mcpServers\": [ { \"name\": \"caveman\" } ] }\n\n// after\n{ \"mcpServers\": { \"caveman\": { \"type\": \"stdio\", \"command\": \"caveman-mcp\", \"args\": [] } } }","handlingStrategy":"validation","validationCode":"import { readFileSync } from \"node:fs\";\nfunction claudeMcpShapeOk(path: string): boolean {\n  try {\n    const mcp = JSON.parse(readFileSync(path, \"utf8\")).mcpServers;\n    return mcp === undefined || (typeof mcp === \"object\" && mcp !== null && !Array.isArray(mcp));\n  } catch { return true; }\n}\nif (!claudeMcpShapeOk(`${homedir}/.claude.json`)) normalizeMcpServers();","typeGuard":"const isJsonObject = (v: unknown): v is Record<string, unknown> =>\n  typeof v === \"object\" && v !== null && !Array.isArray(v);","tryCatchPattern":"try { nativeInstallClaude(); } catch (e) {\n  if (e instanceof Error && /mcpServers must be a JSON object/.test(e.message)) {\n    rewriteMcpServersAsObject(); nativeInstallClaude();\n  } else throw e;\n}","preventionTips":["Keep ~/.claude.json mcpServers as a name-keyed object; never an array or scalar","Prefer tools (claude mcp add) over hand-editing ~/.claude.json","Check shape before install: jq '.mcpServers | type' ~/.claude.json"],"tags":["config","json","claude","mcp","shape-validation","refuse-overwrite"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}