-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Expand file tree
/
Copy pathtool_name_validation.py
More file actions
168 lines (131 loc) · 6.08 KB
/
Copy pathtool_name_validation.py
File metadata and controls
168 lines (131 loc) · 6.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
"""Tool name validation utilities according to SEP-986.
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.
See: https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names
"""
from __future__ import annotations
import logging
import re
import unicodedata
from dataclasses import dataclass, field
logger = logging.getLogger(__name__)
# Regular expression for valid tool names according to SEP-986 specification
TOOL_NAME_REGEX = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
# SEP reference URL for warning messages
SEP_986_URL = "https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names"
# Bidirectional formatting characters that can be used to obfuscate tool names
BIDIRECTIONAL_FORMATTING_CHARS = {
"", # LEFT-TO-RIGHT MARK
"", # RIGHT-TO-LEFT MARK
"", # LEFT-TO-RIGHT EMBEDDING
"", # RIGHT-TO-LEFT EMBEDDING
"", # POP DIRECTIONAL FORMATTING
"", # LEFT-TO-RIGHT OVERRIDE
"", # RIGHT-TO-LEFT OVERRIDE
"", # LEFT-TO-RIGHT ISOLATE
"", # RIGHT-TO-LEFT ISOLATE
"", # FIRST STRONG ISOLATE
"", # POP DIRECTIONAL ISOLATE
"", # ARABIC LETTER MARK
}
@dataclass
class ToolNameValidationResult:
"""Result of tool name validation.
Attributes:
is_valid: Whether the tool name conforms to SEP-986 requirements.
warnings: List of warning messages for non-conforming aspects.
"""
is_valid: bool
warnings: list[str] = field(default_factory=lambda: [])
def validate_tool_name(name: str) -> ToolNameValidationResult:
"""Validate a tool name according to the SEP-986 specification.
Args:
name: The tool name to validate.
Returns:
ToolNameValidationResult containing validation status and any warnings.
"""
warnings: list[str] = []
# Check for empty name
if not name:
return ToolNameValidationResult(
is_valid=False,
warnings=["Tool name cannot be empty"],
)
# Check length
if len(name) > 128:
return ToolNameValidationResult(
is_valid=False,
warnings=[f"Tool name exceeds maximum length of 128 characters (current: {len(name)})"],
)
# Check for bidirectional formatting characters (invisible obfuscation)
for char in name:
if char in BIDIRECTIONAL_FORMATTING_CHARS:
warnings.append(
f"Tool name contains bidirectional formatting character ({repr(char)}). "
"This could be used to obfuscate the tool name's visual representation."
)
return ToolNameValidationResult(is_valid=False, warnings=warnings)
# Check for Unicode normalization issues (homoglyphs and confusable characters)
# NFKC is the compatibility normalization form that converts:
# - Fullwidth characters to ASCII (a → a)
# - Composed characters to canonical form
normalized = unicodedata.normalize("NFKC", name)
if normalized != name:
warnings.append(
f"Tool name contains Unicode characters that normalize to a different form. "
f"This may indicate use of homoglyphs or confusable characters. "
f"Original: {repr(name)}, Normalized: {repr(normalized)}"
)
return ToolNameValidationResult(is_valid=False, warnings=warnings)
# Check for problematic patterns (warnings, not validation failures)
if " " in name:
warnings.append("Tool name contains spaces, which may cause parsing issues")
if "," in name:
warnings.append("Tool name contains commas, which may cause parsing issues")
# Check for potentially confusing leading/trailing characters
if name.startswith("-") or name.endswith("-"):
warnings.append("Tool name starts or ends with a dash, which may cause parsing issues in some contexts")
if name.startswith(".") or name.endswith("."):
warnings.append("Tool name starts or ends with a dot, which may cause parsing issues in some contexts")
# Check for invalid characters
if not TOOL_NAME_REGEX.fullmatch(name):
# Find all invalid characters (unique, preserving order)
invalid_chars: list[str] = []
seen: set[str] = set()
for char in name:
if not re.match(r"[A-Za-z0-9._-]", char) and char not in seen:
invalid_chars.append(char)
seen.add(char)
warnings.append(f"Tool name contains invalid characters: {', '.join(repr(c) for c in invalid_chars)}")
warnings.append("Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)")
return ToolNameValidationResult(is_valid=False, warnings=warnings)
return ToolNameValidationResult(is_valid=True, warnings=warnings)
def issue_tool_name_warning(name: str, warnings: list[str]) -> None:
"""Log warnings for non-conforming tool names.
Args:
name: The tool name that triggered the warnings.
warnings: List of warning messages to log.
"""
if not warnings:
return
logger.warning(f'Tool name validation warning for "{name}":')
for warning in warnings:
logger.warning(f" - {warning}")
logger.warning("Tool registration will proceed, but this may cause compatibility issues.")
logger.warning("Consider updating the tool name to conform to the MCP tool naming standard.")
logger.warning(f"See SEP-986 ({SEP_986_URL}) for more details.")
def validate_and_warn_tool_name(name: str) -> bool:
"""Validate a tool name and issue warnings for non-conforming names.
This is the primary entry point for tool name validation. It validates
the name and logs any warnings via the logging module.
Args:
name: The tool name to validate.
Returns:
True if the name is valid, False otherwise.
"""
result = validate_tool_name(name)
issue_tool_name_warning(name, result.warnings)
return result.is_valid