JuliusBrussee/caveman · error

${settingsPath} mcpServers must be a JSON object; refusing t

Error message

${settingsPath} mcpServers must be a JSON object; refusing to overwrite it

What it means

geminiNativeMutations() merges a `caveman` entry into the mcpServers map of Gemini's settings file (geminiSettingsPath(), typically ~/.gemini/settings.json). If `mcpServers` exists but is not a JSON object, it throws rather than overwrite user server definitions.

Source

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

    GEMINI_NATIVE_ENV_BEGIN,
    `GEMINI_BASE_URL=${route}`,
    `GOOGLE_GEMINI_BASE_URL=${route}`,
    `GOOGLE_VERTEX_BASE_URL=${appendUrlPath(route, "/vertex")}`,
    GEMINI_NATIVE_ENV_END,
  ].join("\n");
  return { text: `${stripped}${stripped ? "\n\n" : ""}${block}\n`, block };
}

function geminiNativeMutations(gw: string, mcpBinary: string): NativeMutation[] {
  if (wrapMode(gw) === "managed") {
    throw new Error("managed Gemini CLI routing is unsupported because Gemini CLI cannot send separate Caveman and upstream credentials");
  }
  const settingsPath = geminiSettingsPath();
  const settingsBefore = fileBytes(settingsPath);
  const settings = parseJsonFileObject(settingsPath, settingsBefore);
  assertNativeHooksShape(settingsPath, settings, "gemini");
  if (settings.mcpServers !== undefined && (typeof settings.mcpServers !== "object" || settings.mcpServers === null || Array.isArray(settings.mcpServers))) {
    throw new Error(`${settingsPath} mcpServers must be a JSON object; refusing to overwrite it`);
  }
  const servers = settings.mcpServers && typeof settings.mcpServers === "object" && !Array.isArray(settings.mcpServers)
    ? settings.mcpServers as Record<string, unknown>
    : {};
  const previousMcp = servers.caveman;
  const installedMcp = { command: mcpBinary, args: [] };
  servers.caveman = installedMcp;
  settings.mcpServers = servers;
  const withHooks = nativeHooksDocument("gemini", true, settings);

  const envPath = join(homedir(), ".gemini", ".env");
  const envBefore = fileBytes(envPath);
  const route = appendUrlPath(gw, "/w/gemini");
  const nativeEnv = geminiNativeEnv(envBefore?.toString("utf8") ?? "", route);
  return [
    {
      file: settingsPath,
      before: settingsBefore,

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Edit the Gemini settings file so mcpServers is an object keyed by server name (or remove the key)
  2. Confirm with `jq '.mcpServers | type' ~/.gemini/settings.json` that it is "object" or absent
  3. Re-run the Gemini native install

Example fix

// before — ~/.gemini/settings.json
{ "mcpServers": null }

// after
{ "mcpServers": { "caveman": { "command": "caveman-mcp", "args": [] } } }
Defensive patterns

Strategy: validation

Validate before calling

import { readFileSync } from "node:fs";
function geminiMcpShapeOk(path: string): boolean {
  try {
    const mcp = JSON.parse(readFileSync(path, "utf8")).mcpServers;
    return mcp === undefined || (typeof mcp === "object" && mcp !== null && !Array.isArray(mcp));
  } 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 { nativeInstallGemini(); } catch (e) {
  if (e instanceof Error && /mcpServers must be a JSON object/.test(e.message)) {
    normalizeGeminiMcpServers(); nativeInstallGemini();
  } else throw e;
}

Prevention

When it happens

Trigger: Native Gemini install (local wrap mode) while the Gemini settings file has "mcpServers": [...] / string / number / null.

Common situations: Hand-edited Gemini settings replacing mcpServers with an array or disabling it via a string; another MCP config tool wrote a non-object shape.

Related errors


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