fastmcp 2.12.4__py3-none-any.whl → 2.13.0__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.
Files changed (72) hide show
  1. fastmcp/cli/cli.py +7 -6
  2. fastmcp/cli/install/claude_code.py +6 -6
  3. fastmcp/cli/install/claude_desktop.py +3 -3
  4. fastmcp/cli/install/cursor.py +7 -7
  5. fastmcp/cli/install/gemini_cli.py +3 -3
  6. fastmcp/cli/install/mcp_json.py +3 -3
  7. fastmcp/cli/run.py +13 -8
  8. fastmcp/client/auth/oauth.py +100 -208
  9. fastmcp/client/client.py +11 -11
  10. fastmcp/client/logging.py +18 -14
  11. fastmcp/client/oauth_callback.py +85 -171
  12. fastmcp/client/transports.py +77 -22
  13. fastmcp/contrib/component_manager/component_service.py +6 -6
  14. fastmcp/contrib/mcp_mixin/README.md +32 -1
  15. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  16. fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
  17. fastmcp/experimental/utilities/openapi/parser.py +23 -3
  18. fastmcp/prompts/prompt.py +13 -6
  19. fastmcp/prompts/prompt_manager.py +16 -101
  20. fastmcp/resources/resource.py +13 -6
  21. fastmcp/resources/resource_manager.py +5 -164
  22. fastmcp/resources/template.py +107 -17
  23. fastmcp/resources/types.py +30 -24
  24. fastmcp/server/auth/auth.py +40 -32
  25. fastmcp/server/auth/handlers/authorize.py +324 -0
  26. fastmcp/server/auth/jwt_issuer.py +236 -0
  27. fastmcp/server/auth/middleware.py +96 -0
  28. fastmcp/server/auth/oauth_proxy.py +1256 -242
  29. fastmcp/server/auth/oidc_proxy.py +23 -6
  30. fastmcp/server/auth/providers/auth0.py +40 -21
  31. fastmcp/server/auth/providers/aws.py +29 -3
  32. fastmcp/server/auth/providers/azure.py +178 -127
  33. fastmcp/server/auth/providers/descope.py +4 -6
  34. fastmcp/server/auth/providers/github.py +29 -8
  35. fastmcp/server/auth/providers/google.py +30 -9
  36. fastmcp/server/auth/providers/introspection.py +281 -0
  37. fastmcp/server/auth/providers/jwt.py +8 -2
  38. fastmcp/server/auth/providers/scalekit.py +179 -0
  39. fastmcp/server/auth/providers/supabase.py +172 -0
  40. fastmcp/server/auth/providers/workos.py +32 -14
  41. fastmcp/server/context.py +122 -36
  42. fastmcp/server/http.py +58 -18
  43. fastmcp/server/low_level.py +121 -2
  44. fastmcp/server/middleware/caching.py +469 -0
  45. fastmcp/server/middleware/error_handling.py +6 -2
  46. fastmcp/server/middleware/logging.py +48 -37
  47. fastmcp/server/middleware/middleware.py +28 -15
  48. fastmcp/server/middleware/rate_limiting.py +3 -3
  49. fastmcp/server/middleware/tool_injection.py +116 -0
  50. fastmcp/server/proxy.py +6 -6
  51. fastmcp/server/server.py +683 -207
  52. fastmcp/settings.py +24 -10
  53. fastmcp/tools/tool.py +7 -3
  54. fastmcp/tools/tool_manager.py +30 -112
  55. fastmcp/tools/tool_transform.py +3 -3
  56. fastmcp/utilities/cli.py +62 -22
  57. fastmcp/utilities/components.py +5 -0
  58. fastmcp/utilities/inspect.py +77 -21
  59. fastmcp/utilities/logging.py +118 -8
  60. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  61. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  62. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  63. fastmcp/utilities/tests.py +87 -4
  64. fastmcp/utilities/types.py +1 -1
  65. fastmcp/utilities/ui.py +617 -0
  66. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/METADATA +10 -6
  67. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/RECORD +70 -63
  68. fastmcp/cli/claude.py +0 -135
  69. fastmcp/utilities/storage.py +0 -204
  70. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/WHEEL +0 -0
  71. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/entry_points.txt +0 -0
  72. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,17 +6,23 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- import httpx
9
+ from typing import TYPE_CHECKING
10
+
11
+ from key_value.aio.protocols import AsyncKeyValue
10
12
  from pydantic import SecretStr, field_validator
