coleam00/Archon · error
MCP server "${serverName}" must be a JSON object (got ${desc
Error message
MCP server "${serverName}" must be a JSON object (got ${describeJsonType(serverConfig)}) What it means
expandEnvVars iterates the top-level server map and requires each server entry to be a JSON object (not null, not an array, not a primitive). This fails fast because per-server handling (env/headers expansion) only makes sense for object-shaped server configs.
Source
Thrown at packages/providers/src/mcp/config.ts:61
return envVal ?? '';
}
);
}
return result;
}
function expandEnvVars(
config: Record<string, unknown>,
envSource: EnvSource
): {
expanded: Record<string, unknown>;
missingVars: string[];
} {
const result: Record<string, unknown> = {};
const missingVars: string[] = [];
for (const [serverName, serverConfig] of Object.entries(config)) {
if (typeof serverConfig !== 'object' || serverConfig === null || Array.isArray(serverConfig)) {
throw new Error(
`MCP server "${serverName}" must be a JSON object (got ${describeJsonType(serverConfig)})`
);
}
const server = { ...(serverConfig as Record<string, unknown>) };
if (server.env !== undefined) {
if (typeof server.env !== 'object' || server.env === null || Array.isArray(server.env)) {
throw new Error(
`MCP config ${serverName}.env must be a JSON object of string values (got ${describeJsonType(server.env)})`
);
}
server.env = expandEnvVarsInRecord(
server.env as Record<string, unknown>,
missingVars,
envSource,
`${serverName}.env`
);
}
if (server.headers !== undefined) {View on GitHub (pinned to 0773b97458)
Solutions
- Make each top-level key map to an object: "filesystem": {"command": "npx", "args": [...]}
- If you intended the wrapper form, put servers under "mcpServers": { ... } as the only top-level key.
- Remove non-server keys (comments, pins, metadata) from the file.
Example fix
// before
{"filesystem": "npx -y @modelcontextprotocol/server-filesystem"}
// after
{"filesystem": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]}} Defensive patterns
Strategy: type-guard
Validate before calling
const cfg = JSON.parse(readFileSync(mcpPath, 'utf-8'));
for (const [name, val] of Object.entries(cfg)) {
if (typeof val !== 'object' || val === null || Array.isArray(val)) {
throw new Error(`server "${name}" must be an object`);
}
} Type guard
const isServerObject = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
Prevention
- Model every server as {command|url, args?, env?, headers?} — never a bare string.
- Validate with a JSON schema in your editor so invalid shapes are flagged as you type.
- Commit a known-good example config and diff new edits against it.
When it happens
Trigger: loadMcpConfig with a config where a key maps to a string/number/array/null, e.g. {"filesystem": "/usr/bin"} or {"servers": [...]} left un-nested after a bad mcpServers migration.
Common situations: Copying a Claude Desktop config that nests differently; pasting a command string as the server value; an editor collapsing a nested object; tools like version-pins written at the same level as servers.
Related errors
- MCP config field "mcpServers" must be a JSON object: ${mcpPa
- MCP config must be a JSON object (Record<string, ServerConfi
- MCP config ${fieldPath}.${key} must be a string (got ${descr
- MCP config ${serverName}.env must be a JSON object of string
- MCP config ${serverName}.headers must be a JSON object of st
AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01).
Data as JSON: /api/errors/f82a260ff63251d1.
Report an issue: GitHub.