nexu-io/open-design · error · Error

existing config at ${where} is not a JSON object

Error message

existing config at ${where} is not a JSON object

What it means

Thrown by parseJsonObject() after JSON.parse() succeeds but the parsed value is not a JSON object — i.e. it is null, an array, or a primitive (string/number/boolean). MCP client config roots must be objects (so a server entry can be inserted at a key path like `mcpServers.<name>`); a non-object root cannot host the nested key path the installer needs.

Source

Thrown at apps/daemon/src/mcp-agent-install.ts:426

    cursor = next as Record<string, unknown>;
  }
  if (!(plan.serverKey in cursor)) return null;
  delete cursor[plan.serverKey];
  return `${JSON.stringify(root, null, 2)}\n`;
}

function parseJsonObject(text: string | null, where: string): Record<string, unknown> {
  if (text == null || text.trim() === '') return {};
  let parsed: unknown;
  try {
    parsed = JSON.parse(text);
  } catch (err) {
    throw new Error(
      `existing config at ${where} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
    );
  }
  if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
    throw new Error(`existing config at ${where} is not a JSON object`);
  }
  return parsed as Record<string, unknown>;
}

// --- Snippets for the manual (print-only) strategy ----------------------

function genericMcpServersSnippet(spec: McpLaunchSpec, name: string): string {
  const server: Record<string, unknown> = {
    command: spec.command,
    args: spec.args,
  };
  if (Object.keys(spec.env).length > 0) server.env = spec.env;
  return JSON.stringify({ mcpServers: { [name]: server } }, null, 2);
}

function hermesYamlSnippet(spec: McpLaunchSpec, name: string): string {
  const lines = [
    'mcp_servers:',

View on GitHub (pinned to 5be4028344)

Solutions

  1. Rewrite the file so its root is a JSON object `{ ... }` with the expected key path (commonly `mcpServers`).
  2. If you genuinely need a list format for another tool, point the installer at a different config file or migrate the data into object form.
  3. Confirm with `jq -e 'type == "object"' <path>` that the root is an object.

Example fix

// before (config.json)
[
  { "name": "foo", "command": "foo" }
]
// after
{
  "mcpServers": {
    "foo": { "command": "foo" }
  }
}
Defensive patterns

Strategy: validation

Validate before calling

function isJsonObjectRoot(text: string | null): boolean {
  if (text == null || text.trim() === '') return true; // empty is treated as {}
  let v: unknown;
  try { v = JSON.parse(text); } catch { return false; }
  return v !== null && typeof v === 'object' && !Array.isArray(v);
}

if (!isJsonObjectRoot(existingText)) {
  throw new Error(`Config root must be a JSON object; migrate or pick another file.`);
}

Type guard

function isPlainJsonObject(value: unknown): value is Record<string, unknown> {
  return value !== null && typeof value === 'object' && !Array.isArray(value);
}

Try / catch

try {
  applyJsonInstall(existingText, plan);
} catch (error) {
  if (error instanceof Error && error.message.includes('is not a JSON object')) {
    throw new Error(`Config root at ${plan.configPath} is not an object; restructure it as { ... } first.`);
  }
  throw error;
}

Prevention

When it happens

Trigger: Target config file contains a bare array (`[{...}]`), a bare string, a bare number, or the literal `null` at its root. Example: a codex config that was accidentally written as an array of server entries.

Common situations: User restructured their config file to a list format; another tool overwrote the config with a non-object schema; file was hand-edited to `null` to 'disable' it.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/1adfd78aa3eb1ca2. Report an issue: GitHub.