From 1216c536937819070981e63177b258b79a615bfd Mon Sep 17 00:00:00 2001 From: Otis Cui Date: Fri, 10 Jul 2026 20:56:54 +0800 Subject: [PATCH] fix: reject trailing newline in tool-name and URI-template varname validation (#3076) Python's $ with re.match also matches just before a single trailing newline, so tool-name validation accepted "name\n" and UriTemplate.parse accepted varnames like "foo\n". Switch both checks to re.fullmatch. Closes #3084 --- src/mcp/shared/tool_name_validation.py | 2 +- src/mcp/shared/uri_template.py | 2 +- tests/shared/test_tool_name_validation.py | 5 +++++ tests/shared/test_uri_template.py | 2 ++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/mcp/shared/tool_name_validation.py b/src/mcp/shared/tool_name_validation.py index f35efa5a..96c34f78 100644 --- a/src/mcp/shared/tool_name_validation.py +++ b/src/mcp/shared/tool_name_validation.py @@ -77,7 +77,7 @@ def validate_tool_name(name: str) -> ToolNameValidationResult: 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.match(name): + if not TOOL_NAME_REGEX.fullmatch(name): # Find all invalid characters (unique, preserving order) invalid_chars: list[str] = [] seen: set[str] = set() diff --git a/src/mcp/shared/uri_template.py b/src/mcp/shared/uri_template.py index dc57bfa7..20d6fa9c 100644 --- a/src/mcp/shared/uri_template.py +++ b/src/mcp/shared/uri_template.py @@ -813,7 +813,7 @@ def _parse_expression(template: str, body: str, pos: int) -> _Expression: explode = spec.endswith("*") name = spec[:-1] if explode else spec - if not _VARNAME_RE.match(name): + if not _VARNAME_RE.fullmatch(name): raise InvalidUriTemplate( f"Invalid variable name {name!r} at position {pos}", template=template, diff --git a/tests/shared/test_tool_name_validation.py b/tests/shared/test_tool_name_validation.py index 97b3dffc..cbf27b87 100644 --- a/tests/shared/test_tool_name_validation.py +++ b/tests/shared/test_tool_name_validation.py @@ -65,12 +65,17 @@ def test_validate_tool_name_rejects_name_exceeding_max_length() -> None: ("get,user,profile", "','"), ("user/profile/update", "'/'"), ("user@domain.com", "'@'"), + # a single trailing newline slipped past `$` with re.match + ("valid_name\n", "'\\n'"), + ("a" * 127 + "\n", "'\\n'"), ], ids=[ "with_spaces", "with_commas", "with_slashes", "with_at_symbol", + "with_trailing_newline", + "max_length_with_trailing_newline", ], ) def test_validate_tool_name_rejects_invalid_characters(tool_name: str, expected_char: str) -> None: diff --git a/tests/shared/test_uri_template.py b/tests/shared/test_uri_template.py index 48f6c662..cddbb82b 100644 --- a/tests/shared/test_uri_template.py +++ b/tests/shared/test_uri_template.py @@ -144,6 +144,8 @@ def test_parse_rejects_operator_without_variable(): # RFC ยง2.3: dots only between varchars, not consecutive or trailing "foo..bar", "foo.", + # a single trailing newline slipped past `$` with re.match + "foo\n", ], ) def test_parse_rejects_invalid_varname(name: str):