fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__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 (175) hide show
  1. fastmcp/_vendor/__init__.py +1 -0
  2. fastmcp/_vendor/docket_di/README.md +7 -0
  3. fastmcp/_vendor/docket_di/__init__.py +163 -0
  4. fastmcp/cli/cli.py +112 -28
  5. fastmcp/cli/install/claude_code.py +1 -5
  6. fastmcp/cli/install/claude_desktop.py +1 -5
  7. fastmcp/cli/install/cursor.py +1 -5
  8. fastmcp/cli/install/gemini_cli.py +1 -5
  9. fastmcp/cli/install/mcp_json.py +1 -6
  10. fastmcp/cli/run.py +146 -5
  11. fastmcp/client/__init__.py +7 -9
  12. fastmcp/client/auth/oauth.py +18 -17
  13. fastmcp/client/client.py +100 -870
  14. fastmcp/client/elicitation.py +1 -1
  15. fastmcp/client/mixins/__init__.py +13 -0
  16. fastmcp/client/mixins/prompts.py +295 -0
  17. fastmcp/client/mixins/resources.py +325 -0
  18. fastmcp/client/mixins/task_management.py +157 -0
  19. fastmcp/client/mixins/tools.py +397 -0
  20. fastmcp/client/sampling/handlers/anthropic.py +2 -2
  21. fastmcp/client/sampling/handlers/openai.py +1 -1
  22. fastmcp/client/tasks.py +3 -3
  23. fastmcp/client/telemetry.py +47 -0
  24. fastmcp/client/transports/__init__.py +38 -0
  25. fastmcp/client/transports/base.py +82 -0
  26. fastmcp/client/transports/config.py +170 -0
  27. fastmcp/client/transports/http.py +145 -0
  28. fastmcp/client/transports/inference.py +154 -0
  29. fastmcp/client/transports/memory.py +90 -0
  30. fastmcp/client/transports/sse.py +89 -0
  31. fastmcp/client/transports/stdio.py +543 -0
  32. fastmcp/contrib/component_manager/README.md +4 -10
  33. fastmcp/contrib/component_manager/__init__.py +1 -2
  34. fastmcp/contrib/component_manager/component_manager.py +95 -160
  35. fastmcp/contrib/component_manager/example.py +1 -1
  36. fastmcp/contrib/mcp_mixin/example.py +4 -4
  37. fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
  38. fastmcp/decorators.py +41 -0
  39. fastmcp/dependencies.py +12 -1
  40. fastmcp/exceptions.py +4 -0
  41. fastmcp/experimental/server/openapi/__init__.py +18 -15
  42. fastmcp/mcp_config.py +13 -4
  43. fastmcp/prompts/__init__.py +6 -3
  44. fastmcp/prompts/function_prompt.py +465 -0
  45. fastmcp/prompts/prompt.py +321 -271
  46. fastmcp/resources/__init__.py +5 -3
  47. fastmcp/resources/function_resource.py +335 -0
  48. fastmcp/resources/resource.py +325 -115
  49. fastmcp/resources/template.py +215 -43
  50. fastmcp/resources/types.py +27 -12
  51. fastmcp/server/__init__.py +2 -2
  52. fastmcp/server/auth/__init__.py +14 -0
  53. fastmcp/server/auth/auth.py +30 -10
  54. fastmcp/server/auth/authorization.py +190 -0
  55. fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
  56. fastmcp/server/auth/oauth_proxy/consent.py +361 -0
  57. fastmcp/server/auth/oauth_proxy/models.py +178 -0
  58. fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
  59. fastmcp/server/auth/oauth_proxy/ui.py +277 -0
  60. fastmcp/server/auth/oidc_proxy.py +2 -2
  61. fastmcp/server/auth/providers/auth0.py +24 -94
  62. fastmcp/server/auth/providers/aws.py +26 -95
  63. fastmcp/server/auth/providers/azure.py +41 -129
  64. fastmcp/server/auth/providers/descope.py +18 -49
  65. fastmcp/server/auth/providers/discord.py +25 -86
  66. fastmcp/server/auth/providers/github.py +23 -87
  67. fastmcp/server/auth/providers/google.py +24 -87
  68. fastmcp/server/auth/providers/introspection.py +60 -79
  69. fastmcp/server/auth/providers/jwt.py +30 -67
  70. fastmcp/server/auth/providers/oci.py +47 -110
  71. fastmcp/server/auth/providers/scalekit.py +23 -61
  72. fastmcp/server/auth/providers/supabase.py +18 -47
  73. fastmcp/server/auth/providers/workos.py +34 -127
  74. fastmcp/server/context.py +372 -419
  75. fastmcp/server/dependencies.py +541 -251
  76. fastmcp/server/elicitation.py +20 -18
  77. fastmcp/server/event_store.py +3 -3
  78. fastmcp/server/http.py +16 -6
  79. fastmcp/server/lifespan.py +198 -0
  80. fastmcp/server/low_level.py +92 -2
  81. fastmcp/server/middleware/__init__.py +5 -1
  82. fastmcp/server/middleware/authorization.py +312 -0
  83. fastmcp/server/middleware/caching.py +101 -54
  84. fastmcp/server/middleware/middleware.py +6 -9
  85. fastmcp/server/middleware/ping.py +70 -0
  86. fastmcp/server/middleware/tool_injection.py +2 -2
  87. fastmcp/server/mixins/__init__.py +7 -0
  88. fastmcp/server/mixins/lifespan.py +217 -0
  89. fastmcp/server/mixins/mcp_operations.py +392 -0
  90. fastmcp/server/mixins/transport.py +342 -0
  91. fastmcp/server/openapi/__init__.py +41 -21
  92. fastmcp/server/openapi/components.py +16 -339
  93. fastmcp/server/openapi/routing.py +34 -118
  94. fastmcp/server/openapi/server.py +67 -392
  95. fastmcp/server/providers/__init__.py +71 -0
  96. fastmcp/server/providers/aggregate.py +261 -0
  97. fastmcp/server/providers/base.py +578 -0
  98. fastmcp/server/providers/fastmcp_provider.py +674 -0
  99. fastmcp/server/providers/filesystem.py +226 -0
  100. fastmcp/server/providers/filesystem_discovery.py +327 -0
  101. fastmcp/server/providers/local_provider/__init__.py +11 -0
  102. fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
  103. fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
  104. fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
  105. fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
  106. fastmcp/server/providers/local_provider/local_provider.py +465 -0
  107. fastmcp/server/providers/openapi/__init__.py +39 -0
  108. fastmcp/server/providers/openapi/components.py +332 -0
  109. fastmcp/server/providers/openapi/provider.py +405 -0
  110. fastmcp/server/providers/openapi/routing.py +109 -0
  111. fastmcp/server/providers/proxy.py +867 -0
  112. fastmcp/server/providers/skills/__init__.py +59 -0
  113. fastmcp/server/providers/skills/_common.py +101 -0
  114. fastmcp/server/providers/skills/claude_provider.py +44 -0
  115. fastmcp/server/providers/skills/directory_provider.py +153 -0
  116. fastmcp/server/providers/skills/skill_provider.py +432 -0
  117. fastmcp/server/providers/skills/vendor_providers.py +142 -0
  118. fastmcp/server/providers/wrapped_provider.py +140 -0
  119. fastmcp/server/proxy.py +34 -700
  120. fastmcp/server/sampling/run.py +341 -2
  121. fastmcp/server/sampling/sampling_tool.py +4 -3
  122. fastmcp/server/server.py +1214 -2171
  123. fastmcp/server/tasks/__init__.py +2 -1
  124. fastmcp/server/tasks/capabilities.py +13 -1
  125. fastmcp/server/tasks/config.py +66 -3
  126. fastmcp/server/tasks/handlers.py +65 -273
  127. fastmcp/server/tasks/keys.py +4 -6
  128. fastmcp/server/tasks/requests.py +474 -0
  129. fastmcp/server/tasks/routing.py +76 -0
  130. fastmcp/server/tasks/subscriptions.py +20 -11
  131. fastmcp/server/telemetry.py +131 -0
  132. fastmcp/server/transforms/__init__.py +244 -0
  133. fastmcp/server/transforms/namespace.py +193 -0
  134. fastmcp/server/transforms/prompts_as_tools.py +175 -0
  135. fastmcp/server/transforms/resources_as_tools.py +190 -0
  136. fastmcp/server/transforms/tool_transform.py +96 -0
  137. fastmcp/server/transforms/version_filter.py +124 -0
  138. fastmcp/server/transforms/visibility.py +526 -0
  139. fastmcp/settings.py +34 -96
  140. fastmcp/telemetry.py +122 -0
  141. fastmcp/tools/__init__.py +10 -3
  142. fastmcp/tools/function_parsing.py +201 -0
  143. fastmcp/tools/function_tool.py +467 -0
  144. fastmcp/tools/tool.py +215 -362
  145. fastmcp/tools/tool_transform.py +38 -21
  146. fastmcp/utilities/async_utils.py +69 -0
  147. fastmcp/utilities/components.py +152 -91
  148. fastmcp/utilities/inspect.py +8 -20
  149. fastmcp/utilities/json_schema.py +12 -5
  150. fastmcp/utilities/json_schema_type.py +17 -15
  151. fastmcp/utilities/lifespan.py +56 -0
  152. fastmcp/utilities/logging.py +12 -4
  153. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  154. fastmcp/utilities/openapi/parser.py +3 -3
  155. fastmcp/utilities/pagination.py +80 -0
  156. fastmcp/utilities/skills.py +253 -0
  157. fastmcp/utilities/tests.py +0 -16
  158. fastmcp/utilities/timeout.py +47 -0
  159. fastmcp/utilities/types.py +1 -1
  160. fastmcp/utilities/versions.py +285 -0
  161. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
  162. fastmcp-3.0.0b1.dist-info/RECORD +228 -0
  163. fastmcp/client/transports.py +0 -1170
  164. fastmcp/contrib/component_manager/component_service.py +0 -209
  165. fastmcp/prompts/prompt_manager.py +0 -117
  166. fastmcp/resources/resource_manager.py +0 -338
  167. fastmcp/server/tasks/converters.py +0 -206
  168. fastmcp/server/tasks/protocol.py +0 -359
  169. fastmcp/tools/tool_manager.py +0 -170
  170. fastmcp/utilities/mcp_config.py +0 -56
  171. fastmcp-2.14.4.dist-info/RECORD +0 -161
  172. /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
  173. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
  174. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
  175. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -23,45 +23,17 @@ from __future__ import annotations
