Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/rmcp/src/handler/server.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ pub mod prompt;
mod resource;
pub mod router;
pub mod tool;
pub mod tool_name_validation;
pub mod wrapper;
impl<H: ServerHandler> Service<RoleServer> for H {
async fn handle_request(
Expand Down
9 changes: 6 additions & 3 deletions crates/rmcp/src/handler/server/router/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,9 @@ use futures::{FutureExt, future::BoxFuture};
use schemars::JsonSchema;

use crate::{
handler::server::tool::{
CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type,
handler::server::{
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type},
tool_name_validation::validate_and_warn_tool_name,
},
model::{CallToolResult, Tool, ToolAnnotations},
};
Expand DownExpand Up@@ -219,7 +220,9 @@ where
}

pub fn add_route(&mut self, item: ToolRoute<S>) {
self.map.insert(item.attr.name.clone(), item);
let new_name = &item.attr.name;
validate_and_warn_tool_name(new_name);
self.map.insert(new_name.clone(), item);
}

pub fn merge(&mut self, other: ToolRouter<S>) {
Expand Down
261 changes: 261 additions & 0 deletions crates/rmcp/src/handler/server/tool_name_validation.rs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
//! Tool name validation utilities according to SEP: Specify Format for Tool Names
//!
//! Tool names SHOULD be between 1 and 128 characters in length (inclusive).
//! Tool names are case-sensitive.
//! Allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits
//! (0-9), underscore (_), dash (-), and dot (.).
//! Tool names SHOULD NOT contain spaces, commas, or other special characters.

use std::collections::HashSet;

/// Result of tool name validation containing validation status and warnings.
#[derive(Debug, Clone, PartialEq, Eq)]
struct ToolNameValidationResult {
/// Whether the tool name is valid according to the specification
is_valid: bool,
/// Array of warning messages about non-conforming aspects of the tool name
warnings: Vec<String>,
}

impl ToolNameValidationResult {
/// Create a new validation result
fn new(is_valid: bool, warnings: Vec<String>) -> Self {
Self { is_valid, warnings }
}
}

/// Validates a tool name according to the SEP specification.
fn validate_tool_name(name: &str) -> ToolNameValidationResult {
let mut warnings = Vec::new();

// Check length
if name.is_empty() {
return ToolNameValidationResult::new(false, vec!["Tool name cannot be empty".to_string()]);
}

if name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec![format!(
"Tool name exceeds maximum length of 128 characters (current: {})",
name.len()
)],
);
}

// Check for specific problematic patterns (these are warnings, not validation failures)
if name.contains(' ') {
warnings.push("Tool name contains spaces, which may cause parsing issues".to_string());
}

if name.contains(',') {
warnings.push("Tool name contains commas, which may cause parsing issues".to_string());
}

// Check for potentially confusing patterns (leading/trailing dashes, dots, slashes)
if name.starts_with('-') || name.ends_with('-') {
warnings.push(
"Tool name starts or ends with a dash, which may cause parsing issues in some contexts"
.to_string(),
);
}

if name.starts_with('.') || name.ends_with('.') {
warnings.push(
"Tool name starts or ends with a dot, which may cause parsing issues in some contexts"
.to_string(),
);
}

// Check for invalid characters
let mut invalid_chars = HashSet::new();
let valid_chars: HashSet<char> =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
.chars()
.collect();

for ch in name.chars() {
if !valid_chars.contains(&ch) {
invalid_chars.insert(ch);
}
}

if !invalid_chars.is_empty() {
let invalid_chars_list: Vec<String> =
invalid_chars.iter().map(|c| format!("\"{}\"", c)).collect();
warnings.push(format!(
"Tool name contains invalid characters: {}",
invalid_chars_list.join(", ")
));
warnings.push(
"Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)"
.to_string(),
);

return ToolNameValidationResult::new(false, warnings);
}

// Verify the pattern matches (double check with character-by-character validation)
// We've already validated characters above, just need to verify length is within bounds
if name.is_empty() || name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec!["Tool name length must be between 1 and 128 characters".to_string()],
);
}

ToolNameValidationResult::new(true, warnings)
}

/// Issues warnings for non-conforming tool names.
fn issue_tool_name_warning(name: &str, warnings: &[String]) {
tracing::warn!("Tool name validation warning for \"{}\":", name);
for warning in warnings {
tracing::warn!(" - {}", warning);
}
tracing::warn!("Tool registration will proceed, but this may cause compatibility issues.");
tracing::warn!("Consider updating the tool name to conform to the MCP tool naming standard.");
tracing::warn!(
"See SEP: Specify Format for Tool Names (https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) for more details."
);
}

/// Validates a tool name and issues warnings for non-conforming names.
pub fn validate_and_warn_tool_name(name: &str) -> bool {
let result = validate_tool_name(name);

if !result.warnings.is_empty() {
issue_tool_name_warning(name, &result.warnings);
}

result.is_valid
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_valid_tool_names() {
let max_length_name = "a".repeat(128);
let valid_names = vec![
"my_tool",
"MyTool",
"my-tool",
"my.tool",
"tool123",
"a",
max_length_name.as_str(), // Maximum length
];

for name in valid_names {
let result = validate_tool_name(name);
assert!(result.is_valid, "Tool name '{}' should be valid", name);
}
}

#[test]
fn test_empty_tool_name() {
let result = validate_tool_name("");
assert!(!result.is_valid);
assert!(
result
.warnings
.contains(&"Tool name cannot be empty".to_string())
);
}

#[test]
fn test_too_long_tool_name() {
let name = "a".repeat(129);
let result = validate_tool_name(&name);
assert!(!result.is_valid);
assert!(result.warnings[0].contains("exceeds maximum length"));
}

#[test]
fn test_tool_name_with_spaces() {
let result = validate_tool_name("my tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains spaces"))
);
}

#[test]
fn test_tool_name_with_commas() {
let result = validate_tool_name("my,tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains commas"))
);
}

#[test]
fn test_tool_name_starting_with_dash() {
let result = validate_tool_name("-tool");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dash"))
);
}

#[test]
fn test_tool_name_ending_with_dot() {
let result = validate_tool_name("tool.");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dot"))
);
}

#[test]
fn test_tool_name_with_invalid_characters() {
let result = validate_tool_name("my@tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains invalid characters"))
);
}

#[test]
fn test_tool_name_all_special_characters_allowed() {
let valid_chars = vec!['_', '-', '.'];
for ch in valid_chars {
let name = format!("tool{}", ch);
let result = validate_tool_name(&name);
assert!(
result.is_valid,
"Tool name with character '{}' should be valid",
ch
);
}
}

#[test]
fn test_minimum_length() {
let result = validate_tool_name("a");
assert!(result.is_valid);
}

