mcp: reject colliding tool names
This commit is contained in:
parent
66fa9d55a1
commit
0080c5b3d4
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::BTreeMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -62,7 +62,6 @@ async fn discover_server_tools(spec: McpStdioServerSpec) -> ProtocolProviderCont
|
||||||
let declaration = provider_declaration(&spec.name, None);
|
let declaration = provider_declaration(&spec.name, None);
|
||||||
let mut contribution = ProtocolProviderContribution::ready(declaration.clone());
|
let mut contribution = ProtocolProviderContribution::ready(declaration.clone());
|
||||||
let server_namespace = sanitize_segment(&spec.name);
|
let server_namespace = sanitize_segment(&spec.name);
|
||||||
let mut seen_names = HashSet::new();
|
|
||||||
|
|
||||||
let mut client = match McpStdioClient::connect(spec, McpStdioLimits::default()).await {
|
let mut client = match McpStdioClient::connect(spec, McpStdioLimits::default()).await {
|
||||||
Ok(client) => client,
|
Ok(client) => client,
|
||||||
|
|
@ -128,7 +127,6 @@ async fn discover_server_tools(spec: McpStdioServerSpec) -> ProtocolProviderCont
|
||||||
server_namespace,
|
server_namespace,
|
||||||
server_version,
|
server_version,
|
||||||
list,
|
list,
|
||||||
&mut seen_names,
|
|
||||||
);
|
);
|
||||||
contribution
|
contribution
|
||||||
}
|
}
|
||||||
|
|
@ -139,8 +137,10 @@ fn normalize_listed_tools(
|
||||||
server_namespace: String,
|
server_namespace: String,
|
||||||
server_version: Option<String>,
|
server_version: Option<String>,
|
||||||
list: ListToolsResult,
|
list: ListToolsResult,
|
||||||
seen_names: &mut HashSet<String>,
|
|
||||||
) -> ProtocolProviderContribution {
|
) -> ProtocolProviderContribution {
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
let mut name_counts = BTreeMap::<String, usize>::new();
|
||||||
|
|
||||||
for tool in list.tools {
|
for tool in list.tools {
|
||||||
match mcp_tool_contribution(
|
match mcp_tool_contribution(
|
||||||
&declaration,
|
&declaration,
|
||||||
|
|
@ -149,20 +149,30 @@ fn normalize_listed_tools(
|
||||||
tool,
|
tool,
|
||||||
) {
|
) {
|
||||||
Ok((name, tool_contribution)) => {
|
Ok((name, tool_contribution)) => {
|
||||||
if !seen_names.insert(name.clone()) {
|
*name_counts.entry(name.clone()).or_default() += 1;
|
||||||
contribution =
|
candidates.push((name, tool_contribution));
|
||||||
contribution.with_diagnostic(FeatureDiagnostic::error(bounded_diagnostic(
|
|
||||||
format!("duplicate MCP tool name `{name}` after namespacing; skipped"),
|
|
||||||
)));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
contribution = contribution.with_tool(tool_contribution);
|
|
||||||
}
|
}
|
||||||
Err(message) => {
|
Err(message) => {
|
||||||
contribution = contribution.with_diagnostic(FeatureDiagnostic::error(message));
|
contribution = contribution.with_diagnostic(FeatureDiagnostic::error(message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (name, count) in &name_counts {
|
||||||
|
if *count > 1 {
|
||||||
|
contribution = contribution.with_diagnostic(FeatureDiagnostic::error(bounded_diagnostic(
|
||||||
|
format!(
|
||||||
|
"duplicate MCP tool name `{name}` after namespacing ({count} definitions); all colliding definitions skipped"
|
||||||
|
),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (name, tool_contribution) in candidates {
|
||||||
|
if name_counts.get(&name).copied().unwrap_or_default() == 1 {
|
||||||
|
contribution = contribution.with_tool(tool_contribution);
|
||||||
|
}
|
||||||
|
}
|
||||||
contribution
|
contribution
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -557,13 +567,13 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn duplicate_names_after_normalization_are_diagnostic_only() {
|
fn duplicate_names_after_normalization_are_not_model_visible() {
|
||||||
let declaration = provider_declaration("demo", None);
|
let declaration = provider_declaration("demo", None);
|
||||||
let mut seen = HashSet::new();
|
|
||||||
let list = ListToolsResult {
|
let list = ListToolsResult {
|
||||||
tools: vec![
|
tools: vec![
|
||||||
mcp_tool("search-files", "one", json!({"type":"object"})),
|
mcp_tool("search-files", "one", json!({"type":"object"})),
|
||||||
mcp_tool("search files", "two", json!({"type":"object"})),
|
mcp_tool("search files", "two", json!({"type":"object"})),
|
||||||
|
mcp_tool("unique", "three", json!({"type":"object"})),
|
||||||
],
|
],
|
||||||
next_cursor: None,
|
next_cursor: None,
|
||||||
meta: None,
|
meta: None,
|
||||||
|
|
@ -575,16 +585,32 @@ mod tests {
|
||||||
"demo".to_string(),
|
"demo".to_string(),
|
||||||
None,
|
None,
|
||||||
list,
|
list,
|
||||||
&mut seen,
|
|
||||||
);
|
);
|
||||||
assert_eq!(seen.len(), 1);
|
|
||||||
assert!(
|
assert!(
|
||||||
contribution
|
contribution
|
||||||
.diagnostics
|
.diagnostics
|
||||||
.iter()
|
.iter()
|
||||||
.any(|diag| diag.severity == FeatureDiagnosticSeverity::Error
|
.any(|diag| diag.severity == FeatureDiagnosticSeverity::Error
|
||||||
&& diag.message.contains("duplicate"))
|
&& diag.message.contains("duplicate")
|
||||||
|
&& diag.message.contains("all colliding definitions skipped"))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut feature = McpStdioToolFeature::new();
|
||||||
|
feature.add_contribution(contribution);
|
||||||
|
let mut pending_tools = Vec::new();
|
||||||
|
let mut hook_builder = HookRegistryBuilder::default();
|
||||||
|
FeatureRegistryBuilder::new()
|
||||||
|
.with_module(feature)
|
||||||
|
.install_into_pending(&mut pending_tools, &mut hook_builder);
|
||||||
|
let names: Vec<_> = pending_tools
|
||||||
|
.iter()
|
||||||
|
.map(|definition| {
|
||||||
|
let (meta, _) = definition();
|
||||||
|
meta.name
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert!(!names.iter().any(|name| name == "Mcp_demo_search_files"));
|
||||||
|
assert!(names.iter().any(|name| name == "Mcp_demo_unique"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user