JuliusBrussee/caveman · error
mcp must be a JSON object; refusing to overwrite it
Error message
${configPath} mcp must be a JSON object; refusing to overwrite it What it means
`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.
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
Example fix
// before: ~/.config/opencode/opencode.json
{ "mcp": "caveman" }
// after
{ "mcp": { "caveman": { "type": "local", "command": ["caveman-mcp"] } } } Defensive patterns
Strategy: type-guard
Validate before calling
// Pre-flight on opencode.json mcp key:
const cfg = JSON.parse(fs.readFileSync(opencodeConfigPath, 'utf8'));
if (cfg.mcp !== undefined && (typeof cfg.mcp !== 'object' || cfg.mcp === null || Array.isArray(cfg.mcp))) {
console.error(`Fix ${opencodeConfigPath}: mcp must be an object`);
} Type guard
function isMcpMap(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null && !Array.isArray(v);
} Try / catch
try {
await cavemanSetup(['opencode']);
} catch (e) {
if (String(e.message).includes('mcp must be a JSON object')) {
// fix mcp shape in opencode.json, then retry
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- provider must be a JSON object; refusing to overwrite it
- mcpServers must be a JSON object; refusing to overwrite it
- mcpServers must be a JSON object; refusing to overwrite it
- : MCP ownership journal failed; native config may already…
- MCP transaction failed and rolled back
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/1db70389765ab2b4.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:7330
},
dispose: async () => {
for (const sessionID of contexts.keys()) call("SessionEnd", { session_id: sessionID });
contexts.clear();
pending.clear();
},
});
`;
}
function opencodeNativeMutations(gw: string, mcpBinary: string): NativeMutation[] {
const configPath = join(homedir(), ".config", "opencode", "opencode.json");
const before = fileBytes(configPath);
const root = parseJsonFileObject(configPath, before);
if (root.provider !== undefined && (typeof root.provider !== "object" || root.provider === null || Array.isArray(root.provider))) {
throw new Error(`${configPath} provider must be a JSON object; refusing to overwrite it`);
}
if (root.mcp !== undefined && (typeof root.mcp !== "object" || root.mcp === null || Array.isArray(root.mcp))) {
throw new Error(`${configPath} mcp must be a JSON object; refusing to overwrite it`);
}
const providers = root.provider && typeof root.provider === "object" && !Array.isArray(root.provider) ? root.provider as Record<string, unknown> : {};
const previousRoutes: Record<string, unknown> = {};
const base = appendUrlPath(gw, "/w/opencode");
const routes = { openai: appendUrlPath(base, "/openai/v1"), anthropic: appendUrlPath(base, "/anthropic/v1") };
for (const [providerID, route] of Object.entries(routes)) {
const provider = providers[providerID] && typeof providers[providerID] === "object" && !Array.isArray(providers[providerID]) ? providers[providerID] as Record<string, unknown> : {};
const options = provider.options && typeof provider.options === "object" && !Array.isArray(provider.options) ? provider.options as Record<string, unknown> : {};
previousRoutes[providerID] = options.baseURL ?? null;
options.baseURL = route;
provider.options = options;
providers[providerID] = provider;
}
root.provider = providers;
const mcp = root.mcp && typeof root.mcp === "object" && !Array.isArray(root.mcp) ? root.mcp as Record<string, unknown> : {};
const previousMcp = mcp.caveman;
const installedMcp = { type: "local", command: [mcpBinary], enabled: true };
mcp.caveman = installedMcp;View on GitHub (pinned to 3ee70a1026)