From 6c98b16b1019103202b8c7b90bc1623b611a97d0 Mon Sep 17 00:00:00 2001 From: OnlyYu1996 <1158673577@qq.com> Date: Sun, 17 May 2026 21:40:57 +0800 Subject: [PATCH] fix(cli): make mcp url truncation utf8 safe --- src/cortex-cli/src/agent_cmd/tests.rs | 5 +-- src/cortex-cli/src/mcp_cmd/auth.rs | 7 +--- src/cortex-cli/src/mcp_cmd/handlers.rs | 13 ++---- src/cortex-cli/src/mcp_cmd/mod.rs | 55 ++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/cortex-cli/src/agent_cmd/tests.rs b/src/cortex-cli/src/agent_cmd/tests.rs index e2ff07f9f..18f7ba753 100644 --- a/src/cortex-cli/src/agent_cmd/tests.rs +++ b/src/cortex-cli/src/agent_cmd/tests.rs @@ -3,10 +3,9 @@ #[cfg(test)] mod tests { use crate::agent_cmd::cli::{CopyArgs, ExportArgs}; - use crate::agent_cmd::loader::{ - load_builtin_agents, parse_frontmatter, read_file_with_encoding, - }; + use crate::agent_cmd::loader::{load_builtin_agents, parse_frontmatter}; use crate::agent_cmd::types::AgentMode; + use crate::utils::file::read_file_with_encoding; #[test] fn test_read_file_with_utf8() { diff --git a/src/cortex-cli/src/mcp_cmd/auth.rs b/src/cortex-cli/src/mcp_cmd/auth.rs index 25d685997..786d4187b 100644 --- a/src/cortex-cli/src/mcp_cmd/auth.rs +++ b/src/cortex-cli/src/mcp_cmd/auth.rs @@ -7,6 +7,7 @@ use anyhow::{Result, bail}; use cortex_engine::create_default_client; use super::config::{get_mcp_server, get_mcp_servers}; +use super::ellipsize_for_display; use super::types::{AuthCommand, AuthListArgs, AuthSubcommand, LogoutArgs}; use super::validation::validate_server_name; @@ -104,11 +105,7 @@ async fn run_auth_list(args: AuthListArgs) -> Result<()> { }; // Truncate URL if too long - let display_url = if url.len() > 38 { - format!("{}...", &url[..35]) - } else { - url.to_string() - }; + let display_url = ellipsize_for_display(url, 38); println!("{name:<20} {display_status:<18} {display_url:<40}"); } diff --git a/src/cortex-cli/src/mcp_cmd/handlers.rs b/src/cortex-cli/src/mcp_cmd/handlers.rs index a2d9189cf..087c31d20 100644 --- a/src/cortex-cli/src/mcp_cmd/handlers.rs +++ b/src/cortex-cli/src/mcp_cmd/handlers.rs @@ -9,6 +9,7 @@ use std::io::{self, BufRead, Write}; use super::auth::{get_auth_status_for_display, remove_auth_silent}; use super::config::{get_mcp_server, get_mcp_servers}; +use super::ellipsize_for_display; use super::types::{ AddArgs, AddMcpSseArgs, AddMcpStreamableHttpArgs, AddMcpTransportArgs, DisableArgs, EnableArgs, GetArgs, ListArgs, RemoveArgs, RenameArgs, @@ -80,11 +81,7 @@ pub(crate) async fn run_list(args: ListArgs) -> Result<()> { .and_then(|v| v.as_str()) .unwrap_or("?"); // Truncate long commands with ellipsis indicator - let transport_str = if cmd.len() > 22 { - format!("stdio: {}...", &cmd[..19]) - } else { - format!("stdio: {cmd}") - }; + let transport_str = format!("stdio: {}", ellipsize_for_display(cmd, 22)); (transport_str, "N/A".to_string()) } "http" => { @@ -94,11 +91,7 @@ pub(crate) async fn run_list(args: ListArgs) -> Result<()> { .await .unwrap_or_else(|_| "Unknown".to_string()); // Truncate URL if too long with ellipsis indicator - let transport_str = if url.len() > 25 { - format!("http: {}...", &url[..22]) - } else { - format!("http: {url}") - }; + let transport_str = format!("http: {}", ellipsize_for_display(url, 25)); (transport_str, auth) } _ => (transport_type.to_string(), "N/A".to_string()), diff --git a/src/cortex-cli/src/mcp_cmd/mod.rs b/src/cortex-cli/src/mcp_cmd/mod.rs index c5168d5b6..6df5e0092 100644 --- a/src/cortex-cli/src/mcp_cmd/mod.rs +++ b/src/cortex-cli/src/mcp_cmd/mod.rs @@ -20,6 +20,21 @@ use anyhow::Result; // Re-export public types pub use types::{McpCli, McpSubcommand}; +pub(super) fn ellipsize_for_display(text: &str, max_chars: usize) -> String { + let char_count = text.chars().count(); + if char_count <= max_chars { + return text.to_string(); + } + + let ellipsis_chars = "...".chars().count(); + if max_chars <= ellipsis_chars { + return ".".repeat(max_chars); + } + + let prefix: String = text.chars().take(max_chars - ellipsis_chars).collect(); + format!("{prefix}...") +} + impl McpCli { /// Run the MCP command. pub async fn run(self) -> Result<()> { @@ -42,3 +57,43 @@ impl McpCli { } } } + +#[cfg(test)] +mod tests { + use super::ellipsize_for_display; + + #[test] + fn test_ellipsize_for_display_preserves_short_text() { + assert_eq!( + ellipsize_for_display("https://example.com", 25), + "https://example.com" + ); + } + + #[test] + fn test_ellipsize_for_display_matches_ascii_byte_slice_output() { + let url = "https://example.com/some/very/long/path"; + assert_eq!(ellipsize_for_display(url, 25), "https://example.com/so..."); + assert_eq!( + ellipsize_for_display(url, 38), + "https://example.com/some/very/long/..." + ); + } + + #[test] + fn test_ellipsize_for_display_handles_utf8_boundaries() { + let url = "http://例え.jp/path/to/endpoint"; + let display = ellipsize_for_display(url, 25); + + assert!(display.ends_with("...")); + assert!(display.len() <= url.len()); + assert!(display.is_char_boundary(display.len())); + } + + #[test] + fn test_ellipsize_for_display_handles_small_limits() { + assert_eq!(ellipsize_for_display("hello", 3), "..."); + assert_eq!(ellipsize_for_display("hello", 2), ".."); + assert_eq!(ellipsize_for_display("hello", 0), ""); + } +}