{"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":6328,"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":6310,"sourceCodeEnd":6346,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/cli/src/index.ts#L6310-L6346","documentation":"opencodeNativeMutations() also merges a `caveman` local MCP server into the `mcp` key of ~/.config/opencode/opencode.json. If `mcp` exists but is not a JSON object (opencode expects a map of server name to {type, command, enabled}), it refuses to overwrite.","triggerScenarios":"Native opencode install while opencode.json has \"mcp\": [\"server\"] , \"mcp\": true, \"mcp\": \"caveman\", or null.","commonSituations":"Copied an MCP config snippet using an array syntax from another tool (Claude-style mcpServers arrays); hand-edited boolean toggles; stale config from an older opencode schema.","solutions":["Convert `mcp` to an object mapping server names to configs, or remove the key","Verify `jq '.mcp | type' ~/.config/opencode/opencode.json` yields \"object\" or the key is absent","Re-run the opencode native install"],"exampleFix":"// before\n{ \"mcp\": [ { \"caveman\": {} } ] }\n\n// after\n{ \"mcp\": { \"caveman\": { \"type\": \"local\", \"command\": [\"caveman-mcp\"], \"enabled\": true } } }","handlingStrategy":"validation","validationCode":"import { readFileSync } from \"node:fs\";\nfunction opencodeMcpShapeOk(path: string): boolean {\n  try {\n    const m = JSON.parse(readFileSync(path, \"utf8\")).mcp;\n    return m === undefined || (typeof m === \"object\" && m !== null && !Array.isArray(m));\n  } catch { return true; }\n}","typeGuard":"const isJsonObject = (v: unknown): v is Record<string, unknown> =>\n  typeof v === \"object\" && v !== null && !Array.isArray(v);","tryCatchPattern":"try { nativeInstallOpencode(); } catch (e) {\n  if (e instanceof Error && /mcp must be a JSON object/.test(e.message)) {\n    rewriteMcpAsObject(); nativeInstallOpencode();\n  } else throw e;\n}","preventionTips":["Use opencode's native MCP object format ({ name: { type, command, enabled } })","Don't paste Claude-style array MCP configs into opencode.json","Check shape with jq before invoking installers"],"tags":["config","json","opencode","mcp","shape-validation","refuse-overwrite"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}