Summary
FastMCP 3.0.2's CIMD redirect URI validation strictly matches ports on loopback addresses, rejecting dynamic-port callbacks from native OAuth clients like Claude Code. Per RFC 8252 §7.3, authorization servers MUST allow any port for loopback redirect URIs.
Reproduction
-
A CIMD metadata document lists http://localhost/callback as a redirect URI (no port = port 80):
{
"client_id": "https://claude.ai/oauth/claude-code-client-metadata",
"redirect_uris": ["http://localhost/callback", "http://127.0.0.1/callback"]
}
-
The OAuth client (Claude Code) starts a callback listener on a dynamic ephemeral port and sends:
redirect_uri=http://localhost:51212/callback
-
FastMCP's _match_port() in fastmcp/server/auth/redirect_validation.py normalizes the missing port to "80" and does a strict comparison:
uri_effective = uri_port if uri_port else default_port # "51212"
pattern_effective = pattern_port if pattern_port else default_port # "80"
return uri_effective == pattern_effective # False — rejected
-
The authorize flow raises:
Redirect URI 'http://localhost:51212/callback' does not match CIMD redirect_uris.
Expected behavior
For loopback addresses (localhost, 127.0.0.1, [::1]), _match_port() should accept any port when the pattern port is the default (or absent), per RFC 8252:
The authorization server MUST allow any port to be specified at the time of the request for loopback IP redirect URIs, to accommodate clients that obtain an available ephemeral port from the operating system at the time of the request.
Suggested fix
In _match_port(), add a loopback check before the strict comparison:
def _match_port(
uri_port: str | None,
pattern_port: str | None,
uri_scheme: str,
uri_host: str | None = None, # new parameter
) -> bool:
if pattern_port == "*":
return True
# RFC 8252 §7.3: accept any port on loopback addresses
loopback_hosts = {"localhost", "127.0.0.1", "::1"}
if uri_host and uri_host.lower() in loopback_hosts:
return True
default_port = "443" if uri_scheme == "https" else "80"
uri_effective = uri_port if uri_port else default_port
pattern_effective = pattern_port if pattern_port else default_port
return uri_effective == pattern_effective
The caller (matches_allowed_pattern) already has uri_host parsed and can pass it through.
Impact
This blocks CIMD adoption for any native MCP client using dynamic loopback ports (which is the standard pattern for CLI/desktop OAuth clients). We had to disable CIMD on all our FastMCP-based MCP servers and fall back to DCR as a workaround.
Environment
Summary
FastMCP 3.0.2's CIMD redirect URI validation strictly matches ports on loopback addresses, rejecting dynamic-port callbacks from native OAuth clients like Claude Code. Per RFC 8252 §7.3, authorization servers MUST allow any port for loopback redirect URIs.
Reproduction
A CIMD metadata document lists
http://localhost/callbackas a redirect URI (no port = port 80):{ "client_id": "https://claude.ai/oauth/claude-code-client-metadata", "redirect_uris": ["http://localhost/callback", "http://127.0.0.1/callback"] }The OAuth client (Claude Code) starts a callback listener on a dynamic ephemeral port and sends:
FastMCP's
_match_port()infastmcp/server/auth/redirect_validation.pynormalizes the missing port to"80"and does a strict comparison:The authorize flow raises:
Expected behavior
For loopback addresses (
localhost,127.0.0.1,[::1]),_match_port()should accept any port when the pattern port is the default (or absent), per RFC 8252:Suggested fix
In
_match_port(), add a loopback check before the strict comparison:The caller (
matches_allowed_pattern) already hasuri_hostparsed and can pass it through.Impact
This blocks CIMD adoption for any native MCP client using dynamic loopback ports (which is the standard pattern for CLI/desktop OAuth clients). We had to disable CIMD on all our FastMCP-based MCP servers and fall back to DCR as a workaround.
Environment