fastmcp 2.12.5__py3-none-any.whl → 2.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. fastmcp/__init__.py +2 -23
  2. fastmcp/cli/__init__.py +0 -3
  3. fastmcp/cli/__main__.py +5 -0
  4. fastmcp/cli/cli.py +19 -33
  5. fastmcp/cli/install/claude_code.py +6 -6
  6. fastmcp/cli/install/claude_desktop.py +3 -3
  7. fastmcp/cli/install/cursor.py +18 -12
  8. fastmcp/cli/install/gemini_cli.py +3 -3
  9. fastmcp/cli/install/mcp_json.py +3 -3
  10. fastmcp/cli/install/shared.py +0 -15
  11. fastmcp/cli/run.py +13 -8
  12. fastmcp/cli/tasks.py +110 -0
  13. fastmcp/client/__init__.py +9 -9
  14. fastmcp/client/auth/oauth.py +123 -225
  15. fastmcp/client/client.py +697 -95
  16. fastmcp/client/elicitation.py +11 -5
  17. fastmcp/client/logging.py +18 -14
  18. fastmcp/client/messages.py +7 -5
  19. fastmcp/client/oauth_callback.py +85 -171
  20. fastmcp/client/roots.py +2 -1
  21. fastmcp/client/sampling.py +1 -1
  22. fastmcp/client/tasks.py +614 -0
  23. fastmcp/client/transports.py +117 -30
  24. fastmcp/contrib/component_manager/__init__.py +1 -1
  25. fastmcp/contrib/component_manager/component_manager.py +2 -2
  26. fastmcp/contrib/component_manager/component_service.py +10 -26
  27. fastmcp/contrib/mcp_mixin/README.md +32 -1
  28. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  29. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  30. fastmcp/dependencies.py +25 -0
  31. fastmcp/experimental/sampling/handlers/openai.py +3 -3
  32. fastmcp/experimental/server/openapi/__init__.py +20 -21
  33. fastmcp/experimental/utilities/openapi/__init__.py +16 -47
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +54 -51
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +43 -21
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +161 -61
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -14
  45. fastmcp/server/auth/auth.py +197 -46
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1469 -298
  50. fastmcp/server/auth/oidc_proxy.py +91 -20
  51. fastmcp/server/auth/providers/auth0.py +40 -21
  52. fastmcp/server/auth/providers/aws.py +29 -3
  53. fastmcp/server/auth/providers/azure.py +312 -131
  54. fastmcp/server/auth/providers/debug.py +114 -0
  55. fastmcp/server/auth/providers/descope.py +86 -29
  56. fastmcp/server/auth/providers/discord.py +308 -0
  57. fastmcp/server/auth/providers/github.py +29 -8
  58. fastmcp/server/auth/providers/google.py +48 -9
  59. fastmcp/server/auth/providers/in_memory.py +29 -5
  60. fastmcp/server/auth/providers/introspection.py +281 -0
  61. fastmcp/server/auth/providers/jwt.py +48 -31
  62. fastmcp/server/auth/providers/oci.py +233 -0
  63. fastmcp/server/auth/providers/scalekit.py +238 -0
  64. fastmcp/server/auth/providers/supabase.py +188 -0
  65. fastmcp/server/auth/providers/workos.py +35 -17
  66. fastmcp/server/context.py +236 -116
  67. fastmcp/server/dependencies.py +503 -18
  68. fastmcp/server/elicitation.py +286 -48
  69. fastmcp/server/event_store.py +177 -0
  70. fastmcp/server/http.py +71 -20
  71. fastmcp/server/low_level.py +165 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +50 -39
  76. fastmcp/server/middleware/middleware.py +29 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi/__init__.py +35 -0
  80. fastmcp/{experimental/server → server}/openapi/components.py +15 -10
  81. fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
  82. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  83. fastmcp/server/proxy.py +72 -48
  84. fastmcp/server/server.py +1415 -733
  85. fastmcp/server/tasks/__init__.py +21 -0
  86. fastmcp/server/tasks/capabilities.py +22 -0
  87. fastmcp/server/tasks/config.py +89 -0
  88. fastmcp/server/tasks/converters.py +205 -0
  89. fastmcp/server/tasks/handlers.py +356 -0
  90. fastmcp/server/tasks/keys.py +93 -0
  91. fastmcp/server/tasks/protocol.py +355 -0
  92. fastmcp/server/tasks/subscriptions.py +205 -0
  93. fastmcp/settings.py +125 -113
  94. fastmcp/tools/__init__.py +1 -1
  95. fastmcp/tools/tool.py +138 -55
  96. fastmcp/tools/tool_manager.py +30 -112
  97. fastmcp/tools/tool_transform.py +12 -21
  98. fastmcp/utilities/cli.py +67 -28
  99. fastmcp/utilities/components.py +10 -5
  100. fastmcp/utilities/inspect.py +79 -23
  101. fastmcp/utilities/json_schema.py +4 -4
  102. fastmcp/utilities/json_schema_type.py +8 -8
  103. fastmcp/utilities/logging.py +118 -8
  104. fastmcp/utilities/mcp_config.py +1 -2
  105. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  106. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  107. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  108. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
  109. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  110. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  111. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  112. fastmcp/utilities/openapi/__init__.py +63 -0
  113. fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
  114. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  115. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
  116. fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
  117. fastmcp/utilities/tests.py +92 -5
  118. fastmcp/utilities/types.py +86 -16
  119. fastmcp/utilities/ui.py +626 -0
  120. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
  121. fastmcp-2.14.0.dist-info/RECORD +156 -0
  122. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
  123. fastmcp/cli/claude.py +0 -135
  124. fastmcp/server/auth/providers/bearer.py +0 -25
  125. fastmcp/server/openapi.py +0 -1083
  126. fastmcp/utilities/openapi.py +0 -1568
  127. fastmcp/utilities/storage.py +0 -204
  128. fastmcp-2.12.5.dist-info/RECORD +0 -134
  129. fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  130. fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
  131. fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
  132. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
  133. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.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=".env",
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 | None | NotSetT = NotSet,
187
- jwks_uri: str | None | NotSetT = NotSet,
188
- issuer: str | None | NotSetT = NotSet,
189
- audience: str | list[str] | None | NotSetT = NotSet,
190
- algorithm: str | None | NotSetT = NotSet,
191
- required_scopes: list[str] | None | NotSetT = NotSet,
192
- base_url: AnyHttpUrl | str | None | NotSetT = NotSet,
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 the JWT token verifier.
196
-
197
- Args:
198
- public_key: For asymmetric algorithms (RS256, ES256, etc.): PEM-encoded public key.
199
- For symmetric algorithms (HS256, HS384, HS512): The shared secret string.
200
- jwks_uri: URI to fetch JSON Web Key Set (only for asymmetric algorithms)
201
- issuer: Expected issuer claim
202
- audience: Expected audience claim(s)
203
- algorithm: JWT signing algorithm. Supported algorithms:
204
- - Asymmetric: RS256/384/512, ES256/384/512, PS256/384/512 (default: RS256)
205
- - Symmetric: HS256, HS384, HS512
206
- required_scopes: Required scopes for all tokens
207
- base_url: Base URL for TokenVerifier protocol
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
- Validates the provided JWT bearer token.
369
+ Validate a JWT bearer token and return an AccessToken when the token is valid.
369
370
 
370
- Args:
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 object if valid, None if invalid or expired
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 = claims.get("client_id") or claims.get("sub") or "unknown"
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
- if claims.get("iss") != self.issuer:
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: dict[str, object] = {
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) # type: ignore[arg-type]
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