Skip to content

Commit 43c7186

Browse files
refactor: improve tool name sanitization
Reorganize tool name sanitization by renaming and moving the function, making it more accessible across the codebase. Update all references to use the new sanitize_tool_name function.
1 parent a02a4af commit 43c7186

1 file changed

Lines changed: 30 additions & 33 deletions

File tree

click_mcp/scanner.py

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,38 @@
33
"""
44

55
from typing import Any, Dict, List, Optional
6+
import re
67

78
import click
89
import mcp.types as types
910

1011
from .decorator import get_mcp_metadata
1112

1213

14+
def sanitize_tool_name(name: str) -> str:
15+
"""
16+
Sanitize a tool name to conform to the regex pattern [a-zA-Z][a-zA-Z0-9_]*
17+
18+
Args:
19+
name: The tool name to sanitize
20+
21+
Returns:
22+
A sanitized tool name that conforms to the pattern
23+
"""
24+
# Replace any non-alphanumeric characters (except underscore) with underscore
25+
sanitized = re.sub(r'[^a-zA-Z0-9_]', '_', name)
26+
27+
# Ensure the name starts with a letter
28+
if sanitized and not re.match(r'^[a-zA-Z]', sanitized):
29+
sanitized = 'tool_' + sanitized
30+
31+
# Handle empty string case
32+
if not sanitized:
33+
sanitized = 'tool'
34+
35+
return sanitized
36+
37+
1338
# Dictionary to store positional arguments for each tool
1439
_tool_positional_args: Dict[str, List[str]] = {}
1540

@@ -40,14 +65,15 @@ def scan_click_command(command: click.Group, parent_path: str = "") -> List[type
4065

4166
# Determine command path
4267
custom_name = metadata.get("name", name)
43-
# Ensure paths use underscore separator instead of dot
44-
cmd_path = _sanitize_tool_name(f"{parent_path}{custom_name}" if parent_path else custom_name)
68+
custom_name = sanitize_tool_name(custom_name)
69+
cmd_path = f"{parent_path}{custom_name}" if parent_path else custom_name
4570

4671
if "commands" in cmd_info:
4772
# Handle subgroup
4873
cmd = command.get_command(ctx, name)
4974
if isinstance(cmd, click.Group):
5075
group_name = metadata.get("name", name)
76+
group_name = sanitize_tool_name(group_name)
5177
group_path = (
5278
f"{parent_path}{group_name}_" if parent_path else f"{group_name}_"
5379
)
@@ -68,43 +94,14 @@ def scan_click_command(command: click.Group, parent_path: str = "") -> List[type
6894

6995

7096
def get_positional_args(tool_name: str) -> List[str]:
71-
# Sanitize the tool name to ensure consistency with how they're stored
72-
sanitized_name = _sanitize_tool_name(tool_name)
73-
return _tool_positional_args.get(sanitized_name, [])
97+
return _tool_positional_args.get(tool_name, [])
7498

7599

76-
def _sanitize_tool_name(name: str) -> str:
77-
"""
78-
Sanitize tool name to conform to regex [a-zA-Z][a-zA-Z0-9_]*
79-
80-
Args:
81-
name: The original tool name
82-
83-
Returns:
84-
A sanitized name that conforms to the regex
85-
"""
86-
# Replace any non-alphanumeric characters with underscore (except we keep existing underscores)
87-
import re
88-
sanitized = re.sub(r'[^a-zA-Z0-9_]', '_', name)
89-
90-
# Ensure first character is a letter
91-
if sanitized and not sanitized[0].isalpha():
92-
sanitized = 'tool_' + sanitized
93-
94-
# Handle empty name
95-
if not sanitized:
96-
sanitized = 'tool'
97-
98-
return sanitized
99-
100100
def _convert_command_to_tool(
101101
command: click.Command, command_info: Dict[str, Any], name: str
102102
) -> tuple[types.Tool, List[str]]:
103103
"""
104104
Convert a Click command to an MCP tool.
105-
106-
Returns:
107-
A tuple of (Tool, positional_args_list)
108105
"""
109106
description = command_info.get("help") or command_info.get("short_help") or ""
110107

@@ -138,7 +135,7 @@ def _convert_command_to_tool(
138135
input_schema["required"] = sorted(required_params) # Sort for consistent output
139136

140137
tool = types.Tool(
141-
name=_sanitize_tool_name(name),
138+
name=sanitize_tool_name(name),
142139
description=description,
143140
inputSchema=input_schema,
144141
)

0 commit comments

Comments
 (0)