Hmbown/CodeWhale · error

unterminated environment placeholder in MCP config value

Error message

unterminated environment placeholder in MCP config value

What it means

expand_env_placeholders_with scans MCP config values for ${...} placeholders and bails when it finds '${' with no closing '}' in the remainder of that value. Every config value is expanded, so one stray literal '${' anywhere - URLs, headers, args - triggers the error before the config is used.

Source

Thrown at crates/tui/src/mcp.rs:77

/// Expand `${NAME}` placeholders in an MCP config value from the process
/// environment. This lets secrets (API keys, bearer tokens, …) be supplied
/// through environment variables instead of being written in cleartext into
/// the MCP config file on disk.
///
/// On a missing or malformed placeholder the error names only the offending
/// variable, never the surrounding value, so a secret-bearing string is never
/// echoed into logs or error output.
fn expand_env_placeholders_with(
    value: &str,
    environment: Option<&crate::plugins::HostEnvironment>,
) -> Result<String> {
    let mut out = String::new();
    let mut rest = value;
    while let Some(start) = rest.find("${") {
        out.push_str(&rest[..start]);
        let after = &rest[start + 2..];
        let Some(end) = after.find('}') else {
            anyhow::bail!("unterminated environment placeholder in MCP config value");
        };
        let name = &after[..end];
        if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
            anyhow::bail!("invalid environment placeholder in MCP config value");
        }
        let env_value = environment
            .map_or_else(|| std::env::var(name), |env| env.var(name))
            .with_context(|| {
                format!("environment variable {name} required by MCP config is not set")
            })?;
        out.push_str(&env_value);
        rest = &after[end + 1..];
    }
    out.push_str(rest);
    Ok(out)
}

#[cfg(test)]

View on GitHub (pinned to 8880682c63)

Solutions

  1. Balance the placeholder so every '${' has a matching '}'
  2. If a literal '${' is required, avoid the sequence in the config value or move that text into the referenced environment variable
  3. Lint the config for unbalanced placeholders after hand or scripted edits

Example fix

// before
"authorization": "Bearer ${MCP_TOKEN"
// after
"authorization": "Bearer ${MCP_TOKEN}"
Defensive patterns

Strategy: validation

Validate before calling

// Lint string values before loading the config
fn unterminated(v: &str) -> bool {
    let mut rest = v;
    while let Some(start) = rest.find("${") {
        let after = &rest[start + 2..];
        match after.find('}') {
            Some(end) => rest = &after[end + 1..],
            None => return true,
        }
    }
    false
}

Prevention

When it happens

Trigger: Values like 'Bearer ${API_KEY' (missing brace), template strings copied from shell scripts using ${VAR ...} forms, JSON or sed edits that consume the closing brace, or a literal '${' intended as text (the expander has no escape syntax).

Common situations: Hand-editing auth headers and endpoint URLs in the MCP config, pasting examples from tools with different placeholder syntaxes, unbalanced braces after scripted edits.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/3d6373df9d132e7e. Report an issue: GitHub.