pub fn escape_error_text(s: &str) -> StringExpand description
Makes s safe to print to a terminal or log sink that does not itself escape untrusted content.
Neutralizes control characters (including line breaks), redacts any embedded URL’s
credentials/query string, and bounds the result to 4000 chars.
Redaction runs before truncation deliberately — truncating first could cut a redacted URL’s marker off and leave a bare secret prefix as the last thing printed.
Delegates to two single-source-of-truth helpers rather than maintaining parallel logic here:
mcp_execution_core::redact_urls_in_text finds and redacts every scheme://… token in s
(a reqwest/rmcp transport error’s Display routinely embeds the full request URL,
including a ?token=…-style query string, inline in prose — see runner::sanitized_error_report,
whose whole reason for calling this function per-cause is to catch exactly that), and
mcp_execution_core::untrusted::sanitize_untrusted_text then neutralizes every character
char::is_control reports (the full C0 and C1 ranges, covering \r, \n, ESC, BEL, and
friends) plus the Markdown/ECMAScript line separators U+2028/U+2029, replacing each with a
space, and caps the result to 4000 chars (MAX_ERROR_TEXT_LEN, not itself public — its value
is documented here since a reader of this function’s public docs cannot otherwise resolve it).
Used to sanitize command errors and log messages that may embed content from an untrusted MCP
server, or a URL the user themselves supplied with a secret in its query string, before they
reach the terminal.
s is treated as a single unit of untrusted text with no internal structure worth preserving
— including any newline it contains, which this collapses like every other control character.
This is why the name is escape_error_text, not e.g. escape_report: this function must
never be called on an already-assembled multi-cause report (that would flatten anyhow’s own
trusted Caused by: structure along with the untrusted content it carries — see
runner::sanitized_error_report’s doc comment for how that structure is instead rebuilt by
calling this function once per cause and rejoining with separators the caller controls, rather
than sanitizing the whole rendered report as one blob). Only ever call this on one piece of
untrusted text at a time.
Sanitized here, at each mcp-execution-cli print/log call site — runner::report_and_classify
(indirectly, via sanitized_error_report) and the warn! logging in commands::server —
rather than where a server-supplied message first enters a mcp_execution_core::Error (e.g.
ConnectionFailed’s boxed source) in
mcp-execution-core/mcp-execution-introspector. source there is a generic Box<dyn std::error::Error + Send + Sync>, not specifically MCP-server text, and mcp_execution_core::Error
is consumed by every crate in this workspace (this one, mcp-execution-server, and any future
one), not just terminal/log output; sanitizing at that shared boundary would force one
escaping policy — lossy, space-collapsing, terminal-oriented — onto every consumer, including
ones that legitimately want the raw text (e.g. mcp-execution-server’s own untrusted-metadata
handling in mcp_execution_core::untrusted, which has different escaping needs for an
LLM-facing prompt than this crate has for a terminal). Scoping the fix to where untrusted text
actually reaches a terminal/log sink — while still delegating the escaping logic itself to the
one authoritative sanitizer — keeps the policy decision local to the output boundary that
needs it, without widening this bug-fix change beyond mcp-execution-cli.
§Examples
use mcp_execution_cli::formatters::escape_error_text;
let cause = "boom\u{1b}[2Jname\nfake extra line";
let escaped = escape_error_text(cause);
assert!(!escaped.contains('\u{1b}'));
assert!(!escaped.contains('\n'));
assert!(escaped.contains("boom"));A URL embedded in the text has its credentials/query string redacted too:
use mcp_execution_cli::formatters::escape_error_text;
let cause = "error sending request for url (https://api.example.com/mcp?token=hunter2secret)";
let escaped = escape_error_text(cause);
assert!(!escaped.contains("hunter2secret"));
assert!(escaped.contains("https://api.example.com/mcp?<redacted>"));