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
- Balance the placeholder so every '${' has a matching '}'
- If a literal '${' is required, avoid the sequence in the config value or move that text into the referenced environment variable
- 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
- Prefer environment variables over literal ${ text in values
- Run a placeholder-lint (unbalanced brace check) on checked-in MCP configs in CI
- Copy the ${UPPER_SNAKE} syntax exactly from docs
- Re-read values after sed-style edits that can unbalance braces
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
- invalid environment placeholder in MCP config value
- invalid JSON payload at key {MCP_SERVER_DEFINITIONS_KEY}; co
- invalid MCP server definition list in key {MCP_SERVER_DEFINI
- DeepSeek Harness credentials line {} is invalid: {reason}
- {} could not be parsed safely
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/3d6373df9d132e7e.
Report an issue: GitHub.