#[test]
fn test_maximum_length() {
let name = "a".repeat(128);
let result = validate_tool_name(&name);
assert!(result.is_valid);
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/rmcp/src/handler/server.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ pub mod prompt;
mod resource;
pub mod router;
pub mod tool;
pub mod tool_name_validation;
pub mod wrapper;
impl<H: ServerHandler> Service<RoleServer> for H {
async fn handle_request(
Expand Down
9 changes: 6 additions & 3 deletions crates/rmcp/src/handler/server/router/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,9 @@ use futures::{FutureExt, future::BoxFuture};
use schemars::JsonSchema;

use crate::{
handler::server::tool::{
CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type,
handler::server::{
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type},
tool_name_validation::validate_and_warn_tool_name,
},
model::{CallToolResult, Tool, ToolAnnotations},
};
Expand DownExpand Up@@ -219,7 +220,9 @@ where
}

pub fn add_route(&mut self, item: ToolRoute<S>) {
self.map.insert(item.attr.name.clone(), item);
let new_name = &item.attr.name;
validate_and_warn_tool_name(new_name);
self.map.insert(new_name.clone(), item);
}

pub fn merge(&mut self, other: ToolRouter<S>) {
Expand Down
261 changes: 261 additions & 0 deletions crates/rmcp/src/handler/server/tool_name_validation.rs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
//! Tool name validation utilities according to SEP: Specify Format for Tool Names
//!
//! Tool names SHOULD be between 1 and 128 characters in length (inclusive).
//! Tool names are case-sensitive.
//! Allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits
//! (0-9), underscore (_), dash (-), and dot (.).
//! Tool names SHOULD NOT contain spaces, commas, or other special characters.

use std::collections::HashSet;

/// Result of tool name validation containing validation status and warnings.
#[derive(Debug, Clone, PartialEq, Eq)]
struct ToolNameValidationResult {
/// Whether the tool name is valid according to the specification
is_valid: bool,
/// Array of warning messages about non-conforming aspects of the tool name
warnings: Vec<String>,
}

impl ToolNameValidationResult {
/// Create a new validation result
fn new(is_valid: bool, warnings: Vec<String>) -> Self {
Self { is_valid, warnings }
}
}

/// Validates a tool name according to the SEP specification.
fn validate_tool_name(name: &str) -> ToolNameValidationResult {
let mut warnings = Vec::new();

// Check length
if name.is_empty() {
return ToolNameValidationResult::new(false, vec!["Tool name cannot be empty".to_string()]);
}

if name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec![format!(
"Tool name exceeds maximum length of 128 characters (current: {})",
name.len()
)],
);
}

// Check for specific problematic patterns (these are warnings, not validation failures)
if name.contains(' ') {
warnings.push("Tool name contains spaces, which may cause parsing issues".to_string());
}

if name.contains(',') {
warnings.push("Tool name contains commas, which may cause parsing issues".to_string());
}

// Check for potentially confusing patterns (leading/trailing dashes, dots, slashes)
if name.starts_with('-') || name.ends_with('-') {
warnings.push(
"Tool name starts or ends with a dash, which may cause parsing issues in some contexts"
.to_string(),
);
}

if name.starts_with('.') || name.ends_with('.') {
warnings.push(
"Tool name starts or ends with a dot, which may cause parsing issues in some contexts"
.to_string(),
);
}

// Check for invalid characters
let mut invalid_chars = HashSet::new();
let valid_chars: HashSet<char> =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
.chars()
.collect();

for ch in name.chars() {
if !valid_chars.contains(&ch) {
invalid_chars.insert(ch);
}
}

if !invalid_chars.is_empty() {
let invalid_chars_list: Vec<String> =
invalid_chars.iter().map(|c| format!("\"{}\"", c)).collect();
warnings.push(format!(
"Tool name contains invalid characters: {}",
invalid_chars_list.join(", ")
));
warnings.push(
"Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)"
.to_string(),
);

return ToolNameValidationResult::new(false, warnings);
}

// Verify the pattern matches (double check with character-by-character validation)
// We've already validated characters above, just need to verify length is within bounds
if name.is_empty() || name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec!["Tool name length must be between 1 and 128 characters".to_string()],
);
}

ToolNameValidationResult::new(true, warnings)
}

/// Issues warnings for non-conforming tool names.
fn issue_tool_name_warning(name: &str, warnings: &[String]) {
tracing::warn!("Tool name validation warning for \"{}\":", name);
for warning in warnings {
tracing::warn!(" - {}", warning);
}
tracing::warn!("Tool registration will proceed, but this may cause compatibility issues.");
tracing::warn!("Consider updating the tool name to conform to the MCP tool naming standard.");
tracing::warn!(
"See SEP: Specify Format for Tool Names (https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) for more details."
);
}

/// Validates a tool name and issues warnings for non-conforming names.
pub fn validate_and_warn_tool_name(name: &str) -> bool {
let result = validate_tool_name(name);

if !result.warnings.is_empty() {
issue_tool_name_warning(name, &result.warnings);
}

result.is_valid
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_valid_tool_names() {
let max_length_name = "a".repeat(128);
let valid_names = vec![
"my_tool",
"MyTool",
"my-tool",
"my.tool",
"tool123",
"a",
max_length_name.as_str(), // Maximum length
];

for name in valid_names {
let result = validate_tool_name(name);
assert!(result.is_valid, "Tool name '{}' should be valid", name);
}
}

#[test]
fn test_empty_tool_name() {
let result = validate_tool_name("");
assert!(!result.is_valid);
assert!(
result
.warnings
.contains(&"Tool name cannot be empty".to_string())
);
}

#[test]
fn test_too_long_tool_name() {
let name = "a".repeat(129);
let result = validate_tool_name(&name);
assert!(!result.is_valid);
assert!(result.warnings[0].contains("exceeds maximum length"));
}

#[test]
fn test_tool_name_with_spaces() {
let result = validate_tool_name("my tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains spaces"))
);
}

#[test]
fn test_tool_name_with_commas() {
let result = validate_tool_name("my,tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains commas"))
);
}

#[test]
fn test_tool_name_starting_with_dash() {
let result = validate_tool_name("-tool");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dash"))
);
}

#[test]
fn test_tool_name_ending_with_dot() {
let result = validate_tool_name("tool.");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dot"))
);
}

#[test]
fn test_tool_name_with_invalid_characters() {
let result = validate_tool_name("my@tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains invalid characters"))
);
}

#[test]
fn test_tool_name_all_special_characters_allowed() {
let valid_chars = vec!['_', '-', '.'];
for ch in valid_chars {
let name = format!("tool{}", ch);
let result = validate_tool_name(&name);
assert!(
result.is_valid,
"Tool name with character '{}' should be valid",
ch
);
}
}

#[test]
fn test_minimum_length() {
let result = validate_tool_name("a");
assert!(result.is_valid);
}

