Skip to content

Commit 97ef6c9

Browse files
authored
Implementation of SEP-986: Specify Format for Tool Names (#551)
* feat: implement SEP-986 tool name validation and error reporting Adds validation for MCP tool naming conventions as specified in SEP-986. Ensures Rust SDK enforces standardized tool name formats, provides clear errors for invalid names, and improves consistency across implementations. Signed-off-by: tanish111 <[email protected]> * fix(doctests): correct import paths in tool_name_validation Update doctest examples to use the correct module path rmcp::handler::server::tool_name_validation instead of rmcp::model::tool_name_validation. Signed-off-by: tanish111 <[email protected]> * fix: only warn when warnings exist Prevent empty warnings array from triggering warning output. Signed-off-by: tanish111 <[email protected]> * fix: remove internal check Signed-off-by: tanish111 <[email protected]> * refactor: remove doc comments from validation functions Signed-off-by: tanish111 <[email protected]> * refactor: remove doc comments from add_route functions Signed-off-by: tanish111 <[email protected]> * refactor: make tool name validation helpers private Made ToolNameValidationResult and its functions and fields private. Made validate_tool_name and issue_tool_name_warning private. Signed-off-by: tanish111 <[email protected]> --------- Signed-off-by: tanish111 <[email protected]>
1 parent cf45070 commit 97ef6c9

File tree

3 files changed

+268
-3
lines changed

3 files changed

+268
-3
lines changed

crates/rmcp/src/handler/server.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod prompt;
99
mod resource;
1010
pub mod router;
1111
pub mod tool;
12+
pub mod tool_name_validation;
1213
pub mod wrapper;
1314
impl<H: ServerHandler> Service<RoleServer> for H {
1415
async fn handle_request(

crates/rmcp/src/handler/server/router/tool.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ use futures::{FutureExt, future::BoxFuture};
44
use schemars::JsonSchema;
55

66
use crate::{
7-
handler::server::tool::{
8-
CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type,
7+
handler::server::{
8+
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type},
9+
tool_name_validation::validate_and_warn_tool_name,
910
},
1011
model::{CallToolResult, Tool, ToolAnnotations},
1112
};
@@ -219,7 +220,9 @@ where
219220
}
220221

221222
pub fn add_route(&mut self, item: ToolRoute<S>) {
222-
self.map.insert(item.attr.name.clone(), item);
223+
let new_name = &item.attr.name;
224+
validate_and_warn_tool_name(new_name);
225+
self.map.insert(new_name.clone(), item);
223226
}
224227

225228
pub fn merge(&mut self, other: ToolRouter<S>) {
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
//! Tool name validation utilities according to SEP: Specify Format for Tool Names
2+
//!
3+
//! Tool names SHOULD be between 1 and 128 characters in length (inclusive).
4+
//! Tool names are case-sensitive.
5+
//! Allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits
6+
//! (0-9), underscore (_), dash (-), and dot (.).
7+
//! Tool names SHOULD NOT contain spaces, commas, or other special characters.
8+
9+
use std::collections::HashSet;
10+
11+
/// Result of tool name validation containing validation status and warnings.
12+
#[derive(Debug, Clone, PartialEq, Eq)]
13+
struct ToolNameValidationResult {
14+
/// Whether the tool name is valid according to the specification
15+
is_valid: bool,
16+
/// Array of warning messages about non-conforming aspects of the tool name
17+
warnings: Vec<String>,
18+
}
19+
20+
impl ToolNameValidationResult {
21+
/// Create a new validation result
22+
fn new(is_valid: bool, warnings: Vec<String>) -> Self {
23+
Self { is_valid, warnings }
24+
}
25+
}
26+
27+
/// Validates a tool name according to the SEP specification.
28+
fn validate_tool_name(name: &str) -> ToolNameValidationResult {
29+
let mut warnings = Vec::new();
30+
31+
// Check length
32+
if name.is_empty() {
33+
return ToolNameValidationResult::new(false, vec!["Tool name cannot be empty".to_string()]);
34+
}
35+
36+
if name.len() > 128 {
37+
return ToolNameValidationResult::new(
38+
false,
39+
vec![format!(
40+
"Tool name exceeds maximum length of 128 characters (current: {})",
41+
name.len()
42+
)],
43+
);
44+
}
45+
46+
// Check for specific problematic patterns (these are warnings, not validation failures)
47+
if name.contains(' ') {
48+
warnings.push("Tool name contains spaces, which may cause parsing issues".to_string());
49+
}
50+
51+
if name.contains(',') {
52+
warnings.push("Tool name contains commas, which may cause parsing issues".to_string());
53+
}
54+
55+
// Check for potentially confusing patterns (leading/trailing dashes, dots, slashes)
56+
if name.starts_with('-') || name.ends_with('-') {
57+
warnings.push(
58+
"Tool name starts or ends with a dash, which may cause parsing issues in some contexts"
59+
.to_string(),
60+
);
61+
}
62+
63+
if name.starts_with('.') || name.ends_with('.') {
64+
warnings.push(
65+
"Tool name starts or ends with a dot, which may cause parsing issues in some contexts"
66+
.to_string(),
67+
);
68+
}
69+
70+
// Check for invalid characters
71+
let mut invalid_chars = HashSet::new();
72+
let valid_chars: HashSet<char> =
73+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
74+
.chars()
75+
.collect();
76+
77+
for ch in name.chars() {
78+
if !valid_chars.contains(&ch) {
79+
invalid_chars.insert(ch);
80+
}
81+
}
82+
83+
if !invalid_chars.is_empty() {
84+
let invalid_chars_list: Vec<String> =
85+
invalid_chars.iter().map(|c| format!("\"{}\"", c)).collect();
86+
warnings.push(format!(
87+
"Tool name contains invalid characters: {}",
88+
invalid_chars_list.join(", ")
89+
));
90+
warnings.push(
91+
"Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)"
92+
.to_string(),
93+
);
94+
95+
return ToolNameValidationResult::new(false, warnings);
96+
}
97+
98+
// Verify the pattern matches (double check with character-by-character validation)
99+
// We've already validated characters above, just need to verify length is within bounds
100+
if name.is_empty() || name.len() > 128 {
101+
return ToolNameValidationResult::new(
102+
false,
103+
vec!["Tool name length must be between 1 and 128 characters".to_string()],
104+
);
105+
}
106+
107+
ToolNameValidationResult::new(true, warnings)
108+
}
109+
110+
/// Issues warnings for non-conforming tool names.
111+
fn issue_tool_name_warning(name: &str, warnings: &[String]) {
112+
tracing::warn!("Tool name validation warning for \"{}\":", name);
113+
for warning in warnings {
114+
tracing::warn!(" - {}", warning);
115+
}
116+
tracing::warn!("Tool registration will proceed, but this may cause compatibility issues.");
117+
tracing::warn!("Consider updating the tool name to conform to the MCP tool naming standard.");
118+
tracing::warn!(
119+
"See SEP: Specify Format for Tool Names (https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) for more details."
120+
);
121+
}
122+
123+
/// Validates a tool name and issues warnings for non-conforming names.
124+
pub fn validate_and_warn_tool_name(name: &str) -> bool {
125+
let result = validate_tool_name(name);
126+
127+
if !result.warnings.is_empty() {
128+
issue_tool_name_warning(name, &result.warnings);
129+
}
130+
131+
result.is_valid
132+
}
133+
#[cfg(test)]
134+
mod tests {
135+
use super::*;
136+
137+
#[test]
138+
fn test_valid_tool_names() {
139+
let max_length_name = "a".repeat(128);
140+
let valid_names = vec![
141+
"my_tool",
142+
"MyTool",
143+
"my-tool",
144+
"my.tool",
145+
"tool123",
146+
"a",
147+
max_length_name.as_str(), // Maximum length
148+
];
149+
150+
for name in valid_names {
151+
let result = validate_tool_name(name);
152+
assert!(result.is_valid, "Tool name '{}' should be valid", name);
153+
}
154+
}
155+
156+
#[test]
157+
fn test_empty_tool_name() {
158+
let result = validate_tool_name("");
159+
assert!(!result.is_valid);
160+
assert!(
161+
result
162+
.warnings
163+
.contains(&"Tool name cannot be empty".to_string())
164+
);
165+
}
166+
167+
#[test]
168+
fn test_too_long_tool_name() {
169+
let name = "a".repeat(129);
170+
let result = validate_tool_name(&name);
171+
assert!(!result.is_valid);
172+
assert!(result.warnings[0].contains("exceeds maximum length"));
173+
}
174+
175+
#[test]
176+
fn test_tool_name_with_spaces() {
177+
let result = validate_tool_name("my tool");
178+
assert!(!result.is_valid);
179+
assert!(
180+
result
181+
.warnings
182+
.iter()
183+
.any(|w| w.contains("contains spaces"))
184+
);
185+
}
186+
187+
#[test]
188+
fn test_tool_name_with_commas() {
189+
let result = validate_tool_name("my,tool");
190+
assert!(!result.is_valid);
191+
assert!(
192+
result
193+
.warnings
194+
.iter()
195+
.any(|w| w.contains("contains commas"))
196+
);
197+
}
198+
199+
#[test]
200+
fn test_tool_name_starting_with_dash() {
201+
let result = validate_tool_name("-tool");
202+
assert!(result.is_valid); // Still valid, but has warning
203+
assert!(
204+
result
205+
.warnings
206+
.iter()
207+
.any(|w| w.contains("starts or ends with a dash"))
208+
);
209+
}
210+
211+
#[test]
212+
fn test_tool_name_ending_with_dot() {
213+
let result = validate_tool_name("tool.");
214+
assert!(result.is_valid); // Still valid, but has warning
215+
assert!(
216+
result
217+
.warnings
218+
.iter()
219+
.any(|w| w.contains("starts or ends with a dot"))
220+
);
221+
}
222+
223+
#[test]
224+
fn test_tool_name_with_invalid_characters() {
225+
let result = validate_tool_name("my@tool");
226+
assert!(!result.is_valid);
227+
assert!(
228+
result
229+
.warnings
230+
.iter()
231+
.any(|w| w.contains("contains invalid characters"))
232+
);
233+
}
234+
235+
#[test]
236+
fn test_tool_name_all_special_characters_allowed() {
237+
let valid_chars = vec!['_', '-', '.'];
238+
for ch in valid_chars {
239+
let name = format!("tool{}", ch);
240+
let result = validate_tool_name(&name);
241+
assert!(
242+
result.is_valid,
243+
"Tool name with character '{}' should be valid",
244+
ch
245+
);
246+
}
247+
}
248+
249+
#[test]
250+
fn test_minimum_length() {
251+
let result = validate_tool_name("a");
252+
assert!(result.is_valid);
253+
}
254+
255+
#[test]
256+
fn test_maximum_length() {
257+
let name = "a".repeat(128);
258+
let result = validate_tool_name(&name);
259+
assert!(result.is_valid);
260+
}
261+
}

0 commit comments

Comments
 (0)