xai-org/grok-build · error

config root is not a table

Error message

config root is not a table

What it means

save_mcp_disabled_tools reads the config TOML into a TomlValue and calls as_table_mut expecting the document root to be a table; if parsing produced a non-table value (scalar/array) this error fires. In practice a valid TOML document is always a table, so this mostly triggers when parse_existing fallback or file content is malformed in an unusual way, or defensively when the value type is wrong.

Solutions

  1. Open the config TOML and ensure the top level is a proper table (key = value sections), not a bare scalar or array.
  2. Delete or repair the malformed config file and let the tool recreate it.
  3. Validate the file with `toml` parsing (e.g. a quick script using the toml crate or a TOML linter) before running the save.
  4. If the loader can produce non-table roots, replace the error with a normalization step that coerces the root to an empty table.

Example fix

// before
let table = root.as_table_mut().ok_or_else(|| anyhow!("config root is not a table"))?;
// after — normalize instead of failing
if !root.is_table() {
    root = TomlValue::Table(TomlMap::new());
}
let table = root.as_table_mut().unwrap();
Defensive patterns

Strategy: validation

Validate before calling

fn config_root_is_table(path: &Path) -> bool {
    std::fs::read_to_string(path).ok()
        .and_then(|s| s.parse::<toml::Value>().ok())
        .map(|v| v.is_table())
        .unwrap_or(false)
}

Type guard

fn as_table(v: &toml::Value) -> Option<&toml::value::Table> { v.as_table() }

Try / catch

match save_mcp_disabled_tools(&cfg, &server, &tools).await {
    Err(e) if e.to_string().contains("config root is not a table") => {
        backup_and_reset_config(&cfg_path)?;
        save_mcp_disabled_tools(&cfg, &server, &tools).await
    }
    other => other,
}

When it happens

Trigger: Calling save_mcp_disabled_tools when the loaded TOML root is not a TomlValue::Table — e.g. the file parses to a non-table value or a fallback path inserted a non-table root.

Common situations: Hand-edited config file whose top-level structure is invalid; a tool wrote a scalar/array TOML value; parser replaced a broken file with a non-table default.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/90711161bbcf026b. Report an issue: GitHub.

Appendix: source

Thrown at crates/codegen/xai-grok-shell/src/util/config/mcp.rs:644

pub const MANAGED_GATEWAY_DISABLED_CONNECTORS_KEY: &str = "__managed_gateway_connectors";

/// Persist `disabled_tools` for a server under `[disabled_mcp_tools]` in config.toml.
///
/// Uses a dedicated top-level section (not `[mcp_servers]`) to avoid creating
/// incomplete server entries that fail to deserialize for managed servers.
pub(crate) async fn save_mcp_disabled_tools(
    server_name: &str,
    disabled_tools: &[String],
) -> Result<()> {
    let path = config_path();
    let mut root: TomlValue = match tokio::fs::read_to_string(&path).await {
        Ok(s) => toml::from_str(&s).unwrap_or(TomlValue::Table(TomlMap::new())),
        Err(_) => TomlValue::Table(TomlMap::new()),
    };
    let table = root
        .as_table_mut()
        .ok_or_else(|| anyhow::anyhow!("config root is not a table"))?;

    let section = table
        .entry("disabled_mcp_tools")
        .or_insert_with(|| TomlValue::Table(TomlMap::new()))
        .as_table_mut()
        .ok_or_else(|| anyhow::anyhow!("disabled_mcp_tools is not a table"))?;

    if disabled_tools.is_empty() {
        section.remove(server_name);
        if section.is_empty() {
            table.remove("disabled_mcp_tools");
        }
    } else {
        let arr = disabled_tools
            .iter()
            .map(|s| TomlValue::String(s.clone()))
            .collect();
        section.insert(server_name.to_string(), TomlValue::Array(arr));

View on GitHub (pinned to bc7f02eddd)