#[test]
fn test_maximum_length() {
let name = "a".repeat(128);
let result = validate_tool_name(&name);
assert!(result.is_valid);
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/rmcp/src/handler/server.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ pub mod prompt;
mod resource;
pub mod router;
pub mod tool;
pub mod tool_name_validation;
pub mod wrapper;
impl<H: ServerHandler> Service<RoleServer> for H {
async fn handle_request(
Expand Down
9 changes: 6 additions & 3 deletions crates/rmcp/src/handler/server/router/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,9 @@ use futures::{FutureExt, future::BoxFuture};
use schemars::JsonSchema;

use crate::{
handler::server::tool::{
CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type,
handler::server::{
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type},
tool_name_validation::validate_and_warn_tool_name,
},
model::{CallToolResult, Tool, ToolAnnotations},
};
Expand DownExpand Up@@ -219,7 +220,9 @@ where
}

pub fn add_route(&mut self, item: ToolRoute<S>) {
self.map.insert(item.attr.name.clone(), item);
let new_name = &item.attr.name;
validate_and_warn_tool_name(new_name);
self.map.insert(new_name.clone(), item);
}

pub fn merge(&mut self, other: ToolRouter<S>) {
Expand Down
261 changes: 261 additions & 0 deletions crates/rmcp/src/handler/server/tool_name_validation.rs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
//! Tool name validation utilities according to SEP: Specify Format for Tool Names
//!
//! Tool names SHOULD be between 1 and 128 characters in length (inclusive).
//! Tool names are case-sensitive.
//! Allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits
//! (0-9), underscore (_), dash (-), and dot (.).
//! Tool names SHOULD NOT contain spaces, commas, or other special characters.

use std::collections::HashSet;

/// Result of tool name validation containing validation status and warnings.
#[derive(Debug, Clone, PartialEq, Eq)]
struct ToolNameValidationResult {
/// Whether the tool name is valid according to the specification
is_valid: bool,
/// Array of warning messages about non-conforming aspects of the tool name
warnings: Vec<String>,
}

impl ToolNameValidationResult {
/// Create a new validation result
fn new(is_valid: bool, warnings: Vec<String>) -> Self {
Self { is_valid, warnings }
}
}

/// Validates a tool name according to the SEP specification.
fn validate_tool_name(name: &str) -> ToolNameValidationResult {
let mut warnings = Vec::new();

// Check length
if name.is_empty() {
return ToolNameValidationResult::new(false, vec!["Tool name cannot be empty".to_string()]);
}

if name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec![format!(
"Tool name exceeds maximum length of 128 characters (current: {})",
name.len()
)],
);
}

// Check for specific problematic patterns (these are warnings, not validation failures)
if name.contains(' ') {
warnings.push("Tool name contains spaces, which may cause parsing issues".to_string());
}

if name.contains(',') {
warnings.push("Tool name contains commas, which may cause parsing issues".to_string());
}

// Check for potentially confusing patterns (leading/trailing dashes, dots, slashes)
if name.starts_with('-') || name.ends_with('-') {
warnings.push(
"Tool name starts or ends with a dash, which may cause parsing issues in some contexts"
.to_string(),
);
}

if name.starts_with('.') || name.ends_with('.') {
warnings.push(
"Tool name starts or ends with a dot, which may cause parsing issues in some contexts"
.to_string(),
);
}

// Check for invalid characters
let mut invalid_chars = HashSet::new();
let valid_chars: HashSet<char> =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
.chars()
.collect();

for ch in name.chars() {
if !valid_chars.contains(&ch) {
invalid_chars.insert(ch);
}
}

if !invalid_chars.is_empty() {
let invalid_chars_list: Vec<String> =
invalid_chars.iter().map(|c| format!("\"{}\"", c)).collect();
warnings.push(format!(
"Tool name contains invalid characters: {}",
invalid_chars_list.join(", ")
));
warnings.push(
"Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)"
.to_string(),
);

return ToolNameValidationResult::new(false, warnings);
}

// Verify the pattern matches (double check with character-by-character validation)
// We've already validated characters above, just need to verify length is within bounds
if name.is_empty() || name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec!["Tool name length must be between 1 and 128 characters".to_string()],
);
}

ToolNameValidationResult::new(true, warnings)
}

/// Issues warnings for non-conforming tool names.
fn issue_tool_name_warning(name: &str, warnings: &[String]) {
tracing::warn!("Tool name validation warning for \"{}\":", name);
for warning in warnings {
tracing::warn!(" - {}", warning);
}
tracing::warn!("Tool registration will proceed, but this may cause compatibility issues.");
tracing::warn!("Consider updating the tool name to conform to the MCP tool naming standard.");
tracing::warn!(
"See SEP: Specify Format for Tool Names (https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) for more details."
);
}

/// Validates a tool name and issues warnings for non-conforming names.
pub fn validate_and_warn_tool_name(name: &str) -> bool {
let result = validate_tool_name(name);

if !result.warnings.is_empty() {
issue_tool_name_warning(name, &result.warnings);
}

result.is_valid
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_valid_tool_names() {
let max_length_name = "a".repeat(128);
let valid_names = vec![
"my_tool",
"MyTool",
"my-tool",
"my.tool",
"tool123",
"a",
max_length_name.as_str(), // Maximum length
];

for name in valid_names {
let result = validate_tool_name(name);
assert!(result.is_valid, "Tool name '{}' should be valid", name);
}
}

#[test]
fn test_empty_tool_name() {
let result = validate_tool_name("");
assert!(!result.is_valid);
assert!(
result
.warnings
.contains(&"Tool name cannot be empty".to_string())
);
}

#[test]
fn test_too_long_tool_name() {
let name = "a".repeat(129);
let result = validate_tool_name(&name);
assert!(!result.is_valid);
assert!(result.warnings[0].contains("exceeds maximum length"));
}

#[test]
fn test_tool_name_with_spaces() {
let result = validate_tool_name("my tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains spaces"))
);
}

#[test]
fn test_tool_name_with_commas() {
let result = validate_tool_name("my,tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains commas"))
);
}

#[test]
fn test_tool_name_starting_with_dash() {
let result = validate_tool_name("-tool");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dash"))
);
}

#[test]
fn test_tool_name_ending_with_dot() {
let result = validate_tool_name("tool.");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dot"))
);
}

#[test]
fn test_tool_name_with_invalid_characters() {
let result = validate_tool_name("my@tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains invalid characters"))
);
}

#[test]
fn test_tool_name_all_special_characters_allowed() {
let valid_chars = vec!['_', '-', '.'];
for ch in valid_chars {
let name = format!("tool{}", ch);
let result = validate_tool_name(&name);
assert!(
result.is_valid,
"Tool name with character '{}' should be valid",
ch
);
}
}

#[test]
fn test_minimum_length() {
let result = validate_tool_name("a");
assert!(result.is_valid);
}