23
23
 
24
24
  import httpx
25
25
  from key_value.aio.protocols import AsyncKeyValue
26
- from pydantic import AnyHttpUrl, SecretStr, field_validator
27
- from pydantic_settings import BaseSettings, SettingsConfigDict
26
+ from pydantic import AnyHttpUrl
28
27
 
29
28
  from fastmcp.server.auth import TokenVerifier
30
29
  from fastmcp.server.auth.auth import AccessToken
31
30
  from fastmcp.server.auth.oauth_proxy import OAuthProxy
32
- from fastmcp.settings import ENV_FILE
33
31
  from fastmcp.utilities.auth import parse_scopes
34
32
  from fastmcp.utilities.logging import get_logger
35
- from fastmcp.utilities.types import NotSet, NotSetT
36
33
 
37
34
  logger = get_logger(__name__)
38
35
 
39
36
 
40
- class GitHubProviderSettings(BaseSettings):
41
- """Settings for GitHub OAuth provider."""
42
-
43
- model_config = SettingsConfigDict(
44
- env_prefix="FASTMCP_SERVER_AUTH_GITHUB_",
45
- env_file=ENV_FILE,
46
- extra="ignore",
47
- )
48
-
49
- client_id: str | None = None
50
- client_secret: SecretStr | None = None
51
- base_url: AnyHttpUrl | str | None = None
52
- issuer_url: AnyHttpUrl | str | None = None
53
- redirect_path: str | None = None
54
- required_scopes: list[str] | None = None
55
- timeout_seconds: int | None = None
56
- allowed_client_redirect_uris: list[str] | None = None
57
- jwt_signing_key: str | None = None
58
-
59
- @field_validator("required_scopes", mode="before")
60
- @classmethod
61
- def _parse_scopes(cls, v):
62
- return parse_scopes(v)
63
-
64
-
65
37
  class GitHubTokenVerifier(TokenVerifier):
