JuliusBrussee/caveman · error

${configPath} mcp must be a JSON object; refusing to overwri

Error message

${configPath} mcp must be a JSON object; refusing to overwrite it

What it means

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.

Source

Thrown at packages/cli/src/index.ts:6328

  },
  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 27d5a3981a)

Solutions

  1. Convert `mcp` to an object mapping server names to configs, or remove the key
  2. Verify `jq '.mcp | type' ~/.config/opencode/opencode.json` yields "object" or the key is absent
  3. Re-run the opencode native install

Example fix

// before
{ "mcp": [ { "caveman": {} } ] }

// after
{ "mcp": { "caveman": { "type": "local", "command": ["caveman-mcp"], "enabled": true } } }
Defensive patterns

Strategy: validation

Validate before calling

import { readFileSync } from "node:fs";
function opencodeMcpShapeOk(path: string): boolean {
  try {
    const m = JSON.parse(readFileSync(path, "utf8")).mcp;
    return m === undefined || (typeof m === "object" && m !== null && !Array.isArray(m));
  } catch { return true; }
}

Type guard

const isJsonObject = (v: unknown): v is Record<string, unknown> =>
  typeof v === "object" && v !== null && !Array.isArray(v);

Try / catch

try { nativeInstallOpencode(); } catch (e) {
  if (e instanceof Error && /mcp must be a JSON object/.test(e.message)) {
    rewriteMcpAsObject(); nativeInstallOpencode();
  } else throw e;
}

Prevention

When it happens

Trigger: Native opencode install while opencode.json has "mcp": ["server"] , "mcp": true, "mcp": "caveman", or null.

Common situations: 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.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/1db70389765ab2b4. Report an issue: GitHub.