11
13
  from pydantic_settings import BaseSettings, SettingsConfigDict
12
14
 
13
- from fastmcp.server.auth import AccessToken, TokenVerifier
14
15
  from fastmcp.server.auth.oauth_proxy import OAuthProxy
16
+ from fastmcp.server.auth.providers.jwt import JWTVerifier
17
+ from fastmcp.settings import ENV_FILE
15
18
  from fastmcp.utilities.auth import parse_scopes
16
19
  from fastmcp.utilities.logging import get_logger
17
- from fastmcp.utilities.storage import KVStorage
18
20
  from fastmcp.utilities.types import NotSet, NotSetT
19
21
 
22
+ if TYPE_CHECKING:
23
+ from mcp.server.auth.provider import AuthorizationParams
24
+ from mcp.shared.auth import OAuthClientInformationFull
25
+
20
26
  logger = get_logger(__name__)
21
27
 
22
28
 
@@ -25,94 +31,31 @@ class AzureProviderSettings(BaseSettings):
25
31
 
26
32
  model_config = SettingsConfigDict(
27
33
  env_prefix="FASTMCP_SERVER_AUTH_AZURE_",
28
- env_file=".env",
34
+ env_file=ENV_FILE,
29
35
  extra="ignore",
30
36
  )
31
37
 
32
38
  client_id: str | None = None
33
39
  client_secret: SecretStr | None = None
34
40
  tenant_id: str | None = None
41
+ identifier_uri: str | None = None
35
42
  base_url: str | None = None
43
+ issuer_url: str | None = None
36
44
  redirect_path: str | None = None
37
45
  required_scopes: list[str] | None = None
38
- timeout_seconds: int | None = None
46
+ additional_authorize_scopes: list[str] | None = None
39
47
  allowed_client_redirect_uris: list[str] | None = None
48
+ jwt_signing_key: str | None = None
40
49
 
41
50
  @field_validator("required_scopes", mode="before")
42
51
  @classmethod
43
- def _parse_scopes(cls, v):
52
+ def _parse_scopes(cls, v: object) -> list[str] | None:
44
53
  return parse_scopes(v)
45
54
 
46
-
47
- class AzureTokenVerifier(TokenVerifier):
48
- """Token verifier for Azure OAuth tokens.
49
-
50
- Azure tokens are JWTs, but we verify them by calling the Microsoft Graph API
51
- to get user information and validate the token.
52
- """
53
-
54
- def __init__(
55
- self,
56
- *,
57
- required_scopes: list[str] | None = None,
58
- timeout_seconds: int = 10,
59
- ):
60
- """Initialize the Azure token verifier.
61
-
62
- Args:
63
- required_scopes: Required OAuth scopes
64
- timeout_seconds: HTTP request timeout
65
- """
66
- super().__init__(required_scopes=required_scopes)
67
- self.timeout_seconds = timeout_seconds
68
-
69
- async def verify_token(self, token: str) -> AccessToken | None:
70
- """Verify Azure OAuth token by calling Microsoft Graph API."""
71
- try:
72
- async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
73
- # Use Microsoft Graph API to validate token and get user info
74
- response = await client.get(
75
- "https://graph.microsoft.com/v1.0/me",
76
- headers={
77
- "Authorization": f"Bearer {token}",
78
- "User-Agent": "FastMCP-Azure-OAuth",
79
- },
80
- )
81
-
82
- if response.status_code != 200:
83
- logger.debug(
84
- "Azure token verification failed: %d - %s",
85
- response.status_code,
86
- response.text[:200],
87
- )
88
- return None
89
-
90
- user_data = response.json()
91
-
92
- # Create AccessToken with Azure user info
93
- return AccessToken(
94
- token=token,
95
- client_id=str(user_data.get("id", "unknown")),
96
- scopes=self.required_scopes or [],
97
- expires_at=None,
98
- claims={
99
- "sub": user_data.get("id"),
100
- "email": user_data.get("mail")
101
- or user_data.get("userPrincipalName"),
102
- "name": user_data.get("displayName"),
103
- "given_name": user_data.get("givenName"),
104
- "family_name": user_data.get("surname"),
105
- "job_title": user_data.get("jobTitle"),
106
- "office_location": user_data.get("officeLocation"),
107
- },
108
- )
109
-
110
- except httpx.RequestError as e:
111
- logger.debug("Failed to verify Azure token: %s", e)
112
- return None
113
- except Exception as e:
114
- logger.debug("Azure token verification error: %s", e)
115
- return None
55
+ @field_validator("additional_authorize_scopes", mode="before")
56
+ @classmethod
57
+ def _parse_additional_authorize_scopes(cls, v: object) -> list[str] | None:
58
+ return parse_scopes(v)
116
59
 