66
38
  """Token verifier for GitHub OAuth tokens.
67
39
 
@@ -198,16 +170,16 @@ class GitHubProvider(OAuthProxy):
198
170
  def __init__(
199
171
  self,
200
172
  *,
201
- client_id: str | NotSetT = NotSet,
202
- client_secret: str | NotSetT = NotSet,
203
- base_url: AnyHttpUrl | str | NotSetT = NotSet,
204
- issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
205
- redirect_path: str | NotSetT = NotSet,
206
- required_scopes: list[str] | NotSetT = NotSet,
207
- timeout_seconds: int | NotSetT = NotSet,
208
- allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
173
+ client_id: str,
174
+ client_secret: str,
175
+ base_url: AnyHttpUrl | str,
176
+ issuer_url: AnyHttpUrl | str | None = None,
177
+ redirect_path: str | None = None,
178
+ required_scopes: list[str] | None = None,
179
+ timeout_seconds: int = 10,
180
+ allowed_client_redirect_uris: list[str] | None = None,
209
181
  client_storage: AsyncKeyValue | None = None,
210
- jwt_signing_key: str | bytes | NotSetT = NotSet,
182
+ jwt_signing_key: str | bytes | None = None,
211
183
  require_authorization_consent: bool = True,
212
184
  ):
213
185
  """Initialize GitHub OAuth provider.
