Skip to content

Commit 2dd1073

Browse files
committed
feat: Implement SEP-986: Tool Name Guidance
1 parent 6f2cd0c commit 2dd1073

File tree

2 files changed

+57
-1
lines changed

2 files changed

+57
-1
lines changed

src/mcp/types.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from collections.abc import Callable
2+
import re
23
from typing import Annotated, Any, Generic, Literal, TypeAlias, TypeVar
34

4-
from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel
5+
from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel, field_validator
56
from pydantic.networks import AnyUrl, UrlConstraints
67
from typing_extensions import deprecated
78

@@ -39,6 +40,10 @@
3940
RequestId = Annotated[int, Field(strict=True)] | str
4041
AnyFunction: TypeAlias = Callable[..., Any]
4142

43+
# Tool name validation pattern (ASCII letters, digits, underscore, dash, dot)
44+
# Pattern ensures entire string contains only valid characters by using ^ and $ anchors
45+
TOOL_NAME_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+$")
46+
4247

4348
class RequestParams(BaseModel):
4449
class Meta(BaseModel):
@@ -891,6 +896,22 @@ class Tool(BaseMetadata):
891896
"""
892897
model_config = ConfigDict(extra="allow")
893898

899+
@field_validator("name")
900+
@classmethod
901+
def _validate_tool_name(cls, value: str) -> str:
902+
if not (1 <= len(value) <= 128):
903+
raise ValueError(f"Invalid tool name length: {len(value)}. Tool name must be between 1 and 128 characters.")
904+
905+
if not TOOL_NAME_PATTERN.fullmatch(value):
906+
raise ValueError("Invalid tool name characters. Allowed: A-Z, a-z, 0-9, underscore (_), dash (-), dot (.).")
907+
908+
return value
909+
910+
"""
911+
See [MCP specification](https://modelcontextprotocol.io/specification/draft/server/tools#tool-names)
912+
for more information on tool naming conventions.
913+
"""
914+
894915

895916
class ListToolsResult(PaginatedResult):
896917
"""The server's response to a tools/list request from the client."""

tests/test_types.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
InitializeRequestParams,
1010
JSONRPCMessage,
1111
JSONRPCRequest,
12+
Tool,
1213
)
1314

1415

@@ -56,3 +57,37 @@ async def test_method_initialization():
5657
assert initialize_request.method == "initialize", "method should be set to 'initialize'"
5758
assert initialize_request.params is not None
5859
assert initialize_request.params.protocolVersion == LATEST_PROTOCOL_VERSION
60+
61+
62+
@pytest.mark.parametrize(
63+
"name",
64+
[
65+
"getUser",
66+
"DATA_EXPORT_v2",
67+
"admin.tools.list",
68+
"a",
69+
"Z9_.-",
70+
"x" * 128, # max length
71+
],
72+
)
73+
def test_tool_allows_valid_names(name: str) -> None:
74+
Tool(name=name, inputSchema={"type": "object"})
75+
76+
77+
@pytest.mark.parametrize(
78+
("name", "expected"),
79+
[
80+
("", "Invalid tool name length: 0. Tool name must be between 1 and 128 characters."),
81+
("x" * 129, "Invalid tool name length: 129. Tool name must be between 1 and 128 characters."),
82+
("has space", "Invalid tool name characters. Allowed: A-Z, a-z, 0-9, underscore (_), dash (-), dot (.)."),
83+
("comma,name", "Invalid tool name characters. Allowed: A-Z, a-z, 0-9, underscore (_), dash (-), dot (.)."),
84+
("not/allowed", "Invalid tool name characters. Allowed: A-Z, a-z, 0-9, underscore (_), dash (-), dot (.)."),
85+
("name@", "Invalid tool name characters. Allowed: A-Z, a-z, 0-9, underscore (_), dash (-), dot (.)."),
86+
("name#", "Invalid tool name characters. Allowed: A-Z, a-z, 0-9, underscore (_), dash (-), dot (.)."),
87+
("name$", "Invalid tool name characters. Allowed: A-Z, a-z, 0-9, underscore (_), dash (-), dot (.)."),
88+
],
89+
)
90+
def test_tool_rejects_invalid_names(name: str, expected: str) -> None:
91+
with pytest.raises(ValueError) as exc_info:
92+
Tool(name=name, inputSchema={"type": "object"})
93+
assert expected in str(exc_info.value)

0 commit comments

Comments
 (0)