117
60
 
118
61
  class AzureProvider(OAuthProxy):
@@ -122,17 +65,28 @@ class AzureProvider(OAuthProxy):
122
65
  OAuth Proxy pattern. It supports both organizational accounts and personal
123
66
  Microsoft accounts depending on the tenant configuration.
124
67
 
125
- Features:
126
- - Transparent OAuth proxy to Azure/Microsoft identity platform
127
- - Automatic token validation via Microsoft Graph API
128
- - User information extraction
129
- - Support for different tenant configurations (common, organizations, consumers)
68
+ Scope Handling:
69
+ - required_scopes: Provide unprefixed scope names (e.g., ["read", "write"])
70
+ Automatically prefixed with identifier_uri during initialization
71
+ Validated on all tokens and advertised to MCP clients
72
+ - additional_authorize_scopes: Provide full format (e.g., ["User.Read"])
73
+ → NOT prefixed, NOT validated, NOT advertised to clients
74
+ → Used to request Microsoft Graph or other upstream API permissions
130
75
 
131
- Setup Requirements:
132
- 1. Register an application in Azure Portal (portal.azure.com)
133
- 2. Configure redirect URI as: http://localhost:8000/auth/callback
134
- 3. Note your Application (client) ID and create a client secret
135
- 4. Optionally note your Directory (tenant) ID for single-tenant apps
76
+ Features:
77
+ - OAuth proxy to Azure/Microsoft identity platform
78
+ - JWT validation using tenant issuer and JWKS
79
+ - Supports tenant configurations: specific tenant ID, "organizations", or "consumers"
80
+ - Custom API scopes and Microsoft Graph scopes in a single provider
81
+
82
+ Setup:
83
+ 1. Create an App registration in Azure Portal
84
+ 2. Configure Web platform redirect URI: http://localhost:8000/auth/callback (or your custom path)
85
+ 3. Add an Application ID URI under "Expose an API" (defaults to api://{client_id})
86
+ 4. Add custom scopes (e.g., "read", "write") under "Expose an API"
87
+ 5. Set access token version to 2 in the App manifest: "requestedAccessTokenVersion": 2
88
+ 6. Create a client secret
89
+ 7. Get Application (client) ID, Directory (tenant) ID, and client secret
136
90
 
137
91
  Example:
138
92
  ```python
@@ -142,8 +96,11 @@ class AzureProvider(OAuthProxy):
142
96
  auth = AzureProvider(
143
97
  client_id="your-client-id",
144
98
  client_secret="your-client-secret",
145
- tenant_id="your-tenant-id", # Required: your Azure tenant ID from Azure Portal
146
- base_url="http://localhost:8000"
99
+ tenant_id="your-tenant-id",
100
+ required_scopes=["read", "write"], # Unprefixed scope names
101
+ additional_authorize_scopes=["User.Read", "Mail.Read"], # Optional Graph scopes
102
+ base_url="http://localhost:8000",
103
+ # identifier_uri defaults to api://{client_id}
147
104
  )
148
105
 
149
106
  mcp = FastMCP("My App", auth=auth)
@@ -156,27 +113,57 @@ class AzureProvider(OAuthProxy):
156
113
  client_id: str | NotSetT = NotSet,
157
114
  client_secret: str | NotSetT = NotSet,
158
115
  tenant_id: str | NotSetT = NotSet,
116
+ identifier_uri: str | None | NotSetT = NotSet,
159
117
  base_url: str | NotSetT = NotSet,
118
+ issuer_url: str | NotSetT = NotSet,
160
119
  redirect_path: str | NotSetT = NotSet,
161
120
  required_scopes: list[str] | None | NotSetT = NotSet,
162
- timeout_seconds: int | NotSetT = NotSet,
121
+ additional_authorize_scopes: list[str] | None | NotSetT = NotSet,
163
122
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
164
- client_storage: KVStorage | None = None,
165
- ):
123
+ client_storage: AsyncKeyValue | None = None,
124
+ jwt_signing_key: str | bytes | NotSetT = NotSet,
125
+ require_authorization_consent: bool = True,
126
+ ) -> None:
166
127
  """Initialize Azure OAuth provider.
167
128
 
168
129
  Args:
169
- client_id: Azure application (client) ID
170
- client_secret: Azure client secret
171
- tenant_id: Azure tenant ID (your specific tenant ID, "organizations", or "consumers")
172
- base_url: Public URL of your FastMCP server (for OAuth callbacks)
173
- redirect_path: Redirect path configured in Azure (defaults to "/auth/callback")
174
- required_scopes: Required scopes (defaults to ["User.Read", "email", "openid", "profile"])
175
- timeout_seconds: HTTP request timeout for Azure API calls
130
+ client_id: Azure application (client) ID from your App registration
131
+ client_secret: Azure client secret from your App registration
132
+ tenant_id: Azure tenant ID (specific tenant GUID, "organizations", or "consumers")
133
+ identifier_uri: Optional Application ID URI for your custom API (defaults to api://{client_id}).
134
+ This URI is automatically prefixed to all required_scopes during initialization.
135
+ Example: identifier_uri="api://my-api" + required_scopes=["read"]
136
+ tokens validated for "api://my-api/read"
137
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
138
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
139
+ to avoid 404s during discovery when mounting under a path.
140
+ redirect_path: Redirect path configured in Azure App registration (defaults to "/auth/callback")
141
+ required_scopes: Custom API scope names WITHOUT prefix (e.g., ["read", "write"]).
142
+ - Automatically prefixed with identifier_uri during initialization
143
+ - Validated on all tokens
144
+ - Advertised in Protected Resource Metadata
145
+ - Must match scope names defined in Azure Portal under "Expose an API"
146
+ Example: ["read", "write"] → validates tokens containing ["api://xxx/read", "api://xxx/write"]
147
+ additional_authorize_scopes: Microsoft Graph or other upstream scopes in full format.
148
+ - NOT prefixed with identifier_uri
149
+ - NOT validated on tokens
150
+ - NOT advertised to MCP clients
151
+ - Used to request additional permissions from Azure (e.g., Graph API access)
152
+ Example: ["User.Read", "Mail.Read", "offline_access"]
153
+ These scopes allow your FastMCP server to call Microsoft Graph APIs using the
154
+ upstream Azure token, but MCP clients are unaware of them.
176
155
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
177
156
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
178
- client_storage: Storage implementation for OAuth client registrations.
179
- Defaults to file-based storage if not specified.
157
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
158
+ If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
159
+ disk store will be encrypted using a key derived from the JWT Signing Key.
160
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
161
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
162
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
163
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
164
+ When True, users see a consent screen before being redirected to Azure.
165
+ When False, authorization proceeds directly without user confirmation.
166
+ SECURITY WARNING: Only disable for local development or testing environments.
180
167
  """
181
168
  settings = AzureProviderSettings.model_validate(
182
169
  {
@@ -185,11 +172,14 @@ class AzureProvider(OAuthProxy):
185
172
  "client_id": client_id,
186
173
  "client_secret": client_secret,
187
174
  "tenant_id": tenant_id,
175
+ "identifier_uri": identifier_uri,
188
176
  "base_url": base_url,
177
+ "issuer_url": issuer_url,
189
178
  "redirect_path": redirect_path,
190
179
  "required_scopes": required_scopes,
191
- "timeout_seconds": timeout_seconds,
180
+ "additional_authorize_scopes": additional_authorize_scopes,
192
181
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
182
+ "jwt_signing_key": jwt_signing_key,
193
183
  }.items()
194
184
  if v is not NotSet
195
185
  }
@@ -197,45 +187,54 @@ class AzureProvider(OAuthProxy):
197
187
 
198
188
  # Validate required settings
199
189
  if not settings.client_id:
200
- raise ValueError(
201
- "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"
202
- )
190
+ msg = "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"
191
+ raise ValueError(msg)
203
192
  if not settings.client_secret:
204
- raise ValueError(
205
- "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET"
206
- )
193
+ msg = "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET"
194
+ raise ValueError(msg)
207
195
 
208
196
  # Validate tenant_id is provided
209
197
  if not settings.tenant_id:
210
- raise ValueError(
211
- "tenant_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_TENANT_ID. "
212
- "Use your Azure tenant ID (found in Azure Portal), 'organizations', or 'consumers'"
198
+ msg = (
199
+ "tenant_id is required - set via parameter or "
200
+ "FASTMCP_SERVER_AUTH_AZURE_TENANT_ID. Use your Azure tenant ID "
201
+ "(found in Azure Portal), 'organizations', or 'consumers'"
213
202
  )
203
+ raise ValueError(msg)
204
+
205
+ if not settings.required_scopes:
206
+ raise ValueError("required_scopes is required")
214
207
 
215
208
  # Apply defaults
209
+ self.identifier_uri = settings.identifier_uri or f"api://{settings.client_id}"
210
+ self.additional_authorize_scopes = settings.additional_authorize_scopes or []
216
211
  tenant_id_final = settings.tenant_id
217
212
 
218
- timeout_seconds_final = settings.timeout_seconds or 10
219
- # Default scopes for Azure - User.Read gives us access to user info via Graph API
220
- scopes_final = settings.required_scopes or [
221
- "User.Read",
222
- "email",
223
- "openid",
224
- "profile",
213
+ # Prefix required scopes with identifier_uri for Azure
214
+ # Azure returns scopes as full URIs (e.g., "api://xxx/read") in tokens
215
+ prefixed_required_scopes = [
216
+ f"{self.identifier_uri}/{scope}" for scope in settings.required_scopes
225
217
  ]
226
- allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
218
+
219
+ # Always validate tokens against the app's API client ID using JWT
220
+ issuer = f"https://login.microsoftonline.com/{tenant_id_final}/v2.0"
221
+ jwks_uri = (
222
+ f"https://login.microsoftonline.com/{tenant_id_final}/discovery/v2.0/keys"
223
+ )
224
+
225
+ token_verifier = JWTVerifier(
226
+ jwks_uri=jwks_uri,
227
+ issuer=issuer,
228
+ audience=settings.client_id,
229
+ algorithm="RS256",
230
+ required_scopes=prefixed_required_scopes,
231
+ )
227
232
 
228
233
  # Extract secret string from SecretStr
229
234
  client_secret_str = (
230
235
  settings.client_secret.get_secret_value() if settings.client_secret else ""
231
236
  )
232
237
 
233
- # Create Azure token verifier
234
- token_verifier = AzureTokenVerifier(
235
- required_scopes=scopes_final,
236
- timeout_seconds=timeout_seconds_final,
237
- )
238
-
239
238
  # Build Azure OAuth endpoints with tenant
240
239
  authorization_endpoint = (
241
240
  f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/authorize"
@@ -253,13 +252,65 @@ class AzureProvider(OAuthProxy):
253
252
  token_verifier=token_verifier,
254
253
  base_url=settings.base_url,
255
254
  redirect_path=settings.redirect_path,
256
- issuer_url=settings.base_url,
257
- allowed_client_redirect_uris=allowed_client_redirect_uris_final,
255
+ issuer_url=settings.issuer_url
256
+ or settings.base_url, # Default to base_url if not specified
257
+ allowed_client_redirect_uris=settings.allowed_client_redirect_uris,
258
258
  client_storage=client_storage,
259
+ jwt_signing_key=settings.jwt_signing_key,
260
+ require_authorization_consent=require_authorization_consent,
259
261
  )
260
262
 
261
263
  logger.info(
262
- "Initialized Azure OAuth provider for client %s with tenant %s",
264
+ "Initialized Azure OAuth provider for client %s with tenant %s%s",
263
265
  settings.client_id,
264
266
  tenant_id_final,
267
+ f" and identifier_uri {self.identifier_uri}" if self.identifier_uri else "",
265
268
  )
269
+
270
+ async def authorize(
271
+ self,
272
+ client: OAuthClientInformationFull,
273
+ params: AuthorizationParams,
274
+ ) -> str:
275
+ """Start OAuth transaction and redirect to Azure AD.
276
+
277
+ Override parent's authorize method to filter out the 'resource' parameter
278
+ which is not supported by Azure AD v2.0 endpoints. The v2.0 endpoints use
279
+ scopes to determine the resource/audience instead of a separate parameter.
280
+
281
+ Args:
282
+ client: OAuth client information
283
+ params: Authorization parameters from the client
284
+
285
+ Returns:
286
+ Authorization URL to redirect the user to Azure AD
287
+ """
288
+ # Clear the resource parameter that Azure AD v2.0 doesn't support
289
+ # This parameter comes from RFC 8707 (OAuth 2.0 Resource Indicators)
290
+ # but Azure AD v2.0 uses scopes instead to determine the audience
291
+ params_to_use = params
292
+ if hasattr(params, "resource"):
293
+ original_resource = getattr(params, "resource", None)
294
+ if original_resource is not None:
295
+ params_to_use = params.model_copy(update={"resource": None})
296
+ if original_resource:
297
+ logger.debug(
298
+ "Filtering out 'resource' parameter '%s' for Azure AD v2.0 (use scopes instead)",
299
+ original_resource,
300
+ )
301
+ # Scopes are already prefixed:
302
+ # - self.required_scopes was prefixed during __init__
303
+ # - Client scopes come from PRM which advertises prefixed scopes
304
+ scopes = params_to_use.scopes or self.required_scopes
305
+
306
+ final_scopes = list(scopes)
307
+ # Add Microsoft Graph scopes separately - these use shorthand format (e.g., "User.Read")
308
+ # and should not be prefixed with identifier_uri. Azure returns them as-is in tokens.
309
+ if self.additional_authorize_scopes:
310
+ final_scopes.extend(self.additional_authorize_scopes)
311
+
312
+ modified_params = params_to_use.model_copy(update={"scopes": final_scopes})
313
+
314
+ auth_url = await super().authorize(client, modified_params)
315
+ separator = "&" if "?" in auth_url else "?"
316
+ return f"{auth_url}{separator}prompt=select_account"
@@ -7,8 +7,6 @@ for seamless MCP client authentication.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from typing import Any
11
-
12
10
  import httpx
13
11
  from pydantic import AnyHttpUrl
14
12
  from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -17,6 +15,7 @@ from starlette.routing import Route
17
15
 
18
16
  from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
19
17
  from fastmcp.server.auth.providers.jwt import JWTVerifier
18
+ from fastmcp.settings import ENV_FILE
20
19
  from fastmcp.utilities.logging import get_logger
21
20
  from fastmcp.utilities.types import NotSet, NotSetT
22
21
 
@@ -26,7 +25,7 @@ logger = get_logger(__name__)
26
25
  class DescopeProviderSettings(BaseSettings):
27
26
  model_config = SettingsConfigDict(
28
27
  env_prefix="FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_",
29
- env_file=".env",
28
+ env_file=ENV_FILE,
30
29
  extra="ignore",
31
30
  )
32
31
 
@@ -127,7 +126,6 @@ class DescopeProvider(RemoteAuthProvider):
127
126
  def get_routes(
128
127
  self,
129
128
  mcp_path: str | None = None,
130
- mcp_endpoint: Any | None = None,
131
129
  ) -> list[Route]:
132
130
  """Get OAuth routes including Descope authorization server metadata forwarding.
133
131
 
@@ -136,10 +134,10 @@ class DescopeProvider(RemoteAuthProvider):
136
134
 
137
135
  Args:
138
136
  mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
139
- mcp_endpoint: The MCP endpoint handler to protect with auth
137
+ This is used to advertise the resource URL in metadata.
140
138
  """
141
139
  # Get the standard protected resource routes from RemoteAuthProvider
142
- routes = super().get_routes(mcp_path, mcp_endpoint)
140
+ routes = super().get_routes(mcp_path)
143
141
 
144
142
  async def oauth_authorization_server_metadata(request):
145
143
  """Forward Descope OAuth authorization server metadata with FastMCP customizations."""
@@ -22,15 +22,16 @@ Example:
22
22
  from __future__ import annotations
23
23
 
24
24
  import httpx
25
+ from key_value.aio.protocols import AsyncKeyValue
25
26
  from pydantic import AnyHttpUrl, SecretStr, field_validator
26
27
  from pydantic_settings import BaseSettings, SettingsConfigDict
27
28
 
28
29
  from fastmcp.server.auth import TokenVerifier
29
30
  from fastmcp.server.auth.auth import AccessToken
30
31
  from fastmcp.server.auth.oauth_proxy import OAuthProxy
32
+ from fastmcp.settings import ENV_FILE
31
33
  from fastmcp.utilities.auth import parse_scopes
32
34
  from fastmcp.utilities.logging import get_logger
33
- from fastmcp.utilities.storage import KVStorage
34
35
  from fastmcp.utilities.types import NotSet, NotSetT
35
36
 
36
37
  logger = get_logger(__name__)
@@ -41,17 +42,19 @@ class GitHubProviderSettings(BaseSettings):
41
42
 
42
43
  model_config = SettingsConfigDict(
43
44
  env_prefix="FASTMCP_SERVER_AUTH_GITHUB_",
44
- env_file=".env",
45
+ env_file=ENV_FILE,
45
46
  extra="ignore",
46
47
  )
47
48
 
48
49
  client_id: str | None = None
49
50
  client_secret: SecretStr | None = None
50
51
  base_url: AnyHttpUrl | str | None = None
52
+ issuer_url: AnyHttpUrl | str | None = None
51
53
  redirect_path: str | None = None
52
54
  required_scopes: list[str] | None = None
53
55
  timeout_seconds: int | None = None
54
56
  allowed_client_redirect_uris: list[str] | None = None
57
+ jwt_signing_key: str | None = None
55
58
 
56
59
  @field_validator("required_scopes", mode="before")
57
60
  @classmethod
@@ -198,25 +201,38 @@ class GitHubProvider(OAuthProxy):
198
201
  client_id: str | NotSetT = NotSet,
199
202
  client_secret: str | NotSetT = NotSet,
200
203
  base_url: AnyHttpUrl | str | NotSetT = NotSet,
204
+ issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
201
205
  redirect_path: str | NotSetT = NotSet,
202
206
  required_scopes: list[str] | NotSetT = NotSet,
203
207
  timeout_seconds: int | NotSetT = NotSet,
204
208
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
205
- client_storage: KVStorage | None = None,
209
+ client_storage: AsyncKeyValue | None = None,
210
+ jwt_signing_key: str | bytes | NotSetT = NotSet,
211
+ require_authorization_consent: bool = True,
206
212
  ):
207
213
  """Initialize GitHub OAuth provider.
208
214
 
209
215
  Args:
210
216
  client_id: GitHub OAuth app client ID (e.g., "Ov23li...")
211
217
  client_secret: GitHub OAuth app client secret
212
- base_url: Public URL of your FastMCP server (for OAuth callbacks)
218
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
219
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
220
+ to avoid 404s during discovery when mounting under a path.
213
221
  redirect_path: Redirect path configured in GitHub OAuth app (defaults to "/auth/callback")
214
222
  required_scopes: Required GitHub scopes (defaults to ["user"])
215
223
  timeout_seconds: HTTP request timeout for GitHub API calls
216
224
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
217
225
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
218
- client_storage: Storage implementation for OAuth client registrations.
219
- Defaults to file-based storage if not specified.
226
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
227
+ If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
228
+ disk store will be encrypted using a key derived from the JWT Signing Key.
229
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
230
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
231
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
232
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
233
+ When True, users see a consent screen before being redirected to GitHub.
234
+ When False, authorization proceeds directly without user confirmation.
235
+ SECURITY WARNING: Only disable for local development or testing environments.
220
236
  """
221
237
 
222
238
  settings = GitHubProviderSettings.model_validate(
@@ -226,10 +242,12 @@ class GitHubProvider(OAuthProxy):
226
242
  "client_id": client_id,
227
243
  "client_secret": client_secret,
228
244
  "base_url": base_url,
245
+ "issuer_url": issuer_url,
229
246
  "redirect_path": redirect_path,
230
247
  "required_scopes": required_scopes,
231
248
  "timeout_seconds": timeout_seconds,
232
249
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
250
+ "jwt_signing_key": jwt_signing_key,
233
251
  }.items()
234
252
  if v is not NotSet
235
253
  }
@@ -271,12 +289,15 @@ class GitHubProvider(OAuthProxy):
271
289
  token_verifier=token_verifier,
272
290
  base_url=settings.base_url,
273
291
  redirect_path=settings.redirect_path,
274
- issuer_url=settings.base_url, # We act as the issuer for client registration
292
+ issuer_url=settings.issuer_url
293
+ or settings.base_url, # Default to base_url if not specified
275
294
  allowed_client_redirect_uris=allowed_client_redirect_uris_final,
276
295
  client_storage=client_storage,
296
+ jwt_signing_key=settings.jwt_signing_key,
297
+ require_authorization_consent=require_authorization_consent,
277
298
  )
278
299
 
279
- logger.info(
300
+ logger.debug(
280
301
  "Initialized GitHub OAuth provider for client %s with scopes: %s",
281
302
  settings.client_id,
282
303
  required_scopes_final,