fastmcp 2.12.0__py3-none-any.whl → 2.12.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,12 +18,15 @@ production use with enterprise identity providers.
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ import hashlib
21
22
  import secrets
22
23
  import time
24
+ from base64 import urlsafe_b64encode
23
25
  from typing import TYPE_CHECKING, Any, Final
24
26
  from urllib.parse import urlencode
25
27
 
26
28
  import httpx
29
+ from authlib.common.security import generate_token
27
30
  from authlib.integrations.httpx_client import AsyncOAuth2Client
28
31
  from mcp.server.auth.provider import (
29
32
  AccessToken,
@@ -39,7 +42,7 @@ from mcp.server.auth.settings import (
39
42
  from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
40
43
  from pydantic import AnyHttpUrl, AnyUrl, SecretStr
41
44
  from starlette.requests import Request
42
- from starlette.responses import JSONResponse, RedirectResponse
45
+ from starlette.responses import RedirectResponse
43
46
  from starlette.routing import Route
44
47
 
45
48
  from fastmcp.server.auth.auth import OAuthProvider, TokenVerifier
@@ -172,7 +175,6 @@ class OAuthProxy(OAuthProvider):
172
175
  1. Client Registration (DCR):
173
176
  - Accept any client registration request
174
177
  - Store ProxyDCRClient that accepts dynamic redirect URIs
175
- - Return shared upstream credentials to all clients
176
178
 
177
179
  2. Authorization:
178
180
  - Store transaction mapping client details to proxy flow
@@ -241,9 +243,13 @@ class OAuthProxy(OAuthProvider):
241
243
  redirect_path: str = "/auth/callback",
242
244
  issuer_url: AnyHttpUrl | str | None = None,
243
245
  service_documentation_url: AnyHttpUrl | str | None = None,
244
- resource_server_url: AnyHttpUrl | str | None = None,
245
246
  # Client redirect URI validation
246
247
  allowed_client_redirect_uris: list[str] | None = None,
248
+ valid_scopes: list[str] | None = None,
249
+ # PKCE configuration
250
+ forward_pkce: bool = True,
251
+ # Token endpoint authentication
252
+ token_endpoint_auth_method: str | None = None,
247
253
  ):
248
254
  """Initialize the OAuth proxy provider.
249
255
 
@@ -259,17 +265,25 @@ class OAuthProxy(OAuthProvider):
259
265
  redirect_path: Redirect path configured in upstream OAuth app (defaults to "/auth/callback")
260
266
  issuer_url: Issuer URL for OAuth metadata (defaults to base_url)
261
267
  service_documentation_url: Optional service documentation URL
262
- resource_server_url: Path of the FastMCP server. If None, FastMCP will
263
- attempt to overwrite this with the correct path to the server
264
- e.g. {base_url}/mcp
265
268
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
266
269
  Patterns support wildcards (e.g., "http://localhost:*", "https://*.example.com/*").
267
270
  If None (default), only localhost redirect URIs are allowed.
268
271
  If empty list, all redirect URIs are allowed (not recommended for production).
269
272
  These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app.
273
+ valid_scopes: List of all the possible valid scopes for a client.
274
+ These are advertised to clients through the `/.well-known` endpoints. Defaults to `required_scopes` if not provided.
275
+ forward_pkce: Whether to forward PKCE to upstream server (default True).
276
+ Enable for providers that support/require PKCE (Google, Azure, etc.).
277
+ Disable only if upstream provider doesn't support PKCE.
278
+ token_endpoint_auth_method: Token endpoint authentication method for upstream server.
279
+ Common values: "client_secret_basic", "client_secret_post", "none".
280
+ If None, authlib will use its default (typically "client_secret_basic").
270
281
  """
271
282
  # Always enable DCR since we implement it locally for MCP clients
272
- client_registration_options = ClientRegistrationOptions(enabled=True)
283
+ client_registration_options = ClientRegistrationOptions(
284
+ enabled=True,
285
+ valid_scopes=valid_scopes or token_verifier.required_scopes,
286
+ )
273
287
 
274
288
  # Enable revocation only if upstream endpoint provided
275
289
  revocation_options = (
@@ -283,7 +297,6 @@ class OAuthProxy(OAuthProvider):
283
297
  client_registration_options=client_registration_options,
284
298
  revocation_options=revocation_options,
285
299
  required_scopes=token_verifier.required_scopes,
286
- resource_server_url=resource_server_url,
287
300
  )
288
301
 
289
302
  # Store upstream configuration
@@ -300,6 +313,12 @@ class OAuthProxy(OAuthProvider):
300
313
  )
