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.
- fastmcp/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
202
|
-
client_secret: str
|
|
203
|
-
base_url: AnyHttpUrl | str
|
|
204
|
-
issuer_url: AnyHttpUrl | str |
|
|
205
|
-
redirect_path: str |
|
|
206
|
-
required_scopes: list[str] |
|
|
207
|
-
timeout_seconds: int
|
|
208
|
-
allowed_client_redirect_uris: list[str] |
|
|
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 |
|
|
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
|
-
|
|
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=
|
|
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=
|
|
288
|
-
upstream_client_secret=
|
|
224
|
+
upstream_client_id=client_id,
|
|
225
|
+
upstream_client_secret=client_secret,
|
|
289
226
|
token_verifier=token_verifier,
|
|
290
|
-
base_url=
|
|
291
|
-
redirect_path=
|
|
292
|
-
issuer_url=
|
|
293
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
|
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
|
|
218
|
-
client_secret: str
|
|
219
|
-
base_url: AnyHttpUrl | str
|
|
220
|
-
issuer_url: AnyHttpUrl | str |
|
|
221
|
-
redirect_path: str |
|
|
222
|
-
required_scopes: list[str] |
|
|
223
|
-
timeout_seconds: int
|
|
224
|
-
allowed_client_redirect_uris: list[str] |
|
|
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 |
|
|
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 =
|
|
294
|
-
|
|
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=
|
|
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=
|
|
324
|
-
upstream_client_secret=
|
|
261
|
+
upstream_client_id=client_id,
|
|
262
|
+
upstream_client_secret=client_secret,
|
|
325
263
|
token_verifier=token_verifier,
|
|
326
|
-
base_url=
|
|
327
|
-
redirect_path=
|
|
328
|
-
issuer_url=
|
|
329
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
|
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
|
|
74
|
-
|
|
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
|
|
97
|
-
client_id: str
|
|
98
|
-
client_secret: str |
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|