From d7bd9c4f4414cdc2beb1f3ae8f0a606daf6648c1 Mon Sep 17 00:00:00 2001 From: ege-arhan Date: Wed, 16 Sep 2026 06:06:00 +0000 Subject: [PATCH] fix(oauth): merge auth params into existing authorization_endpoint query (RFC 6749 3.1) --- src/mcp/client/auth/oauth2.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/mcp/client/auth/oauth2.py b/src/mcp/client/auth/oauth2.py index 8588208924..4db4f8f9fb 100644 --- a/src/mcp/client/auth/oauth2.py +++ b/src/mcp/client/auth/oauth2.py @@ -12,7 +12,7 @@ from collections.abc import AsyncGenerator, Awaitable, Callable from dataclasses import dataclass, field from typing import Any, Protocol, get_args -from urllib.parse import quote, urlencode, urljoin, urlparse +from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlparse, urlsplit, urlunsplit import anyio import httpx2 @@ -424,7 +424,12 @@ async def _perform_authorization_code_grant(self) -> tuple[str, str]: if "offline_access" in self.context.client_metadata.scope.split(): auth_params["prompt"] = "consent" - authorization_url = f"{auth_endpoint}?{urlencode(auth_params)}" + # RFC 6749 ยง3.1: authorization_endpoint MAY already carry a query + # component which MUST be retained. Merge instead of appending + # a second '?'. (TS SDK does the same via URL + searchParams.) + parts = urlsplit(auth_endpoint) + merged = parse_qsl(parts.query, keep_blank_values=True) + list(auth_params.items()) + authorization_url = urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(merged), parts.fragment)) await self.context.redirect_handler(authorization_url) # Wait for callback