301
314
  self._allowed_client_redirect_uris = allowed_client_redirect_uris
302
315
 
316
+ # PKCE configuration
317
+ self._forward_pkce = forward_pkce
318
+
319
+ # Token endpoint authentication
320
+ self._token_endpoint_auth_method = token_endpoint_auth_method
321
+
303
322
  # Local state for DCR and token bookkeeping
304
323
  self._clients: dict[str, OAuthClientInformationFull] = {}
305
324
  self._access_tokens: dict[str, AccessToken] = {}
@@ -323,72 +342,52 @@ class OAuthProxy(OAuthProvider):
323
342
  self._upstream_authorization_endpoint,
324
343
  )
325
344
 
345
+ # -------------------------------------------------------------------------
346
+ # PKCE Helper Methods
347
+ # -------------------------------------------------------------------------
348
+
349
+ def _generate_pkce_pair(self) -> tuple[str, str]:
350
+ """Generate PKCE code verifier and challenge pair.
351
+
352
+ Returns:
353
+ Tuple of (code_verifier, code_challenge) using S256 method
354
+ """
355
+ # Generate code verifier: 43-128 characters from unreserved set
356
+ code_verifier = generate_token(48)
357
+
358
+ # Generate code challenge using S256 (SHA256 + base64url)
359
+ challenge_bytes = hashlib.sha256(code_verifier.encode()).digest()
360
+ code_challenge = urlsafe_b64encode(challenge_bytes).decode().rstrip("=")
361
+
362
+ return code_verifier, code_challenge
363
+
326
364
  # -------------------------------------------------------------------------
327
365
  # Client Registration (Local Implementation)
328
366
  # -------------------------------------------------------------------------
329
367
 
330
368
  async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
331
- """Get client information by ID.
369
+ """Get client information by ID. This is generally the random ID
370
+ provided to the DCR client during registration, not the upstream client ID.
332
371
 
333
- For unregistered clients, returns a ProxyDCRClient that accepts
334
- any localhost redirect URI for DCR clients.
335
-
336
- Even registered clients use ProxyDCRClient to ensure they can
337
- authenticate with different dynamic ports on reconnection. This
338
- handles the case where a client with cached tokens reconnects
339
- on a different port.
372
+ For unregistered clients, returns None (which will raise an error in the SDK).
340
373
  """
341
374
  client = self._clients.get(client_id)
342
375
 
343
- if client is None:
344
- # For unregistered DCR clients, create a permissive client
345
- # that will accept any localhost redirect URI
346
- # We need at least one URI for Pydantic validation, but our custom
347
- # validate_redirect_uri will accept any localhost URI
348
- client = ProxyDCRClient(
349
- client_id=client_id,
350
- client_secret=None,
351
- redirect_uris=[
352
- AnyUrl("http://localhost")
353
- ], # Placeholder, validation uses allowed_patterns
354
- grant_types=["authorization_code", "refresh_token"],
355
- scope=self._default_scope_str,
356
- token_endpoint_auth_method="none",
357
- allowed_redirect_uri_patterns=self._allowed_client_redirect_uris,
358
- )
359
- logger.debug("Created ProxyDCRClient for unregistered client %s", client_id)
360
-
361
376
  return client
362
377
 
363
378
  async def register_client(self, client_info: OAuthClientInformationFull) -> None:
364
- """Register a client locally using fixed upstream credentials.
365
-
366
- This implementation always uses the upstream client_id and client_secret
367
- regardless of what the client requests. It modifies the client_info object
368
- in place since the MCP framework ignores return values.
369
-
370
- This ensures all clients use the same credentials that are registered
371
- with the upstream server.
379
+ """Register a client locally
372
380
 
373
- Implementation Detail:
374
- We store a ProxyDCRClient (not the original client_info) to ensure
375
- the client can reconnect with different dynamic redirect URIs. This is
376
- essential for cached token scenarios where the client port changes.
377
-
378
- The flow:
379
- 1. Client provides its desired redirect URIs (dynamic localhost ports)
380
- 2. We create a ProxyDCRClient that will accept ANY localhost URI
381
- 3. We store this flexible client for future authentications
382
- 4. When client reconnects with a different port, ProxyDCRClient accepts it
381
+ When a client registers, we create a ProxyDCRClient that is more
382
+ forgiving about validating redirect URIs, since the DCR client's
383
+ redirect URI will likely be localhost or unknown to the proxied IDP. The
384
+ proxied IDP only knows about this server's fixed redirect URI.
383
385
  """
