fastmcp 2.13.0rc2__py3-none-any.whl → 2.13.0.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.
Files changed (81) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +3 -2
  3. fastmcp/cli/install/claude_code.py +3 -3
  4. fastmcp/client/__init__.py +9 -9
  5. fastmcp/client/auth/oauth.py +7 -6
  6. fastmcp/client/client.py +10 -10
  7. fastmcp/client/oauth_callback.py +6 -2
  8. fastmcp/client/sampling.py +1 -1
  9. fastmcp/client/transports.py +35 -34
  10. fastmcp/contrib/component_manager/__init__.py +1 -1
  11. fastmcp/contrib/component_manager/component_manager.py +2 -2
  12. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  13. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  14. fastmcp/experimental/server/openapi/__init__.py +5 -8
  15. fastmcp/experimental/server/openapi/components.py +11 -7
  16. fastmcp/experimental/server/openapi/routing.py +2 -2
  17. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  18. fastmcp/experimental/utilities/openapi/director.py +1 -1
  19. fastmcp/experimental/utilities/openapi/json_schema_converter.py +2 -2
  20. fastmcp/experimental/utilities/openapi/models.py +3 -3
  21. fastmcp/experimental/utilities/openapi/parser.py +3 -5
  22. fastmcp/experimental/utilities/openapi/schemas.py +2 -2
  23. fastmcp/mcp_config.py +2 -3
  24. fastmcp/prompts/__init__.py +1 -1
  25. fastmcp/prompts/prompt.py +9 -13
  26. fastmcp/resources/__init__.py +5 -5
  27. fastmcp/resources/resource.py +1 -3
  28. fastmcp/resources/resource_manager.py +1 -1
  29. fastmcp/resources/types.py +30 -24
  30. fastmcp/server/__init__.py +1 -1
  31. fastmcp/server/auth/__init__.py +5 -5
  32. fastmcp/server/auth/auth.py +2 -2
  33. fastmcp/server/auth/handlers/authorize.py +324 -0
  34. fastmcp/server/auth/jwt_issuer.py +39 -92
  35. fastmcp/server/auth/middleware.py +96 -0
  36. fastmcp/server/auth/oauth_proxy.py +236 -217
  37. fastmcp/server/auth/oidc_proxy.py +18 -3
  38. fastmcp/server/auth/providers/auth0.py +28 -15
  39. fastmcp/server/auth/providers/aws.py +16 -1
  40. fastmcp/server/auth/providers/azure.py +101 -40
  41. fastmcp/server/auth/providers/bearer.py +1 -1
  42. fastmcp/server/auth/providers/github.py +16 -1
  43. fastmcp/server/auth/providers/google.py +16 -1
  44. fastmcp/server/auth/providers/in_memory.py +2 -2
  45. fastmcp/server/auth/providers/introspection.py +2 -2
  46. fastmcp/server/auth/providers/jwt.py +17 -18
  47. fastmcp/server/auth/providers/supabase.py +1 -1
  48. fastmcp/server/auth/providers/workos.py +18 -3
  49. fastmcp/server/context.py +41 -12
  50. fastmcp/server/dependencies.py +5 -6
  51. fastmcp/server/elicitation.py +1 -1
  52. fastmcp/server/http.py +3 -4
  53. fastmcp/server/middleware/__init__.py +1 -1
  54. fastmcp/server/middleware/caching.py +1 -1
  55. fastmcp/server/middleware/error_handling.py +8 -8
  56. fastmcp/server/middleware/middleware.py +1 -1
  57. fastmcp/server/middleware/tool_injection.py +116 -0
  58. fastmcp/server/openapi.py +10 -6
  59. fastmcp/server/proxy.py +5 -4
  60. fastmcp/server/server.py +74 -55
  61. fastmcp/settings.py +2 -1
  62. fastmcp/tools/__init__.py +1 -1
  63. fastmcp/tools/tool.py +12 -12
  64. fastmcp/tools/tool_manager.py +8 -4
  65. fastmcp/tools/tool_transform.py +6 -6
  66. fastmcp/utilities/cli.py +50 -21
  67. fastmcp/utilities/inspect.py +2 -2
  68. fastmcp/utilities/json_schema_type.py +4 -4
  69. fastmcp/utilities/logging.py +14 -18
  70. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  71. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  72. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  73. fastmcp/utilities/openapi.py +9 -9
  74. fastmcp/utilities/tests.py +2 -4
  75. fastmcp/utilities/ui.py +126 -6
  76. {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/METADATA +5 -5
  77. fastmcp-2.13.0.1.dist-info/RECORD +141 -0
  78. fastmcp-2.13.0rc2.dist-info/RECORD +0 -138
  79. {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/WHEEL +0 -0
  80. {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/entry_points.txt +0 -0
  81. {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -123,10 +123,10 @@ class OIDCConfiguration(BaseModel):
123
123
 
124
124
  try:
125
125
  AnyHttpUrl(value)
126
- except Exception:
126
+ except Exception as e:
127
127
  message = f"Invalid URL for configuration metadata: {attr}"
128
128
  logger.error(message)
129
- raise ValueError(message)
129
+ raise ValueError(message) from e
130
130
 
131
131
  enforce("issuer", True)
132
132
  enforce("authorization_endpoint", True)
@@ -215,8 +215,12 @@ class OIDCProxy(OAuthProxy):
215
215
  # Client configuration
216
216
  allowed_client_redirect_uris: list[str] | None = None,
217
217
  client_storage: AsyncKeyValue | None = None,
218
+ # JWT and encryption keys
219
+ jwt_signing_key: str | bytes | None = None,
218
220
  # Token validation configuration
219
221
  token_endpoint_auth_method: str | None = None,
222
+ # Consent screen configuration
223
+ require_authorization_consent: bool = True,
220
224
  ) -> None:
221
225
  """Initialize the OIDC proxy provider.
222
226
 
@@ -238,10 +242,19 @@ class OIDCProxy(OAuthProxy):
238
242
  If None (default), only localhost redirect URIs are allowed.
239
243
  If empty list, all redirect URIs are allowed (not recommended for production).
240
244
  These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app.
241
- client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
245
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
246
+ If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
247
+ disk store will be encrypted using a key derived from the JWT Signing Key.
248
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
249
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
250
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
242
251
  token_endpoint_auth_method: Token endpoint authentication method for upstream server.
243
252
  Common values: "client_secret_basic", "client_secret_post", "none".
244
253
  If None, authlib will use its default (typically "client_secret_basic").
254
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
255
+ When True, users see a consent screen before being redirected to the upstream IdP.
256
+ When False, authorization proceeds directly without user confirmation.
257
+ SECURITY WARNING: Only disable for local development or testing environments.
245
258
  """
246
259
  if not config_url:
247
260
  raise ValueError("Missing required config URL")
@@ -295,7 +308,9 @@ class OIDCProxy(OAuthProxy):
295
308
  "service_documentation_url": self.oidc_config.service_documentation,
296
309
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
297
310
  "client_storage": client_storage,
311
+ "jwt_signing_key": jwt_signing_key,
298
312
  "token_endpoint_auth_method": token_endpoint_auth_method,
313
+ "require_authorization_consent": require_authorization_consent,
299
314
  }
300
315
 
301
316
  if redirect_path:
@@ -52,6 +52,7 @@ class Auth0ProviderSettings(BaseSettings):
52
52
  redirect_path: str | None = None
53
53
  required_scopes: list[str] | None = None
54
54
  allowed_client_redirect_uris: list[str] | None = None
55
+ jwt_signing_key: str | None = None
55
56
 
56
57
  @field_validator("required_scopes", mode="before")
57
58
  @classmethod
@@ -96,6 +97,8 @@ class Auth0Provider(OIDCProxy):
96
97
  redirect_path: str | NotSetT = NotSet,
97
98
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
98
99
  client_storage: AsyncKeyValue | None = None,
100
+ jwt_signing_key: str | bytes | NotSetT = NotSet,
101
+ require_authorization_consent: bool = True,
99
102
  ) -> None:
100
103
  """Initialize Auth0 OAuth provider.
101
104
 
@@ -111,7 +114,16 @@ class Auth0Provider(OIDCProxy):
111
114
  redirect_path: Redirect path configured in Auth0 application
112
115
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
113
116
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
114
- client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
117
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
118
+ If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
119
+ disk store will be encrypted using a key derived from the JWT Signing Key.
120
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
121
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
122
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
123
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
124
+ When True, users see a consent screen before being redirected to Auth0.
125
+ When False, authorization proceeds directly without user confirmation.
126
+ SECURITY WARNING: Only disable for local development or testing environments.
115
127
  """
116
128
  settings = Auth0ProviderSettings.model_validate(
117
129
  {
@@ -126,6 +138,7 @@ class Auth0Provider(OIDCProxy):
126
138
  "required_scopes": required_scopes,
127
139
  "redirect_path": redirect_path,
128
140
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
141
+ "jwt_signing_key": jwt_signing_key,
129
142
  }.items()
130
143
  if v is not NotSet
131
144
  }
@@ -158,20 +171,20 @@ class Auth0Provider(OIDCProxy):
158
171
 
159
172
  auth0_required_scopes = settings.required_scopes or ["openid"]
160
173
 
161
- init_kwargs = {
162
- "config_url": settings.config_url,
163
- "client_id": settings.client_id,
164
- "client_secret": settings.client_secret.get_secret_value(),
165
- "audience": settings.audience,
166
- "base_url": settings.base_url,
167
- "issuer_url": settings.issuer_url,
168
- "redirect_path": settings.redirect_path,
169
- "required_scopes": auth0_required_scopes,
170
- "allowed_client_redirect_uris": settings.allowed_client_redirect_uris,
171
- "client_storage": client_storage,
172
- }
173
-
174
- super().__init__(**init_kwargs)
174
+ super().__init__(
175
+ config_url=settings.config_url,
176
+ client_id=settings.client_id,
177
+ client_secret=settings.client_secret.get_secret_value(),
178
+ audience=settings.audience,
179
+ base_url=settings.base_url,
180
+ issuer_url=settings.issuer_url,
181
+ redirect_path=settings.redirect_path,
182
+ required_scopes=auth0_required_scopes,
183
+ allowed_client_redirect_uris=settings.allowed_client_redirect_uris,
184
+ client_storage=client_storage,
185
+ jwt_signing_key=settings.jwt_signing_key,
186
+ require_authorization_consent=require_authorization_consent,
187
+ )
175
188
 
176
189
  logger.debug(
177
190
  "Initialized Auth0 OAuth provider for client %s with scopes: %s",
@@ -57,6 +57,7 @@ class AWSCognitoProviderSettings(BaseSettings):
57
57
  redirect_path: str | None = None
58
58
  required_scopes: list[str] | None = None
59
59
  allowed_client_redirect_uris: list[str] | None = None
60
+ jwt_signing_key: str | None = None
60
61
 
61
62
  @field_validator("required_scopes", mode="before")
62
63
  @classmethod
@@ -135,6 +136,8 @@ class AWSCognitoProvider(OIDCProxy):
135
136
  required_scopes: list[str] | NotSetT = NotSet,
136
137
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
137
138
  client_storage: AsyncKeyValue | None = None,
139
+ jwt_signing_key: str | bytes | NotSetT = NotSet,
140
+ require_authorization_consent: bool = True,
138
141
  ):
139
142
  """Initialize AWS Cognito OAuth provider.
140
143
 
@@ -150,7 +153,16 @@ class AWSCognitoProvider(OIDCProxy):
150
153
  required_scopes: Required Cognito scopes (defaults to ["openid"])
151
154
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
152
155
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
153
- client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
156
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
157
+ If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
158
+ disk store will be encrypted using a key derived from the JWT Signing Key.
159
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
160
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
161
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
162
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
163
+ When True, users see a consent screen before being redirected to AWS Cognito.
164
+ When False, authorization proceeds directly without user confirmation.
165
+ SECURITY WARNING: Only disable for local development or testing environments.
154
166
  """
155
167
 
156
168
  settings = AWSCognitoProviderSettings.model_validate(
@@ -166,6 +178,7 @@ class AWSCognitoProvider(OIDCProxy):
166
178
  "redirect_path": redirect_path,
167
179
  "required_scopes": required_scopes,
168
180
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
181
+ "jwt_signing_key": jwt_signing_key,
169
182
  }.items()
170
183
  if v is not NotSet
171
184
  }