#[test]
fn test_maximum_length() {
let name = "a".repeat(128);
let result = validate_tool_name(&name);
assert!(result.is_valid);
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/rmcp/src/handler/server.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ pub mod prompt;
mod resource;
pub mod router;
pub mod tool;
pub mod tool_name_validation;
pub mod wrapper;
impl<H: ServerHandler> Service<RoleServer> for H {
async fn handle_request(
Expand Down
9 changes: 6 additions & 3 deletions crates/rmcp/src/handler/server/router/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,9 @@ use futures::{FutureExt, future::BoxFuture};
use schemars::JsonSchema;

use crate::{
handler::server::tool::{
CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type,
handler::server::{
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type},
tool_name_validation::validate_and_warn_tool_name,
},
model::{CallToolResult, Tool, ToolAnnotations},
};
Expand DownExpand Up@@ -219,7 +220,9 @@ where
}

pub fn add_route(&mut self, item: ToolRoute<S>) {
self.map.insert(item.attr.name.clone(), item);
let new_name = &item.attr.name;
validate_and_warn_tool_name(new_name);
self.map.insert(new_name.clone(), item);
}

pub fn merge(&mut self, other: ToolRouter<S>) {
Expand Down
261 changes: 261 additions & 0 deletions crates/rmcp/src/handler/server/tool_name_validation.rs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
//! Tool name validation utilities according to SEP: Specify Format for Tool Names
//!
//! Tool names SHOULD be between 1 and 128 characters in length (inclusive).
//! Tool names are case-sensitive.
//! Allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits
//! (0-9), underscore (_), dash (-), and dot (.).
//! Tool names SHOULD NOT contain spaces, commas, or other special characters.

use std::collections::HashSet;

/// Result of tool name validation containing validation status and warnings.
#[derive(Debug, Clone, PartialEq, Eq)]
struct ToolNameValidationResult {
/// Whether the tool name is valid according to the specification
is_valid: bool,
/// Array of warning messages about non-conforming aspects of the tool name
warnings: Vec<String>,
}

impl ToolNameValidationResult {
/// Create a new validation result
fn new(is_valid: bool, warnings: Vec<String>) -> Self {
Self { is_valid, warnings }
}
}

/// Validates a tool name according to the SEP specification.
fn validate_tool_name(name: &str) -> ToolNameValidationResult {
let mut warnings = Vec::new();

// Check length
if name.is_empty() {
return ToolNameValidationResult::new(false, vec!["Tool name cannot be empty".to_string()]);
}

if name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec![format!(
"Tool name exceeds maximum length of 128 characters (current: {})",
name.len()
)],
);
}

// Check for specific problematic patterns (these are warnings, not validation failures)
if name.contains(' ') {
warnings.push("Tool name contains spaces, which may cause parsing issues".to_string());
}

if name.contains(',') {
warnings.push("Tool name contains commas, which may cause parsing issues".to_string());
}

// Check for potentially confusing patterns (leading/trailing dashes, dots, slashes)
if name.starts_with('-') || name.ends_with('-') {
warnings.push(
"Tool name starts or ends with a dash, which may cause parsing issues in some contexts"
.to_string(),
);
}

if name.starts_with('.') || name.ends_with('.') {
warnings.push(
"Tool name starts or ends with a dot, which may cause parsing issues in some contexts"
.to_string(),
);
}

// Check for invalid characters
let mut invalid_chars = HashSet::new();
let valid_chars: HashSet<char> =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
.chars()
.collect();

for ch in name.chars() {
if !valid_chars.contains(&ch) {
invalid_chars.insert(ch);
}
}

if !invalid_chars.is_empty() {
let invalid_chars_list: Vec<String> =
invalid_chars.iter().map(|c| format!("\"{}\"", c)).collect();
warnings.push(format!(
"Tool name contains invalid characters: {}",
invalid_chars_list.join(", ")
));
warnings.push(
"Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)"
.to_string(),
);

return ToolNameValidationResult::new(false, warnings);
}

// Verify the pattern matches (double check with character-by-character validation)
// We've already validated characters above, just need to verify length is within bounds
if name.is_empty() || name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec!["Tool name length must be between 1 and 128 characters".to_string()],
);
}

ToolNameValidationResult::new(true, warnings)
}

/// Issues warnings for non-conforming tool names.
fn issue_tool_name_warning(name: &str, warnings: &[String]) {
tracing::warn!("Tool name validation warning for \"{}\":", name);
for warning in warnings {
tracing::warn!(" - {}", warning);
}
tracing::warn!("Tool registration will proceed, but this may cause compatibility issues.");
tracing::warn!("Consider updating the tool name to conform to the MCP tool naming standard.");
tracing::warn!(
"See SEP: Specify Format for Tool Names (https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) for more details."
);
}

/// Validates a tool name and issues warnings for non-conforming names.
pub fn validate_and_warn_tool_name(name: &str) -> bool {
let result = validate_tool_name(name);

if !result.warnings.is_empty() {
issue_tool_name_warning(name, &result.warnings);
}

result.is_valid
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_valid_tool_names() {
let max_length_name = "a".repeat(128);
let valid_names = vec![
"my_tool",
"MyTool",
"my-tool",
"my.tool",
"tool123",
"a",
max_length_name.as_str(), // Maximum length
];

for name in valid_names {
let result = validate_tool_name(name);
assert!(result.is_valid, "Tool name '{}' should be valid", name);
}
}

#[test]
fn test_empty_tool_name() {
let result = validate_tool_name("");
assert!(!result.is_valid);
assert!(
result
.warnings
.contains(&"Tool name cannot be empty".to_string())
);
}

#[test]
fn test_too_long_tool_name() {
let name = "a".repeat(129);
let result = validate_tool_name(&name);
assert!(!result.is_valid);
assert!(result.warnings[0].contains("exceeds maximum length"));
}

#[test]
fn test_tool_name_with_spaces() {
let result = validate_tool_name("my tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains spaces"))
);
}

#[test]
fn test_tool_name_with_commas() {
let result = validate_tool_name("my,tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains commas"))
);
}

#[test]
fn test_tool_name_starting_with_dash() {
let result = validate_tool_name("-tool");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dash"))
);
}

#[test]
fn test_tool_name_ending_with_dot() {
let result = validate_tool_name("tool.");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dot"))
);
}

#[test]
fn test_tool_name_with_invalid_characters() {
let result = validate_tool_name("my@tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains invalid characters"))
);
}

#[test]
fn test_tool_name_all_special_characters_allowed() {
let valid_chars = vec!['_', '-', '.'];
for ch in valid_chars {
let name = format!("tool{}", ch);
let result = validate_tool_name(&name);
assert!(
result.is_valid,
"Tool name with character '{}' should be valid",
ch
);
}
}

#[test]
fn test_minimum_length() {
let result = validate_tool_name("a");
assert!(result.is_valid);
}

