{"record":{"id":"1db70389765ab2b4","repo":"JuliusBrussee/caveman","slug":"configpath-mcp-must-be-a-json-object-refusing","errorCode":null,"errorMessage":"${configPath} mcp must be a JSON object; refusing to overwrite it","messagePattern":"(.+?) mcp must be a JSON object; refusing to overwrite it","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":7330,"sourceCode":"  },\n  dispose: async () => {\n    for (const sessionID of contexts.keys()) call(\"SessionEnd\", { session_id: sessionID });\n    contexts.clear();\n    pending.clear();\n  },\n});\n`;\n}\n\nfunction opencodeNativeMutations(gw: string, mcpBinary: string): NativeMutation[] {\n  const configPath = join(homedir(), \".config\", \"opencode\", \"opencode.json\");\n  const before = fileBytes(configPath);\n  const root = parseJsonFileObject(configPath, before);\n  if (root.provider !== undefined && (typeof root.provider !== \"object\" || root.provider === null || Array.isArray(root.provider))) {\n    throw new Error(`${configPath} provider must be a JSON object; refusing to overwrite it`);\n  }\n  if (root.mcp !== undefined && (typeof root.mcp !== \"object\" || root.mcp === null || Array.isArray(root.mcp))) {\n    throw new Error(`${configPath} mcp must be a JSON object; refusing to overwrite it`);\n  }\n  const providers = root.provider && typeof root.provider === \"object\" && !Array.isArray(root.provider) ? root.provider as Record<string, unknown> : {};\n  const previousRoutes: Record<string, unknown> = {};\n  const base = appendUrlPath(gw, \"/w/opencode\");\n  const routes = { openai: appendUrlPath(base, \"/openai/v1\"), anthropic: appendUrlPath(base, \"/anthropic/v1\") };\n  for (const [providerID, route] of Object.entries(routes)) {\n    const provider = providers[providerID] && typeof providers[providerID] === \"object\" && !Array.isArray(providers[providerID]) ? providers[providerID] as Record<string, unknown> : {};\n    const options = provider.options && typeof provider.options === \"object\" && !Array.isArray(provider.options) ? provider.options as Record<string, unknown> : {};\n    previousRoutes[providerID] = options.baseURL ?? null;\n    options.baseURL = route;\n    provider.options = options;\n    providers[providerID] = provider;\n  }\n  root.provider = providers;\n  const mcp = root.mcp && typeof root.mcp === \"object\" && !Array.isArray(root.mcp) ? root.mcp as Record<string, unknown> : {};\n  const previousMcp = mcp.caveman;\n  const installedMcp = { type: \"local\", command: [mcpBinary], enabled: true };\n  mcp.caveman = installedMcp;","sourceCodeStart":7312,"sourceCodeEnd":7348,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L7312-L7348","documentation":"`opencodeNativeMutations()` also validates the top-level `mcp` key in opencode.json before injecting the Caveman MCP entry. If `mcp` exists but is not a plain JSON object, the CLI throws instead of overwriting, preserving whatever invalid-but-user-owned value is there. Fix the file manually and re-run setup.","triggerScenarios":"Running native opencode setup when the parsed opencode.json has `mcp` defined with a non-object value (`typeof root.mcp !== \"object\" || null || Array.isArray`) — this check runs after the provider check passes.","commonSituations":"Hand-edited opencode.json where mcp was set to true/false or a string; generated config placeholder null; a tool wrote mcp as an array of server definitions; merge conflict resolved to a wrong shape.","solutions":["Open opencode.json and rewrite `mcp` as a JSON object keyed by server name","Back up the file first (cp opencode.json opencode.json.bak)","If mcp is an array, convert each server entry to an object property keyed by its id","Re-run `caveman setup` once corrected"],"exampleFix":"// before: ~/.config/opencode/opencode.json\n{ \"mcp\": \"caveman\" }\n// after\n{ \"mcp\": { \"caveman\": { \"type\": \"local\", \"command\": [\"caveman-mcp\"] } } }","handlingStrategy":"type-guard","validationCode":"// Pre-flight on opencode.json mcp key:\nconst cfg = JSON.parse(fs.readFileSync(opencodeConfigPath, 'utf8'));\nif (cfg.mcp !== undefined && (typeof cfg.mcp !== 'object' || cfg.mcp === null || Array.isArray(cfg.mcp))) {\n  console.error(`Fix ${opencodeConfigPath}: mcp must be an object`);\n}","typeGuard":"function isMcpMap(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v);\n}","tryCatchPattern":"try {\n  await cavemanSetup(['opencode']);\n} catch (e) {\n  if (String(e.message).includes('mcp must be a JSON object')) {\n    // fix mcp shape in opencode.json, then retry\n  }\n  throw e;\n}","preventionTips":["Keep `mcp` as a keyed object of server definitions in opencode.json","Back up the config before bulk edits or migrations","Lint opencode.json after external tools modify it","Replace boolean/string placeholders with proper server objects"],"tags":["config","json-shape","opencode","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"}