fastmcp 2.12.2__py3-none-any.whl → 2.12.4__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/cli/claude.py +1 -10
- fastmcp/cli/cli.py +45 -25
- fastmcp/cli/install/__init__.py +2 -0
- fastmcp/cli/install/claude_code.py +1 -10
- fastmcp/cli/install/claude_desktop.py +1 -9
- fastmcp/cli/install/cursor.py +2 -18
- fastmcp/cli/install/gemini_cli.py +241 -0
- fastmcp/cli/install/mcp_json.py +1 -9
- fastmcp/cli/run.py +2 -86
- fastmcp/client/auth/oauth.py +50 -37
- fastmcp/client/client.py +18 -8
- fastmcp/client/elicitation.py +6 -1
- fastmcp/client/transports.py +1 -1
- fastmcp/contrib/component_manager/component_service.py +1 -1
- fastmcp/contrib/mcp_mixin/README.md +3 -3
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +41 -6
- fastmcp/experimental/utilities/openapi/director.py +8 -1
- fastmcp/experimental/utilities/openapi/schemas.py +31 -5
- fastmcp/prompts/prompt.py +10 -8
- fastmcp/resources/resource.py +14 -11
- fastmcp/resources/template.py +12 -10
- fastmcp/server/auth/auth.py +10 -4
- fastmcp/server/auth/oauth_proxy.py +93 -23
- fastmcp/server/auth/oidc_proxy.py +348 -0
- fastmcp/server/auth/providers/auth0.py +174 -0
- fastmcp/server/auth/providers/aws.py +237 -0
- fastmcp/server/auth/providers/azure.py +6 -2
- fastmcp/server/auth/providers/descope.py +172 -0
- fastmcp/server/auth/providers/github.py +6 -2
- fastmcp/server/auth/providers/google.py +6 -2
- fastmcp/server/auth/providers/workos.py +6 -2
- fastmcp/server/context.py +17 -16
- fastmcp/server/dependencies.py +18 -5
- fastmcp/server/http.py +1 -1
- fastmcp/server/middleware/logging.py +147 -116
- fastmcp/server/middleware/middleware.py +3 -2
- fastmcp/server/openapi.py +5 -1
- fastmcp/server/server.py +43 -36
- fastmcp/settings.py +42 -6
- fastmcp/tools/tool.py +105 -87
- fastmcp/tools/tool_transform.py +1 -1
- fastmcp/utilities/json_schema.py +18 -1
- fastmcp/utilities/logging.py +66 -4
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +4 -39
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -2
- fastmcp/utilities/mcp_server_config/v1/schema.json +2 -1
- fastmcp/utilities/storage.py +204 -0
- fastmcp/utilities/tests.py +8 -6
- fastmcp/utilities/types.py +9 -5
- {fastmcp-2.12.2.dist-info → fastmcp-2.12.4.dist-info}/METADATA +121 -48
- {fastmcp-2.12.2.dist-info → fastmcp-2.12.4.dist-info}/RECORD +54 -48
- {fastmcp-2.12.2.dist-info → fastmcp-2.12.4.dist-info}/WHEEL +0 -0
- {fastmcp-2.12.2.dist-info → fastmcp-2.12.4.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.2.dist-info → fastmcp-2.12.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -45,9 +45,11 @@ from starlette.requests import Request
|
|
|
45
45
|
from starlette.responses import RedirectResponse
|
|
46
46
|
from starlette.routing import Route
|
|
47
47
|
|
|
48
|
+
import fastmcp
|
|
48
49
|
from fastmcp.server.auth.auth import OAuthProvider, TokenVerifier
|
|
49
50
|
from fastmcp.server.auth.redirect_validation import validate_redirect_uri
|
|
50
51
|
from fastmcp.utilities.logging import get_logger
|
|
52
|
+
from fastmcp.utilities.storage import JSONFileStorage, KVStorage
|
|
51
53
|
|
|
52
54
|
if TYPE_CHECKING:
|
|
53
55
|
pass
|
|
@@ -240,7 +242,7 @@ class OAuthProxy(OAuthProvider):
|
|
|
240
242
|
token_verifier: TokenVerifier,
|
|
241
243
|
# FastMCP server configuration
|
|
242
244
|
base_url: AnyHttpUrl | str,
|
|
243
|
-
redirect_path: str =
|
|
245
|
+
redirect_path: str | None = None,
|
|
244
246
|
issuer_url: AnyHttpUrl | str | None = None,
|
|
245
247
|
service_documentation_url: AnyHttpUrl | str | None = None,
|
|
246
248
|
# Client redirect URI validation
|
|
@@ -250,6 +252,12 @@ class OAuthProxy(OAuthProvider):
|
|
|
250
252
|
forward_pkce: bool = True,
|
|
251
253
|
# Token endpoint authentication
|
|
252
254
|
token_endpoint_auth_method: str | None = None,
|
|
255
|
+
# Extra parameters to forward to authorization endpoint
|
|
256
|
+
extra_authorize_params: dict[str, str] | None = None,
|
|
257
|
+
# Extra parameters to forward to token endpoint
|
|
258
|
+
extra_token_params: dict[str, str] | None = None,
|
|
259
|
+
# Client storage
|
|
260
|
+
client_storage: KVStorage | None = None,
|
|
253
261
|
):
|
|
254
262
|
"""Initialize the OAuth proxy provider.
|
|
255
263
|
|
|
@@ -273,11 +281,19 @@ class OAuthProxy(OAuthProvider):
|
|
|
273
281
|
valid_scopes: List of all the possible valid scopes for a client.
|
|
274
282
|
These are advertised to clients through the `/.well-known` endpoints. Defaults to `required_scopes` if not provided.
|
|
275
283
|
forward_pkce: Whether to forward PKCE to upstream server (default True).
|
|
276
|
-
Enable for providers that support/require PKCE (Google, Azure, etc.).
|
|
284
|
+
Enable for providers that support/require PKCE (Google, Azure, AWS, etc.).
|
|
277
285
|
Disable only if upstream provider doesn't support PKCE.
|
|
278
286
|
token_endpoint_auth_method: Token endpoint authentication method for upstream server.
|
|
279
287
|
Common values: "client_secret_basic", "client_secret_post", "none".
|
|
280
288
|
If None, authlib will use its default (typically "client_secret_basic").
|
|
289
|
+
extra_authorize_params: Additional parameters to forward to the upstream authorization endpoint.
|
|
290
|
+
Useful for provider-specific parameters like Auth0's "audience".
|
|
291
|
+
Example: {"audience": "https://api.example.com"}
|
|
292
|
+
extra_token_params: Additional parameters to forward to the upstream token endpoint.
|
|
293
|
+
Useful for provider-specific parameters during token exchange.
|
|
294
|
+
client_storage: Storage implementation for OAuth client registrations.
|
|
295
|
+
Defaults to file-based storage in ~/.fastmcp/oauth-proxy-clients/ if not specified.
|
|
296
|
+
Pass any KVStorage implementation for custom storage backends.
|
|
281
297
|
"""
|
|
282
298
|
# Always enable DCR since we implement it locally for MCP clients
|
|
283
299
|
client_registration_options = ClientRegistrationOptions(
|
|
@@ -308,9 +324,12 @@ class OAuthProxy(OAuthProvider):
|
|
|
308
324
|
self._default_scope_str = " ".join(self.required_scopes or [])
|
|
309
325
|
|
|
310
326
|
# Store redirect configuration
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
327
|
+
if not redirect_path:
|
|
328
|
+
self._redirect_path = "/auth/callback"
|
|
329
|
+
else:
|
|
330
|
+
self._redirect_path = (
|
|
331
|
+
redirect_path if redirect_path.startswith("/") else f"/{redirect_path}"
|
|
332
|
+
)
|
|
314
333
|
self._allowed_client_redirect_uris = allowed_client_redirect_uris
|
|
315
334
|
|
|
316
335
|
# PKCE configuration
|
|
@@ -319,8 +338,17 @@ class OAuthProxy(OAuthProvider):
|
|
|
319
338
|
# Token endpoint authentication
|
|
320
339
|
self._token_endpoint_auth_method = token_endpoint_auth_method
|
|
321
340
|
|
|
322
|
-
#
|
|
323
|
-
self.
|
|
341
|
+
# Extra parameters for authorization and token endpoints
|
|
342
|
+
self._extra_authorize_params = extra_authorize_params or {}
|
|
343
|
+
self._extra_token_params = extra_token_params or {}
|
|
344
|
+
|
|
345
|
+
# Initialize client storage (default to file-based if not provided)
|
|
346
|
+
if client_storage is None:
|
|
347
|
+
cache_dir = fastmcp.settings.home / "oauth-proxy-clients"
|
|
348
|
+
client_storage = JSONFileStorage(cache_dir)
|
|
349
|
+
self._client_storage = client_storage
|
|
350
|
+
|
|
351
|
+
# Local state for token bookkeeping only (no client caching)
|
|
324
352
|
self._access_tokens: dict[str, AccessToken] = {}
|
|
325
353
|
self._refresh_tokens: dict[str, RefreshToken] = {}
|
|
326
354
|
|
|
@@ -371,9 +399,20 @@ class OAuthProxy(OAuthProvider):
|
|
|
371
399
|
|
|
372
400
|
For unregistered clients, returns None (which will raise an error in the SDK).
|
|
373
401
|
"""
|
|
374
|
-
|
|
402
|
+
# Load from storage
|
|
403
|
+
data = await self._client_storage.get(client_id)
|
|
404
|
+
if not data:
|
|
405
|
+
return None
|
|
375
406
|
|
|
376
|
-
|
|
407
|
+
if client_data := data.get("client", None):
|
|
408
|
+
return ProxyDCRClient(
|
|
409
|
+
allowed_redirect_uri_patterns=data.get(
|
|
410
|
+
"allowed_redirect_uri_patterns", self._allowed_client_redirect_uris
|
|
411
|
+
),
|
|
412
|
+
**client_data,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
return None
|
|
377
416
|
|
|
378
417
|
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
|
|
379
418
|
"""Register a client locally
|
|
@@ -391,13 +430,17 @@ class OAuthProxy(OAuthProvider):
|
|
|
391
430
|
redirect_uris=client_info.redirect_uris or [AnyUrl("http://localhost")],
|
|
392
431
|
grant_types=client_info.grant_types
|
|
393
432
|
or ["authorization_code", "refresh_token"],
|
|
394
|
-
scope=self._default_scope_str,
|
|
433
|
+
scope=client_info.scope or self._default_scope_str,
|
|
395
434
|
token_endpoint_auth_method="none",
|
|
396
435
|
allowed_redirect_uri_patterns=self._allowed_client_redirect_uris,
|
|
397
436
|
)
|
|
398
437
|
|
|
399
|
-
# Store
|
|
400
|
-
|
|
438
|
+
# Store as structured dict with all needed metadata
|
|
439
|
+
storage_data = {
|
|
440
|
+
"client": proxy_client.model_dump(mode="json"),
|
|
441
|
+
"allowed_redirect_uri_patterns": self._allowed_client_redirect_uris,
|
|
442
|
+
}
|
|
443
|
+
await self._client_storage.set(client_info.client_id, storage_data)
|
|
401
444
|
|
|
402
445
|
# Log redirect URIs to help users discover what patterns they might need
|
|
403
446
|
if client_info.redirect_uris:
|
|
@@ -485,6 +528,24 @@ class OAuthProxy(OAuthProvider):
|
|
|
485
528
|
txn_id,
|
|
486
529
|
)
|
|
487
530
|
|
|
531
|
+
# Forward resource parameter if provided (RFC 8707)
|
|
532
|
+
if params.resource:
|
|
533
|
+
query_params["resource"] = params.resource
|
|
534
|
+
logger.debug(
|
|
535
|
+
"Forwarding resource indicator '%s' to upstream for transaction %s",
|
|
536
|
+
params.resource,
|
|
537
|
+
txn_id,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# Add any extra authorization parameters configured for this proxy
|
|
541
|
+
if self._extra_authorize_params:
|
|
542
|
+
query_params.update(self._extra_authorize_params)
|
|
543
|
+
logger.debug(
|
|
544
|
+
"Adding extra authorization parameters for transaction %s: %s",
|
|
545
|
+
txn_id,
|
|
546
|
+
list(self._extra_authorize_params.keys()),
|
|
547
|
+
)
|
|
548
|
+
|
|
488
549
|
# Build the upstream authorization URL
|
|
489
550
|
separator = "&" if "?" in self._upstream_authorization_endpoint else "?"
|
|
490
551
|
upstream_url = f"{self._upstream_authorization_endpoint}{separator}{urlencode(query_params)}"
|
|
@@ -870,26 +931,35 @@ class OAuthProxy(OAuthProvider):
|
|
|
870
931
|
f"Exchanging IdP code for tokens with redirect_uri: {idp_redirect_uri}"
|
|
871
932
|
)
|
|
872
933
|
|
|
934
|
+
# Build token exchange parameters
|
|
935
|
+
token_params = {
|
|
936
|
+
"url": self._upstream_token_endpoint,
|
|
937
|
+
"code": idp_code,
|
|
938
|
+
"redirect_uri": idp_redirect_uri,
|
|
939
|
+
}
|
|
940
|
+
|
|
873
941
|
# Include proxy's code_verifier if we forwarded PKCE
|
|
874
942
|
proxy_code_verifier = transaction.get("proxy_code_verifier")
|
|
875
943
|
if proxy_code_verifier:
|
|
944
|
+
token_params["code_verifier"] = proxy_code_verifier
|
|
876
945
|
logger.debug(
|
|
877
946
|
"Including proxy code_verifier in token exchange for transaction %s",
|
|
878
947
|
txn_id,
|
|
879
948
|
)
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
url=self._upstream_token_endpoint,
|
|
889
|
-
code=idp_code,
|
|
890
|
-
redirect_uri=idp_redirect_uri,
|
|
949
|
+
|
|
950
|
+
# Add any extra token parameters configured for this proxy
|
|
951
|
+
if self._extra_token_params:
|
|
952
|
+
token_params.update(self._extra_token_params)
|
|
953
|
+
logger.debug(
|
|
954
|
+
"Adding extra token parameters for transaction %s: %s",
|
|
955
|
+
txn_id,
|
|
956
|
+
list(self._extra_token_params.keys()),
|
|
891
957
|
)
|
|
892
958
|
|
|
959
|
+
idp_tokens: dict[str, Any] = await oauth_client.fetch_token(
|
|
960
|
+
**token_params
|
|
961
|
+
) # type: ignore[misc]
|
|
962
|
+
|
|
893
963
|
logger.debug(
|
|
894
964
|
f"Successfully exchanged IdP code for tokens (transaction: {txn_id}, PKCE: {bool(proxy_code_verifier)})"
|
|
895
965
|
)
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""OIDC Proxy Provider for FastMCP.
|
|
2
|
+
|
|
3
|
+
This provider acts as a transparent proxy to an upstream OIDC compliant Authorization
|
|
4
|
+
Server. It leverages the OAuthProxy class to handle Dynamic Client Registration and
|
|
5
|
+
forwarding of all OAuth flows.
|
|
6
|
+
|
|
7
|
+
This implementation is based on:
|
|
8
|
+
OpenID Connect Discovery 1.0 - https://openid.net/specs/openid-connect-discovery-1_0.html
|
|
9
|
+
OAuth 2.0 Authorization Server Metadata - https://datatracker.ietf.org/doc/html/rfc8414
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from pydantic import AnyHttpUrl, BaseModel, model_validator
|
|
16
|
+
from typing_extensions import Self
|
|
17
|
+
|
|
18
|
+
from fastmcp.server.auth import TokenVerifier
|
|
19
|
+
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
20
|
+
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
21
|
+
from fastmcp.utilities.logging import get_logger
|
|
22
|
+
from fastmcp.utilities.storage import KVStorage
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OIDCConfiguration(BaseModel):
|
|
28
|
+
"""OIDC Configuration.
|
|
29
|
+
|
|
30
|
+
See:
|
|
31
|
+
https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
|
32
|
+
https://datatracker.ietf.org/doc/html/rfc8414#section-2
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
strict: bool = True
|
|
36
|
+
|
|
37
|
+
# OpenID Connect Discovery 1.0
|
|
38
|
+
issuer: AnyHttpUrl | str | None = None # Strict
|
|
39
|
+
|
|
40
|
+
authorization_endpoint: AnyHttpUrl | str | None = None # Strict
|
|
41
|
+
token_endpoint: AnyHttpUrl | str | None = None # Strict
|
|
42
|
+
userinfo_endpoint: AnyHttpUrl | str | None = None
|
|
43
|
+
|
|
44
|
+
jwks_uri: AnyHttpUrl | str | None = None # Strict
|
|
45
|
+
|
|
46
|
+
registration_endpoint: AnyHttpUrl | str | None = None
|
|
47
|
+
|
|
48
|
+
scopes_supported: Sequence[str] | None = None
|
|
49
|
+
|
|
50
|
+
response_types_supported: Sequence[str] | None = None # Strict
|
|
51
|
+
response_modes_supported: Sequence[str] | None = None
|
|
52
|
+
|
|
53
|
+
grant_types_supported: Sequence[str] | None = None
|
|
54
|
+
|
|
55
|
+
acr_values_supported: Sequence[str] | None = None
|
|
56
|
+
|
|
57
|
+
subject_types_supported: Sequence[str] | None = None # Strict
|
|
58
|
+
|
|
59
|
+
id_token_signing_alg_values_supported: Sequence[str] | None = None # Strict
|
|
60
|
+
id_token_encryption_alg_values_supported: Sequence[str] | None = None
|
|
61
|
+
id_token_encryption_enc_values_supported: Sequence[str] | None = None
|
|
62
|
+
|
|
63
|
+
userinfo_signing_alg_values_supported: Sequence[str] | None = None
|
|
64
|
+
userinfo_encryption_alg_values_supported: Sequence[str] | None = None
|
|
65
|
+
userinfo_encryption_enc_values_supported: Sequence[str] | None = None
|
|
66
|
+
|
|
67
|
+
request_object_signing_alg_values_supported: Sequence[str] | None = None
|
|
68
|
+
request_object_encryption_alg_values_supported: Sequence[str] | None = None
|
|
69
|
+
request_object_encryption_enc_values_supported: Sequence[str] | None = None
|
|
70
|
+
|
|
71
|
+
token_endpoint_auth_methods_supported: Sequence[str] | None = None
|
|
72
|
+
token_endpoint_auth_signing_alg_values_supported: Sequence[str] | None = None
|
|
73
|
+
|
|
74
|
+
display_values_supported: Sequence[str] | None = None
|
|
75
|
+
|
|
76
|
+
claim_types_supported: Sequence[str] | None = None
|
|
77
|
+
claims_supported: Sequence[str] | None = None
|
|
78
|
+
|
|
79
|
+
service_documentation: AnyHttpUrl | str | None = None
|
|
80
|
+
|
|
81
|
+
claims_locales_supported: Sequence[str] | None = None
|
|
82
|
+
ui_locales_supported: Sequence[str] | None = None
|
|
83
|
+
|
|
84
|
+
claims_parameter_supported: bool | None = None
|
|
85
|
+
request_parameter_supported: bool | None = None
|
|
86
|
+
request_uri_parameter_supported: bool | None = None
|
|
87
|
+
|
|
88
|
+
require_request_uri_registration: bool | None = None
|
|
89
|
+
|
|
90
|
+
op_policy_uri: AnyHttpUrl | str | None = None
|
|
91
|
+
op_tos_uri: AnyHttpUrl | str | None = None
|
|
92
|
+
|
|
93
|
+
# OAuth 2.0 Authorization Server Metadata
|
|
94
|
+
revocation_endpoint: AnyHttpUrl | str | None = None
|
|
95
|
+
revocation_endpoint_auth_methods_supported: Sequence[str] | None = None
|
|
96
|
+
revocation_endpoint_auth_signing_alg_values_supported: Sequence[str] | None = None
|
|
97
|
+
|
|
98
|
+
introspection_endpoint: AnyHttpUrl | str | None = None
|
|
99
|
+
introspection_endpoint_auth_methods_supported: Sequence[str] | None = None
|
|
100
|
+
introspection_endpoint_auth_signing_alg_values_supported: Sequence[str] | None = (
|
|
101
|
+
None
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
code_challenge_methods_supported: Sequence[str] | None = None
|
|
105
|
+
|
|
106
|
+
signed_metadata: str | None = None
|
|
107
|
+
|
|
108
|
+
@model_validator(mode="after")
|
|
109
|
+
def _enforce_strict(self) -> Self:
|
|
110
|
+
"""Enforce strict rules."""
|
|
111
|
+
if not self.strict:
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def enforce(attr: str, is_url: bool = False) -> None:
|
|
115
|
+
value = getattr(self, attr, None)
|
|
116
|
+
if not value:
|
|
117
|
+
message = f"Missing required configuration metadata: {attr}"
|
|
118
|
+
logger.error(message)
|
|
119
|
+
raise ValueError(message)
|
|
120
|
+
|
|
121
|
+
if not is_url or isinstance(value, AnyHttpUrl):
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
AnyHttpUrl(value)
|
|
126
|
+
except Exception:
|
|
127
|
+
message = f"Invalid URL for configuration metadata: {attr}"
|
|
128
|
+
logger.error(message)
|
|
129
|
+
raise ValueError(message)
|
|
130
|
+
|
|
131
|
+
enforce("issuer", True)
|
|
132
|
+
enforce("authorization_endpoint", True)
|
|
133
|
+
enforce("token_endpoint", True)
|
|
134
|
+
enforce("jwks_uri", True)
|
|
135
|
+
enforce("response_types_supported")
|
|
136
|
+
enforce("subject_types_supported")
|
|
137
|
+
enforce("id_token_signing_alg_values_supported")
|
|
138
|
+
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def get_oidc_configuration(
|
|
143
|
+
cls, config_url: AnyHttpUrl, *, strict: bool | None, timeout_seconds: int | None
|
|
144
|
+
) -> Self:
|
|
145
|
+
"""Get the OIDC configuration for the specified config URL.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
config_url: The OIDC config URL
|
|
149
|
+
strict: The strict flag for the configuration
|
|
150
|
+
timeout_seconds: HTTP request timeout in seconds
|
|
151
|
+
"""
|
|
152
|
+
get_kwargs = {}
|
|
153
|
+
if timeout_seconds is not None:
|
|
154
|
+
get_kwargs["timeout"] = timeout_seconds
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
response = httpx.get(str(config_url), **get_kwargs)
|
|
158
|
+
response.raise_for_status()
|
|
159
|
+
|
|
160
|
+
config_data = response.json()
|
|
161
|
+
if strict is not None:
|
|
162
|
+
config_data["strict"] = strict
|
|
163
|
+
|
|
164
|
+
return cls.model_validate(config_data)
|
|
165
|
+
except Exception:
|
|
166
|
+
logger.exception(
|
|
167
|
+
f"Unable to get OIDC configuration for config url: {config_url}"
|
|
168
|
+
)
|
|
169
|
+
raise
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class OIDCProxy(OAuthProxy):
|
|
173
|
+
"""OAuth provider that wraps OAuthProxy to provide configuration via an OIDC configuration URL.
|
|
174
|
+
|
|
175
|
+
This provider makes it easier to add OAuth protection for any upstream provider
|
|
176
|
+
that is OIDC compliant.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
```python
|
|
180
|
+
from fastmcp import FastMCP
|
|
181
|
+
from fastmcp.server.auth.oidc_proxy import OIDCProxy
|
|
182
|
+
|
|
183
|
+
# Simple OIDC based protection
|
|
184
|
+
auth = OIDCProxy(
|
|
185
|
+
config_url="https://oidc.config.url",
|
|
186
|
+
client_id="your-oidc-client-id",
|
|
187
|
+
client_secret="your-oidc-client-secret",
|
|
188
|
+
base_url="https://your.server.url",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
mcp = FastMCP("My Protected Server", auth=auth)
|
|
192
|
+
```
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
oidc_config: OIDCConfiguration
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
*,
|
|
200
|
+
# OIDC configuration
|
|
201
|
+
config_url: AnyHttpUrl | str,
|
|
202
|
+
strict: bool | None = None,
|
|
203
|
+
# Upstream server configuration
|
|
204
|
+
client_id: str,
|
|
205
|
+
client_secret: str,
|
|
206
|
+
audience: str | None = None,
|
|
207
|
+
timeout_seconds: int | None = None,
|
|
208
|
+
# Token verifier
|
|
209
|
+
algorithm: str | None = None,
|
|
210
|
+
required_scopes: list[str] | None = None,
|
|
211
|
+
# FastMCP server configuration
|
|
212
|
+
base_url: AnyHttpUrl | str,
|
|
213
|
+
redirect_path: str | None = None,
|
|
214
|
+
# Client configuration
|
|
215
|
+
allowed_client_redirect_uris: list[str] | None = None,
|
|
216
|
+
client_storage: KVStorage | None = None,
|
|
217
|
+
# Token validation configuration
|
|
218
|
+
token_endpoint_auth_method: str | None = None,
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Initialize the OIDC proxy provider.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
config_url: URL of upstream configuration
|
|
224
|
+
strict: Optional strict flag for the configuration
|
|
225
|
+
client_id: Client ID registered with upstream server
|
|
226
|
+
client_secret: Client secret for upstream server
|
|
227
|
+
audience: Audience for upstream server
|
|
228
|
+
timeout_seconds: HTTP request timeout in seconds
|
|
229
|
+
algorithm: Token verifier algorithm
|
|
230
|
+
required_scopes: Required OAuth scopes
|
|
231
|
+
base_url: Public URL of the server that exposes this FastMCP server; redirect path is
|
|
232
|
+
relative to this URL
|
|
233
|
+
redirect_path: Redirect path configured in upstream OAuth app (defaults to "/auth/callback")
|
|
234
|
+
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
235
|
+
Patterns support wildcards (e.g., "http://localhost:*", "https://*.example.com/*").
|
|
236
|
+
If None (default), only localhost redirect URIs are allowed.
|
|
237
|
+
If empty list, all redirect URIs are allowed (not recommended for production).
|
|
238
|
+
These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app.
|
|
239
|
+
client_storage: Storage implementation for OAuth client registrations.
|
|
240
|
+
Defaults to file-based storage if not specified.
|
|
241
|
+
token_endpoint_auth_method: Token endpoint authentication method for upstream server.
|
|
242
|
+
Common values: "client_secret_basic", "client_secret_post", "none".
|
|
243
|
+
If None, authlib will use its default (typically "client_secret_basic").
|
|
244
|
+
"""
|
|
245
|
+
if not config_url:
|
|
246
|
+
raise ValueError("Missing required config URL")
|
|
247
|
+
|
|
248
|
+
if not client_id:
|
|
249
|
+
raise ValueError("Missing required client id")
|
|
250
|
+
|
|
251
|
+
if not client_secret:
|
|
252
|
+
raise ValueError("Missing required client secret")
|
|
253
|
+
|
|
254
|
+
if not base_url:
|
|
255
|
+
raise ValueError("Missing required base URL")
|
|
256
|
+
|
|
257
|
+
if isinstance(config_url, str):
|
|
258
|
+
config_url = AnyHttpUrl(config_url)
|
|
259
|
+
|
|
260
|
+
self.oidc_config = self.get_oidc_configuration(
|
|
261
|
+
config_url, strict, timeout_seconds
|
|
262
|
+
)
|
|
263
|
+
if (
|
|
264
|
+
not self.oidc_config.authorization_endpoint
|
|
265
|
+
or not self.oidc_config.token_endpoint
|
|
266
|
+
):
|
|
267
|
+
logger.debug(f"Invalid OIDC Configuration: {self.oidc_config}")
|
|
268
|
+
raise ValueError("Missing required OIDC endpoints")
|
|
269
|
+
|
|
270
|
+
revocation_endpoint = (
|
|
271
|
+
str(self.oidc_config.revocation_endpoint)
|
|
272
|
+
if self.oidc_config.revocation_endpoint
|
|
273
|
+
else None
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
token_verifier = self.get_token_verifier(
|
|
277
|
+
algorithm=algorithm,
|
|
278
|
+
audience=audience,
|
|
279
|
+
required_scopes=required_scopes,
|
|
280
|
+
timeout_seconds=timeout_seconds,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
init_kwargs = {
|
|
284
|
+
"upstream_authorization_endpoint": str(
|
|
285
|
+
self.oidc_config.authorization_endpoint
|
|
286
|
+
),
|
|
287
|
+
"upstream_token_endpoint": str(self.oidc_config.token_endpoint),
|
|
288
|
+
"upstream_client_id": client_id,
|
|
289
|
+
"upstream_client_secret": client_secret,
|
|
290
|
+
"upstream_revocation_endpoint": revocation_endpoint,
|
|
291
|
+
"token_verifier": token_verifier,
|
|
292
|
+
"base_url": base_url,
|
|
293
|
+
"service_documentation_url": self.oidc_config.service_documentation,
|
|
294
|
+
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
295
|
+
"client_storage": client_storage,
|
|
296
|
+
"token_endpoint_auth_method": token_endpoint_auth_method,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if redirect_path:
|
|
300
|
+
init_kwargs["redirect_path"] = redirect_path
|
|
301
|
+
|
|
302
|
+
if audience:
|
|
303
|
+
extra_params = {"audience": audience}
|
|
304
|
+
init_kwargs["extra_authorize_params"] = extra_params
|
|
305
|
+
init_kwargs["extra_token_params"] = extra_params
|
|
306
|
+
|
|
307
|
+
super().__init__(**init_kwargs)
|
|
308
|
+
|
|
309
|
+
def get_oidc_configuration(
|
|
310
|
+
self,
|
|
311
|
+
config_url: AnyHttpUrl,
|
|
312
|
+
strict: bool | None,
|
|
313
|
+
timeout_seconds: int | None,
|
|
314
|
+
) -> OIDCConfiguration:
|
|
315
|
+
"""Gets the OIDC configuration for the specified configuration URL.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
config_url: The OIDC configuration URL
|
|
319
|
+
strict: The strict flag for the configuration
|
|
320
|
+
timeout_seconds: HTTP request timeout in seconds
|
|
321
|
+
"""
|
|
322
|
+
return OIDCConfiguration.get_oidc_configuration(
|
|
323
|
+
config_url, strict=strict, timeout_seconds=timeout_seconds
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def get_token_verifier(
|
|
327
|
+
self,
|
|
328
|
+
*,
|
|
329
|
+
algorithm: str | None = None,
|
|
330
|
+
audience: str | None = None,
|
|
331
|
+
required_scopes: list[str] | None = None,
|
|
332
|
+
timeout_seconds: int | None = None,
|
|
333
|
+
) -> TokenVerifier:
|
|
334
|
+
"""Creates the token verifier for the specified OIDC configuration and arguments.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
algorithm: Optional token verifier algorithm
|
|
338
|
+
audience: Optional token verifier audience
|
|
339
|
+
required_scopes: Optional token verifier required_scopes
|
|
340
|
+
timeout_seconds: HTTP request timeout in seconds
|
|
341
|
+
"""
|
|
342
|
+
return JWTVerifier(
|
|
343
|
+
jwks_uri=str(self.oidc_config.jwks_uri),
|
|
344
|
+
issuer=str(self.oidc_config.issuer),
|
|
345
|
+
algorithm=algorithm,
|
|
346
|
+
audience=audience,
|
|
347
|
+
required_scopes=required_scopes,
|
|
348
|
+
)
|