Skip to content
Closed
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
17 changes: 13 additions & 4 deletions src/mcp/client/auth/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re
from typing import Any, cast
from urllib.parse import urljoin, urlparse
from urllib.parse import unquote, urljoin, urlparse

from httpx2 import Request, Response
from mcp_types import LATEST_PROTOCOL_VERSION
Expand Down Expand Up @@ -320,19 +320,28 @@ async def handle_registration_response(response: Response) -> OAuthClientInforma
def is_valid_client_metadata_url(url: str | None) -> bool:
"""Validate that a URL is suitable for use as a client_id (CIMD).

The URL must be HTTPS with a non-root pathname.
Draft-ietf-oauth-client-id-metadata-document section 3 requires the URL to be HTTPS, to have a
non-root pathname, and to contain no userinfo, no `.`/`..` path segments and no fragment.

Args:
url: The URL to validate

Returns:
True if the URL is a valid HTTPS URL with a non-root pathname
True if the URL satisfies the client metadata document URL rules
"""
if not url:
return False
try:
parsed = urlparse(url)
return parsed.scheme == "https" and parsed.path not in ("", "/")
if parsed.scheme != "https" or parsed.path in ("", "/"):
return False
if parsed.username or parsed.password:
return False
if parsed.fragment:
return False
# Compare dot segments after percent-decoding: RFC 3986 treats %2E as a dot for the purposes
# of path segment normalisation, so "%2E%2E" is a ".." segment.
return not any(unquote(segment) in (".", "..") for segment in parsed.path.split("/"))
except Exception:
return False

Expand Down
13 changes: 13 additions & 0 deletions tests/client/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2363,6 +2363,19 @@ class TestCIMD:
# Invalid URLs - root path
("https://example.com", False),
("https://example.com/", False),
# Invalid URLs - userinfo
("https://user@example.com/client", False),
("https://user:secret@example.com/client", False),
# Invalid URLs - `.` and `..` path segments
("https://example.com/./client", False),
("https://example.com/path/../client", False),
("https://example.com/%2E%2E/client", False),
("https://example.com/path/%2e/client", False),
# Invalid URLs - fragment
("https://example.com/client#section", False),
# Valid URLs - dot-free segments, query strings and ports are allowed
("https://example.com/path.with.dots/client", True),
("https://example.com/client?version=2", True),
# Invalid URLs - None or empty
(None, False),
("", False),
Expand Down
Loading