Skip to content
Open
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
6 changes: 4 additions & 2 deletions src/mcp/shared/tool_name_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

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}$")
# Regular expression for valid tool names according to SEP-986 specification.
# End-anchored with \Z rather than $: in Python's default mode $ also matches
# just before a single trailing newline, which would let "name\n" validate.
TOOL_NAME_REGEX = re.compile(r"^[A-Za-z0-9._-]{1,128}\Z")

# SEP reference URL for warning messages
SEP_986_URL = "https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names"
Expand Down
16 changes: 16 additions & 0 deletions tests/shared/test_tool_name_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ def test_validate_tool_name_rejects_invalid_characters(tool_name: str, expected_
assert any("invalid characters" in w and expected_char in w for w in result.warnings)


@pytest.mark.parametrize(
"tool_name",
["valid_name\n", "a" * 127 + "\n"],
ids=["trailing_newline", "max_length_plus_newline"],
)
def test_validate_tool_name_rejects_trailing_newline(tool_name: str) -> None:
"""A trailing newline is not an allowed character and must be rejected.

Regression test: Python's ``$`` (unlike ``\\Z``) also matches just before a
single trailing newline, so a name like ``"valid_name\\n"`` slipped through.
"""
result = validate_tool_name(tool_name)
assert result.is_valid is False
assert any("invalid characters" in w for w in result.warnings)


def test_validate_tool_name_rejects_multiple_invalid_chars() -> None:
"""Names with multiple invalid chars should list all of them."""
result = validate_tool_name("user name@domain,com")
Expand Down
Loading