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.
- fastmcp/__init__.py +2 -2
- fastmcp/cli/cli.py +3 -2
- fastmcp/cli/install/claude_code.py +3 -3
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +7 -6
- fastmcp/client/client.py +10 -10
- fastmcp/client/oauth_callback.py +6 -2
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +35 -34
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +1 -1
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +2 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +3 -5
- fastmcp/experimental/utilities/openapi/schemas.py +2 -2
- fastmcp/mcp_config.py +2 -3
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +9 -13
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +1 -3
- fastmcp/resources/resource_manager.py +1 -1
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +5 -5
- fastmcp/server/auth/auth.py +2 -2
- fastmcp/server/auth/handlers/authorize.py +324 -0
- fastmcp/server/auth/jwt_issuer.py +39 -92
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +236 -217
- fastmcp/server/auth/oidc_proxy.py +18 -3
- fastmcp/server/auth/providers/auth0.py +28 -15
- fastmcp/server/auth/providers/aws.py +16 -1
- fastmcp/server/auth/providers/azure.py +101 -40
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/github.py +16 -1
- fastmcp/server/auth/providers/google.py +16 -1
- fastmcp/server/auth/providers/in_memory.py +2 -2
- fastmcp/server/auth/providers/introspection.py +2 -2
- fastmcp/server/auth/providers/jwt.py +17 -18
- fastmcp/server/auth/providers/supabase.py +1 -1
- fastmcp/server/auth/providers/workos.py +18 -3
- fastmcp/server/context.py +41 -12
- fastmcp/server/dependencies.py +5 -6
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +3 -4
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +1 -1
- fastmcp/server/middleware/error_handling.py +8 -8
- fastmcp/server/middleware/middleware.py +1 -1
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +10 -6
- fastmcp/server/proxy.py +5 -4
- fastmcp/server/server.py +74 -55
- fastmcp/settings.py +2 -1
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +12 -12
- fastmcp/tools/tool_manager.py +8 -4
- fastmcp/tools/tool_transform.py +6 -6
- fastmcp/utilities/cli.py +50 -21
- fastmcp/utilities/inspect.py +2 -2
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +14 -18
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +9 -9
- fastmcp/utilities/tests.py +2 -4
- fastmcp/utilities/ui.py +126 -6
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/METADATA +5 -5
- fastmcp-2.13.0.1.dist-info/RECORD +141 -0
- fastmcp-2.13.0rc2.dist-info/RECORD +0 -138
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/entry_points.txt +0 -0
- {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:
|
|
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:
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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:
|
|
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
|
|
76
|
-
4. Add
|
|
77
|
-
5.
|
|
78
|
-
6.
|
|
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=["
|
|
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 |
|
|
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] |
|
|
109
|
-
additional_authorize_scopes: list[str] |
|
|
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 (
|
|
119
|
-
identifier_uri: Optional Application ID URI for your API
|
|
120
|
-
|
|
121
|
-
|
|
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:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
334
|
+
# Temporarily modify transaction dict for parent's URL building
|
|
335
|
+
modified_transaction = transaction.copy()
|
|
336
|
+
modified_transaction["scopes"] = prefixed_scopes
|
|
275
337
|
|
|
276
|
-
|
|
277
|
-
|
|
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", "
|
|
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:
|
|
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:
|
|
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] |
|
|
101
|
-
base_url: AnyHttpUrl | str |
|
|
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 |
|
|
188
|
-
jwks_uri: str |
|
|
189
|
-
issuer: str |
|
|
190
|
-
audience: str | list[str] |
|
|
191
|
-
algorithm: str |
|
|
192
|
-
required_scopes: list[str] |
|
|
193
|
-
base_url: AnyHttpUrl | str |
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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] |
|
|
86
|
+
required_scopes: list[str] | NotSetT | None = NotSet,
|
|
87
87
|
token_verifier: TokenVerifier | None = None,
|
|
88
88
|
):
|
|
89
89
|
"""Initialize Supabase metadata provider.
|