Skip to content

Commit 74c5d48

Browse files
committed
Resolve merge conflicts and retain OAuth grant support
1 parent 50833e0 commit 74c5d48

File tree

3 files changed

+3
-172
lines changed

3 files changed

+3
-172
lines changed

src/mcp/client/auth/oauth2.py

Lines changed: 3 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -431,67 +431,7 @@ def _select_scopes(self, init_response: httpx.Response) -> None:
431431
# Priority 3: Omit scope parameter
432432
self.context.client_metadata.scope = None
433433

434-
#<<<<<<< main
435434
# Discovery and registration helpers provided by BaseOAuthProvider
436-
#=======
437-
def _get_discovery_urls(self) -> list[str]:
438-
"""Generate ordered list of (url, type) tuples for discovery attempts."""
439-
urls: list[str] = []
440-
auth_server_url = self.context.auth_server_url or self.context.server_url
441-
parsed = urlparse(auth_server_url)
442-
base_url = f"{parsed.scheme}://{parsed.netloc}"
443-
444-
# RFC 8414: Path-aware OAuth discovery
445-
if parsed.path and parsed.path != "/":
446-
oauth_path = f"/.well-known/oauth-authorization-server{parsed.path.rstrip('/')}"
447-
urls.append(urljoin(base_url, oauth_path))
448-
449-
# OAuth root fallback
450-
urls.append(urljoin(base_url, "/.well-known/oauth-authorization-server"))
451-
452-
# RFC 8414 section 5: Path-aware OIDC discovery
453-
# See https://www.rfc-editor.org/rfc/rfc8414.html#section-5
454-
if parsed.path and parsed.path != "/":
455-
oidc_path = f"/.well-known/openid-configuration{parsed.path.rstrip('/')}"
456-
urls.append(urljoin(base_url, oidc_path))
457-
458-
# OIDC 1.0 fallback (appends to full URL per OIDC spec)
459-
oidc_fallback = f"{auth_server_url.rstrip('/')}/.well-known/openid-configuration"
460-
urls.append(oidc_fallback)
461-
462-
return urls
463-
464-
async def _register_client(self) -> httpx.Request | None:
465-
"""Build registration request or skip if already registered."""
466-
if self.context.client_info:
467-
return None
468-
469-
if self.context.oauth_metadata and self.context.oauth_metadata.registration_endpoint:
470-
registration_url = str(self.context.oauth_metadata.registration_endpoint)
471-
else:
472-
auth_base_url = self.context.get_authorization_base_url(self.context.server_url)
473-
registration_url = urljoin(auth_base_url, "/register")
474-
475-
registration_data = self.context.client_metadata.model_dump(by_alias=True, mode="json", exclude_none=True)
476-
477-
return httpx.Request(
478-
"POST", registration_url, json=registration_data, headers={"Content-Type": "application/json"}
479-
)
480-
481-
async def _handle_registration_response(self, response: httpx.Response) -> None:
482-
"""Handle registration response."""
483-
if response.status_code not in (200, 201):
484-
await response.aread()
485-
raise OAuthRegistrationError(f"Registration failed: {response.status_code} {response.text}")
486-
487-
try:
488-
content = await response.aread()
489-
client_info = OAuthClientInformationFull.model_validate_json(content)
490-
self.context.client_info = client_info
491-
await self.context.storage.set_client_info(client_info)
492-
except ValidationError as e: # pragma: no cover
493-
raise OAuthRegistrationError(f"Invalid registration response: {e}")
494-
#>>>>>>> main
495435

496436
async def _perform_authorization(self) -> httpx.Request:
497437
"""Perform the authorization flow."""
@@ -644,13 +584,8 @@ async def _refresh_token(self) -> httpx.Request:
644584
if self.context.should_include_resource_param(self.context.protocol_version):
645585
refresh_data["resource"] = self.context.get_resource_url() # RFC 8707
646586

647-
#<<<<<<< main
648587
headers = {"Content-Type": "application/x-www-form-urlencoded"}
649588
self._apply_client_auth(refresh_data, headers, self.context.client_info)
650-
#=======
651-
if self.context.client_info.client_secret: # pragma: no branch
652-
refresh_data["client_secret"] = self.context.client_info.client_secret
653-
#>>>>>>> main
654589

655590
return httpx.Request("POST", token_url, data=refresh_data, headers=headers)
656591

@@ -734,13 +669,10 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
734669
self._select_scopes(response)
735670

736671
# Step 3: Discover OAuth metadata (with fallback for legacy servers)
737-
#<<<<<<< main
738-
discovery_urls = self._get_discovery_urls(self.context.auth_server_url or self.context.server_url)
672+
discovery_urls = self._get_discovery_urls(
673+
self.context.auth_server_url or self.context.server_url
674+
)
739675
for url in discovery_urls:
740-
#=======
741-
discovery_urls = self._get_discovery_urls()
742-
for url in discovery_urls: # pragma: no branch
743-
#>>>>>>> main
744676
oauth_metadata_request = self._create_oauth_metadata_request(url)
745677
oauth_metadata_response = yield oauth_metadata_request
746678

src/mcp/server/auth/handlers/token.py