384
- # Always use the upstream credentials
385
- upstream_id = self._upstream_client_id
386
- upstream_secret = self._upstream_client_secret.get_secret_value()
387
386
 
388
387
  # Create a ProxyDCRClient with configured redirect URI validation
389
388
  proxy_client = ProxyDCRClient(
390
- client_id=upstream_id,
391
- client_secret=upstream_secret,
389
+ client_id=client_info.client_id,
390
+ client_secret=client_info.client_secret,
392
391
  redirect_uris=client_info.redirect_uris or [AnyUrl("http://localhost")],
393
392
  grant_types=client_info.grant_types
394
393
  or ["authorization_code", "refresh_token"],
@@ -397,8 +396,8 @@ class OAuthProxy(OAuthProvider):
397
396
  allowed_redirect_uri_patterns=self._allowed_client_redirect_uris,
398
397
  )
399
398
 
400
- # Store the ProxyDCRClient using the upstream ID
401
- self._clients[upstream_id] = proxy_client
399
+ # Store the ProxyDCRClient
400
+ self._clients[client_info.client_id] = proxy_client
402
401
 
403
402
  # Log redirect URIs to help users discover what patterns they might need
404
403
  if client_info.redirect_uris:
@@ -411,7 +410,7 @@ class OAuthProxy(OAuthProvider):
411
410
 
412
411
  logger.debug(
413
412
  "Registered client %s with %d redirect URIs",
414
- upstream_id,
413
+ client_info.client_id,
415
414
  len(proxy_client.redirect_uris),
416
415
  )
417
416
 
@@ -428,14 +427,25 @@ class OAuthProxy(OAuthProvider):
428
427
 
429
428
  This implements the DCR-compliant proxy pattern:
430
429
  1. Store transaction with client details and PKCE challenge