@@ -215,6 +228,8 @@ class AWSCognitoProvider(OIDCProxy):
215
228
  redirect_path=redirect_path_final,
216
229
  allowed_client_redirect_uris=allowed_client_redirect_uris_final,
217
230
  client_storage=client_storage,
231
+ jwt_signing_key=settings.jwt_signing_key,
232
+ require_authorization_consent=require_authorization_consent,
218
233
  )
219
234
 
220
235
  logger.debug(
@@ -6,7 +6,7 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from typing import TYPE_CHECKING
9
+ from typing import TYPE_CHECKING, Any
10
10
 
11
11
  from key_value.aio.protocols import AsyncKeyValue
12
12
  from pydantic import SecretStr, field_validator
@@ -45,6 +45,7 @@ class AzureProviderSettings(BaseSettings):
45
45
  required_scopes: list[str] | None = None
46
46
  additional_authorize_scopes: list[str] | None = None
47
47
  allowed_client_redirect_uris: list[str] | None = None
48
+ jwt_signing_key: str | None = None
48
49
 
49
50
  @field_validator("required_scopes", mode="before")
50
51
  @classmethod
@@ -64,18 +65,28 @@ class AzureProvider(OAuthProxy):
64
65
  OAuth Proxy pattern. It supports both organizational accounts and personal
65
66
  Microsoft accounts depending on the tenant configuration.
66
67
 
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
75
+
67
76
  Features:
68
77
  - OAuth proxy to Azure/Microsoft identity platform
69
78
  - JWT validation using tenant issuer and JWKS
70
79
  - Supports tenant configurations: specific tenant ID, "organizations", or "consumers"
80
+ - Custom API scopes and Microsoft Graph scopes in a single provider
71
81
 
72
82
  Setup:
73
83
  1. Create an App registration in Azure Portal
74
84
  2. Configure Web platform redirect URI: http://localhost:8000/auth/callback (or your custom path)
75
- 3. Add an Application ID URI. Either use the default (api://{client_id}) or set a custom one.
76
- 4. Add a custom scope.
77
- 5. Create a client secret.
78
- 6. Get Application (client) ID, Directory (tenant) ID, and client secret
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
79
90
 
80
91
  Example:
81
92
  ```python
@@ -86,7 +97,8 @@ class AzureProvider(OAuthProxy):
86
97
  client_id="your-client-id",
87
98
  client_secret="your-client-secret",
88
99
  tenant_id="your-tenant-id",
89
- required_scopes=["your-scope"],
100
+ required_scopes=["read", "write"], # Unprefixed scope names
101
+ additional_authorize_scopes=["User.Read", "Mail.Read"], # Optional Graph scopes
90
102
  base_url="http://localhost:8000",
91
103
  # identifier_uri defaults to api://{client_id}
92
104
  )
@@ -101,36 +113,57 @@ class AzureProvider(OAuthProxy):
101
113
  client_id: str | NotSetT = NotSet,
102
114
  client_secret: str | NotSetT = NotSet,
103
115
  tenant_id: str | NotSetT = NotSet,
104
- identifier_uri: str | None | NotSetT = NotSet,
116
+ identifier_uri: str | NotSetT | None = NotSet,
105
117
  base_url: str | NotSetT = NotSet,
106
118
  issuer_url: str | NotSetT = NotSet,
107
119
  redirect_path: str | NotSetT = NotSet,
108
- required_scopes: list[str] | None | NotSetT = NotSet,
109
- additional_authorize_scopes: list[str] | None | NotSetT = NotSet,
120
+ required_scopes: list[str] | NotSetT | None = NotSet,
121
+ additional_authorize_scopes: list[str] | NotSetT | None = NotSet,
110
122
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
111
123
  client_storage: AsyncKeyValue | None = None,
124
+ jwt_signing_key: str | bytes | NotSetT = NotSet,
125
+ require_authorization_consent: bool = True,
112
126
  ) -> None:
113
127
  """Initialize Azure OAuth provider.
114
128
 
115
129
  Args:
116
- client_id: Azure application (client) ID
117
- client_secret: Azure client secret
118
- tenant_id: Azure tenant ID (your specific tenant ID, "organizations", or "consumers")
119
- identifier_uri: Optional Application ID URI for your API. (defaults to api://{client_id})
120
- Used only to prefix scopes in authorization requests. Tokens are always validated
121
- against your app's client ID.
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"
122
137
  base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
123
138
  issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
124
139
  to avoid 404s during discovery when mounting under a path.
125
- redirect_path: Redirect path configured in Azure (defaults to "/auth/callback")
126
- required_scopes: Required scopes. These are validated on tokens and used as defaults
127
- when the client does not request specific scopes.
128
- additional_authorize_scopes: Additional scopes to include in the authorization request
129
- without prefixing. Use this to request upstream scopes such as Microsoft Graph
130
- permissions. These are not used for token validation.
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.
131
155
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
132
156
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
133
- client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
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.
134
167
  """
135
168
  settings = AzureProviderSettings.model_validate(
136
169
  {
@@ -146,6 +179,7 @@ class AzureProvider(OAuthProxy):
146
179
  "required_scopes": required_scopes,
147
180
  "additional_authorize_scopes": additional_authorize_scopes,
148
181
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
182
+ "jwt_signing_key": jwt_signing_key,
149
183
  }.items()
150
184
  if v is not NotSet
151
185
  }
@@ -168,8 +202,15 @@ class AzureProvider(OAuthProxy):
168
202
  )
169
203
  raise ValueError(msg)
170
204
 
205
+ # Validate required_scopes has at least one scope
171
206
  if not settings.required_scopes:
172
- raise ValueError("required_scopes is required")
207
+ msg = (
208
+ "required_scopes must include at least one scope - set via parameter or "
209
+ "FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES. Azure's OAuth API requires "
210
+ "the 'scope' parameter in authorization requests. Use the unprefixed scope "
211
+ "names from your Azure App registration (e.g., ['read', 'write'])"
212
+ )
213
+ raise ValueError(msg)
173
214
 
174
215
  # Apply defaults
175
216
  self.identifier_uri = settings.identifier_uri or f"api://{settings.client_id}"
@@ -182,12 +223,13 @@ class AzureProvider(OAuthProxy):
182
223
  f"https://login.microsoftonline.com/{tenant_id_final}/discovery/v2.0/keys"
183
224
  )
184
225
 
226
+ # Azure returns unprefixed scopes in JWT tokens, so validate against unprefixed scopes
185
227
  token_verifier = JWTVerifier(
186
228
  jwks_uri=jwks_uri,
187
229
  issuer=issuer,
188
230
  audience=settings.client_id,
189
231
  algorithm="RS256",
190
- required_scopes=settings.required_scopes,
232
+ required_scopes=settings.required_scopes, # Unprefixed scopes for validation
191
233
  )
192
234
 
193
235
  # Extract secret string from SecretStr
@@ -216,6 +258,8 @@ class AzureProvider(OAuthProxy):
216
258
  or settings.base_url, # Default to base_url if not specified
217
259
  allowed_client_redirect_uris=settings.allowed_client_redirect_uris,
218
260
  client_storage=client_storage,
261
+ jwt_signing_key=settings.jwt_signing_key,
262
+ require_authorization_consent=require_authorization_consent,
219
263
  )
220
264
 
221
265
  logger.info(
@@ -256,23 +300,40 @@ class AzureProvider(OAuthProxy):
256
300
  "Filtering out 'resource' parameter '%s' for Azure AD v2.0 (use scopes instead)",
257
301
  original_resource,
258
302
  )
259
- original_scopes = params_to_use.scopes or self.required_scopes
260
- prefixed_scopes = (
261
- self._add_prefix_to_scopes(original_scopes)
262
- if self.identifier_uri
263
- else original_scopes
264
- )
303
+ # Don't modify the scopes in params - they stay unprefixed for MCP clients
304
+ # We'll prefix them when building the Azure authorization URL (in _build_upstream_authorize_url)
305
+ auth_url = await super().authorize(client, params_to_use)
306
+ separator = "&" if "?" in auth_url else "?"
307
+ return f"{auth_url}{separator}prompt=select_account"
265
308
 
266
- final_scopes = list(prefixed_scopes)
267
- if self.additional_authorize_scopes:
268
- final_scopes.extend(self.additional_authorize_scopes)
309
+ def _build_upstream_authorize_url(
310
+ self, txn_id: str, transaction: dict[str, Any]
311
+ ) -> str:
312
+ """Build Azure authorization URL with prefixed scopes.
269
313
 
270
- modified_params = params_to_use.model_copy(update={"scopes": final_scopes})
314
+ Overrides parent to prefix scopes with identifier_uri before sending to Azure,
315
+ while keeping unprefixed scopes in the transaction for MCP clients.
316
+ """
317
+ # Get unprefixed scopes from transaction
318
+ unprefixed_scopes = transaction.get("scopes") or self.required_scopes or []
319
+
320
+ # Prefix scopes for Azure authorization request
321
+ prefixed_scopes = []
322
+ for scope in unprefixed_scopes:
323
+ if "://" in scope or "/" in scope:
324
+ # Already a full URI or path (e.g., "api://xxx/read" or "User.Read")
325
+ prefixed_scopes.append(scope)
326
+ else:
327
+ # Unprefixed scope name - prefix it with identifier_uri
328
+ prefixed_scopes.append(f"{self.identifier_uri}/{scope}")
329
+
330
+ # Add Microsoft Graph scopes (not validated, not prefixed)
331
+ if self.additional_authorize_scopes:
332
+ prefixed_scopes.extend(self.additional_authorize_scopes)
271
333
 
272
- auth_url = await super().authorize(client, modified_params)
273
- separator = "&" if "?" in auth_url else "?"
274
- return f"{auth_url}{separator}prompt=select_account"
334
+ # Temporarily modify transaction dict for parent's URL building
335
+ modified_transaction = transaction.copy()
336
+ modified_transaction["scopes"] = prefixed_scopes
275
337
 
276
- def _add_prefix_to_scopes(self, scopes: list[str]) -> list[str]:
277
- """Add Application ID URI prefix for authorization request."""
278
- return [f"{self.identifier_uri}/{scope}" for scope in scopes]
338
+ # Let parent build the URL with prefixed scopes
339
+ return super()._build_upstream_authorize_url(txn_id, modified_transaction)
@@ -11,7 +11,7 @@ from fastmcp.server.auth.providers.jwt import JWKData, JWKSData, RSAKeyPair
11
11
  from fastmcp.server.auth.providers.jwt import JWTVerifier as BearerAuthProvider
12
12
 
13
13
  # Re-export for backwards compatibility
14
- __all__ = ["BearerAuthProvider", "RSAKeyPair", "JWKData", "JWKSData"]
14
+ __all__ = ["BearerAuthProvider", "JWKData", "JWKSData", "RSAKeyPair"]
15
15
 
16
16
  # Deprecated in 2.11
17
17
  if fastmcp.settings.deprecation_warnings:
@@ -54,6 +54,7 @@ class GitHubProviderSettings(BaseSettings):
54
54
  required_scopes: list[str] | None = None
55
55
  timeout_seconds: int | None = None
56
56
  allowed_client_redirect_uris: list[str] | None = None
57
+ jwt_signing_key: str | None = None
57
58
 
58
59
  @field_validator("required_scopes", mode="before")
59
60
  @classmethod
@@ -206,6 +207,8 @@ class GitHubProvider(OAuthProxy):
206
207
  timeout_seconds: int | NotSetT = NotSet,
207
208
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
208
209
  client_storage: AsyncKeyValue | None = None,
210
+ jwt_signing_key: str | bytes | NotSetT = NotSet,
211
+ require_authorization_consent: bool = True,
209
212
  ):
210
213
  """Initialize GitHub OAuth provider.
211
214
 
@@ -220,7 +223,16 @@ class GitHubProvider(OAuthProxy):
220
223
  timeout_seconds: HTTP request timeout for GitHub API calls
221
224
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
222
225
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
223
- client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
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.
224
236
  """
225
237
 
226
238
  settings = GitHubProviderSettings.model_validate(
@@ -235,6 +247,7 @@ class GitHubProvider(OAuthProxy):
235
247
  "required_scopes": required_scopes,
236
248
  "timeout_seconds": timeout_seconds,
237
249
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
250
+ "jwt_signing_key": jwt_signing_key,
238
251
  }.items()
239
252
  if v is not NotSet
240
253
  }
@@ -280,6 +293,8 @@ class GitHubProvider(OAuthProxy):
280
293
  or settings.base_url, # Default to base_url if not specified
281
294
  allowed_client_redirect_uris=allowed_client_redirect_uris_final,
282
295
  client_storage=client_storage,
296
+ jwt_signing_key=settings.jwt_signing_key,
297
+ require_authorization_consent=require_authorization_consent,
283
298
  )
284
299
 
285
300
  logger.debug(
@@ -56,6 +56,7 @@ class GoogleProviderSettings(BaseSettings):
56
56
  required_scopes: list[str] | None = None
57
57
  timeout_seconds: int | None = None
58
58
  allowed_client_redirect_uris: list[str] | None = None
59
+ jwt_signing_key: str | None = None
59
60
 
60
61
  @field_validator("required_scopes", mode="before")
61
62
  @classmethod
@@ -222,6 +223,8 @@ class GoogleProvider(OAuthProxy):
222
223
  timeout_seconds: int | NotSetT = NotSet,
223
224
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
224
225
  client_storage: AsyncKeyValue | None = None,
226
+ jwt_signing_key: str | bytes | NotSetT = NotSet,
227
+ require_authorization_consent: bool = True,
225
228
  ):
226
229
  """Initialize Google OAuth provider.
227
230
 
@@ -239,7 +242,16 @@ class GoogleProvider(OAuthProxy):
239
242
  timeout_seconds: HTTP request timeout for Google API calls
240
243
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
241
244
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
242
- client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
245
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
246
+ If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
247
+ disk store will be encrypted using a key derived from the JWT Signing Key.
248
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
249
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
250
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
251
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
252
+ When True, users see a consent screen before being redirected to Google.
253
+ When False, authorization proceeds directly without user confirmation.
254
+ SECURITY WARNING: Only disable for local development or testing environments.
243
255
  """
244
256
 
245
257
  settings = GoogleProviderSettings.model_validate(
@@ -254,6 +266,7 @@ class GoogleProvider(OAuthProxy):
254
266
  "required_scopes": required_scopes,
255
267
  "timeout_seconds": timeout_seconds,
256
268
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
269
+ "jwt_signing_key": jwt_signing_key,
257
270
  }.items()
258
271
  if v is not NotSet
259
272
  }
@@ -299,6 +312,8 @@ class GoogleProvider(OAuthProxy):
299
312
  or settings.base_url, # Default to base_url if not specified
300
313
  allowed_client_redirect_uris=allowed_client_redirect_uris_final,
301
314
  client_storage=client_storage,
315
+ jwt_signing_key=settings.jwt_signing_key,
316
+ require_authorization_consent=require_authorization_consent,
302
317
  )
303
318
 
304
319
  logger.debug(
@@ -96,10 +96,10 @@ class InMemoryOAuthProvider(OAuthProvider):
96
96
  # or if params.redirect_uri is None and client has a default.
97
97
  # However, the AuthorizationHandler handles the primary validation.
98
98
  pass # Let's assume AuthorizationHandler did its job.
99
- except Exception: # Replace with specific validation error if client.validate_redirect_uri existed
99
+ except Exception as e: # Replace with specific validation error if client.validate_redirect_uri existed
100
100
  raise AuthorizeError(
101
101
  error="invalid_request", error_description="Invalid redirect_uri."
102
- )
102
+ ) from e
103
103
 
104
104
  auth_code_value = f"test_auth_code_{secrets.token_hex(16)}"
105
105
  expires_at = time.time() + DEFAULT_AUTH_CODE_EXPIRY_SECONDS
@@ -97,8 +97,8 @@ class IntrospectionTokenVerifier(TokenVerifier):
97
97
  client_id: str | NotSetT = NotSet,
98
98
  client_secret: str | NotSetT = NotSet,
99
99
  timeout_seconds: int | NotSetT = NotSet,
100
- required_scopes: list[str] | None | NotSetT = NotSet,
101
- base_url: AnyHttpUrl | str | None | NotSetT = NotSet,
100
+ required_scopes: list[str] | NotSetT | None = NotSet,
101
+ base_url: AnyHttpUrl | str | NotSetT | None = NotSet,
102
102
  ):
103
103
  """
104
104
  Initialize the introspection token verifier.
@@ -184,13 +184,13 @@ class JWTVerifier(TokenVerifier):
184
184
  def __init__(
185
185
  self,
186
186
  *,
187
- public_key: str | None | NotSetT = NotSet,
188
- jwks_uri: str | None | NotSetT = NotSet,
189
- issuer: str | None | NotSetT = NotSet,
190
- audience: str | list[str] | None | NotSetT = NotSet,
191
- algorithm: str | None | NotSetT = NotSet,
192
- required_scopes: list[str] | None | NotSetT = NotSet,
193
- base_url: AnyHttpUrl | str | None | NotSetT = NotSet,
187
+ public_key: str | NotSetT | None = NotSet,
188
+ jwks_uri: str | NotSetT | None = NotSet,
189
+ issuer: str | NotSetT | None = NotSet,
190
+ audience: str | list[str] | NotSetT | None = NotSet,
191
+ algorithm: str | NotSetT | None = NotSet,
192
+ required_scopes: list[str] | NotSetT | None = NotSet,
193
+ base_url: AnyHttpUrl | str | NotSetT | None = NotSet,
194
194
  ):
195
195
  """
196
196
  Initialize the JWT token verifier.
@@ -283,7 +283,7 @@ class JWTVerifier(TokenVerifier):
283
283
  return await self._get_jwks_key(kid)
284
284
 
285
285
  except Exception as e:
286
- raise ValueError(f"Failed to extract key ID from token: {e}")
286
+ raise ValueError(f"Failed to extract key ID from token: {e}") from e
287
287
 
288
288
  async def _get_jwks_key(self, kid: str | None) -> str:
289
289
  """Fetch key from JWKS with simple caching."""
@@ -342,10 +342,10 @@ class JWTVerifier(TokenVerifier):
342
342
  raise ValueError("No keys found in JWKS")
343
343
 
344
344
  except httpx.HTTPError as e:
345
- raise ValueError(f"Failed to fetch JWKS: {e}")
345
+ raise ValueError(f"Failed to fetch JWKS: {e}") from e
346
346
  except Exception as e:
347
347
  self.logger.debug(f"JWKS fetch failed: {e}")
348
- raise ValueError(f"Failed to fetch JWKS: {e}")
348
+ raise ValueError(f"Failed to fetch JWKS: {e}") from e
349
349
 
350
350
  def _extract_scopes(self, claims: dict[str, Any]) -> list[str]:
351
351
  """
@@ -400,14 +400,13 @@ class JWTVerifier(TokenVerifier):
400
400
 
401
401
  # Validate issuer - note we use issuer instead of issuer_url here because
402
402
  # issuer is optional, allowing users to make this check optional
403
- if self.issuer:
404
- if claims.get("iss") != self.issuer:
405
- self.logger.debug(
406
- "Token validation failed: issuer mismatch for client %s",
407
- client_id,
408
- )
409
- self.logger.info("Bearer token rejected for client %s", client_id)
410
- return None
403
+ if self.issuer and claims.get("iss") != self.issuer:
404
+ self.logger.debug(
405
+ "Token validation failed: issuer mismatch for client %s",
406
+ client_id,
407
+ )
408
+ self.logger.info("Bearer token rejected for client %s", client_id)
409
+ return None
411
410
 
412
411
  # Validate audience if configured
413
412
  if self.audience:
@@ -83,7 +83,7 @@ class SupabaseProvider(RemoteAuthProvider):
83
83
  *,
84
84
  project_url: AnyHttpUrl | str | NotSetT = NotSet,
85
85
  base_url: AnyHttpUrl | str | NotSetT = NotSet,
86
- required_scopes: list[str] | None | NotSetT = NotSet,
86
+ required_scopes: list[str] | NotSetT | None = NotSet,
87
87
  token_verifier: TokenVerifier | None = None,
88
88
  ):
89
89
  """Initialize Supabase metadata provider.