Kuberwastaken/claurst · error

mcpServers must be an object

Error message

mcpServers must be an object

What it means

parse_mcp_servers in import_config.rs requires the JSON value under the `mcpServers` key to be a JSON object mapping server names to server configs. If the value is any other JSON type (string, array, number, null), this error is thrown. It guards the MCP-server import path during settings/config import from another tool's config file.

Solutions

  1. Open the source settings file and make "mcpServers" a JSON object keyed by server name
  2. Remove the mcpServers key entirely if no MCP servers should be imported (it is then skipped instead of parsed)
  3. If the source tool stores servers as an array, convert entries into an object keyed by server name

Example fix

// before
{ "mcpServers": [ {"name": "fs", "command": "npx"} ] }
// after
{ "mcpServers": { "fs": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-fs"] } } }
Defensive patterns

Strategy: validation

Validate before calling

const cfg = JSON.parse(fs.readFileSync(path, 'utf8'));
if (cfg.mcpServers !== undefined && (typeof cfg.mcpServers !== 'object' || Array.isArray(cfg.mcpServers) || cfg.mcpServers === null)) {
  throw new Error('mcpServers must be a JSON object keyed by server name');
}

Type guard

const isMcpServersMap = (v) => typeof v === 'object' && v !== null && !Array.isArray(v) && Object.values(v).every((e) => typeof e === 'object' && e !== null);

Try / catch

try { importConfig(path) } catch (e) { if (String(e).includes('mcpServers must be an object')) { fixMcpServersShape(path); } else { throw e; } }

Prevention

When it happens

Trigger: Calling the config importer (via map_mcp_servers_field or parse_mcp_servers_object) with a settings file where "mcpServers" is null, a string, or an array instead of an object like {"server": {...}}.

Common situations: Importing a settings.json where mcpServers was manually emptied (set to null), a typo like "mcpServer", copying config fragments between tools, or a foreign config format that stores servers as a list.

Related errors


AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10). Data as JSON: /api/errors/a9ab4d399a7d8f34. Report an issue: GitHub.

Appendix: source

Thrown at src-rust/crates/core/src/import_config.rs:646

        PreviewAction::Import
    };
    preview_fields.push(PreviewField {
        name: format!("hooks ({})", hooks.len()),
        action,
        reason: None,
    });
    target.config.hooks = hooks;
    imported_fields.push("hooks".to_string());
    if action == PreviewAction::Replace {
        *replaced_count += 1;
    } else {
        *imported_count += 1;
    }
}

fn parse_mcp_servers(value: &Value) -> Result<Vec<McpServerConfig>> {
    let Some(obj) = value.as_object() else {
        return Err(anyhow!("mcpServers must be an object"));
    };

    let mut servers = Vec::new();
    for (name, entry) in obj {
        let entry_obj = entry
            .as_object()
            .ok_or_else(|| anyhow!("mcpServers.{name} must be an object"))?;
        let command = entry_obj
            .get("command")
            .and_then(Value::as_str)
            .map(ToString::to_string);
        let url = entry_obj
            .get("url")
            .and_then(Value::as_str)
            .map(ToString::to_string);
        if command.is_none() && url.is_none() {
            return Err(anyhow!("mcpServers.{name} is missing command/url"));
        }

View on GitHub (pinned to b0637c97ec)