use crate::mcp::{McpError, McpResult};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use std::collections::BTreeMap;
pub(crate) const REDACTED_HEADER_VALUE: &str = "[REDACTED]";
pub(crate) fn resolve_http_headers(headers: &BTreeMap<String, String>) -> McpResult<HeaderMap> {
let mut resolved = HeaderMap::new();
for (name, value) in headers {
let header_name = HeaderName::from_bytes(name.as_bytes())
.map_err(|_| McpError::Config(format!("invalid MCP HTTP header name '{name}'")))?;
let header_value = HeaderValue::from_str(value)
.map_err(|_| McpError::Config(format!("invalid MCP HTTP header value for '{name}'")))?;
resolved.insert(header_name, header_value);
}
Ok(resolved)
}
pub(crate) fn redact_headers(headers: &BTreeMap<String, String>) -> BTreeMap<String, String> {
headers
.keys()
.map(|name| (name.clone(), REDACTED_HEADER_VALUE.to_string()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_resolved_sensitive_and_literal_headers_without_expansion() {
let headers = BTreeMap::from([
(
"Authorization".to_string(),
"Bearer resolved-token".to_string(),
),
("X-Team".to_string(), "platform".to_string()),
(
"X-Template".to_string(),
"${MCP_HEADER_TEST_MISSING}".to_string(),
),
(
"X-Legacy".to_string(),
"{env:MCP_HEADER_TEST_MISSING}".to_string(),
),
("X-Literal".to_string(), "{env:NOT-A-VAR}".to_string()),
]);
let resolved = resolve_http_headers(&headers).unwrap();
for (name, value) in &headers {
assert_eq!(resolved.get(name).unwrap(), value.as_str());
}
}
#[test]
fn rejects_invalid_header_values_without_echoing_secrets() {
let headers = BTreeMap::from([(
"Authorization".to_string(),
"secret-token\r\nInjected: value".to_string(),
)]);
let error = resolve_http_headers(&headers).unwrap_err().to_string();
assert!(
error.contains("invalid MCP HTTP header value for 'Authorization'"),
"{error}"
);
assert!(!error.contains("secret-token"), "{error}");
assert!(!error.contains("Injected"), "{error}");
}
#[test]
fn rejects_invalid_header_names_without_echoing_values() {
let headers = BTreeMap::from([("Invalid Name".to_string(), "secret-token".to_string())]);
let error = resolve_http_headers(&headers).unwrap_err().to_string();
assert!(error.contains("invalid MCP HTTP header name"), "{error}");
assert!(!error.contains("secret-token"), "{error}");
}
#[test]
fn redacts_all_header_values() {
let mut headers = BTreeMap::new();
headers.insert("X-Team".to_string(), "platform".to_string());
headers.insert("Authorization".to_string(), "resolved-token".to_string());
let redacted = redact_headers(&headers);
assert_eq!(redacted["X-Team"], REDACTED_HEADER_VALUE);
assert_eq!(redacted["Authorization"], REDACTED_HEADER_VALUE);
assert!(!format!("{redacted:?}").contains("platform"));
}
}