@@ -220,7 +192,7 @@ class GitHubProvider(OAuthProxy):
220
192
  to avoid 404s during discovery when mounting under a path.
221
193
  redirect_path: Redirect path configured in GitHub OAuth app (defaults to "/auth/callback")
222
194
  required_scopes: Required GitHub scopes (defaults to ["user"])
223
- timeout_seconds: HTTP request timeout for GitHub API calls
195
+ timeout_seconds: HTTP request timeout for GitHub API calls (defaults to 10)
224
196
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
225
197
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
226
198
  client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
@@ -234,71 +206,35 @@ class GitHubProvider(OAuthProxy):
234
206
  When False, authorization proceeds directly without user confirmation.
235
207
  SECURITY WARNING: Only disable for local development or testing environments.
236
208
  """
237
-
238
- settings = GitHubProviderSettings.model_validate(
239
- {
240
- k: v
241
- for k, v in {
242
- "client_id": client_id,
243
- "client_secret": client_secret,
244
- "base_url": base_url,
245
- "issuer_url": issuer_url,
246
- "redirect_path": redirect_path,
247
- "required_scopes": required_scopes,
248
- "timeout_seconds": timeout_seconds,
249
- "allowed_client_redirect_uris": allowed_client_redirect_uris,
250
- "jwt_signing_key": jwt_signing_key,
251
- }.items()
252
- if v is not NotSet
253
- }
209
+ # Parse scopes if provided as string
210
+ required_scopes_final = (
211
+ parse_scopes(required_scopes) if required_scopes is not None else ["user"]
254
212
  )
255
213
 
256
- # Validate required settings
257
- if not settings.client_id:
258
- raise ValueError(
259
- "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID"
260
- )
261
- if not settings.client_secret:
262
- raise ValueError(
263
- "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET"
264
- )
265
-
266
- # Apply defaults
267
-
268
- timeout_seconds_final = settings.timeout_seconds or 10
269
- required_scopes_final = settings.required_scopes or ["user"]
270
- allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
271
-
272
214
  # Create GitHub token verifier
273
215
  token_verifier = GitHubTokenVerifier(
274
216
  required_scopes=required_scopes_final,
275
- timeout_seconds=timeout_seconds_final,
276
- )
277
-
278
- # Extract secret string from SecretStr
279
- client_secret_str = (
280
- settings.client_secret.get_secret_value() if settings.client_secret else ""
217
+ timeout_seconds=timeout_seconds,
281
218
  )
282
219
 
283
220
  # Initialize OAuth proxy with GitHub endpoints
284
221
  super().__init__(
285
222
  upstream_authorization_endpoint="https://github.com/login/oauth/authorize",
286
223
  upstream_token_endpoint="https://github.com/login/oauth/access_token",
287
- upstream_client_id=settings.client_id,
288
- upstream_client_secret=client_secret_str,
224
+ upstream_client_id=client_id,
225
+ upstream_client_secret=client_secret,
289
226
  token_verifier=token_verifier,
290
- base_url=settings.base_url,
291
- redirect_path=settings.redirect_path,
292
- issuer_url=settings.issuer_url
293
- or settings.base_url, # Default to base_url if not specified
294
- allowed_client_redirect_uris=allowed_client_redirect_uris_final,
227
+ base_url=base_url,
228
+ redirect_path=redirect_path,
229
+ issuer_url=issuer_url or base_url, # Default to base_url if not specified
230
+ allowed_client_redirect_uris=allowed_client_redirect_uris,
295
231
  client_storage=client_storage,
296
- jwt_signing_key=settings.jwt_signing_key,
232
+ jwt_signing_key=jwt_signing_key,
297
233
  require_authorization_consent=require_authorization_consent,
298
234
  )
299
235
 
300
236
  logger.debug(
301
237
  "Initialized GitHub OAuth provider for client %s with scopes: %s",
302
- settings.client_id,
238
+ client_id,
303
239
  required_scopes_final,
304
240
  )
@@ -25,45 +25,17 @@ import time
25
25
 
26
26
  import httpx
27
27
  from key_value.aio.protocols import AsyncKeyValue
28
- from pydantic import AnyHttpUrl, SecretStr, field_validator
29
- from pydantic_settings import BaseSettings, SettingsConfigDict
28
+ from pydantic import AnyHttpUrl
30
29
 
31
30
  from fastmcp.server.auth import TokenVerifier
32
31
  from fastmcp.server.auth.auth import AccessToken
33
32
  from fastmcp.server.auth.oauth_proxy import OAuthProxy
34
- from fastmcp.settings import ENV_FILE
35
33
  from fastmcp.utilities.auth import parse_scopes
36
34
  from fastmcp.utilities.logging import get_logger
37
- from fastmcp.utilities.types import NotSet, NotSetT
38
35
 
39
36
  logger = get_logger(__name__)
40
37
 
41
38
 
42
- class GoogleProviderSettings(BaseSettings):
43
- """Settings for Google OAuth provider."""
44
-
45
- model_config = SettingsConfigDict(
46
- env_prefix="FASTMCP_SERVER_AUTH_GOOGLE_",
47
- env_file=ENV_FILE,
48
- extra="ignore",
49
- )
50
-
51
- client_id: str | None = None
52
- client_secret: SecretStr | None = None
53
- base_url: AnyHttpUrl | str | None = None
54
- issuer_url: AnyHttpUrl | str | None = None
55
- redirect_path: str | None = None
56
- required_scopes: list[str] | None = None
57
- timeout_seconds: int | None = None
58
- allowed_client_redirect_uris: list[str] | None = None
59
- jwt_signing_key: str | None = None
60
-
61
- @field_validator("required_scopes", mode="before")
62
- @classmethod
63
- def _parse_scopes(cls, v):
64
- return parse_scopes(v)
65
-
66
-
67
39
  class GoogleTokenVerifier(TokenVerifier):
68
40
  """Token verifier for Google OAuth tokens.