431
- 2. Use transaction ID as state for IdP
432
- 3. Redirect to IdP with our fixed callback URL
430
+ 2. Generate proxy's own PKCE parameters if forwarding is enabled
431
+ 3. Use transaction ID as state for IdP
432
+ 4. Redirect to IdP with our fixed callback URL and proxy's PKCE
433
433
  """
434
434
  # Generate transaction ID for this authorization request
435
435
  txn_id = secrets.token_urlsafe(32)
436
436
 
437
+ # Generate proxy's own PKCE parameters if forwarding is enabled
438
+ proxy_code_verifier = None
439
+ proxy_code_challenge = None
440
+ if self._forward_pkce and params.code_challenge:
441
+ proxy_code_verifier, proxy_code_challenge = self._generate_pkce_pair()
442
+ logger.debug(
443
+ "Generated proxy PKCE for transaction %s (forwarding client PKCE to upstream)",
444
+ txn_id,
445
+ )
446
+
437
447
  # Store transaction data for IdP callback processing
438
- self._oauth_transactions[txn_id] = {
448
+ transaction_data = {
439
449
  "client_id": client.client_id,
440
450
  "client_redirect_uri": str(params.redirect_uri),
441
451
  "client_state": params.state,
@@ -445,6 +455,12 @@ class OAuthProxy(OAuthProvider):
445
455
  "created_at": time.time(),
446
456
  }
447
457
 
458
+ # Store proxy's PKCE verifier if we're forwarding
459
+ if proxy_code_verifier:
460
+ transaction_data["proxy_code_verifier"] = proxy_code_verifier
461
+
462
+ self._oauth_transactions[txn_id] = transaction_data
463
+
448
464
  # Build query parameters for upstream IdP authorization request
449
465
  # Use our fixed IdP callback and transaction ID as state
450
466
  query_params: dict[str, Any] = {
@@ -460,14 +476,24 @@ class OAuthProxy(OAuthProvider):
460
476
  if scopes_to_use:
461
477
  query_params["scope"] = " ".join(scopes_to_use)
462
478
 
479
+ # Forward proxy's PKCE challenge to upstream if enabled
480
+ if proxy_code_challenge:
481
+ query_params["code_challenge"] = proxy_code_challenge
482
+ query_params["code_challenge_method"] = "S256"
483
+ logger.debug(
484
+ "Forwarding proxy PKCE challenge to upstream for transaction %s",
485
+ txn_id,
486
+ )
487
+
463
488
  # Build the upstream authorization URL
464
489
  separator = "&" if "?" in self._upstream_authorization_endpoint else "?"
465
490
  upstream_url = f"{self._upstream_authorization_endpoint}{separator}{urlencode(query_params)}"
466
491
 
467
492
  logger.debug(
468
- "Starting OAuth transaction %s for client %s, redirecting to IdP",
493
+ "Starting OAuth transaction %s for client %s, redirecting to IdP (PKCE forwarding: %s)",
469
494
  txn_id,
470
495
  client.client_id,
496
+ "enabled" if proxy_code_challenge else "disabled",
471
497
  )
472
498
  return upstream_url
473
499
 
@@ -604,6 +630,7 @@ class OAuthProxy(OAuthProvider):
604
630
  oauth_client = AsyncOAuth2Client(
605
631
  client_id=self._upstream_client_id,
606
632
  client_secret=self._upstream_client_secret.get_secret_value(),
633
+ token_endpoint_auth_method=self._token_endpoint_auth_method,
607
634
  timeout=HTTP_TIMEOUT_SECONDS,
608
635
  )
609
636
 
@@ -728,163 +755,22 @@ class OAuthProxy(OAuthProvider):
728
755
 
729
756
  logger.debug("Token revoked successfully")
730
757
 
731
- # -------------------------------------------------------------------------
732
- # Custom Route Handling
733
- # -------------------------------------------------------------------------
734
-
735
- async def _handle_proxy_token_request(self, request: Request) -> JSONResponse:
736
- """Custom token endpoint using authlib for upstream requests.
737
-
738
- This handler uses authlib's OAuth2Client to forward token requests to the
739
- upstream OAuth server, automatically handling response format differences.
740
- """
741
- try:
742
- # Parse the incoming request form data
743
- form_data = await request.form()
744
-
745
- # Log the incoming request (with sensitive data redacted)
746
- redacted_form = {
747
- k: (
748
- str(v)[:8] + "..."
749
- if k in {"code", "code_verifier", "client_secret", "refresh_token"}
750
- and v
751
- else str(v)
752
- )
753
- for k, v in form_data.items()
754
- }
755
- logger.debug("Proxy token request form data: %s", redacted_form)
756
-
757
- # Create authlib OAuth2 client
758
- oauth_client = AsyncOAuth2Client(
759
- client_id=self._upstream_client_id,
760
- client_secret=self._upstream_client_secret.get_secret_value(),
761
- timeout=HTTP_TIMEOUT_SECONDS,
762
- )
763
-
764
- grant_type = str(form_data.get("grant_type", ""))
765
-
766
- if grant_type == "authorization_code":
767
- # Authorization code grant
768
- try:
769
- token_data: dict[str, Any] = await oauth_client.fetch_token( # type: ignore[misc]
770
- url=self._upstream_token_endpoint,
771
- code=str(form_data.get("code", "")),
772
- redirect_uri=str(form_data.get("redirect_uri", "")),
773
- code_verifier=str(form_data.get("code_verifier"))
774
- if "code_verifier" in form_data
775
- else None,
776
- )
777
-
778
- # Store tokens locally for tracking
779
- if "access_token" in token_data:
780
- self._store_tokens_from_response(token_data)
781
-
782
- logger.debug(
783
- "Successfully proxied authorization code exchange via authlib"
784
- )
785
-
786
- except Exception as e:
787
- logger.error("Authlib authorization code exchange failed: %s", e)
788
- return JSONResponse(
789
- content={
790
- "error": "invalid_grant",
791
- "error_description": f"Authorization code exchange failed: {e}",
792
- },
793
- status_code=400,
794
- )
795
-
796
- elif grant_type == "refresh_token":
797
- # Refresh token grant
798
- try:
799
- token_data: dict[str, Any] = await oauth_client.refresh_token( # type: ignore[misc]
800
- url=self._upstream_token_endpoint,
801
- refresh_token=str(form_data.get("refresh_token", "")),
802
- scope=str(form_data.get("scope"))
803
- if "scope" in form_data
804
- else None,
805
- )
806
-
807
- logger.debug(
808
- "Successfully proxied refresh token exchange via authlib"
809
- )
810
-
811
- except Exception as e:
812
- logger.error("Authlib refresh token exchange failed: %s", e)
813
- return JSONResponse(
814
- content={
815
- "error": "invalid_grant",
816
- "error_description": f"Refresh token exchange failed: {e}",
817
- },
818
- status_code=400,
819
- )
820
- else:
821
- # Unsupported grant type
822
- logger.error("Unsupported grant type: %s", grant_type)
823
- return JSONResponse(
824
- content={
825
- "error": "unsupported_grant_type",
826
- "error_description": f"Grant type '{grant_type}' not supported by proxy",
827
- },
828
- status_code=400,
829
- )
830
-
831
- return JSONResponse(content=token_data)
832
-
833
- except Exception as e:
834
- logger.error("Error in proxy token handler: %s", e, exc_info=True)
835
- return JSONResponse(
836
- content={
837
- "error": "server_error",
838
- "error_description": "Internal server error",
839
- },
840
- status_code=500,
841
- )
842
-
843
- def _store_tokens_from_response(self, token_data: dict[str, Any]) -> None:
844
- """Store tokens from upstream response for local tracking."""
845
- try:
846
- access_token_value = token_data.get("access_token")
847
- refresh_token_value = token_data.get("refresh_token")
848
- expires_in = int(
849
- token_data.get("expires_in", DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS)
850
- )
851
- expires_at = int(time.time() + expires_in)
852
-
853
- if access_token_value:
854
- access_token = AccessToken(
855
- token=access_token_value,
856
- client_id=self._upstream_client_id,
857
- scopes=[], # Will be determined by token validation
858
- expires_at=expires_at,
859
- )
860
- self._access_tokens[access_token_value] = access_token
861
-
862
- if refresh_token_value:
863
- refresh_token = RefreshToken(
864
- token=refresh_token_value,
865
- client_id=self._upstream_client_id,
866
- scopes=[],
867
- expires_at=None,
868
- )
869
- self._refresh_tokens[refresh_token_value] = refresh_token
870
-
871
- # Maintain token relationships
872
- self._access_to_refresh[access_token_value] = refresh_token_value
873
- self._refresh_to_access[refresh_token_value] = access_token_value
874
-
875
- logger.debug("Stored tokens from upstream response for tracking")
876
-
877
- except Exception as e:
878
- logger.warning("Failed to store tokens from upstream response: %s", e)
879
-
880
- def get_routes(self) -> list[Route]:
758
+ def get_routes(
759
+ self,
760
+ mcp_path: str | None = None,
761
+ mcp_endpoint: Any | None = None,
762
+ ) -> list[Route]:
881
763
  """Get OAuth routes with custom proxy token handler.
