Detailed changes
@@ -1,7 +1,7 @@
use std::sync::Arc;
use agent_settings::{AgentProfileId, AgentProfileSettings, AgentSettings};
-use assistant_tool::{Tool, ToolSource, ToolWorkingSet};
+use assistant_tool::{Tool, ToolSource, ToolWorkingSet, UniqueToolName};
use collections::IndexMap;
use convert_case::{Case, Casing};
use fs::Fs;
@@ -72,7 +72,7 @@ impl AgentProfile {
&self.id
}
- pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
+ pub fn enabled_tools(&self, cx: &App) -> Vec<(UniqueToolName, Arc<dyn Tool>)> {
let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else {
return Vec::new();
};
@@ -81,7 +81,7 @@ impl AgentProfile {
.read(cx)
.tools(cx)
.into_iter()
- .filter(|tool| Self::is_enabled(settings, tool.source(), tool.name()))
+ .filter(|(_, tool)| Self::is_enabled(settings, tool.source(), tool.name()))
.collect()
}
@@ -137,7 +137,7 @@ mod tests {
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
.into_iter()
- .map(|tool| tool.name())
+ .map(|(_, tool)| tool.name())
.collect::<Vec<_>>();
enabled_tools.sort();
@@ -174,7 +174,7 @@ mod tests {
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
.into_iter()
- .map(|tool| tool.name())
+ .map(|(_, tool)| tool.name())
.collect::<Vec<_>>();
enabled_tools.sort();
@@ -207,7 +207,7 @@ mod tests {
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
.into_iter()
- .map(|tool| tool.name())
+ .map(|(_, tool)| tool.name())
.collect::<Vec<_>>();
enabled_tools.sort();
@@ -267,10 +267,10 @@ mod tests {
}
fn default_tool_set(cx: &mut TestAppContext) -> Entity<ToolWorkingSet> {
- cx.new(|_| {
+ cx.new(|cx| {
let mut tool_set = ToolWorkingSet::default();
- tool_set.insert(Arc::new(FakeTool::new("enabled_mcp_tool", "mcp")));
- tool_set.insert(Arc::new(FakeTool::new("disabled_mcp_tool", "mcp")));
+ tool_set.insert(Arc::new(FakeTool::new("enabled_mcp_tool", "mcp")), cx);
+ tool_set.insert(Arc::new(FakeTool::new("disabled_mcp_tool", "mcp")), cx);
tool_set
})
}
@@ -13,7 +13,7 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use client::{ModelRequestUsage, RequestUsage};
-use collections::{HashMap, HashSet};
+use collections::HashMap;
use feature_flags::{self, FeatureFlagAppExt};
use futures::{FutureExt, StreamExt as _, future::Shared};
use git::repository::DiffType;
@@ -960,13 +960,14 @@ impl Thread {
model: Arc<dyn LanguageModel>,
) -> Vec<LanguageModelRequestTool> {
if model.supports_tools() {
- resolve_tool_name_conflicts(self.profile.enabled_tools(cx).as_slice())
+ self.profile
+ .enabled_tools(cx)
.into_iter()
.filter_map(|(name, tool)| {
// Skip tools that cannot be supported
let input_schema = tool.input_schema(model.tool_input_format()).ok()?;
Some(LanguageModelRequestTool {
- name,
+ name: name.into(),
description: tool.description(),
input_schema,
})
@@ -2386,7 +2387,7 @@ impl Thread {
let tool_list = available_tools
.iter()
- .map(|tool| format!("- {}: {}", tool.name(), tool.description()))
+ .map(|(name, tool)| format!("- {}: {}", name, tool.description()))
.collect::<Vec<_>>()
.join("\n");
@@ -2606,7 +2607,7 @@ impl Thread {
.profile
.enabled_tools(cx)
.iter()
- .map(|tool| tool.name())
+ .map(|(name, _)| name.clone().into())
.collect();
self.message_feedback.insert(message_id, feedback);
@@ -3144,85 +3145,6 @@ struct PendingCompletion {
_task: Task<()>,
}
-/// Resolves tool name conflicts by ensuring all tool names are unique.
-///
-/// When multiple tools have the same name, this function applies the following rules:
-/// 1. Native tools always keep their original name
-/// 2. Context server tools get prefixed with their server ID and an underscore
-/// 3. All tool names are truncated to MAX_TOOL_NAME_LENGTH (64 characters)
-/// 4. If conflicts still exist after prefixing, the conflicting tools are filtered out
-///
-/// Note: This function assumes that built-in tools occur before MCP tools in the tools list.
-fn resolve_tool_name_conflicts(tools: &[Arc<dyn Tool>]) -> Vec<(String, Arc<dyn Tool>)> {
- fn resolve_tool_name(tool: &Arc<dyn Tool>) -> String {
- let mut tool_name = tool.name();
- tool_name.truncate(MAX_TOOL_NAME_LENGTH);
- tool_name
- }
-
- const MAX_TOOL_NAME_LENGTH: usize = 64;
-
- let mut duplicated_tool_names = HashSet::default();
- let mut seen_tool_names = HashSet::default();
- for tool in tools {
- let tool_name = resolve_tool_name(tool);
- if seen_tool_names.contains(&tool_name) {
- debug_assert!(
- tool.source() != assistant_tool::ToolSource::Native,
- "There are two built-in tools with the same name: {}",
- tool_name
- );
- duplicated_tool_names.insert(tool_name);
- } else {
- seen_tool_names.insert(tool_name);
- }
- }
-
- if duplicated_tool_names.is_empty() {
- return tools
- .into_iter()
- .map(|tool| (resolve_tool_name(tool), tool.clone()))
- .collect();
- }
-
- tools
- .into_iter()
- .filter_map(|tool| {
- let mut tool_name = resolve_tool_name(tool);
- if !duplicated_tool_names.contains(&tool_name) {
- return Some((tool_name, tool.clone()));
- }
- match tool.source() {
- assistant_tool::ToolSource::Native => {
- // Built-in tools always keep their original name
- Some((tool_name, tool.clone()))
- }
- assistant_tool::ToolSource::ContextServer { id } => {
- // Context server tools are prefixed with the context server ID, and truncated if necessary
- tool_name.insert(0, '_');
- if tool_name.len() + id.len() > MAX_TOOL_NAME_LENGTH {
- let len = MAX_TOOL_NAME_LENGTH - tool_name.len();
- let mut id = id.to_string();
- id.truncate(len);
- tool_name.insert_str(0, &id);
- } else {
- tool_name.insert_str(0, &id);
- }
-
- tool_name.truncate(MAX_TOOL_NAME_LENGTH);
-
- if seen_tool_names.contains(&tool_name) {
- log::error!("Cannot resolve tool name conflict for tool {}", tool.name());
- None
- } else {
- Some((tool_name, tool.clone()))
- }
- }
- }
- })
- .collect()
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -3238,7 +3160,6 @@ mod tests {
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use gpui::TestAppContext;
- use icons::IconName;
use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider};
use language_model::{
LanguageModelCompletionError, LanguageModelName, LanguageModelProviderId,
@@ -3883,148 +3804,6 @@ fn main() {{
});
}
- #[gpui::test]
- fn test_resolve_tool_name_conflicts() {
- use assistant_tool::{Tool, ToolSource};
-
- assert_resolve_tool_name_conflicts(
- vec![
- TestTool::new("tool1", ToolSource::Native),
- TestTool::new("tool2", ToolSource::Native),
- TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
- ],
- vec!["tool1", "tool2", "tool3"],
- );
-
- assert_resolve_tool_name_conflicts(
- vec![
- TestTool::new("tool1", ToolSource::Native),
- TestTool::new("tool2", ToolSource::Native),
- TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
- TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
- ],
- vec!["tool1", "tool2", "mcp-1_tool3", "mcp-2_tool3"],
- );
-
- assert_resolve_tool_name_conflicts(
- vec![
- TestTool::new("tool1", ToolSource::Native),
- TestTool::new("tool2", ToolSource::Native),
- TestTool::new("tool3", ToolSource::Native),
- TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
- TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
- ],
- vec!["tool1", "tool2", "tool3", "mcp-1_tool3", "mcp-2_tool3"],
- );
-
- // Test that tool with very long name is always truncated
- assert_resolve_tool_name_conflicts(
- vec![TestTool::new(
- "tool-with-more-then-64-characters-blah-blah-blah-blah-blah-blah-blah-blah",
- ToolSource::Native,
- )],
- vec!["tool-with-more-then-64-characters-blah-blah-blah-blah-blah-blah-"],
- );
-
- // Test deduplication of tools with very long names, in this case the mcp server name should be truncated
- assert_resolve_tool_name_conflicts(
- vec![
- TestTool::new("tool-with-very-very-very-long-name", ToolSource::Native),
- TestTool::new(
- "tool-with-very-very-very-long-name",
- ToolSource::ContextServer {
- id: "mcp-with-very-very-very-long-name".into(),
- },
- ),
- ],
- vec![
- "tool-with-very-very-very-long-name",
- "mcp-with-very-very-very-long-_tool-with-very-very-very-long-name",
- ],
- );
-
- fn assert_resolve_tool_name_conflicts(
- tools: Vec<TestTool>,
- expected: Vec<impl Into<String>>,
- ) {
- let tools: Vec<Arc<dyn Tool>> = tools
- .into_iter()
- .map(|t| Arc::new(t) as Arc<dyn Tool>)
- .collect();
- let tools = resolve_tool_name_conflicts(&tools);
- assert_eq!(tools.len(), expected.len());
- for (i, expected_name) in expected.into_iter().enumerate() {
- let expected_name = expected_name.into();
- let actual_name = &tools[i].0;
- assert_eq!(
- actual_name, &expected_name,
- "Expected '{}' got '{}' at index {}",
- expected_name, actual_name, i
- );
- }
- }
-
- struct TestTool {
- name: String,
- source: ToolSource,
- }
-
- impl TestTool {
- fn new(name: impl Into<String>, source: ToolSource) -> Self {
- Self {
- name: name.into(),
- source,
- }
- }
- }
-
- impl Tool for TestTool {
- fn name(&self) -> String {
- self.name.clone()
- }
-
- fn icon(&self) -> IconName {
- IconName::Ai
- }
-
- fn may_perform_edits(&self) -> bool {
- false
- }
-
- fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
- true
- }
-
- fn source(&self) -> ToolSource {
- self.source.clone()
- }
-
- fn description(&self) -> String {
- "Test tool".to_string()
- }
-
- fn ui_text(&self, _input: &serde_json::Value) -> String {
- "Test tool".to_string()
- }
-
- fn run(
- self: Arc<Self>,
- _input: serde_json::Value,
- _request: Arc<LanguageModelRequest>,
- _project: Entity<Project>,
- _action_log: Entity<ActionLog>,
- _model: Arc<dyn LanguageModel>,
- _window: Option<AnyWindowHandle>,
- _cx: &mut App,
- ) -> assistant_tool::ToolResult {
- assistant_tool::ToolResult {
- output: Task::ready(Err(anyhow::anyhow!("No content"))),
- card: None,
- }
- }
- }
- }
-
// Helper to create a model that returns errors
enum TestError {
Overloaded,
@@ -6,7 +6,7 @@ use crate::{
};
use agent_settings::{AgentProfileId, CompletionMode};
use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ToolId, ToolWorkingSet};
+use assistant_tool::{Tool, ToolId, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
use context_server::ContextServerId;
@@ -537,8 +537,8 @@ impl ThreadStore {
}
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
- tool_working_set.update(cx, |tool_working_set, _| {
- tool_working_set.remove(&tool_ids);
+ tool_working_set.update(cx, |tool_working_set, cx| {
+ tool_working_set.remove(&tool_ids, cx);
});
}
}
@@ -569,19 +569,17 @@ impl ThreadStore {
.log_err()
{
let tool_ids = tool_working_set
- .update(cx, |tool_working_set, _| {
- response
- .tools
- .into_iter()
- .map(|tool| {
- log::info!("registering context server tool: {:?}", tool.name);
- tool_working_set.insert(Arc::new(ContextServerTool::new(
+ .update(cx, |tool_working_set, cx| {
+ tool_working_set.extend(
+ response.tools.into_iter().map(|tool| {
+ Arc::new(ContextServerTool::new(
context_server_store.clone(),
server.id(),
tool,
- )))
- })
- .collect::<Vec<_>>()
+ )) as Arc<dyn Tool>
+ }),
+ cx,
+ )
})
.log_err();
@@ -42,8 +42,8 @@ impl IncompatibleToolsState {
.profile()
.enabled_tools(cx)
.iter()
- .filter(|tool| tool.input_schema(model.tool_input_format()).is_err())
- .cloned()
+ .filter(|(_, tool)| tool.input_schema(model.tool_input_format()).is_err())
+ .map(|(_, tool)| tool.clone())
.collect()
})
}
@@ -22,6 +22,7 @@ gpui.workspace = true
icons.workspace = true
language.workspace = true
language_model.workspace = true
+log.workspace = true
parking_lot.workspace = true
project.workspace = true
regex.workspace = true
@@ -1,18 +1,52 @@
-use std::sync::Arc;
-
-use collections::{HashMap, IndexMap};
-use gpui::App;
+use std::{borrow::Borrow, sync::Arc};
use crate::{Tool, ToolRegistry, ToolSource};
+use collections::{HashMap, HashSet, IndexMap};
+use gpui::{App, SharedString};
+use util::debug_panic;
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
pub struct ToolId(usize);
+/// A unique identifier for a tool within a working set.
+#[derive(Clone, PartialEq, Eq, Hash, Default)]
+pub struct UniqueToolName(SharedString);
+
+impl Borrow<str> for UniqueToolName {
+ fn borrow(&self) -> &str {
+ &self.0
+ }
+}
+
+impl From<String> for UniqueToolName {
+ fn from(value: String) -> Self {
+ UniqueToolName(SharedString::new(value))
+ }
+}
+
+impl Into<String> for UniqueToolName {
+ fn into(self) -> String {
+ self.0.into()
+ }
+}
+
+impl std::fmt::Debug for UniqueToolName {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl std::fmt::Display for UniqueToolName {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0.as_ref())
+ }
+}
+
/// A working set of tools for use in one instance of the Assistant Panel.
#[derive(Default)]
pub struct ToolWorkingSet {
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
- context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
+ context_server_tools_by_name: HashMap<UniqueToolName, Arc<dyn Tool>>,
next_tool_id: ToolId,
}
@@ -24,16 +58,20 @@ impl ToolWorkingSet {
.or_else(|| ToolRegistry::global(cx).tool(name))
}
- pub fn tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
- let mut tools = ToolRegistry::global(cx).tools();
- tools.extend(self.context_server_tools_by_id.values().cloned());
+ pub fn tools(&self, cx: &App) -> Vec<(UniqueToolName, Arc<dyn Tool>)> {
+ let mut tools = ToolRegistry::global(cx)
+ .tools()
+ .into_iter()
+ .map(|tool| (UniqueToolName(tool.name().into()), tool))
+ .collect::<Vec<_>>();
+ tools.extend(self.context_server_tools_by_name.clone());
tools
}
pub fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
let mut tools_by_source = IndexMap::default();
- for tool in self.tools(cx) {
+ for (_, tool) in self.tools(cx) {
tools_by_source
.entry(tool.source())
.or_insert_with(Vec::new)
@@ -49,27 +87,324 @@ impl ToolWorkingSet {
tools_by_source
}
- pub fn insert(&mut self, tool: Arc<dyn Tool>) -> ToolId {
+ pub fn insert(&mut self, tool: Arc<dyn Tool>, cx: &App) -> ToolId {
+ let tool_id = self.register_tool(tool);
+ self.tools_changed(cx);
+ tool_id
+ }
+
+ pub fn extend(&mut self, tools: impl Iterator<Item = Arc<dyn Tool>>, cx: &App) -> Vec<ToolId> {
+ let ids = tools.map(|tool| self.register_tool(tool)).collect();
+ self.tools_changed(cx);
+ ids
+ }
+
+ pub fn remove(&mut self, tool_ids_to_remove: &[ToolId], cx: &App) {
+ self.context_server_tools_by_id
+ .retain(|id, _| !tool_ids_to_remove.contains(id));
+ self.tools_changed(cx);
+ }
+
+ fn register_tool(&mut self, tool: Arc<dyn Tool>) -> ToolId {
let tool_id = self.next_tool_id;
self.next_tool_id.0 += 1;
self.context_server_tools_by_id
.insert(tool_id, tool.clone());
- self.tools_changed();
tool_id
}
- pub fn remove(&mut self, tool_ids_to_remove: &[ToolId]) {
- self.context_server_tools_by_id
- .retain(|id, _| !tool_ids_to_remove.contains(id));
- self.tools_changed();
+ fn tools_changed(&mut self, cx: &App) {
+ self.context_server_tools_by_name = resolve_context_server_tool_name_conflicts(
+ &self
+ .context_server_tools_by_id
+ .values()
+ .cloned()
+ .collect::<Vec<_>>(),
+ &ToolRegistry::global(cx).tools(),
+ );
+ }
+}
+
+fn resolve_context_server_tool_name_conflicts(
+ context_server_tools: &[Arc<dyn Tool>],
+ native_tools: &[Arc<dyn Tool>],
+) -> HashMap<UniqueToolName, Arc<dyn Tool>> {
+ fn resolve_tool_name(tool: &Arc<dyn Tool>) -> String {
+ let mut tool_name = tool.name();
+ tool_name.truncate(MAX_TOOL_NAME_LENGTH);
+ tool_name
}
- fn tools_changed(&mut self) {
- self.context_server_tools_by_name.clear();
- self.context_server_tools_by_name.extend(
- self.context_server_tools_by_id
- .values()
- .map(|tool| (tool.name(), tool.clone())),
+ const MAX_TOOL_NAME_LENGTH: usize = 64;
+
+ let mut duplicated_tool_names = HashSet::default();
+ let mut seen_tool_names = HashSet::default();
+ seen_tool_names.extend(native_tools.iter().map(|tool| tool.name()));
+ for tool in context_server_tools {
+ let tool_name = resolve_tool_name(tool);
+ if seen_tool_names.contains(&tool_name) {
+ debug_assert!(
+ tool.source() != ToolSource::Native,
+ "Expected MCP tool but got a native tool: {}",
+ tool_name
+ );
+ duplicated_tool_names.insert(tool_name);
+ } else {
+ seen_tool_names.insert(tool_name);
+ }
+ }
+
+ if duplicated_tool_names.is_empty() {
+ return context_server_tools
+ .into_iter()
+ .map(|tool| (resolve_tool_name(tool).into(), tool.clone()))
+ .collect();
+ }
+
+ context_server_tools
+ .into_iter()
+ .filter_map(|tool| {
+ let mut tool_name = resolve_tool_name(tool);
+ if !duplicated_tool_names.contains(&tool_name) {
+ return Some((tool_name.into(), tool.clone()));
+ }
+ match tool.source() {
+ ToolSource::Native => {
+ debug_panic!("Expected MCP tool but got a native tool: {}", tool_name);
+ // Built-in tools always keep their original name
+ Some((tool_name.into(), tool.clone()))
+ }
+ ToolSource::ContextServer { id } => {
+ // Context server tools are prefixed with the context server ID, and truncated if necessary
+ tool_name.insert(0, '_');
+ if tool_name.len() + id.len() > MAX_TOOL_NAME_LENGTH {
+ let len = MAX_TOOL_NAME_LENGTH - tool_name.len();
+ let mut id = id.to_string();
+ id.truncate(len);
+ tool_name.insert_str(0, &id);
+ } else {
+ tool_name.insert_str(0, &id);
+ }
+
+ tool_name.truncate(MAX_TOOL_NAME_LENGTH);
+
+ if seen_tool_names.contains(&tool_name) {
+ log::error!("Cannot resolve tool name conflict for tool {}", tool.name());
+ None
+ } else {
+ Some((tool_name.into(), tool.clone()))
+ }
+ }
+ }
+ })
+ .collect()
+}
+#[cfg(test)]
+mod tests {
+ use gpui::{AnyWindowHandle, Entity, Task, TestAppContext};
+ use language_model::{LanguageModel, LanguageModelRequest};
+ use project::Project;
+
+ use crate::{ActionLog, ToolResult};
+
+ use super::*;
+
+ #[gpui::test]
+ fn test_unique_tool_names(cx: &mut TestAppContext) {
+ fn assert_tool(
+ tool_working_set: &ToolWorkingSet,
+ unique_name: &str,
+ expected_name: &str,
+ expected_source: ToolSource,
+ cx: &App,
+ ) {
+ let tool = tool_working_set.tool(unique_name, cx).unwrap();
+ assert_eq!(tool.name(), expected_name);
+ assert_eq!(tool.source(), expected_source);
+ }
+
+ let tool_registry = cx.update(ToolRegistry::default_global);
+ tool_registry.register_tool(TestTool::new("tool1", ToolSource::Native));
+ tool_registry.register_tool(TestTool::new("tool2", ToolSource::Native));
+
+ let mut tool_working_set = ToolWorkingSet::default();
+ cx.update(|cx| {
+ tool_working_set.extend(
+ vec![
+ Arc::new(TestTool::new(
+ "tool2",
+ ToolSource::ContextServer { id: "mcp-1".into() },
+ )) as Arc<dyn Tool>,
+ Arc::new(TestTool::new(
+ "tool2",
+ ToolSource::ContextServer { id: "mcp-2".into() },
+ )) as Arc<dyn Tool>,
+ ]
+ .into_iter(),
+ cx,
+ );
+ });
+
+ cx.update(|cx| {
+ assert_tool(&tool_working_set, "tool1", "tool1", ToolSource::Native, cx);
+ assert_tool(&tool_working_set, "tool2", "tool2", ToolSource::Native, cx);
+ assert_tool(
+ &tool_working_set,
+ "mcp-1_tool2",
+ "tool2",
+ ToolSource::ContextServer { id: "mcp-1".into() },
+ cx,
+ );
+ assert_tool(
+ &tool_working_set,
+ "mcp-2_tool2",
+ "tool2",
+ ToolSource::ContextServer { id: "mcp-2".into() },
+ cx,
+ );
+ })
+ }
+
+ #[gpui::test]
+ fn test_resolve_context_server_tool_name_conflicts() {
+ assert_resolve_context_server_tool_name_conflicts(
+ vec![
+ TestTool::new("tool1", ToolSource::Native),
+ TestTool::new("tool2", ToolSource::Native),
+ ],
+ vec![TestTool::new(
+ "tool3",
+ ToolSource::ContextServer { id: "mcp-1".into() },
+ )],
+ vec!["tool3"],
);
+
+ assert_resolve_context_server_tool_name_conflicts(
+ vec![
+ TestTool::new("tool1", ToolSource::Native),
+ TestTool::new("tool2", ToolSource::Native),
+ ],
+ vec![
+ TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
+ TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
+ ],
+ vec!["mcp-1_tool3", "mcp-2_tool3"],
+ );
+
+ assert_resolve_context_server_tool_name_conflicts(
+ vec![
+ TestTool::new("tool1", ToolSource::Native),
+ TestTool::new("tool2", ToolSource::Native),
+ TestTool::new("tool3", ToolSource::Native),
+ ],
+ vec![
+ TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
+ TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
+ ],
+ vec!["mcp-1_tool3", "mcp-2_tool3"],
+ );
+
+ // Test deduplication of tools with very long names, in this case the mcp server name should be truncated
+ assert_resolve_context_server_tool_name_conflicts(
+ vec![TestTool::new(
+ "tool-with-very-very-very-long-name",
+ ToolSource::Native,
+ )],
+ vec![TestTool::new(
+ "tool-with-very-very-very-long-name",
+ ToolSource::ContextServer {
+ id: "mcp-with-very-very-very-long-name".into(),
+ },
+ )],
+ vec!["mcp-with-very-very-very-long-_tool-with-very-very-very-long-name"],
+ );
+
+ fn assert_resolve_context_server_tool_name_conflicts(
+ builtin_tools: Vec<TestTool>,
+ context_server_tools: Vec<TestTool>,
+ expected: Vec<&'static str>,
+ ) {
+ let context_server_tools: Vec<Arc<dyn Tool>> = context_server_tools
+ .into_iter()
+ .map(|t| Arc::new(t) as Arc<dyn Tool>)
+ .collect();
+ let builtin_tools: Vec<Arc<dyn Tool>> = builtin_tools
+ .into_iter()
+ .map(|t| Arc::new(t) as Arc<dyn Tool>)
+ .collect();
+ let tools =
+ resolve_context_server_tool_name_conflicts(&context_server_tools, &builtin_tools);
+ assert_eq!(tools.len(), expected.len());
+ for (i, (name, _)) in tools.into_iter().enumerate() {
+ assert_eq!(
+ name.0.as_ref(),
+ expected[i],
+ "Expected '{}' got '{}' at index {}",
+ expected[i],
+ name,
+ i
+ );
+ }
+ }
+ }
+
+ struct TestTool {
+ name: String,
+ source: ToolSource,
+ }
+
+ impl TestTool {
+ fn new(name: impl Into<String>, source: ToolSource) -> Self {
+ Self {
+ name: name.into(),
+ source,
+ }
+ }
+ }
+
+ impl Tool for TestTool {
+ fn name(&self) -> String {
+ self.name.clone()
+ }
+
+ fn icon(&self) -> icons::IconName {
+ icons::IconName::Ai
+ }
+
+ fn may_perform_edits(&self) -> bool {
+ false
+ }
+
+ fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
+ true
+ }
+
+ fn source(&self) -> ToolSource {
+ self.source.clone()
+ }
+
+ fn description(&self) -> String {
+ "Test tool".to_string()
+ }
+
+ fn ui_text(&self, _input: &serde_json::Value) -> String {
+ "Test tool".to_string()
+ }
+
+ fn run(
+ self: Arc<Self>,
+ _input: serde_json::Value,
+ _request: Arc<LanguageModelRequest>,
+ _project: Entity<Project>,
+ _action_log: Entity<ActionLog>,
+ _model: Arc<dyn LanguageModel>,
+ _window: Option<AnyWindowHandle>,
+ _cx: &mut App,
+ ) -> ToolResult {
+ ToolResult {
+ output: Task::ready(Err(anyhow::anyhow!("No content"))),
+ card: None,
+ }
+ }
}
}