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

  1. Open opencode.json and rewrite `mcp` as a JSON object keyed by server name
  2. Back up the file first (cp opencode.json opencode.json.bak)
  3. If mcp is an array, convert each server entry to an object property keyed by its id
  4. 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

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


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)