882
764
 
883
765
  This method creates standard OAuth routes and replaces the token endpoint
884
766
  with our proxy handler that forwards requests to the upstream OAuth server.
767
+
768
+ Args:
769
+ mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
770
+ mcp_endpoint: The MCP endpoint handler to protect with auth
885
771
  """
886
772
  # Get standard OAuth routes from parent class
887
- routes = super().get_routes()
773
+ routes = super().get_routes(mcp_path, mcp_endpoint)
888
774
  custom_routes = []
889
775
  token_route_found = False
890
776
 
@@ -972,6 +858,7 @@ class OAuthProxy(OAuthProvider):
972
858
  oauth_client = AsyncOAuth2Client(
973
859
  client_id=self._upstream_client_id,
974
860
  client_secret=self._upstream_client_secret.get_secret_value(),
861
+ token_endpoint_auth_method=self._token_endpoint_auth_method,
975
862
  timeout=HTTP_TIMEOUT_SECONDS,
976
863
  )
977
864
 
@@ -983,14 +870,28 @@ class OAuthProxy(OAuthProvider):
983
870
  f"Exchanging IdP code for tokens with redirect_uri: {idp_redirect_uri}"
984
871
  )
985
872
 
986
- idp_tokens: dict[str, Any] = await oauth_client.fetch_token( # type: ignore[misc]
987
- url=self._upstream_token_endpoint,
988
- code=idp_code,
989
- redirect_uri=idp_redirect_uri,
990
- )
873
+ # Include proxy's code_verifier if we forwarded PKCE
874
+ proxy_code_verifier = transaction.get("proxy_code_verifier")
875
+ if proxy_code_verifier:
876
+ logger.debug(
877
+ "Including proxy code_verifier in token exchange for transaction %s",
878
+ txn_id,
879
+ )
880
+ idp_tokens: dict[str, Any] = await oauth_client.fetch_token( # type: ignore[misc]
881
+ url=self._upstream_token_endpoint,
882
+ code=idp_code,
883
+ redirect_uri=idp_redirect_uri,
884
+ code_verifier=proxy_code_verifier,
885
+ )
886
+ else:
887
+ idp_tokens: dict[str, Any] = await oauth_client.fetch_token( # type: ignore[misc]
888
+ url=self._upstream_token_endpoint,
889
+ code=idp_code,
890
+ redirect_uri=idp_redirect_uri,
891
+ )
991
892
 
992
893
  logger.debug(
993
- f"Successfully exchanged IdP code for tokens (transaction: {txn_id})"
894
+ f"Successfully exchanged IdP code for tokens (transaction: {txn_id}, PKCE: {bool(proxy_code_verifier)})"
994
895
  )
995
896
 
996
897
  except Exception as e:
@@ -12,7 +12,6 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
12
12
 
13
13
  from fastmcp.server.auth import AccessToken, TokenVerifier
14
14
  from fastmcp.server.auth.oauth_proxy import OAuthProxy
15
- from fastmcp.server.auth.registry import register_provider
16
15
  from fastmcp.utilities.auth import parse_scopes
17
16
  from fastmcp.utilities.logging import get_logger
18
17
  from fastmcp.utilities.types import NotSet, NotSetT
@@ -36,7 +35,6 @@ class AzureProviderSettings(BaseSettings):
36
35
  redirect_path: str | None = None
37
36
  required_scopes: list[str] | None = None
38
37
  timeout_seconds: int | None = None
39
- resource_server_url: str | None = None
40
38
  allowed_client_redirect_uris: list[str] | None = None
41
39
 
42
40
  @field_validator("required_scopes", mode="before")
@@ -116,7 +114,6 @@ class AzureTokenVerifier(TokenVerifier):
116
114
  return None
117
115
 
118
116
 
119
- @register_provider("AZURE")
120
117
  class AzureProvider(OAuthProxy):
121
118
  """Azure (Microsoft Entra) OAuth provider for FastMCP.
