use tracing::Level;
pub const WIRE_TARGET: &str = "pmcp::wire";
pub const REDACTED_HEADERS: &[&str] = &[
"authorization",
"proxy-authorization",
"cookie",
"set-cookie",
"mcp-session-id",
"x-api-key",
"api-key",
];
pub const BODY_PREVIEW_BYTES: usize = 2048;
#[must_use]
pub fn enabled() -> bool {
tracing::enabled!(target: WIRE_TARGET, Level::DEBUG)
}
#[must_use]
pub fn render_header_value(name: &str, value: &str) -> String {
if REDACTED_HEADERS
.iter()
.any(|redacted| name.eq_ignore_ascii_case(redacted))
{
format!("<redacted {} bytes>", value.len())
} else {
value.to_string()
}
}
#[must_use]
pub fn render_body(body: &[u8]) -> String {
if body.len() <= BODY_PREVIEW_BYTES {
return String::from_utf8_lossy(body).into_owned();
}
format!(
"{}… <truncated, {} bytes total>",
String::from_utf8_lossy(&body[..BODY_PREVIEW_BYTES]),
body.len()
)
}
#[must_use]
pub fn render_headers<'a, I>(headers: I) -> String
where
I: IntoIterator<Item = (&'a str, &'a str)>,
{
headers
.into_iter()
.map(|(name, value)| format!("{name}: {}", render_header_value(name, value)))
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn credential_headers_are_redacted_by_default() {
let rendered = render_header_value("Authorization", "Bearer super-secret-token");
assert!(
!rendered.contains("super-secret-token"),
"the token must never be rendered: {rendered}"
);
assert!(
rendered.contains("25"),
"the LENGTH is kept so present-but-empty stays distinguishable: {rendered}"
);
}
#[test]
fn redaction_is_case_insensitive() {
for name in ["authorization", "AUTHORIZATION", "Mcp-Session-Id", "COOKIE"] {
let rendered = render_header_value(name, "secret-value");
assert!(
!rendered.contains("secret-value"),
"{name} must be redacted regardless of casing: {rendered}"
);
}
}
#[test]
fn protocol_headers_are_shown_verbatim() {
for (name, value) in [
("MCP-Protocol-Version", "2026-07-28"),
("Mcp-Method", "resources/read"),
("Mcp-Name", "ui://app/keypad"),
("Content-Type", "application/json"),
] {
assert_eq!(
render_header_value(name, value),
value,
"{name} is the diagnostic payload and must not be redacted"
);
}
}
#[test]
fn a_large_body_is_truncated_and_says_so() {
let body = vec![b'x'; BODY_PREVIEW_BYTES * 3];
let rendered = render_body(&body);
assert!(
rendered.contains("truncated"),
"a silent truncation would be a lie about what was sent"
);
assert!(
rendered.contains(&(BODY_PREVIEW_BYTES * 3).to_string()),
"the TRUE size must survive truncation: {}",
&rendered[rendered.len().saturating_sub(80)..]
);
assert!(rendered.len() < BODY_PREVIEW_BYTES * 2);
}
#[test]
fn a_small_body_is_rendered_whole() {
let body = br#"{"jsonrpc":"2.0","id":1,"method":"resources/read"}"#;
let rendered = render_body(body);
assert_eq!(rendered, String::from_utf8_lossy(body));
assert!(!rendered.contains("truncated"));
}
#[test]
fn invalid_utf8_is_lossy_not_fatal() {
let rendered = render_body(&[0xff, 0xfe, b'o', b'k']);
assert!(rendered.contains("ok"), "the readable part must survive");
}
#[test]
fn a_rendered_block_shows_routing_and_hides_secrets() {
let block = render_headers([
("MCP-Protocol-Version", "2026-07-28"),
("Mcp-Method", "resources/read"),
("Authorization", "Bearer nope"),
]);
assert!(block.contains("MCP-Protocol-Version: 2026-07-28"));
assert!(block.contains("Mcp-Method: resources/read"));
assert!(block.contains("Authorization: <redacted"));
assert!(!block.contains("nope"));
}
}