69
41
 
@@ -214,16 +186,16 @@ class GoogleProvider(OAuthProxy):
214
186
  def __init__(
215
187
  self,
216
188
  *,
217
- client_id: str | NotSetT = NotSet,
218
- client_secret: str | NotSetT = NotSet,
219
- base_url: AnyHttpUrl | str | NotSetT = NotSet,
220
- issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
221
- redirect_path: str | NotSetT = NotSet,
222
- required_scopes: list[str] | NotSetT = NotSet,
223
- timeout_seconds: int | NotSetT = NotSet,
224
- allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
189
+ client_id: str,
190
+ client_secret: str,
191
+ base_url: AnyHttpUrl | str,
192
+ issuer_url: AnyHttpUrl | str | None = None,
193
+ redirect_path: str | None = None,
194
+ required_scopes: list[str] | None = None,
195
+ timeout_seconds: int = 10,
196
+ allowed_client_redirect_uris: list[str] | None = None,
225
197
  client_storage: AsyncKeyValue | None = None,
226
- jwt_signing_key: str | bytes | NotSetT = NotSet,
198
+ jwt_signing_key: str | bytes | None = None,
227
199
  require_authorization_consent: bool = True,
228
200
  extra_authorize_params: dict[str, str] | None = None,
229
201
  ):
@@ -240,7 +212,7 @@ class GoogleProvider(OAuthProxy):
240
212
  - "openid" for OpenID Connect (default)
241
213
  - "https://www.googleapis.com/auth/userinfo.email" for email access