#[test]
fn test_maximum_length() {
let name = "a".repeat(128);
let result = validate_tool_name(&name);
assert!(result.is_valid);
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/rmcp/src/handler/server.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ pub mod prompt;
mod resource;
pub mod router;
pub mod tool;
pub mod tool_name_validation;
pub mod wrapper;
impl<H: ServerHandler> Service<RoleServer> for H {
async fn handle_request(
Expand Down
9 changes: 6 additions & 3 deletions crates/rmcp/src/handler/server/router/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,9 @@ use futures::{FutureExt, future::BoxFuture};
use schemars::JsonSchema;

use crate::{
handler::server::tool::{
CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type,
handler::server::{
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type},
tool_name_validation::validate_and_warn_tool_name,
},
model::{CallToolResult, Tool, ToolAnnotations},
};
Expand DownExpand Up@@ -219,7 +220,9 @@ where
}

pub fn add_route(&mut self, item: ToolRoute<S>) {
self.map.insert(item.attr.name.clone(), item);
let new_name = &item.attr.name;
validate_and_warn_tool_name(new_name);
self.map.insert(new_name.clone(), item);
}

pub fn merge(&mut self, other: ToolRouter<S>) {
Expand Down
261 changes: 261 additions & 0 deletions crates/rmcp/src/handler/server/tool_name_validation.rs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
//! Tool name validation utilities according to SEP: Specify Format for Tool Names
//!
//! Tool names SHOULD be between 1 and 128 characters in length (inclusive).
//! Tool names are case-sensitive.
//! Allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits
//! (0-9), underscore (_), dash (-), and dot (.).
//! Tool names SHOULD NOT contain spaces, commas, or other special characters.

use std::collections::HashSet;

/// Result of tool name validation containing validation status and warnings.
#[derive(Debug, Clone, PartialEq, Eq)]
struct ToolNameValidationResult {
/// Whether the tool name is valid according to the specification
is_valid: bool,
/// Array of warning messages about non-conforming aspects of the tool name
warnings: Vec<String>,
}

impl ToolNameValidationResult {
/// Create a new validation result
fn new(is_valid: bool, warnings: Vec<String>) -> Self {
Self { is_valid, warnings }
}
}

/// Validates a tool name according to the SEP specification.
fn validate_tool_name(name: &str) -> ToolNameValidationResult {
let mut warnings = Vec::new();

// Check length
if name.is_empty() {
return ToolNameValidationResult::new(false, vec!["Tool name cannot be empty".to_string()]);
}

if name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec![format!(
"Tool name exceeds maximum length of 128 characters (current: {})",
name.len()
)],
);
}

// Check for specific problematic patterns (these are warnings, not validation failures)
if name.contains(' ') {
warnings.push("Tool name contains spaces, which may cause parsing issues".to_string());
}

if name.contains(',') {
warnings.push("Tool name contains commas, which may cause parsing issues".to_string());
}

// Check for potentially confusing patterns (leading/trailing dashes, dots, slashes)
if name.starts_with('-') || name.ends_with('-') {
warnings.push(
"Tool name starts or ends with a dash, which may cause parsing issues in some contexts"
.to_string(),
);
}

if name.starts_with('.') || name.ends_with('.') {
warnings.push(
"Tool name starts or ends with a dot, which may cause parsing issues in some contexts"
.to_string(),
);
}

// Check for invalid characters
let mut invalid_chars = HashSet::new();
let valid_chars: HashSet<char> =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
.chars()
.collect();

for ch in name.chars() {
if !valid_chars.contains(&ch) {
invalid_chars.insert(ch);
}
}

if !invalid_chars.is_empty() {
let invalid_chars_list: Vec<String> =
invalid_chars.iter().map(|c| format!("\"{}\"", c)).collect();
warnings.push(format!(
"Tool name contains invalid characters: {}",
invalid_chars_list.join(", ")
));
warnings.push(
"Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)"
.to_string(),
);

return ToolNameValidationResult::new(false, warnings);
}

// Verify the pattern matches (double check with character-by-character validation)
// We've already validated characters above, just need to verify length is within bounds
if name.is_empty() || name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec!["Tool name length must be between 1 and 128 characters".to_string()],
);
}

ToolNameValidationResult::new(true, warnings)
}

/// Issues warnings for non-conforming tool names.
fn issue_tool_name_warning(name: &str, warnings: &[String]) {
tracing::warn!("Tool name validation warning for \"{}\":", name);
for warning in warnings {
tracing::warn!(" - {}", warning);
}
tracing::warn!("Tool registration will proceed, but this may cause compatibility issues.");
tracing::warn!("Consider updating the tool name to conform to the MCP tool naming standard.");
tracing::warn!(
"See SEP: Specify Format for Tool Names (https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) for more details."
);
}

/// Validates a tool name and issues warnings for non-conforming names.
pub fn validate_and_warn_tool_name(name: &str) -> bool {
let result = validate_tool_name(name);

if !result.warnings.is_empty() {
issue_tool_name_warning(name, &result.warnings);
}

result.is_valid
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_valid_tool_names() {
let max_length_name = "a".repeat(128);
let valid_names = vec![
"my_tool",
"MyTool",
"my-tool",
"my.tool",
"tool123",
"a",
max_length_name.as_str(), // Maximum length
];

for name in valid_names {
let result = validate_tool_name(name);
assert!(result.is_valid, "Tool name '{}' should be valid", name);
}
}

#[test]
fn test_empty_tool_name() {
let result = validate_tool_name("");
assert!(!result.is_valid);
assert!(
result
.warnings
.contains(&"Tool name cannot be empty".to_string())
);
}

#[test]
fn test_too_long_tool_name() {
let name = "a".repeat(129);
let result = validate_tool_name(&name);
assert!(!result.is_valid);
assert!(result.warnings[0].contains("exceeds maximum length"));
}

#[test]
fn test_tool_name_with_spaces() {
let result = validate_tool_name("my tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains spaces"))
);
}

#[test]
fn test_tool_name_with_commas() {
let result = validate_tool_name("my,tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains commas"))
);
}

#[test]
fn test_tool_name_starting_with_dash() {
let result = validate_tool_name("-tool");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dash"))
);
}

#[test]
fn test_tool_name_ending_with_dot() {
let result = validate_tool_name("tool.");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dot"))
);
}

#[test]
fn test_tool_name_with_invalid_characters() {
let result = validate_tool_name("my@tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains invalid characters"))
);
}

#[test]
fn test_tool_name_all_special_characters_allowed() {
let valid_chars = vec!['_', '-', '.'];
for ch in valid_chars {
let name = format!("tool{}", ch);
let result = validate_tool_name(&name);
assert!(
result.is_valid,
"Tool name with character '{}' should be valid",
ch
);
}
}

#[test]
fn test_minimum_length() {
let result = validate_tool_name("a");
assert!(result.is_valid);
}