122
119
 
@@ -162,7 +159,6 @@ class AzureProvider(OAuthProxy):
162
159
  redirect_path: str | NotSetT = NotSet,
163
160
  required_scopes: list[str] | None | NotSetT = NotSet,
164
161
  timeout_seconds: int | NotSetT = NotSet,
165
- resource_server_url: str | NotSetT = NotSet,
166
162
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
167
163
  ):
168
164
  """Initialize Azure OAuth provider.
@@ -175,8 +171,6 @@ class AzureProvider(OAuthProxy):
175
171
  redirect_path: Redirect path configured in Azure (defaults to "/auth/callback")
176
172
  required_scopes: Required scopes (defaults to ["User.Read", "email", "openid", "profile"])
177
173
  timeout_seconds: HTTP request timeout for Azure API calls
178
- resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at
179
- a different path like {base_url}/mcp, specify it here for RFC 8707 compliance.
180
174
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
181
175
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
182
176
  """
@@ -191,7 +185,6 @@ class AzureProvider(OAuthProxy):
191
185
  "redirect_path": redirect_path,
192
186
  "required_scopes": required_scopes,
193
187
  "timeout_seconds": timeout_seconds,
194
- "resource_server_url": resource_server_url,
195
188
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
196
189
  }.items()
197
190
  if v is not NotSet