242
214
  - "https://www.googleapis.com/auth/userinfo.profile" for profile info
243
- timeout_seconds: HTTP request timeout for Google API calls
215
+ timeout_seconds: HTTP request timeout for Google API calls (defaults to 10)
244
216
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
245
217
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
246
218
  client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
@@ -258,50 +230,16 @@ class GoogleProvider(OAuthProxy):
258
230
  refresh tokens are returned. You can override these defaults or add additional parameters.
259
231
  Example: {"prompt": "select_account"} to let users choose their Google account.
260
232
  """
261
-
262
- settings = GoogleProviderSettings.model_validate(
263
- {
264
- k: v
265
- for k, v in {
266
- "client_id": client_id,
267
- "client_secret": client_secret,
268
- "base_url": base_url,
269
- "issuer_url": issuer_url,
270
- "redirect_path": redirect_path,
271
- "required_scopes": required_scopes,
272
- "timeout_seconds": timeout_seconds,
273
- "allowed_client_redirect_uris": allowed_client_redirect_uris,
274
- "jwt_signing_key": jwt_signing_key,
275
- }.items()
276
- if v is not NotSet
277
- }
278
- )
279
-
280
- # Validate required settings
281
- if not settings.client_id:
282
- raise ValueError(
283
- "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID"
284
- )
285
- if not settings.client_secret:
286
- raise ValueError(
287
- "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET"
288
- )
289
-
290
- # Apply defaults
291
- timeout_seconds_final = settings.timeout_seconds or 10
233
+ # Parse scopes if provided as string
292
234
  # Google requires at least one scope - openid is the minimal OIDC scope
293
- required_scopes_final = settings.required_scopes or ["openid"]
294
- allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
235
+ required_scopes_final = (
236
+ parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
237
+ )
295
238
 
296
239
  # Create Google token verifier
297
240
  token_verifier = GoogleTokenVerifier(
298
241
  required_scopes=required_scopes_final,
299
- timeout_seconds=timeout_seconds_final,
300
- )
301
-
302
- # Extract secret string from SecretStr
303
- client_secret_str = (
304
- settings.client_secret.get_secret_value() if settings.client_secret else ""
242
+ timeout_seconds=timeout_seconds,
305
243
  )
306
244
 
307
245
  # Set Google-specific defaults for extra authorize params
@@ -320,22 +258,21 @@ class GoogleProvider(OAuthProxy):
320
258
  super().__init__(
321
259
  upstream_authorization_endpoint="https://accounts.google.com/o/oauth2/v2/auth",
322
260
  upstream_token_endpoint="https://oauth2.googleapis.com/token",
323
- upstream_client_id=settings.client_id,
324
- upstream_client_secret=client_secret_str,
261
+ upstream_client_id=client_id,
262
+ upstream_client_secret=client_secret,
325
263
  token_verifier=token_verifier,
326
- base_url=settings.base_url,
327
- redirect_path=settings.redirect_path,
328
- issuer_url=settings.issuer_url
329
- or settings.base_url, # Default to base_url if not specified
330
- allowed_client_redirect_uris=allowed_client_redirect_uris_final,
264
+ base_url=base_url,
265
+ redirect_path=redirect_path,
266
+ issuer_url=issuer_url or base_url, # Default to base_url if not specified
267
+ allowed_client_redirect_uris=allowed_client_redirect_uris,
331
268
  client_storage=client_storage,
332
- jwt_signing_key=settings.jwt_signing_key,
269
+ jwt_signing_key=jwt_signing_key,
333
270
  require_authorization_consent=require_authorization_consent,
334
271
  extra_authorize_params=extra_authorize_params_final,
335
272
  )
336
273
 
337
274
  logger.debug(
338
275
  "Initialized Google OAuth provider for client %s with scopes: %s",
339
- settings.client_id,
276
+ client_id,
340
277
  required_scopes_final,
341
278
  )
@@ -25,41 +25,18 @@ from __future__ import annotations
25
25
 
26
26
  import base64
27
27
  import time
28
- from typing import Any
28
+ from typing import Any, Literal, get_args
29
29
 
30
30
  import httpx
31
- from pydantic import AnyHttpUrl, SecretStr, field_validator
32
- from pydantic_settings import BaseSettings, SettingsConfigDict
31
+ from pydantic import AnyHttpUrl, SecretStr
33
32
 
34
33
  from fastmcp.server.auth import AccessToken, TokenVerifier
35
- from fastmcp.settings import ENV_FILE
36
34
  from fastmcp.utilities.auth import parse_scopes
37
35
  from fastmcp.utilities.logging import get_logger
38
- from fastmcp.utilities.types import NotSet, NotSetT
39
36
 
40
37
  logger = get_logger(__name__)
41
38
 
42
-
43
- class IntrospectionTokenVerifierSettings(BaseSettings):
44
- """Settings for OAuth 2.0 Token Introspection verification."""
45
-
46
- model_config = SettingsConfigDict(
47
- env_prefix="FASTMCP_SERVER_AUTH_INTROSPECTION_",
48
- env_file=ENV_FILE,
49
- extra="ignore",
50
- )
51
-
52
- introspection_url: str | None = None
53
- client_id: str | None = None
54
- client_secret: SecretStr | None = None
55
- timeout_seconds: int = 10
56
- required_scopes: list[str] | None = None
57
- base_url: AnyHttpUrl | str | None = None
58
-
59
- @field_validator("required_scopes", mode="before")
60
- @classmethod
61
- def _parse_scopes(cls, v):
62
- return parse_scopes(v)
39
+ ClientAuthMethod = Literal["client_secret_basic", "client_secret_post"]
63
40
 
64
41
 
65
42
  class IntrospectionTokenVerifier(TokenVerifier):
@@ -70,8 +47,11 @@ class IntrospectionTokenVerifier(TokenVerifier):
70
47
  endpoint. Unlike JWT verification which is stateless, token introspection requires
71
48
  a network call to the authorization server for each token validation.
72
49
 
73
- The verifier authenticates to the introspection endpoint using HTTP Basic Auth
74
- with the provided client_id and client_secret, as specified in RFC 7662.
50
+ The verifier authenticates to the introspection endpoint using either:
51
+ - HTTP Basic Auth (client_secret_basic, default): credentials in Authorization header
52
+ - POST body authentication (client_secret_post): credentials in request body
53
+
54
+ Both methods are specified in RFC 6749 (OAuth 2.0) and RFC 7662 (Token Introspection).
75
55
 
76
56
  Use this when:
77
57
  - Your authorization server issues opaque (non-JWT) tokens
@@ -93,12 +73,13 @@ class IntrospectionTokenVerifier(TokenVerifier):
93
73
  def __init__(
94
74
  self,
95
75
  *,
96
- introspection_url: str | NotSetT = NotSet,
97
- client_id: str | NotSetT = NotSet,
98
- client_secret: str | NotSetT = NotSet,
99
- timeout_seconds: int | NotSetT = NotSet,
100
- required_scopes: list[str] | NotSetT | None = NotSet,
101
- base_url: AnyHttpUrl | str | NotSetT | None = NotSet,
76
+ introspection_url: str,
77
+ client_id: str,
78
+ client_secret: str | SecretStr,
79
+ client_auth_method: ClientAuthMethod = "client_secret_basic",
80
+ timeout_seconds: int = 10,
81
+ required_scopes: list[str] | None = None,
82
+ base_url: AnyHttpUrl | str | None = None,
102
83
  ):
103
84
  """