#[test]
fn test_maximum_length() {
let name = "a".repeat(128);
let result = validate_tool_name(&name);
assert!(result.is_valid);
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/rmcp/src/handler/server.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ pub mod prompt;
mod resource;
pub mod router;
pub mod tool;
pub mod tool_name_validation;
pub mod wrapper;
impl<H: ServerHandler> Service<RoleServer> for H {
async fn handle_request(
Expand Down
9 changes: 6 additions & 3 deletions crates/rmcp/src/handler/server/router/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,9 @@ use futures::{FutureExt, future::BoxFuture};
use schemars::JsonSchema;

use crate::{
handler::server::tool::{
CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type,
handler::server::{
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type},
tool_name_validation::validate_and_warn_tool_name,
},
model::{CallToolResult, Tool, ToolAnnotations},
};
Expand DownExpand Up@@ -219,7 +220,9 @@ where
}

pub fn add_route(&mut self, item: ToolRoute<S>) {
self.map.insert(item.attr.name.clone(), item);
let new_name = &item.attr.name;
validate_and_warn_tool_name(new_name);
self.map.insert(new_name.clone(), item);
}

pub fn merge(&mut self, other: ToolRouter<S>) {
Expand Down
261 changes: 261 additions & 0 deletions crates/rmcp/src/handler/server/tool_name_validation.rs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
//! Tool name validation utilities according to SEP: Specify Format for Tool Names
//!
//! Tool names SHOULD be between 1 and 128 characters in length (inclusive).
//! Tool names are case-sensitive.
//! Allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits
//! (0-9), underscore (_), dash (-), and dot (.).
//! Tool names SHOULD NOT contain spaces, commas, or other special characters.

use std::collections::HashSet;

/// Result of tool name validation containing validation status and warnings.
#[derive(Debug, Clone, PartialEq, Eq)]
struct ToolNameValidationResult {
/// Whether the tool name is valid according to the specification
is_valid: bool,
/// Array of warning messages about non-conforming aspects of the tool name
warnings: Vec<String>,
}

impl ToolNameValidationResult {
/// Create a new validation result
fn new(is_valid: bool, warnings: Vec<String>) -> Self {
Self { is_valid, warnings }
}
}

/// Validates a tool name according to the SEP specification.
fn validate_tool_name(name: &str) -> ToolNameValidationResult {
let mut warnings = Vec::new();

// Check length
if name.is_empty() {
return ToolNameValidationResult::new(false, vec!["Tool name cannot be empty".to_string()]);
}

if name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec![format!(
"Tool name exceeds maximum length of 128 characters (current: {})",
name.len()
)],
);
}

// Check for specific problematic patterns (these are warnings, not validation failures)
if name.contains(' ') {
warnings.push("Tool name contains spaces, which may cause parsing issues".to_string());
}

if name.contains(',') {
warnings.push("Tool name contains commas, which may cause parsing issues".to_string());
}

// Check for potentially confusing patterns (leading/trailing dashes, dots, slashes)
if name.starts_with('-') || name.ends_with('-') {
warnings.push(
"Tool name starts or ends with a dash, which may cause parsing issues in some contexts"
.to_string(),
);
}

if name.starts_with('.') || name.ends_with('.') {
warnings.push(
"Tool name starts or ends with a dot, which may cause parsing issues in some contexts"
.to_string(),
);
}

// Check for invalid characters
let mut invalid_chars = HashSet::new();
let valid_chars: HashSet<char> =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
.chars()
.collect();

for ch in name.chars() {
if !valid_chars.contains(&ch) {
invalid_chars.insert(ch);
}
}

if !invalid_chars.is_empty() {
let invalid_chars_list: Vec<String> =
invalid_chars.iter().map(|c| format!("\"{}\"", c)).collect();
warnings.push(format!(
"Tool name contains invalid characters: {}",
invalid_chars_list.join(", ")
));
warnings.push(
"Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)"
.to_string(),
);

return ToolNameValidationResult::new(false, warnings);
}

// Verify the pattern matches (double check with character-by-character validation)
// We've already validated characters above, just need to verify length is within bounds
if name.is_empty() || name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec!["Tool name length must be between 1 and 128 characters".to_string()],
);
}

ToolNameValidationResult::new(true, warnings)
}

/// Issues warnings for non-conforming tool names.
fn issue_tool_name_warning(name: &str, warnings: &[String]) {
tracing::warn!("Tool name validation warning for \"{}\":", name);
for warning in warnings {
tracing::warn!(" - {}", warning);
}
tracing::warn!("Tool registration will proceed, but this may cause compatibility issues.");
tracing::warn!("Consider updating the tool name to conform to the MCP tool naming standard.");
tracing::warn!(
"See SEP: Specify Format for Tool Names (https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) for more details."
);
}

/// Validates a tool name and issues warnings for non-conforming names.
pub fn validate_and_warn_tool_name(name: &str) -> bool {
let result = validate_tool_name(name);

if !result.warnings.is_empty() {
issue_tool_name_warning(name, &result.warnings);
}

result.is_valid
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_valid_tool_names() {
let max_length_name = "a".repeat(128);
let valid_names = vec![
"my_tool",
"MyTool",
"my-tool",
"my.tool",
"tool123",
"a",
max_length_name.as_str(), // Maximum length
];

for name in valid_names {
let result = validate_tool_name(name);
assert!(result.is_valid, "Tool name '{}' should be valid", name);
}
}

#[test]
fn test_empty_tool_name() {
let result = validate_tool_name("");
assert!(!result.is_valid);
assert!(
result
.warnings
.contains(&"Tool name cannot be empty".to_string())
);
}

#[test]
fn test_too_long_tool_name() {
let name = "a".repeat(129);
let result = validate_tool_name(&name);
assert!(!result.is_valid);
assert!(result.warnings[0].contains("exceeds maximum length"));
}

#[test]
fn test_tool_name_with_spaces() {
let result = validate_tool_name("my tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains spaces"))
);
}

#[test]
fn test_tool_name_with_commas() {
let result = validate_tool_name("my,tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains commas"))
);
}

#[test]
fn test_tool_name_starting_with_dash() {
let result = validate_tool_name("-tool");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dash"))
);
}

#[test]
fn test_tool_name_ending_with_dot() {
let result = validate_tool_name("tool.");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dot"))
);
}

#[test]
fn test_tool_name_with_invalid_characters() {
let result = validate_tool_name("my@tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains invalid characters"))
);
}

#[test]
fn test_tool_name_all_special_characters_allowed() {
let valid_chars = vec!['_', '-', '.'];
for ch in valid_chars {
let name = format!("tool{}", ch);
let result = validate_tool_name(&name);
assert!(
result.is_valid,
"Tool name with character '{}' should be valid",
ch
);
}
}

#[test]
fn test_minimum_length() {
let result = validate_tool_name("a");
assert!(result.is_valid);
}