@@ -217,7 +210,7 @@ class AzureProvider(OAuthProxy):
217
210
 
218
211
  # Apply defaults
219
212
  tenant_id_final = settings.tenant_id
220
- base_url_final = settings.base_url or "http://localhost:8000"
213
+
221
214
  redirect_path_final = settings.redirect_path or "/auth/callback"
222
215
  timeout_seconds_final = settings.timeout_seconds or 10
223
216
  # Default scopes for Azure - User.Read gives us access to user info via Graph API
@@ -227,7 +220,6 @@ class AzureProvider(OAuthProxy):
227
220
  "openid",
228
221
  "profile",
229
222
  ]
230
- resource_server_url_final = settings.resource_server_url or base_url_final
231
223
  allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
232
224
 
233
225
  # Extract secret string from SecretStr
@@ -256,11 +248,10 @@ class AzureProvider(OAuthProxy):
256
248
  upstream_client_id=settings.client_id,
257
249
  upstream_client_secret=client_secret_str,
258
250
  token_verifier=token_verifier,
259
- base_url=base_url_final,
251
+ base_url=settings.base_url,
260
252
  redirect_path=redirect_path_final,
261
- issuer_url=base_url_final,
253
+ issuer_url=settings.base_url,
262
254
  allowed_client_redirect_uris=allowed_client_redirect_uris_final,
263
- resource_server_url=resource_server_url_final,
264
255
  )
265
256
 
