fastmcp 2.12.5__py3-none-any.whl → 2.13.2__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 +11 -11
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +18 -12
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/run.py +13 -8
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +115 -217
- fastmcp/client/client.py +105 -39
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +80 -25
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +6 -6
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -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 +14 -15
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +2 -2
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +22 -19
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +14 -9
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +107 -17
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +70 -43
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1510 -289
- fastmcp/server/auth/oidc_proxy.py +84 -20
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +312 -131
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +86 -29
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +48 -9
- fastmcp/server/auth/providers/in_memory.py +27 -3
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +35 -17
- fastmcp/server/context.py +177 -51
- fastmcp/server/dependencies.py +39 -12
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +56 -17
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +50 -39
- fastmcp/server/middleware/middleware.py +29 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +10 -6
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +725 -242
- fastmcp/settings.py +24 -10
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +70 -23
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -10
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +118 -8
- 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/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +4 -4
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +85 -4
- fastmcp/utilities/types.py +78 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -16,6 +16,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
16
16
|
from typing_extensions import TypedDict
|
|
17
17
|
|
|
18
18
|
from fastmcp.server.auth import AccessToken, TokenVerifier
|
|
19
|
+
from fastmcp.settings import ENV_FILE
|
|
19
20
|
from fastmcp.utilities.auth import parse_scopes
|
|
20
21
|
from fastmcp.utilities.logging import get_logger
|
|
21
22
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
@@ -143,13 +144,13 @@ class JWTVerifierSettings(BaseSettings):
|
|
|
143
144
|
|
|
144
145
|
model_config = SettingsConfigDict(
|
|
145
146
|
env_prefix="FASTMCP_SERVER_AUTH_JWT_",
|
|
146
|
-
env_file=
|
|
147
|
+
env_file=ENV_FILE,
|
|
147
148
|
extra="ignore",
|
|
148
149
|
)
|
|
149
150
|
|
|
150
151
|
public_key: str | None = None
|
|
151
152
|
jwks_uri: str | None = None
|
|
152
|
-
issuer: str | None = None
|
|
153
|
+
issuer: str | list[str] | None = None
|
|
153
154
|
algorithm: str | None = None
|
|
154
155
|
audience: str | list[str] | None = None
|
|
155
156
|
required_scopes: list[str] | None = None
|
|
@@ -183,28 +184,28 @@ class JWTVerifier(TokenVerifier):
|
|
|
183
184
|
def __init__(
|
|
184
185
|
self,
|
|
185
186
|
*,
|
|
186
|
-
public_key: str |
|
|
187
|
-
jwks_uri: str |
|
|
188
|
-
issuer: str |
|
|
189
|
-
audience: str | list[str] |
|
|
190
|
-
algorithm: str |
|
|
191
|
-
required_scopes: list[str] |
|
|
192
|
-
base_url: AnyHttpUrl | str |
|
|
187
|
+
public_key: str | NotSetT | None = NotSet,
|
|
188
|
+
jwks_uri: str | NotSetT | None = NotSet,
|
|
189
|
+
issuer: str | list[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,
|
|
193
194
|
):
|
|
194
195
|
"""
|
|
195
|
-
Initialize
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
public_key
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
196
|
+
Initialize a JWTVerifier configured to validate JWTs using either a static key or a JWKS endpoint.
|
|
197
|
+
|
|
198
|
+
Parameters:
|
|
199
|
+
public_key (str | NotSetT | None): PEM-encoded public key for asymmetric algorithms or shared secret for symmetric algorithms.
|
|
200
|
+
jwks_uri (str | NotSetT | None): URI to fetch a JSON Web Key Set; used when verifying tokens with remote JWKS.
|
|
201
|
+
issuer (str | list[str] | NotSetT | None): Expected issuer claim value or list of allowed issuer values.
|
|
202
|
+
audience (str | list[str] | NotSetT | None): Expected audience claim value or list of allowed audience values.
|
|
203
|
+
algorithm (str | NotSetT | None): JWT signing algorithm to accept (default: "RS256"). Supported: HS256/384/512, RS256/384/512, ES256/384/512, PS256/384/512.
|
|
204
|
+
required_scopes (list[str] | NotSetT | None): Scopes that must be present in validated tokens.
|
|
205
|
+
base_url (AnyHttpUrl | str | NotSetT | None): Base URL passed to the parent TokenVerifier.
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
ValueError: If neither or both of `public_key` and `jwks_uri` are provided, or if `algorithm` is unsupported.
|
|
208
209
|
"""
|
|
209
210
|
settings = JWTVerifierSettings.model_validate(
|
|
210
211
|
{
|
|
@@ -282,7 +283,7 @@ class JWTVerifier(TokenVerifier):
|
|
|
282
283
|
return await self._get_jwks_key(kid)
|
|
283
284
|
|
|
284
285
|
except Exception as e:
|
|
285
|
-
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
|
|
286
287
|
|
|
287
288
|
async def _get_jwks_key(self, kid: str | None) -> str:
|
|
288
289
|
"""Fetch key from JWKS with simple caching."""
|
|
@@ -341,10 +342,10 @@ class JWTVerifier(TokenVerifier):
|
|
|
341
342
|
raise ValueError("No keys found in JWKS")
|
|
342
343
|
|
|
343
344
|
except httpx.HTTPError as e:
|
|
344
|
-
raise ValueError(f"Failed to fetch JWKS: {e}")
|
|
345
|
+
raise ValueError(f"Failed to fetch JWKS: {e}") from e
|
|
345
346
|
except Exception as e:
|
|
346
347
|
self.logger.debug(f"JWKS fetch failed: {e}")
|
|
347
|
-
raise ValueError(f"Failed to fetch JWKS: {e}")
|
|
348
|
+
raise ValueError(f"Failed to fetch JWKS: {e}") from e
|
|
348
349
|
|
|
349
350
|
def _extract_scopes(self, claims: dict[str, Any]) -> list[str]:
|
|
350
351
|
"""
|
|
@@ -365,13 +366,13 @@ class JWTVerifier(TokenVerifier):
|
|
|
365
366
|
|
|
366
367
|
async def load_access_token(self, token: str) -> AccessToken | None:
|
|
367
368
|
"""
|
|
368
|
-
|
|
369
|
+
Validate a JWT bearer token and return an AccessToken when the token is valid.
|
|
369
370
|
|
|
370
|
-
|
|
371
|
-
token: The JWT token string to validate
|
|
371
|
+
Parameters:
|
|
372
|
+
token (str): The JWT bearer token string to validate.
|
|
372
373
|
|
|
373
374
|
Returns:
|
|
374
|
-
AccessToken
|
|
375
|
+
AccessToken | None: An AccessToken populated from token claims if the token is valid; `None` if the token is expired, has an invalid signature or format, fails issuer/audience/scope validation, or any other validation error occurs.
|
|
375
376
|
"""
|
|
376
377
|
try:
|
|
377
378
|
# Get verification key (static or from JWKS)
|
|
@@ -381,7 +382,12 @@ class JWTVerifier(TokenVerifier):
|
|
|
381
382
|
claims = self.jwt.decode(token, verification_key)
|
|
382
383
|
|
|
383
384
|
# Extract client ID early for logging
|
|
384
|
-
client_id =
|
|
385
|
+
client_id = (
|
|
386
|
+
claims.get("client_id")
|
|
387
|
+
or claims.get("azp")
|
|
388
|
+
or claims.get("sub")
|
|
389
|
+
or "unknown"
|
|
390
|
+
)
|
|
385
391
|
|
|
386
392
|
# Validate expiration
|
|
387
393
|
exp = claims.get("exp")
|
|
@@ -395,7 +401,18 @@ class JWTVerifier(TokenVerifier):
|
|
|
395
401
|
# Validate issuer - note we use issuer instead of issuer_url here because
|
|
396
402
|
# issuer is optional, allowing users to make this check optional
|
|
397
403
|
if self.issuer:
|
|
398
|
-
|
|
404
|
+
iss = claims.get("iss")
|
|
405
|
+
|
|
406
|
+
# Handle different combinations of issuer types
|
|
407
|
+
issuer_valid = False
|
|
408
|
+
if isinstance(self.issuer, list):
|
|
409
|
+
# self.issuer is a list - check if token issuer matches any expected issuer
|
|
410
|
+
issuer_valid = iss in self.issuer
|
|
411
|
+
else:
|
|
412
|
+
# self.issuer is a string - check for equality
|
|
413
|
+
issuer_valid = iss == self.issuer
|
|
414
|
+
|
|
415
|
+
if not issuer_valid:
|
|
399
416
|
self.logger.debug(
|
|
400
417
|
"Token validation failed: issuer mismatch for client %s",
|
|
401
418
|
client_id,
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""OCI OIDC provider for FastMCP.
|
|
2
|
+
|
|
3
|
+
The pull request for the provider is submitted to fastmcp.
|
|
4
|
+
|
|
5
|
+
This module provides OIDC Implementation to integrate MCP servers with OCI.
|
|
6
|
+
You only need OCI Identity Domain's discovery URL, client ID, client secret, and base URL.
|
|
7
|
+
|
|
8
|
+
Post Authentication, you get OCI IAM domain access token. That is not authorized to invoke OCI control plane.
|
|
9
|
+
You need to exchange the IAM domain access token for OCI UPST token to invoke OCI control plane APIs.
|
|
10
|
+
The sample code below has get_oci_signer function that returns OCI TokenExchangeSigner object.
|
|
11
|
+
You can use the signer object to create OCI service object.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
```python
|
|
15
|
+
from fastmcp import FastMCP
|
|
16
|
+
from fastmcp.server.auth.providers.oci import OCIProvider
|
|
17
|
+
from fastmcp.server.dependencies import get_access_token
|
|
18
|
+
from fastmcp.utilities.logging import get_logger
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
# Load configuration from environment
|
|
23
|
+
FASTMCP_SERVER_AUTH_OCI_CONFIG_URL = os.environ["FASTMCP_SERVER_AUTH_OCI_CONFIG_URL"]
|
|
24
|
+
FASTMCP_SERVER_AUTH_OCI_CLIENT_ID = os.environ["FASTMCP_SERVER_AUTH_OCI_CLIENT_ID"]
|
|
25
|
+
FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET = os.environ["FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET"]
|
|
26
|
+
FASTMCP_SERVER_AUTH_OCI_IAM_GUID = os.environ["FASTMCP_SERVER_AUTH_OCI_IAM_GUID"]
|
|
27
|
+
|
|
28
|
+
import oci
|
|
29
|
+
from oci.auth.signers import TokenExchangeSigner
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
# Simple OCI OIDC protection
|
|
34
|
+
auth = OCIProvider(
|
|
35
|
+
config_url=FASTMCP_SERVER_AUTH_OCI_CONFIG_URL, #config URL is the OCI IAM Domain OIDC discovery URL.
|
|
36
|
+
client_id=FASTMCP_SERVER_AUTH_OCI_CLIENT_ID, #This is same as the client ID configured for the OCI IAM Domain Integrated Application
|
|
37
|
+
client_secret=FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET, #This is same as the client secret configured for the OCI IAM Domain Integrated Application
|
|
38
|
+
required_scopes=["openid", "profile", "email"],
|
|
39
|
+
redirect_path="/auth/callback",
|
|
40
|
+
base_url="http://localhost:8000",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# NOTE: For production use, replace this with a thread-safe cache implementation
|
|
44
|
+
# such as threading.Lock-protected dict or a proper caching library
|
|
45
|
+
_global_token_cache = {} #In memory cache for OCI session token signer
|
|
46
|
+
|
|
47
|
+
def get_oci_signer() -> TokenExchangeSigner:
|
|
48
|
+
|
|
49
|
+
authntoken = get_access_token()
|
|
50
|
+
tokenID = authntoken.claims.get("jti")
|
|
51
|
+
token = authntoken.token
|
|
52
|
+
|
|
53
|
+
#Check if the signer exists for the token ID in memory cache
|
|
54
|
+
cached_signer = _global_token_cache.get(tokenID)
|
|
55
|
+
logger.debug(f"Global cached signer: {cached_signer}")
|
|
56
|
+
if cached_signer:
|
|
57
|
+
logger.debug(f"Using globally cached signer for token ID: {tokenID}")
|
|
58
|
+
return cached_signer
|
|
59
|
+
|
|
60
|
+
#If the signer is not yet created for the token then create new OCI signer object
|
|
61
|
+
logger.debug(f"Creating new signer for token ID: {tokenID}")
|
|
62
|
+
signer = TokenExchangeSigner(
|
|
63
|
+
jwt_or_func=token,
|
|
64
|
+
oci_domain_id=FASTMCP_SERVER_AUTH_OCI_IAM_GUID.split(".")[0], #This is same as IAM GUID configured for the OCI IAM Domain
|
|
65
|
+
client_id=FASTMCP_SERVER_AUTH_OCI_CLIENT_ID, #This is same as the client ID configured for the OCI IAM Domain Integrated Application
|
|
66
|
+
client_secret=FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET #This is same as the client secret configured for the OCI IAM Domain Integrated Application
|
|
67
|
+
)
|
|
68
|
+
logger.debug(f"Signer {signer} created for token ID: {tokenID}")
|
|
69
|
+
|
|
70
|
+
#Cache the signer object in memory cache
|
|
71
|
+
_global_token_cache[tokenID] = signer
|
|
72
|
+
logger.debug(f"Signer cached for token ID: {tokenID}")
|
|
73
|
+
|
|
74
|
+
return signer
|
|
75
|
+
|
|
76
|
+
mcp = FastMCP("My Protected Server", auth=auth)
|
|
77
|
+
```
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
from key_value.aio.protocols import AsyncKeyValue
|
|
81
|
+
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
|
82
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
83
|
+
|
|
84
|
+
from fastmcp.server.auth.oidc_proxy import OIDCProxy
|
|
85
|
+
from fastmcp.settings import ENV_FILE
|
|
86
|
+
from fastmcp.utilities.auth import parse_scopes
|
|
87
|
+
from fastmcp.utilities.logging import get_logger
|
|
88
|
+
from fastmcp.utilities.types import NotSet, NotSetT
|
|
89
|
+
|
|
90
|
+
logger = get_logger(__name__)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class OCIProviderSettings(BaseSettings):
|
|
94
|
+
"""Settings for OCI IAM domain OIDC provider."""
|
|
95
|
+
|
|
96
|
+
model_config = SettingsConfigDict(
|
|
97
|
+
env_prefix="FASTMCP_SERVER_AUTH_OCI_",
|
|
98
|
+
env_file=ENV_FILE,
|
|
99
|
+
extra="ignore",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
config_url: AnyHttpUrl | None = None
|
|
103
|
+
client_id: str | None = None
|
|
104
|
+
client_secret: SecretStr | None = None
|
|
105
|
+
audience: str | None = None
|
|
106
|
+
base_url: AnyHttpUrl | None = None
|
|
107
|
+
issuer_url: AnyHttpUrl | None = None
|
|
108
|
+
redirect_path: str | None = None
|
|
109
|
+
required_scopes: list[str] | None = None
|
|
110
|
+
allowed_client_redirect_uris: list[str] | None = None
|
|
111
|
+
jwt_signing_key: str | bytes | None = None
|
|
112
|
+
|
|
113
|
+
@field_validator("required_scopes", mode="before")
|
|
114
|
+
@classmethod
|
|
115
|
+
def _parse_scopes(cls, v):
|
|
116
|
+
return parse_scopes(v)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class OCIProvider(OIDCProxy):
|
|
120
|
+
"""An OCI IAM Domain provider implementation for FastMCP.
|
|
121
|
+
|
|
122
|
+
This provider is a complete OCI integration that's ready to use with
|
|
123
|
+
just the configuration URL, client ID, client secret, and base URL.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
```python
|
|
127
|
+
from fastmcp import FastMCP
|
|
128
|
+
from fastmcp.server.auth.providers.oci import OCIProvider
|
|
129
|
+
|
|
130
|
+
# Simple OCI OIDC protection
|
|
131
|
+
auth = OCIProvider(
|
|
132
|
+
config_url=FASTMCP_SERVER_AUTH_OCI_CONFIG_URL, #config URL is the OCI IAM Domain OIDC discovery URL.
|
|
133
|
+
client_id=FASTMCP_SERVER_AUTH_OCI_CLIENT_ID, #This is same as the client ID configured for the OCI IAM Domain Integrated Application
|
|
134
|
+
client_secret=FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET, #This is same as the client secret configured for the OCI IAM Domain Integrated Application
|
|
135
|
+
base_url="http://localhost:8000",
|
|
136
|
+
required_scopes=["openid", "profile", "email"],
|
|
137
|
+
redirect_path="/auth/callback",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
mcp = FastMCP("My Protected Server", auth=auth)
|
|
141
|
+
```
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
*,
|
|
147
|
+
config_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
148
|
+
client_id: str | NotSetT = NotSet,
|
|
149
|
+
client_secret: str | NotSetT = NotSet,
|
|
150
|
+
audience: str | NotSetT = NotSet,
|
|
151
|
+
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
152
|
+
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
153
|
+
required_scopes: list[str] | NotSetT = NotSet,
|
|
154
|
+
redirect_path: str | NotSetT = NotSet,
|
|
155
|
+
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
156
|
+
client_storage: AsyncKeyValue | None = None,
|
|
157
|
+
jwt_signing_key: str | bytes | NotSetT = NotSet,
|
|
158
|
+
require_authorization_consent: bool = True,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Initialize OCI OIDC provider.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
config_url: OCI OIDC Discovery URL
|
|
164
|
+
client_id: OCI IAM Domain Integrated Application client id
|
|
165
|
+
client_secret: OCI Integrated Application client secret
|
|
166
|
+
audience: OCI API audience (optional)
|
|
167
|
+
base_url: Public URL where OIDC endpoints will be accessible (includes any mount path)
|
|
168
|
+
issuer_url: Issuer URL for OCI IAM Domain metadata. This will override issuer URL from the discovery URL.
|
|
169
|
+
required_scopes: Required OCI scopes (defaults to ["openid"])
|
|
170
|
+
redirect_path: Redirect path configured in OCI IAM Domain Integrated Application. The default is "/auth/callback".
|
|
171
|
+
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
overrides = {
|
|
175
|
+
k: v
|
|
176
|
+
for k, v in {
|
|
177
|
+
"config_url": config_url,
|
|
178
|
+
"client_id": client_id,
|
|
179
|
+
"client_secret": client_secret,
|
|
180
|
+
"audience": audience,
|
|
181
|
+
"base_url": base_url,
|
|
182
|
+
"issuer_url": issuer_url,
|
|
183
|
+
"required_scopes": required_scopes,
|
|
184
|
+
"redirect_path": redirect_path,
|
|
185
|
+
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
186
|
+
"jwt_signing_key": jwt_signing_key,
|
|
187
|
+
}.items()
|
|
188
|
+
if v is not NotSet
|
|
189
|
+
}
|
|
190
|
+
settings = OCIProviderSettings(**overrides)
|
|
191
|
+
|
|
192
|
+
if not settings.config_url:
|
|
193
|
+
raise ValueError(
|
|
194
|
+
"config_url is required - set via parameter or FASTMCP_SERVER_AUTH_OCI_CONFIG_URL"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if not settings.client_id:
|
|
198
|
+
raise ValueError(
|
|
199
|
+
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_OCI_CLIENT_ID"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if not settings.client_secret:
|
|
203
|
+
raise ValueError(
|
|
204
|
+
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if not settings.base_url:
|
|
208
|
+
raise ValueError(
|
|
209
|
+
"base_url is required - set via parameter or FASTMCP_SERVER_AUTH_OCI_BASE_URL"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
oci_required_scopes = settings.required_scopes or ["openid"]
|
|
213
|
+
|
|
214
|
+
super().__init__(
|
|
215
|
+
config_url=settings.config_url,
|
|
216
|
+
client_id=settings.client_id,
|
|
217
|
+
client_secret=settings.client_secret.get_secret_value(),
|
|
218
|
+
audience=settings.audience,
|
|
219
|
+
base_url=settings.base_url,
|
|
220
|
+
issuer_url=settings.issuer_url,
|
|
221
|
+
redirect_path=settings.redirect_path,
|
|
222
|
+
required_scopes=oci_required_scopes,
|
|
223
|
+
allowed_client_redirect_uris=settings.allowed_client_redirect_uris,
|
|
224
|
+
client_storage=client_storage,
|
|
225
|
+
jwt_signing_key=settings.jwt_signing_key,
|
|
226
|
+
require_authorization_consent=require_authorization_consent,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
logger.debug(
|
|
230
|
+
"Initialized OCI OAuth provider for client %s with scopes: %s",
|
|
231
|
+
settings.client_id,
|
|
232
|
+
oci_required_scopes,
|
|
233
|
+
)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Scalekit authentication provider for FastMCP.
|
|
2
|
+
|
|
3
|
+
This module provides ScalekitProvider - a complete authentication solution that integrates
|
|
4
|
+
with Scalekit's OAuth 2.1 and OpenID Connect services, supporting Resource Server
|
|
5
|
+
authentication for seamless MCP client authentication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from pydantic import AnyHttpUrl, field_validator, model_validator
|
|
12
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
13
|
+
from starlette.responses import JSONResponse
|
|
14
|
+
from starlette.routing import Route
|
|
15
|
+
|
|
16
|
+
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
|
|
17
|
+
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
18
|
+
from fastmcp.settings import ENV_FILE
|
|
19
|
+
from fastmcp.utilities.auth import parse_scopes
|
|
20
|
+
from fastmcp.utilities.logging import get_logger
|
|
21
|
+
from fastmcp.utilities.types import NotSet, NotSetT
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ScalekitProviderSettings(BaseSettings):
|
|
27
|
+
model_config = SettingsConfigDict(
|
|
28
|
+
env_prefix="FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_",
|
|
29
|
+
env_file=ENV_FILE,
|
|
30
|
+
extra="ignore",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
environment_url: AnyHttpUrl
|
|
34
|
+
resource_id: str
|
|
35
|
+
base_url: AnyHttpUrl | None = None
|
|
36
|
+
mcp_url: AnyHttpUrl | None = None
|
|
37
|
+
required_scopes: list[str] | None = None
|
|
38
|
+
|
|
39
|
+
@field_validator("required_scopes", mode="before")
|
|
40
|
+
@classmethod
|
|
41
|
+
def _parse_scopes(cls, value: object):
|
|
42
|
+
return parse_scopes(value)
|
|
43
|
+
|
|
44
|
+
@model_validator(mode="after")
|
|
45
|
+
def _resolve_base_url(self):
|
|
46
|
+
resolved = self.base_url or self.mcp_url
|
|
47
|
+
if resolved is None:
|
|
48
|
+
msg = "Either base_url or mcp_url must be provided for ScalekitProvider"
|
|
49
|
+
raise ValueError(msg)
|
|
50
|
+
|
|
51
|
+
object.__setattr__(self, "base_url", resolved)
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ScalekitProvider(RemoteAuthProvider):
|
|
56
|
+
"""Scalekit resource server provider for OAuth 2.1 authentication.
|
|
57
|
+
|
|
58
|
+
This provider implements Scalekit integration using resource server pattern.
|
|
59
|
+
FastMCP acts as a protected resource server that validates access tokens issued
|
|
60
|
+
by Scalekit's authorization server.
|
|
61
|
+
|
|
62
|
+
IMPORTANT SETUP REQUIREMENTS:
|
|
63
|
+
|
|
64
|
+
1. Create an MCP Server in Scalekit Dashboard:
|
|
65
|
+
- Go to your [Scalekit Dashboard](https://app.scalekit.com/)
|
|
66
|
+
- Navigate to MCP Servers section
|
|
67
|
+
- Register a new MCP Server with appropriate scopes
|
|
68
|
+
- Ensure the Resource Identifier matches exactly what you configure as MCP URL
|
|
69
|
+
- Note the Resource ID
|
|
70
|
+
|
|
71
|
+
2. Environment Configuration:
|
|
72
|
+
- Set SCALEKIT_ENVIRONMENT_URL (e.g., https://your-env.scalekit.com)
|
|
73
|
+
- Set SCALEKIT_RESOURCE_ID from your created resource
|
|
74
|
+
- Set BASE_URL to your FastMCP server's public URL
|
|
75
|
+
|
|
76
|
+
For detailed setup instructions, see:
|
|
77
|
+
https://docs.scalekit.com/mcp/overview/
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
```python
|
|
81
|
+
from fastmcp.server.auth.providers.scalekit import ScalekitProvider
|
|
82
|
+
|
|
83
|
+
# Create Scalekit resource server provider
|
|
84
|
+
scalekit_auth = ScalekitProvider(
|
|
85
|
+
environment_url="https://your-env.scalekit.com",
|
|
86
|
+
resource_id="sk_resource_...",
|
|
87
|
+
base_url="https://your-fastmcp-server.com",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Use with FastMCP
|
|
91
|
+
mcp = FastMCP("My App", auth=scalekit_auth)
|
|
92
|
+
```
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
environment_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
99
|
+
client_id: str | NotSetT = NotSet,
|
|
100
|
+
resource_id: str | NotSetT = NotSet,
|
|
101
|
+
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
102
|
+
mcp_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
103
|
+
required_scopes: list[str] | NotSetT = NotSet,
|
|
104
|
+
token_verifier: TokenVerifier | None = None,
|
|
105
|
+
):
|
|
106
|
+
"""Initialize Scalekit resource server provider.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
environment_url: Your Scalekit environment URL (e.g., "https://your-env.scalekit.com")
|
|
110
|
+
resource_id: Your Scalekit resource ID
|
|
111
|
+
base_url: Public URL of this FastMCP server
|
|
112
|
+
required_scopes: Optional list of scopes that must be present in tokens
|
|
113
|
+
token_verifier: Optional token verifier. If None, creates JWT verifier for Scalekit
|
|
114
|
+
"""
|
|
115
|
+
legacy_client_id = client_id is not NotSet
|
|
116
|
+
|
|
117
|
+
settings = ScalekitProviderSettings.model_validate(
|
|
118
|
+
{
|
|
119
|
+
k: v
|
|
120
|
+
for k, v in {
|
|
121
|
+
"environment_url": environment_url,
|
|
122
|
+
"resource_id": resource_id,
|
|
123
|
+
"base_url": base_url,
|
|
124
|
+
"mcp_url": mcp_url,
|
|
125
|
+
"required_scopes": required_scopes,
|
|
126
|
+
}.items()
|
|
127
|
+
if v is not NotSet
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if settings.mcp_url is not None:
|
|
132
|
+
logger.warning(
|
|
133
|
+
"ScalekitProvider parameter 'mcp_url' is deprecated and will be removed in a future release. "
|
|
134
|
+
"Rename it to 'base_url'."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if legacy_client_id:
|
|
138
|
+
logger.warning(
|
|
139
|
+
"ScalekitProvider no longer requires 'client_id'. The parameter is accepted only for backward "
|
|
140
|
+
"compatibility and will be removed in a future release."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
self.environment_url = str(settings.environment_url).rstrip("/")
|
|
144
|
+
self.resource_id = settings.resource_id
|
|
145
|
+
self.required_scopes = settings.required_scopes or []
|
|
146
|
+
base_url_value = str(settings.base_url)
|
|
147
|
+
|
|
148
|
+
logger.debug(
|
|
149
|
+
"Initializing ScalekitProvider: environment_url=%s resource_id=%s base_url=%s required_scopes=%s",
|
|
150
|
+
self.environment_url,
|
|
151
|
+
self.resource_id,
|
|
152
|
+
base_url_value,
|
|
153
|
+
self.required_scopes,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Create default JWT verifier if none provided
|
|
157
|
+
if token_verifier is None:
|
|
158
|
+
logger.debug(
|
|
159
|
+
"Creating default JWTVerifier for Scalekit: jwks_uri=%s issuer=%s required_scopes=%s",
|
|
160
|
+
f"{self.environment_url}/keys",
|
|
161
|
+
self.environment_url,
|
|
162
|
+
self.required_scopes,
|
|
163
|
+
)
|
|
164
|
+
token_verifier = JWTVerifier(
|
|
165
|
+
jwks_uri=f"{self.environment_url}/keys",
|
|
166
|
+
issuer=self.environment_url,
|
|
167
|
+
algorithm="RS256",
|
|
168
|
+
required_scopes=self.required_scopes or None,
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
logger.debug("Using custom token verifier for ScalekitProvider")
|
|
172
|
+
|
|
173
|
+
# Initialize RemoteAuthProvider with Scalekit as the authorization server
|
|
174
|
+
super().__init__(
|
|
175
|
+
token_verifier=token_verifier,
|
|
176
|
+
authorization_servers=[
|
|
177
|
+
AnyHttpUrl(f"{self.environment_url}/resources/{self.resource_id}")
|
|
178
|
+
],
|
|
179
|
+
base_url=base_url_value,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def get_routes(
|
|
183
|
+
self,
|
|
184
|
+
mcp_path: str | None = None,
|
|
185
|
+
) -> list[Route]:
|
|
186
|
+
"""Get OAuth routes including Scalekit authorization server metadata forwarding.
|
|
187
|
+
|
|
188
|
+
This returns the standard protected resource routes plus an authorization server
|
|
189
|
+
metadata endpoint that forwards Scalekit's OAuth metadata to clients.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
|
|
193
|
+
This is used to advertise the resource URL in metadata.
|
|
194
|
+
"""
|
|
195
|
+
# Get the standard protected resource routes from RemoteAuthProvider
|
|
196
|
+
routes = super().get_routes(mcp_path)
|
|
197
|
+
logger.debug(
|
|
198
|
+
"Preparing Scalekit metadata routes: mcp_path=%s resource_id=%s",
|
|
199
|
+
mcp_path,
|
|
200
|
+
self.resource_id,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
async def oauth_authorization_server_metadata(request):
|
|
204
|
+
"""Forward Scalekit OAuth authorization server metadata with FastMCP customizations."""
|
|
205
|
+
try:
|
|
206
|
+
metadata_url = f"{self.environment_url}/.well-known/oauth-authorization-server/resources/{self.resource_id}"
|
|
207
|
+
logger.debug(
|
|
208
|
+
"Fetching Scalekit OAuth metadata: metadata_url=%s", metadata_url
|
|
209
|
+
)
|
|
210
|
+
async with httpx.AsyncClient() as client:
|
|
211
|
+
response = await client.get(metadata_url)
|
|
212
|
+
response.raise_for_status()
|
|
213
|
+
metadata = response.json()
|
|
214
|
+
logger.debug(
|
|
215
|
+
"Scalekit metadata fetched successfully: metadata_keys=%s",
|
|
216
|
+
list(metadata.keys()),
|
|
217
|
+
)
|
|
218
|
+
return JSONResponse(metadata)
|
|
219
|
+
except Exception as e:
|
|
220
|
+
logger.error(f"Failed to fetch Scalekit metadata: {e}")
|
|
221
|
+
return JSONResponse(
|
|
222
|
+
{
|
|
223
|
+
"error": "server_error",
|
|
224
|
+
"error_description": f"Failed to fetch Scalekit metadata: {e}",
|
|
225
|
+
},
|
|
226
|
+
status_code=500,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Add Scalekit authorization server metadata forwarding
|
|
230
|
+
routes.append(
|
|
231
|
+
Route(
|
|
232
|
+
"/.well-known/oauth-authorization-server",
|
|
233
|
+
endpoint=oauth_authorization_server_metadata,
|
|
234
|
+
methods=["GET"],
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return routes
|