104
85
  Initialize the introspection token verifier.
@@ -107,49 +88,38 @@ class IntrospectionTokenVerifier(TokenVerifier):
107
88
  introspection_url: URL of the OAuth 2.0 token introspection endpoint
108
89
  client_id: OAuth client ID for authenticating to the introspection endpoint
109
90
  client_secret: OAuth client secret for authenticating to the introspection endpoint
91
+ client_auth_method: Client authentication method. "client_secret_basic" (default)
92
+ uses HTTP Basic Auth header, "client_secret_post" sends credentials in POST body
110
93
  timeout_seconds: HTTP request timeout in seconds (default: 10)
111
94
  required_scopes: Required scopes for all tokens (optional)
112
95
  base_url: Base URL for TokenVerifier protocol
113
96
  """
114
- settings = IntrospectionTokenVerifierSettings.model_validate(
115
- {
116
- k: v
117
- for k, v in {
118
- "introspection_url": introspection_url,
119
- "client_id": client_id,
120
- "client_secret": client_secret,
121
- "timeout_seconds": timeout_seconds,
122
- "required_scopes": required_scopes,
123
- "base_url": base_url,
124
- }.items()
125
- if v is not NotSet
126
- }
97
+ # Parse scopes if provided as string
98
+ parsed_required_scopes = (
99
+ parse_scopes(required_scopes) if required_scopes is not None else None
127
100
  )
128
101
 
129
- if not settings.introspection_url:
130
- raise ValueError(
131
- "introspection_url is required - set via parameter or "
132
- "FASTMCP_SERVER_AUTH_INTROSPECTION_INTROSPECTION_URL"
133
- )
134
- if not settings.client_id:
135
- raise ValueError(
136
- "client_id is required - set via parameter or "
137
- "FASTMCP_SERVER_AUTH_INTROSPECTION_CLIENT_ID"
138
- )
139
- if not settings.client_secret:
140
- raise ValueError(
141
- "client_secret is required - set via parameter or "
142
- "FASTMCP_SERVER_AUTH_INTROSPECTION_CLIENT_SECRET"
143
- )
102
+ super().__init__(base_url=base_url, required_scopes=parsed_required_scopes)
144
103
 
145
- super().__init__(
146
- base_url=settings.base_url, required_scopes=settings.required_scopes
104
+ self.introspection_url = introspection_url
105
+ self.client_id = client_id
106
+ self.client_secret = (
107
+ client_secret.get_secret_value()
108
+ if isinstance(client_secret, SecretStr)
109
+ else client_secret
147
110
  )
148
111
 
149
- self.introspection_url = settings.introspection_url
150
- self.client_id = settings.client_id
151
- self.client_secret = settings.client_secret.get_secret_value()
152
- self.timeout_seconds = settings.timeout_seconds
112
+ # Validate client_auth_method to catch typos/invalid values early
113
+ valid_methods = get_args(ClientAuthMethod)
114
+ if client_auth_method not in valid_methods:
115
+ options = " or ".join(f"'{m}'" for m in valid_methods)
116
+ raise ValueError(
117
+ f"Invalid client_auth_method: {client_auth_method!r}. "
118
+ f"Must be {options}."
119
+ )
120
+ self.client_auth_method: ClientAuthMethod = client_auth_method
121
+
122
+ self.timeout_seconds = timeout_seconds
153
123
  self.logger = get_logger(__name__)
154
124
 
155
125
  def _create_basic_auth_header(self) -> str:
@@ -186,7 +156,8 @@ class IntrospectionTokenVerifier(TokenVerifier):
186
156
  Verify a bearer token using OAuth 2.0 Token Introspection (RFC 7662).
187
157
 
188
158
  This method makes a POST request to the introspection endpoint with the token,
189
- authenticated using HTTP Basic Auth with the client credentials.
159
+ authenticated using the configured client authentication method (client_secret_basic
160
+ or client_secret_post).
190
161
 
191
162
  Args:
192
163
  token: The opaque token string to validate
@@ -197,19 +168,29 @@ class IntrospectionTokenVerifier(TokenVerifier):
197
168
  try:
198
169
  async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
199
170
  # Prepare introspection request per RFC 7662
200
- auth_header = self._create_basic_auth_header()
171
+ # Build request data with token and token_type_hint
172
+ data = {
173
+ "token": token,
174
+ "token_type_hint": "access_token",
175
+ }
176
+
177
+ # Build headers
178
+ headers = {
179
+ "Content-Type": "application/x-www-form-urlencoded",
180
+ "Accept": "application/json",
181
+ }
182
+
183
+ # Add client authentication based on method
184
+ if self.client_auth_method == "client_secret_basic":
185
+ headers["Authorization"] = self._create_basic_auth_header()
186
+ elif self.client_auth_method == "client_secret_post":
187
+ data["client_id"] = self.client_id
188
+ data["client_secret"] = self.client_secret
201
189
 
202
190
  response = await client.post(
203
191
  self.introspection_url,
204
- data={
205
- "token": token,
206
- "token_type_hint": "access_token",
207
- },
208
- headers={
209
- "Authorization": auth_header,
210
- "Content-Type": "application/x-www-form-urlencoded",
211
- "Accept": "application/json",
212
- },
192
+ data=data,
193
+ headers=headers,
213
194
  )
214
195
 
215
196
  # Check for HTTP errors