#[test]
fn test_maximum_length() {
let name = "a".repeat(128);
let result = validate_tool_name(&name);
assert!(result.is_valid);
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/rmcp/src/handler/server.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ pub mod prompt;
mod resource;
pub mod router;
pub mod tool;
pub mod tool_name_validation;
pub mod wrapper;
impl<H: ServerHandler> Service<RoleServer> for H {
async fn handle_request(
Expand Down
9 changes: 6 additions & 3 deletions crates/rmcp/src/handler/server/router/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,9 @@ use futures::{FutureExt, future::BoxFuture};
use schemars::JsonSchema;

use crate::{
handler::server::tool::{
CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type,
handler::server::{
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type},
tool_name_validation::validate_and_warn_tool_name,
},
model::{CallToolResult, Tool, ToolAnnotations},
};
Expand DownExpand Up@@ -219,7 +220,9 @@ where
}

pub fn add_route(&mut self, item: ToolRoute<S>) {
self.map.insert(item.attr.name.clone(), item);
let new_name = &item.attr.name;
validate_and_warn_tool_name(new_name);
self.map.insert(new_name.clone(), item);
}

pub fn merge(&mut self, other: ToolRouter<S>) {
Expand Down
261 changes: 261 additions & 0 deletions crates/rmcp/src/handler/server/tool_name_validation.rs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
//! Tool name validation utilities according to SEP: Specify Format for Tool Names
//!
//! Tool names SHOULD be between 1 and 128 characters in length (inclusive).
//! Tool names are case-sensitive.
//! Allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits
//! (0-9), underscore (_), dash (-), and dot (.).
//! Tool names SHOULD NOT contain spaces, commas, or other special characters.

use std::collections::HashSet;

/// Result of tool name validation containing validation status and warnings.
#[derive(Debug, Clone, PartialEq, Eq)]
struct ToolNameValidationResult {
/// Whether the tool name is valid according to the specification
is_valid: bool,
/// Array of warning messages about non-conforming aspects of the tool name
warnings: Vec<String>,
}

impl ToolNameValidationResult {
/// Create a new validation result
fn new(is_valid: bool, warnings: Vec<String>) -> Self {
Self { is_valid, warnings }
}
}

/// Validates a tool name according to the SEP specification.
fn validate_tool_name(name: &str) -> ToolNameValidationResult {
let mut warnings = Vec::new();

// Check length
if name.is_empty() {
return ToolNameValidationResult::new(false, vec!["Tool name cannot be empty".to_string()]);
}

if name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec![format!(
"Tool name exceeds maximum length of 128 characters (current: {})",
name.len()
)],
);
}

// Check for specific problematic patterns (these are warnings, not validation failures)
if name.contains(' ') {
warnings.push("Tool name contains spaces, which may cause parsing issues".to_string());
}

if name.contains(',') {
warnings.push("Tool name contains commas, which may cause parsing issues".to_string());
}

// Check for potentially confusing patterns (leading/trailing dashes, dots, slashes)
if name.starts_with('-') || name.ends_with('-') {
warnings.push(
"Tool name starts or ends with a dash, which may cause parsing issues in some contexts"
.to_string(),
);
}

if name.starts_with('.') || name.ends_with('.') {
warnings.push(
"Tool name starts or ends with a dot, which may cause parsing issues in some contexts"
.to_string(),
);
}

// Check for invalid characters
let mut invalid_chars = HashSet::new();
let valid_chars: HashSet<char> =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
.chars()
.collect();

for ch in name.chars() {
if !valid_chars.contains(&ch) {
invalid_chars.insert(ch);
}
}

if !invalid_chars.is_empty() {
let invalid_chars_list: Vec<String> =
invalid_chars.iter().map(|c| format!("\"{}\"", c)).collect();
warnings.push(format!(
"Tool name contains invalid characters: {}",
invalid_chars_list.join(", ")
));
warnings.push(
"Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)"
.to_string(),
);

return ToolNameValidationResult::new(false, warnings);
}

// Verify the pattern matches (double check with character-by-character validation)
// We've already validated characters above, just need to verify length is within bounds
if name.is_empty() || name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec!["Tool name length must be between 1 and 128 characters".to_string()],
);
}

ToolNameValidationResult::new(true, warnings)
}

/// Issues warnings for non-conforming tool names.
fn issue_tool_name_warning(name: &str, warnings: &[String]) {
tracing::warn!("Tool name validation warning for \"{}\":", name);
for warning in warnings {
tracing::warn!(" - {}", warning);
}
tracing::warn!("Tool registration will proceed, but this may cause compatibility issues.");
tracing::warn!("Consider updating the tool name to conform to the MCP tool naming standard.");
tracing::warn!(
"See SEP: Specify Format for Tool Names (https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) for more details."
);
}

/// Validates a tool name and issues warnings for non-conforming names.
pub fn validate_and_warn_tool_name(name: &str) -> bool {
let result = validate_tool_name(name);

if !result.warnings.is_empty() {
issue_tool_name_warning(name, &result.warnings);
}

result.is_valid
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_valid_tool_names() {
let max_length_name = "a".repeat(128);
let valid_names = vec![
"my_tool",
"MyTool",
"my-tool",
"my.tool",
"tool123",
"a",
max_length_name.as_str(), // Maximum length
];

for name in valid_names {
let result = validate_tool_name(name);
assert!(result.is_valid, "Tool name '{}' should be valid", name);
}
}

#[test]
fn test_empty_tool_name() {
let result = validate_tool_name("");
assert!(!result.is_valid);
assert!(
result
.warnings
.contains(&"Tool name cannot be empty".to_string())
);
}

#[test]
fn test_too_long_tool_name() {
let name = "a".repeat(129);
let result = validate_tool_name(&name);
assert!(!result.is_valid);
assert!(result.warnings[0].contains("exceeds maximum length"));
}

#[test]
fn test_tool_name_with_spaces() {
let result = validate_tool_name("my tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains spaces"))
);
}

#[test]
fn test_tool_name_with_commas() {
let result = validate_tool_name("my,tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains commas"))
);
}

#[test]
fn test_tool_name_starting_with_dash() {
let result = validate_tool_name("-tool");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dash"))
);
}

#[test]
fn test_tool_name_ending_with_dot() {
let result = validate_tool_name("tool.");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dot"))
);
}

#[test]
fn test_tool_name_with_invalid_characters() {
let result = validate_tool_name("my@tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains invalid characters"))
);
}

#[test]
fn test_tool_name_all_special_characters_allowed() {
let valid_chars = vec!['_', '-', '.'];
for ch in valid_chars {
let name = format!("tool{}", ch);
let result = validate_tool_name(&name);
assert!(
result.is_valid,
"Tool name with character '{}' should be valid",
ch
);
}
}

#[test]
fn test_minimum_length() {
let result = validate_tool_name("a");
assert!(result.is_valid);
}