Lines changed: 0 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -288,97 +288,15 @@ async def handle(self, request: Request):
288288

289289
match token_request:
290290
case AuthorizationCodeRequest():
291-
#<<<<<<< main
292291
result = await self._handle_authorization_code(client_info, token_request)
293-
#=======
294-
auth_code = await self.provider.load_authorization_code(client_info, token_request.code)
295-
if auth_code is None or auth_code.client_id != token_request.client_id:
296-
# if code belongs to different client, pretend it doesn't exist
297-
return self.response(
298-
TokenErrorResponse(
299-
error="invalid_grant",
300-
error_description="authorization code does not exist",
301-
)
302-
)
303-
304-
# make auth codes expire after a deadline
305-
# see https://datatracker.ietf.org/doc/html/rfc6749#section-10.5
306-
if auth_code.expires_at < time.time():
307-
return self.response(
308-
TokenErrorResponse(
309-
error="invalid_grant",
310-
error_description="authorization code has expired",
311-
)
312-
)
313-
314-
# verify redirect_uri doesn't change between /authorize and /tokens
315-
# see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6
316-
if auth_code.redirect_uri_provided_explicitly:
317-
authorize_request_redirect_uri = auth_code.redirect_uri
318-
else: # pragma: no cover
319-
authorize_request_redirect_uri = None
320-
321-
# Convert both sides to strings for comparison to handle AnyUrl vs string issues
322-
token_redirect_str = str(token_request.redirect_uri) if token_request.redirect_uri is not None else None
323-
auth_redirect_str = (
324-
str(authorize_request_redirect_uri) if authorize_request_redirect_uri is not None else None
325-
)
326-
#>>>>>>> main
327292

328293
case ClientCredentialsRequest():
329294
result = await self._handle_client_credentials(client_info, token_request)
330295

331296
case TokenExchangeRequest():
332297
result = await self._handle_token_exchange(client_info, token_request)
333298

334-
#<<<<<<< main
335299
case RefreshTokenRequest():
336300
result = await self._handle_refresh_token(client_info, token_request)
337301

338302
return self.response(result)
339-
#=======
340-
case RefreshTokenRequest(): # pragma: no cover
341-
refresh_token = await self.provider.load_refresh_token(client_info, token_request.refresh_token)
342-
if refresh_token is None or refresh_token.client_id != token_request.client_id:
343-
# if token belongs to different client, pretend it doesn't exist
344-
return self.response(
345-
TokenErrorResponse(
346-
error="invalid_grant",
347-
error_description="refresh token does not exist",
348-
)
349-
)
350-
351-
if refresh_token.expires_at and refresh_token.expires_at < time.time():
352-
# if the refresh token has expired, pretend it doesn't exist
353-
return self.response(
354-
TokenErrorResponse(
355-
error="invalid_grant",
356-
error_description="refresh token has expired",
357-
)
358-
)
359-
360-
# Parse scopes if provided
361-
scopes = token_request.scope.split(" ") if token_request.scope else refresh_token.scopes
362-
363-
for scope in scopes:
364-
if scope not in refresh_token.scopes:
365-
return self.response(
366-
TokenErrorResponse(
367-
error="invalid_scope",
368-
error_description=(f"cannot request scope `{scope}` not provided by refresh token"),
369-
)
370-
)
371-
372-
try:
373-
# Exchange refresh token for new tokens
374-
tokens = await self.provider.exchange_refresh_token(client_info, refresh_token, scopes)
375-
except TokenError as e:
376-
return self.response(
377-
TokenErrorResponse(
378-
error=e.error,
379-
error_description=e.error_description,
380-
)
381-
)
382-
383-
return self.response(TokenSuccessResponse(root=tokens))
384-
#>>>>>>> main

tests/server/fastmcp/resources/test_file_resources.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,6 @@ async def test_missing_file_error(self, temp_file: Path):
100100
with pytest.raises(ValueError, match="Error reading file"):
101101
await resource.read()
102102

103-
#<<<<<<< main
104-
105103
@pytest.mark.skipif(os.name == "nt", reason="File permissions behave differently on Windows")
106104
@pytest.mark.anyio
107105
async def test_permission_error(temp_file: Path):
@@ -119,20 +117,3 @@ async def test_permission_error(temp_file: Path):
119117
await resource.read()
120118
finally:
121119
temp_file.chmod(0o644) # Restore permissions
122-
#=======
123-
@pytest.mark.skipif(os.name == "nt", reason="File permissions behave differently on Windows")
124-
@pytest.mark.anyio
125-
async def test_permission_error(self, temp_file: Path): # pragma: no cover
126-
"""Test reading a file without permissions."""
127-
temp_file.chmod(0o000) # Remove all permissions
128-
try:
129-
resource = FileResource(
130-
uri=FileUrl(temp_file.as_uri()),
131-
name="test",
132-
path=temp_file,
133-
)
134-
with pytest.raises(ValueError, match="Error reading file"):
135-
await resource.read()
136-
finally:
137-
temp_file.chmod(0o644) # Restore permissions
138-
#>>>>>>> main

0 commit comments

Comments
 (0)