Kuberwastaken/claurst · error

mcpServers. must be an object

Error message

mcpServers.{name} must be an object

What it means

Within parse_mcp_servers, each value of the mcpServers object must itself be a JSON object describing one server. If a value is a string, number, null, or array, this error names the offending key (mcpServers.{name}) and aborts the import.

Solutions

  1. Replace the mcpServers.{name} value with a JSON object containing at least "command" or "url"
  2. Delete the malformed entry if that server is no longer needed
  3. Validate the source JSON with a schema or jq before importing

Example fix

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

Strategy: type-guard

Validate before calling

for (const [name, entry] of Object.entries(cfg.mcpServers ?? {})) {
  if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) throw new Error(`mcpServers.${name} must be an object`);
}

Type guard

const isServerEntry = (v) => typeof v === 'object' && v !== null && !Array.isArray(v) && (typeof v.command === 'string' || typeof v.url === 'string');

Try / catch

try { importConfig(path) } catch (e) { const m = String(e).match(/mcpServers\.(\S+) must be an object/); if (m) { console.error(`Fix server "${m[1]}" in ${path}`); } else { throw e; } }

Prevention

When it happens

Trigger: Importing a config where mcpServers maps some name to a non-object, e.g. "mcpServers": {"fs": "npx ..."} or {"fs": null}.

Common situations: Hand-edited settings.json where a server entry was replaced by a command string, nulling out a disabled server, or merging configs incorrectly so a nested object got flattened.

Related errors


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

Appendix: source

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

    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"));
        }
        let args = entry_obj
            .get("args")
            .and_then(Value::as_array)
            .map(|items| {
                items
                    .iter()
                    .filter_map(Value::as_str)

View on GitHub (pinned to b0637c97ec)