#[test]
fn test_maximum_length() {
let name = "a".repeat(128);
let result = validate_tool_name(&name);
assert!(result.is_valid);
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/rmcp/src/handler/server.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ pub mod prompt;
mod resource;
pub mod router;
pub mod tool;
pub mod tool_name_validation;
pub mod wrapper;
impl<H: ServerHandler> Service<RoleServer> for H {
async fn handle_request(
Expand Down
9 changes: 6 additions & 3 deletions crates/rmcp/src/handler/server/router/tool.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,9 @@ use futures::{FutureExt, future::BoxFuture};
use schemars::JsonSchema;

use crate::{
handler::server::tool::{
CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type,
handler::server::{
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type},
tool_name_validation::validate_and_warn_tool_name,
},
model::{CallToolResult, Tool, ToolAnnotations},
};
Expand DownExpand Up@@ -219,7 +220,9 @@ where
}

pub fn add_route(&mut self, item: ToolRoute<S>) {
self.map.insert(item.attr.name.clone(), item);
let new_name = &item.attr.name;
validate_and_warn_tool_name(new_name);
self.map.insert(new_name.clone(), item);
}

pub fn merge(&mut self, other: ToolRouter<S>) {
Expand Down
261 changes: 261 additions & 0 deletions crates/rmcp/src/handler/server/tool_name_validation.rs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
//! Tool name validation utilities according to SEP: Specify Format for Tool Names
//!
//! Tool names SHOULD be between 1 and 128 characters in length (inclusive).
//! Tool names are case-sensitive.
//! Allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits
//! (0-9), underscore (_), dash (-), and dot (.).
//! Tool names SHOULD NOT contain spaces, commas, or other special characters.

use std::collections::HashSet;

/// Result of tool name validation containing validation status and warnings.
#[derive(Debug, Clone, PartialEq, Eq)]
struct ToolNameValidationResult {
/// Whether the tool name is valid according to the specification
is_valid: bool,
/// Array of warning messages about non-conforming aspects of the tool name
warnings: Vec<String>,
}

impl ToolNameValidationResult {
/// Create a new validation result
fn new(is_valid: bool, warnings: Vec<String>) -> Self {
Self { is_valid, warnings }
}
}

/// Validates a tool name according to the SEP specification.
fn validate_tool_name(name: &str) -> ToolNameValidationResult {
let mut warnings = Vec::new();

// Check length
if name.is_empty() {
return ToolNameValidationResult::new(false, vec!["Tool name cannot be empty".to_string()]);
}

if name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec![format!(
"Tool name exceeds maximum length of 128 characters (current: {})",
name.len()
)],
);
}

// Check for specific problematic patterns (these are warnings, not validation failures)
if name.contains(' ') {
warnings.push("Tool name contains spaces, which may cause parsing issues".to_string());
}

if name.contains(',') {
warnings.push("Tool name contains commas, which may cause parsing issues".to_string());
}

// Check for potentially confusing patterns (leading/trailing dashes, dots, slashes)
if name.starts_with('-') || name.ends_with('-') {
warnings.push(
"Tool name starts or ends with a dash, which may cause parsing issues in some contexts"
.to_string(),
);
}

if name.starts_with('.') || name.ends_with('.') {
warnings.push(
"Tool name starts or ends with a dot, which may cause parsing issues in some contexts"
.to_string(),
);
}

// Check for invalid characters
let mut invalid_chars = HashSet::new();
let valid_chars: HashSet<char> =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
.chars()
.collect();

for ch in name.chars() {
if !valid_chars.contains(&ch) {
invalid_chars.insert(ch);
}
}

if !invalid_chars.is_empty() {
let invalid_chars_list: Vec<String> =
invalid_chars.iter().map(|c| format!("\"{}\"", c)).collect();
warnings.push(format!(
"Tool name contains invalid characters: {}",
invalid_chars_list.join(", ")
));
warnings.push(
"Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)"
.to_string(),
);

return ToolNameValidationResult::new(false, warnings);
}

// Verify the pattern matches (double check with character-by-character validation)
// We've already validated characters above, just need to verify length is within bounds
if name.is_empty() || name.len() > 128 {
return ToolNameValidationResult::new(
false,
vec!["Tool name length must be between 1 and 128 characters".to_string()],
);
}

ToolNameValidationResult::new(true, warnings)
}

/// Issues warnings for non-conforming tool names.
fn issue_tool_name_warning(name: &str, warnings: &[String]) {
tracing::warn!("Tool name validation warning for \"{}\":", name);
for warning in warnings {
tracing::warn!(" - {}", warning);
}
tracing::warn!("Tool registration will proceed, but this may cause compatibility issues.");
tracing::warn!("Consider updating the tool name to conform to the MCP tool naming standard.");
tracing::warn!(
"See SEP: Specify Format for Tool Names (https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) for more details."
);
}

/// Validates a tool name and issues warnings for non-conforming names.
pub fn validate_and_warn_tool_name(name: &str) -> bool {
let result = validate_tool_name(name);

if !result.warnings.is_empty() {
issue_tool_name_warning(name, &result.warnings);
}

result.is_valid
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_valid_tool_names() {
let max_length_name = "a".repeat(128);
let valid_names = vec![
"my_tool",
"MyTool",
"my-tool",
"my.tool",
"tool123",
"a",
max_length_name.as_str(), // Maximum length
];

for name in valid_names {
let result = validate_tool_name(name);
assert!(result.is_valid, "Tool name '{}' should be valid", name);
}
}

#[test]
fn test_empty_tool_name() {
let result = validate_tool_name("");
assert!(!result.is_valid);
assert!(
result
.warnings
.contains(&"Tool name cannot be empty".to_string())
);
}

#[test]
fn test_too_long_tool_name() {
let name = "a".repeat(129);
let result = validate_tool_name(&name);
assert!(!result.is_valid);
assert!(result.warnings[0].contains("exceeds maximum length"));
}

#[test]
fn test_tool_name_with_spaces() {
let result = validate_tool_name("my tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains spaces"))
);
}

#[test]
fn test_tool_name_with_commas() {
let result = validate_tool_name("my,tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains commas"))
);
}

#[test]
fn test_tool_name_starting_with_dash() {
let result = validate_tool_name("-tool");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dash"))
);
}

#[test]
fn test_tool_name_ending_with_dot() {
let result = validate_tool_name("tool.");
assert!(result.is_valid); // Still valid, but has warning
assert!(
result
.warnings
.iter()
.any(|w| w.contains("starts or ends with a dot"))
);
}

#[test]
fn test_tool_name_with_invalid_characters() {
let result = validate_tool_name("my@tool");
assert!(!result.is_valid);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("contains invalid characters"))
);
}

#[test]
fn test_tool_name_all_special_characters_allowed() {
let valid_chars = vec!['_', '-', '.'];
for ch in valid_chars {
let name = format!("tool{}", ch);
let result = validate_tool_name(&name);
assert!(
result.is_valid,
"Tool name with character '{}' should be valid",
ch
);
}
}

#[test]
fn test_minimum_length() {
let result = validate_tool_name("a");
assert!(result.is_valid);
}

#[test]
fn test_maximum_length() {
let name = "a".repeat(128);
let result = validate_tool_name(&name);
assert!(result.is_valid);
}
}