{"record":{"id":"3d6373df9d132e7e","repo":"Hmbown/CodeWhale","slug":"unterminated-environment-placeholder-in-mcp-config","errorCode":null,"errorMessage":"unterminated environment placeholder in MCP config value","messagePattern":"unterminated environment placeholder in MCP config value","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp.rs","lineNumber":77,"sourceCode":"/// Expand `${NAME}` placeholders in an MCP config value from the process\n/// environment. This lets secrets (API keys, bearer tokens, …) be supplied\n/// through environment variables instead of being written in cleartext into\n/// the MCP config file on disk.\n///\n/// On a missing or malformed placeholder the error names only the offending\n/// variable, never the surrounding value, so a secret-bearing string is never\n/// echoed into logs or error output.\nfn expand_env_placeholders_with(\n    value: &str,\n    environment: Option<&crate::plugins::HostEnvironment>,\n) -> Result<String> {\n    let mut out = String::new();\n    let mut rest = value;\n    while let Some(start) = rest.find(\"${\") {\n        out.push_str(&rest[..start]);\n        let after = &rest[start + 2..];\n        let Some(end) = after.find('}') else {\n            anyhow::bail!(\"unterminated environment placeholder in MCP config value\");\n        };\n        let name = &after[..end];\n        if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {\n            anyhow::bail!(\"invalid environment placeholder in MCP config value\");\n        }\n        let env_value = environment\n            .map_or_else(|| std::env::var(name), |env| env.var(name))\n            .with_context(|| {\n                format!(\"environment variable {name} required by MCP config is not set\")\n            })?;\n        out.push_str(&env_value);\n        rest = &after[end + 1..];\n    }\n    out.push_str(rest);\n    Ok(out)\n}\n\n#[cfg(test)]","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp.rs#L59-L95","documentation":"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.","triggerScenarios":"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).","commonSituations":"Hand-editing auth headers and endpoint URLs in the MCP config, pasting examples from tools with different placeholder syntaxes, unbalanced braces after scripted edits.","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"],"exampleFix":"// before\n\"authorization\": \"Bearer ${MCP_TOKEN\"\n// after\n\"authorization\": \"Bearer ${MCP_TOKEN}\"","handlingStrategy":"validation","validationCode":"// Lint string values before loading the config\nfn unterminated(v: &str) -> bool {\n    let mut rest = v;\n    while let Some(start) = rest.find(\"${\") {\n        let after = &rest[start + 2..];\n        match after.find('}') {\n            Some(end) => rest = &after[end + 1..],\n            None => return true,\n        }\n    }\n    false\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["mcp","config","env","parsing","placeholders"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}