266
257
  logger.info(
@@ -28,7 +28,6 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
28
28
  from fastmcp.server.auth import TokenVerifier
29
29
  from fastmcp.server.auth.auth import AccessToken
30
30
  from fastmcp.server.auth.oauth_proxy import OAuthProxy
31
- from fastmcp.server.auth.registry import register_provider
32
31
  from fastmcp.utilities.auth import parse_scopes
33
32
  from fastmcp.utilities.logging import get_logger
34
33
  from fastmcp.utilities.types import NotSet, NotSetT
@@ -51,7 +50,6 @@ class GitHubProviderSettings(BaseSettings):
51
50
  redirect_path: str | None = None
52
51
  required_scopes: list[str] | None = None
53
52
  timeout_seconds: int | None = None
54
- resource_server_url: AnyHttpUrl | str | None = None
55
53
  allowed_client_redirect_uris: list[str] | None = None
56
54
 
57
55
  @field_validator("required_scopes", mode="before")
@@ -165,7 +163,6 @@ class GitHubTokenVerifier(TokenVerifier):
165
163
  return None
166
164
 
167
165
 
168
- @register_provider("GitHub")
169
166
  class GitHubProvider(OAuthProxy):
170
167
  """Complete GitHub OAuth provider for FastMCP.
171
168
 
@@ -187,7 +184,7 @@ class GitHubProvider(OAuthProxy):
187
184
  auth = GitHubProvider(
188
185
  client_id="Ov23li...",
189
186
  client_secret="abc123...",
190
- base_url="https://my-server.com" # Optional, defaults to http://localhost:8000
187
+ base_url="https://my-server.com"
191
188
  )
192
189
 
193
190
  mcp = FastMCP("My App", auth=auth)
@@ -203,7 +200,6 @@ class GitHubProvider(OAuthProxy):
203
200
  redirect_path: str | NotSetT = NotSet,
204
201
  required_scopes: list[str] | NotSetT = NotSet,
205
202
  timeout_seconds: int | NotSetT = NotSet,
206
- resource_server_url: AnyHttpUrl | str | NotSetT = NotSet,
207
203
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
208
204
  ):
209
205
  """Initialize GitHub OAuth provider.
@@ -215,11 +211,10 @@ class GitHubProvider(OAuthProxy):
215
211
  redirect_path: Redirect path configured in GitHub OAuth app (defaults to "/auth/callback")
216
212
  required_scopes: Required GitHub scopes (defaults to ["user"])
217
213
  timeout_seconds: HTTP request timeout for GitHub API calls
218
- resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at
219
- a different path like {base_url}/mcp, specify it here for RFC 8707 compliance.
220
214
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
221
215
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
222
216
  """
217
+
223
218
  settings = GitHubProviderSettings.model_validate(
224
219
  {
225
220
  k: v
@@ -230,7 +225,6 @@ class GitHubProvider(OAuthProxy):
230
225
  "redirect_path": redirect_path,
231
226
  "required_scopes": required_scopes,
232
227
  "timeout_seconds": timeout_seconds,
233
- "resource_server_url": resource_server_url,
234
228
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
235
229
  }.items()
236
230
  if v is not NotSet
@@ -248,11 +242,10 @@ class GitHubProvider(OAuthProxy):
248
242
  )
249
243
 
250
244
  # Apply defaults
251
- base_url_final = settings.base_url or "http://localhost:8000"
245
+
252
246
  redirect_path_final = settings.redirect_path or "/auth/callback"
253
247
  timeout_seconds_final = settings.timeout_seconds or 10
254
248
  required_scopes_final = settings.required_scopes or ["user"]
255
- resource_server_url_final = settings.resource_server_url or base_url_final
256
249
  allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
257
250
 
258
251
  # Create GitHub token verifier
@@ -273,11 +266,10 @@ class GitHubProvider(OAuthProxy):
273
266
  upstream_client_id=settings.client_id,
274
267
  upstream_client_secret=client_secret_str,
275
268
  token_verifier=token_verifier,
276
- base_url=base_url_final,
269
+ base_url=settings.base_url,
277
270
  redirect_path=redirect_path_final,
278
- issuer_url=base_url_final, # We act as the issuer for client registration
271
+ issuer_url=settings.base_url, # We act as the issuer for client registration
279
272
  allowed_client_redirect_uris=allowed_client_redirect_uris_final,
280
- resource_server_url=resource_server_url_final,
281
273
  )
282
274
 
283
275
  logger.info(