Hmbown/CodeWhale · error · anyhow::Error
MCP config must be an object
Error message
MCP config must be an object
What it means
mutate_config in crates/tui/src/mcp.rs parses the config file into a serde_json::Value and requires the top level to be a JSON object. If the file parses as valid JSON but is an array, string, number, or null, this ensure! fails. The schema expects an object (with a servers/mcpServers map).
Solutions
- Wrap the config top level in an object: {"mcpServers": { ... }} (or the servers map shape the app expects) and retry
- If the file is an export/list meant for import, use the external import path instead of pointing the app config at it
- Reset the file with init_config --force to regenerate a valid template
Example fix
// before (valid JSON, wrong shape)
[{ "name": "fs", "command": "npx" }]
// after
{ "mcpServers": { "fs": { "command": "npx" } } } Defensive patterns
Strategy: validation
Validate before calling
fn top_level_is_object(path: &std::path::Path) -> bool {
std::fs::read_to_string(path).ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
.map(|v| v.is_object())
.unwrap_or(false)
} Type guard
fn as_config_object(v: &serde_json::Value) -> Option<&serde_json::Map<String, serde_json::Value>> {
v.as_object()
} Try / catch
match result {
Err(e) if e.to_string() == "MCP config must be an object" => {
eprintln!("top level of mcp config must be {{...}}, got another JSON kind");
}
_ => {}
} Prevention
- Always keep a top-level object with mcpServers/servers as the root of the config
- Do not confuse external-import source shapes (arrays allowed) with the app config shape (object required)
- Run init_config --force to regenerate a valid template when in doubt
- Type-check the file: jq 'type' mcp.json should print "object"
When it happens
Trigger: Calling any mutate_config-backed operation (add/remove/update server, set_server_enabled) while the config file contains valid JSON that is not an object, e.g. a bare array or a quoted string.
Common situations: A file holding `[ ... ]` (the array shape accepted by external import sources, not the app's own config); an accidentally over-written file containing just "null" or a number; confusing the external-import source format with the app config format.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- invalid MCP server definition list in key
- {error}
- Failed to parse MCP config
- Failed to parse MCP config
- Failed to parse MCP config; file contents were omitted
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/c8e4f4fdd6d85fc9.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/mcp.rs:5838
pub fn mutate_config<T>(
path: &Path,
expected_revision: Option<&str>,
mutate: impl FnOnce(&mut McpConfig) -> Result<T>,
) -> Result<(T, String)> {
validate_mcp_config_path(path)?;
codewhale_config::with_config_write_lock(path, |path| {
let original = read_mcp_config_file(path)?;
let revision = config_revision(original.as_deref());
if expected_revision.is_some_and(|expected| expected != revision) {
return Err(McpRevisionConflict.into());
}
let mut raw: serde_json::Value = match original.as_deref() {
Some(raw) => serde_json::from_str(raw).map_err(|_| {
anyhow::anyhow!("Failed to parse MCP config; file contents were omitted")
})?,
None => serde_json::json!({}),
};
anyhow::ensure!(raw.is_object(), "MCP config must be an object");
let mut config: McpConfig = serde_json::from_value(raw.clone())
.map_err(|_| anyhow::anyhow!("Invalid MCP config; file contents were omitted"))?;
let before = serde_json::to_value(&config)?;
let result = mutate(&mut config)?;
let after = serde_json::to_value(&config)?;
if before == after {
return Ok((result, revision));
}
// Preserve legacy spelling while applying the canonical typed delta.
let legacy = raw.get("mcpServers").is_some();
if legacy {
let object = raw
.as_object_mut()
.context("MCP config must be an object")?;
let servers = object.remove("mcpServers").expect("checked above");
object.insert("servers".into(), servers);
}
apply_json_delta(&mut raw, &before, &after);View on GitHub (pinned to 73e0f67d83)