Cargo.lock 🔗
@@ -179,6 +179,7 @@ dependencies = [
"gpui",
"gpui_tokio",
"handlebars 4.5.0",
+ "heck 0.5.0",
"html_to_markdown",
"http_client",
"indoc",
Jonathan Camp , Claude Opus 4.5 , and Bennet Bo Fenner created
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>
Cargo.lock | 1 +
crates/agent/Cargo.toml | 1 +
crates/agent/src/tests/mod.rs | 18 ++++++++++++++++++
crates/agent/src/thread.rs | 8 +++++---
4 files changed, 25 insertions(+), 3 deletions(-)
@@ -179,6 +179,7 @@ dependencies = [
"gpui",
"gpui_tokio",
"handlebars 4.5.0",
+ "heck 0.5.0",
"html_to_markdown",
"http_client",
"indoc",
@@ -38,6 +38,7 @@ futures.workspace = true
git.workspace = true
gpui.workspace = true
handlebars = { workspace = true, features = ["rust-embed"] }
+heck.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indoc.workspace = true
@@ -1767,6 +1767,23 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
cx,
);
+ // Server with spaces in name - tests snake_case conversion for API compatibility
+ let _server4_calls = setup_context_server(
+ "Azure DevOps",
+ vec![context_server::types::Tool {
+ name: "echo".into(), // Also conflicts - will be disambiguated as azure_dev_ops_echo
+ description: None,
+ input_schema: serde_json::to_value(EchoTool::input_schema(
+ LanguageModelToolSchemaFormat::JsonSchema,
+ ))
+ .unwrap(),
+ output_schema: None,
+ annotations: None,
+ }],
+ &context_server_store,
+ cx,
+ );
+
thread
.update(cx, |thread, cx| {
thread.send(UserMessageId::new(), ["Go"], cx)
@@ -1777,6 +1794,7 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
assert_eq!(
tool_names_for_completion(&completion),
vec![
+ "azure_dev_ops_echo",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"delay",
@@ -32,6 +32,7 @@ use futures::{
use gpui::{
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
};
+use heck::ToSnakeCase as _;
use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest,
@@ -2466,13 +2467,14 @@ impl Thread {
}
// When there are duplicate tool names, disambiguate by prefixing them
- // with the server ID. In the rare case there isn't enough space for the
- // disambiguated tool name, keep only the last tool with this name.
+ // with the server ID (converted to snake_case for API compatibility).
+ // In the rare case there isn't enough space for the disambiguated tool
+ // name, keep only the last tool with this name.
for (server_id, tool_name, tool) in context_server_tools {
if duplicate_tool_names.contains(&tool_name) {
let available = MAX_TOOL_NAME_LENGTH.saturating_sub(tool_name.len());
if available >= 2 {
- let mut disambiguated = server_id.0.to_string();
+ let mut disambiguated = server_id.0.to_snake_case();
disambiguated.truncate(available - 1);
disambiguated.push('_');
disambiguated.push_str(&tool_name);