Skip to content

Commit 1ea2f2f

Browse files
kung-fooclaudebennetbo
authored
agent: Sanitize MCP server IDs in tool name disambiguation (zed-industries#45789)
Release Notes: - Fixed an issue where a request could fail if an MCP server with names containing whitespace was used ## Summary When multiple MCP servers expose tools with the same name, Zed disambiguates them by prefixing the tool name with the server ID from settings.json. If the server ID contains spaces or special characters (e.g., `"Azure DevOps"`), the resulting tool name like `Azure DevOps_echo` violates Anthropic's API pattern `^[a-zA-Z0-9_-]{1,128}$`, causing API errors: > "Received an error from the Anthropic API: tools.0.custom.name: String should match pattern '^[a-zA-Z0-9_-]{1,128}$'" ## Solution Convert server IDs to snake_case (using the `heck` crate already available in the workspace) before using them as prefixes during tool name disambiguation. | Server ID in settings.json | Disambiguated Tool Name | |---------------------------|------------------------| | `"Azure DevOps"` | `azure_dev_ops_echo` | | `"My MCP Server"` | `my_mcp_server_echo` | ## Test plan - [x] Added test case for server name with spaces ("Azure DevOps") in `test_mcp_tool_truncation` - [x] Verified existing tests pass - [x] Manually tested with two MCP servers having overlapping tool names After (left), Before (right): <img width="2880" height="1800" alt="Screenshot_20251228_163249" src="https://github.com/user-attachments/assets/09c4e8f0-e282-4620-9db3-3e2c7d428d15" /> 🤖 Generated with (some) help from [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
1 parent 5026280 commit 1ea2f2f

4 files changed

Lines changed: 25 additions & 3 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/agent/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ futures.workspace = true
3838
git.workspace = true
3939
gpui.workspace = true
4040
handlebars = { workspace = true, features = ["rust-embed"] }
41+
heck.workspace = true
4142
html_to_markdown.workspace = true
4243
http_client.workspace = true
4344
indoc.workspace = true

‎crates/agent/src/tests/mod.rs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1767,6 +1767,23 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
17671767
cx,
17681768
);
17691769

1770+
// Server with spaces in name - tests snake_case conversion for API compatibility
1771+
let _server4_calls = setup_context_server(
1772+
"Azure DevOps",
1773+
vec![context_server::types::Tool {
1774+
name: "echo".into(), // Also conflicts - will be disambiguated as azure_dev_ops_echo
1775+
description: None,
1776+
input_schema: serde_json::to_value(EchoTool::input_schema(
1777+
LanguageModelToolSchemaFormat::JsonSchema,
1778+
))
1779+
.unwrap(),
1780+
output_schema: None,
1781+
annotations: None,
1782+
}],
1783+
&context_server_store,
1784+
cx,
1785+
);
1786+
17701787
thread
17711788
.update(cx, |thread, cx| {
17721789
thread.send(UserMessageId::new(), ["Go"], cx)
@@ -1777,6 +1794,7 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
17771794
assert_eq!(
17781795
tool_names_for_completion(&completion),
17791796
vec![
1797+
"azure_dev_ops_echo",
17801798
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
17811799
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
17821800
"delay",

‎crates/agent/src/thread.rs‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use futures::{
3232
use gpui::{
3333
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
3434
};
35+
use heck::ToSnakeCase as _;
3536
use language_model::{
3637
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
3738
LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest,
@@ -2466,13 +2467,14 @@ impl Thread {
24662467
}
24672468

24682469
// When there are duplicate tool names, disambiguate by prefixing them
2469-
// with the server ID. In the rare case there isn't enough space for the
2470-
// disambiguated tool name, keep only the last tool with this name.
2470+
// with the server ID (converted to snake_case for API compatibility).
2471+
// In the rare case there isn't enough space for the disambiguated tool
2472+
// name, keep only the last tool with this name.
24712473
for (server_id, tool_name, tool) in context_server_tools {
24722474
if duplicate_tool_names.contains(&tool_name) {
24732475
let available = MAX_TOOL_NAME_LENGTH.saturating_sub(tool_name.len());
24742476
if available >= 2 {
2475-
let mut disambiguated = server_id.0.to_string();
2477+
let mut disambiguated = server_id.0.to_snake_case();
24762478
disambiguated.truncate(available - 1);
24772479
disambiguated.push('_');
24782480
disambiguated.push_str(&tool_name);

0 commit comments

Comments
 (0)