-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix(mcp): normalize server names, fix UTF-8 truncation, skip auth when header set #2379
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
f6cec39
7fd2eed
1c4bf21
2f4fdd3
65e35a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,7 @@ use crate::secrets::SecretsStore; | |
| use crate::tools::mcp::{ | ||
| McpClient, McpProcessManager, McpServerConfig, McpSessionManager, OAuthConfig, | ||
| auth::{authorize_mcp_server, is_authenticated}, | ||
| config::{self, EffectiveTransport, McpServersFile}, | ||
| config::{self, EffectiveTransport, McpServersFile, normalize_server_name}, | ||
| factory::create_client_from_config, | ||
| }; | ||
|
|
||
|
|
@@ -166,7 +166,7 @@ pub async fn run_mcp_command(cmd: McpCommand) -> anyhow::Result<()> { | |
| /// Add a new MCP server. | ||
| async fn add_server(args: McpAddArgs) -> anyhow::Result<()> { | ||
| let McpAddArgs { | ||
| name, | ||
| name: raw_name, | ||
| url, | ||
| transport, | ||
| command, | ||
|
|
@@ -181,6 +181,16 @@ async fn add_server(args: McpAddArgs) -> anyhow::Result<()> { | |
| description, | ||
| } = args; | ||
|
|
||
| // Normalize the server name: lowercase, replace special chars with | ||
| // underscores so descriptive names like "My Twitter Server" work (#2236). | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. High Severity — CLI lookup commands don't normalize the
After this PR, Suggested fix: Add
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in bc3f863. All four CLI lookup commands (remove_server, auth_server, test_server, toggle_server) now normalize the name argument before lookup, matching add_server behavior. |
||
| let name = normalize_server_name(&raw_name); | ||
| if name.is_empty() { | ||
| anyhow::bail!("Server name '{}' contains no valid characters", raw_name); | ||
| } | ||
| if name != raw_name { | ||
| println!(" (normalized name: '{}' -> '{}')", raw_name, name); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — Lookup commands don't normalize names
Suggested fix: Either normalize in lookup commands too (with a "did you mean 'my_twitter_server'?" fallback), or include the normalized form in the error message when lookup fails. |
||
| } | ||
|
|
||
| let transport_lower = transport.to_lowercase(); | ||
|
|
||
| let mut config = match transport_lower.as_str() { | ||
|
|
@@ -290,6 +300,7 @@ async fn add_server(args: McpAddArgs) -> anyhow::Result<()> { | |
|
|
||
| /// Remove an MCP server. | ||
| async fn remove_server(name: String) -> anyhow::Result<()> { | ||
| let name = normalize_server_name(&name); | ||
| let (db, owner_id) = connect_db().await; | ||
| let mut servers = load_servers(db.as_deref(), &owner_id).await?; | ||
| if !servers.remove(&name) { | ||
|
|
@@ -411,6 +422,7 @@ async fn list_servers(verbose: bool) -> anyhow::Result<()> { | |
|
|
||
| /// Authenticate with an MCP server. | ||
| async fn auth_server(name: String, user_id: String) -> anyhow::Result<()> { | ||
| let name = normalize_server_name(&name); | ||
| // Get server config | ||
| let (db, owner_id) = connect_db().await; | ||
| let servers = load_servers(db.as_deref(), &owner_id).await?; | ||
|
|
@@ -495,6 +507,7 @@ async fn auth_server(name: String, user_id: String) -> anyhow::Result<()> { | |
|
|
||
| /// Test connection to an MCP server. | ||
| async fn test_server(name: String, user_id: String) -> anyhow::Result<()> { | ||
| let name = normalize_server_name(&name); | ||
| // Get server config | ||
| let (db, owner_id) = connect_db().await; | ||
| let servers = load_servers(db.as_deref(), &owner_id).await?; | ||
|
|
@@ -568,9 +581,11 @@ async fn test_server(name: String, user_id: String) -> anyhow::Result<()> { | |
| }; | ||
| println!(" • {}{}", tool.name, approval); | ||
| if !tool.description.is_empty() { | ||
| // Truncate long descriptions | ||
| let desc = if tool.description.len() > 60 { | ||
| format!("{}...", &tool.description[..57]) | ||
| // Truncate long descriptions (char-safe to avoid | ||
| // panicking on multi-byte UTF-8 boundaries, #1947) | ||
| let desc = if tool.description.chars().count() > 60 { | ||
| let truncated: String = tool.description.chars().take(57).collect(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical — Pre-existing byte-slicing panic (same pattern) This PR fixes byte-level truncation in Suggested fix: Also fix
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in dcbb7a7. The |
||
| format!("{truncated}...") | ||
| } else { | ||
| tool.description.clone() | ||
| }; | ||
|
|
@@ -618,6 +633,7 @@ async fn test_server(name: String, user_id: String) -> anyhow::Result<()> { | |
|
|
||
| /// Toggle server enabled/disabled state. | ||
| async fn toggle_server(name: String, enable: bool, disable: bool) -> anyhow::Result<()> { | ||
| let name = normalize_server_name(&name); | ||
| let (db, owner_id) = connect_db().await; | ||
| let mut servers = load_servers(db.as_deref(), &owner_id).await?; | ||
|
|
||
|
|
@@ -741,4 +757,54 @@ mod tests { | |
| assert!(result.is_err()); | ||
| assert!(result.unwrap_err().contains("invalid env var format")); | ||
| } | ||
|
|
||
| /// Regression test for #1947: byte-index slicing panics on multi-byte UTF-8. | ||
| /// | ||
| /// Reproduces the original bug: a description with multi-byte chars where | ||
| /// byte index 57 falls inside a character boundary would panic with | ||
| /// `&tool.description[..57]`. | ||
| #[test] | ||
| fn test_tool_description_truncation_multibyte_utf8() { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — Tests duplicate logic instead of testing production code The truncation regression tests ( Suggested fix: Extract the truncation logic into a helper function used by both |
||
| // Each CJK character is 3 bytes. 20 chars = 60 bytes. | ||
| // Byte index 57 falls inside the 20th character (bytes 57..59). | ||
| let description = "\u{4e00}".repeat(20); // 20 CJK chars, 60 bytes | ||
| assert_eq!(description.len(), 60); | ||
| assert_eq!(description.chars().count(), 20); | ||
|
|
||
| // Simulate the fixed truncation logic | ||
| let desc = if description.chars().count() > 60 { | ||
| let truncated: String = description.chars().take(57).collect(); | ||
| format!("{truncated}...") | ||
| } else { | ||
| description.clone() | ||
| }; | ||
| // 20 chars <= 60, so no truncation needed | ||
| assert_eq!(desc, description); | ||
|
|
||
| // Now test with a string that actually exceeds 60 chars | ||
| let long_desc = "\u{4e00}".repeat(61); // 61 CJK chars | ||
| assert_eq!(long_desc.chars().count(), 61); | ||
| let desc = if long_desc.chars().count() > 60 { | ||
| let truncated: String = long_desc.chars().take(57).collect(); | ||
| format!("{truncated}...") | ||
| } else { | ||
| long_desc.clone() | ||
| }; | ||
| assert_eq!(desc.chars().count(), 60); // 57 chars + "..." | ||
| assert!(desc.ends_with("...")); | ||
| } | ||
|
|
||
| /// Verify ASCII descriptions still truncate correctly. | ||
| #[test] | ||
| fn test_tool_description_truncation_ascii() { | ||
| let description = "a".repeat(80); | ||
| let desc = if description.chars().count() > 60 { | ||
| let truncated: String = description.chars().take(57).collect(); | ||
| format!("{truncated}...") | ||
| } else { | ||
| description.clone() | ||
| }; | ||
| assert_eq!(desc.len(), 60); | ||
| assert!(desc.ends_with("...")); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Medium Severity — Missing empty-name guard after normalization
normalize_server_name("@#$")returns"". The CLI'sadd_serverchecks for this (line 186–188) and returns an error, and the JS frontend checksif (!name)too. But this web handler passes the empty string straight toext_mgr.install().A direct API caller (bypassing the JS frontend) could create a server with an empty name.
Suggested fix:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in bc3f863. Added empty-name guard after normalization in the web API handler. Direct API callers sending names like "@#$" now get a clear error instead of creating a server with an empty name.