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
|
@@ -9,15 +9,11 @@ from __future__ import annotations
|
|
|
9
9
|
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
11
|
from key_value.aio.protocols import AsyncKeyValue
|
|
12
|
-
from pydantic import SecretStr, field_validator
|
|
13
|
-
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
14
12
|
|
|
15
13
|
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
16
14
|
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
17
|
-
from fastmcp.settings import ENV_FILE
|
|
18
15
|
from fastmcp.utilities.auth import parse_scopes
|
|
19
16
|
from fastmcp.utilities.logging import get_logger
|
|
20
|
-
from fastmcp.utilities.types import NotSet, NotSetT
|
|
21
17
|
|
|
22
18
|
if TYPE_CHECKING:
|
|
23
19
|
from mcp.server.auth.provider import AuthorizationParams
|
|
@@ -31,39 +27,6 @@ logger = get_logger(__name__)
|
|
|
31
27
|
OIDC_SCOPES = frozenset({"openid", "profile", "email", "offline_access"})
|
|
32
28
|
|
|
33
29
|
|
|
34
|
-
class AzureProviderSettings(BaseSettings):
|
|
35
|
-
"""Settings for Azure OAuth provider."""
|
|
36
|
-
|
|
37
|
-
model_config = SettingsConfigDict(
|
|
38
|
-
env_prefix="FASTMCP_SERVER_AUTH_AZURE_",
|
|
39
|
-
env_file=ENV_FILE,
|
|
40
|
-
extra="ignore",
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
client_id: str | None = None
|
|
44
|
-
client_secret: SecretStr | None = None
|
|
45
|
-
tenant_id: str | None = None
|
|
46
|
-
identifier_uri: str | None = None
|
|
47
|
-
base_url: str | None = None
|
|
48
|
-
issuer_url: str | None = None
|
|
49
|
-
redirect_path: str | None = None
|
|
50
|
-
required_scopes: list[str] | None = None
|
|
51
|
-
additional_authorize_scopes: list[str] | None = None
|
|
52
|
-
allowed_client_redirect_uris: list[str] | None = None
|
|
53
|
-
jwt_signing_key: str | None = None
|
|
54
|
-
base_authority: str = "login.microsoftonline.com"
|
|
55
|
-
|
|
56
|
-
@field_validator("required_scopes", mode="before")
|
|
57
|
-
@classmethod
|
|
58
|
-
def _parse_scopes(cls, v: object) -> list[str] | None:
|
|
59
|
-
return parse_scopes(v)
|
|
60
|
-
|
|
61
|
-
@field_validator("additional_authorize_scopes", mode="before")
|
|
62
|
-
@classmethod
|
|
63
|
-
def _parse_additional_authorize_scopes(cls, v: object) -> list[str] | None:
|
|
64
|
-
return parse_scopes(v)
|
|
65
|
-
|
|
66
|
-
|
|
67
30
|
class AzureProvider(OAuthProxy):
|
|
68
31
|
"""Azure (Microsoft Entra) OAuth provider for FastMCP.
|
|
69
32
|
|
|
@@ -127,20 +90,20 @@ class AzureProvider(OAuthProxy):
|
|
|
127
90
|
def __init__(
|
|
128
91
|
self,
|
|
129
92
|
*,
|
|
130
|
-
client_id: str
|
|
131
|
-
client_secret: str
|
|
132
|
-
tenant_id: str
|
|
133
|
-
|
|
134
|
-
base_url: str
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
additional_authorize_scopes: list[str] |
|
|
139
|
-
allowed_client_redirect_uris: list[str] |
|
|
93
|
+
client_id: str,
|
|
94
|
+
client_secret: str,
|
|
95
|
+
tenant_id: str,
|
|
96
|
+
required_scopes: list[str],
|
|
97
|
+
base_url: str,
|
|
98
|
+
identifier_uri: str | None = None,
|
|
99
|
+
issuer_url: str | None = None,
|
|
100
|
+
redirect_path: str | None = None,
|
|
101
|
+
additional_authorize_scopes: list[str] | None = None,
|
|
102
|
+
allowed_client_redirect_uris: list[str] | None = None,
|
|
140
103
|
client_storage: AsyncKeyValue | None = None,
|
|
141
|
-
jwt_signing_key: str | bytes |
|
|
104
|
+
jwt_signing_key: str | bytes | None = None,
|
|
142
105
|
require_authorization_consent: bool = True,
|
|
143
|
-
base_authority: str
|
|
106
|
+
base_authority: str = "login.microsoftonline.com",
|
|
144
107
|
) -> None:
|
|
145
108
|
"""Initialize Azure OAuth provider.
|
|
146
109
|
|
|
@@ -185,126 +148,75 @@ class AzureProvider(OAuthProxy):
|
|
|
185
148
|
When False, authorization proceeds directly without user confirmation.
|
|
186
149
|
SECURITY WARNING: Only disable for local development or testing environments.
|
|
187
150
|
"""
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
"tenant_id": tenant_id,
|
|
195
|
-
"identifier_uri": identifier_uri,
|
|
196
|
-
"base_url": base_url,
|
|
197
|
-
"issuer_url": issuer_url,
|
|
198
|
-
"redirect_path": redirect_path,
|
|
199
|
-
"required_scopes": required_scopes,
|
|
200
|
-
"additional_authorize_scopes": additional_authorize_scopes,
|
|
201
|
-
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
202
|
-
"jwt_signing_key": jwt_signing_key,
|
|
203
|
-
"base_authority": base_authority,
|
|
204
|
-
}.items()
|
|
205
|
-
if v is not NotSet
|
|
206
|
-
}
|
|
151
|
+
# Parse scopes if provided as string
|
|
152
|
+
parsed_required_scopes = parse_scopes(required_scopes)
|
|
153
|
+
parsed_additional_scopes = (
|
|
154
|
+
parse_scopes(additional_authorize_scopes)
|
|
155
|
+
if additional_authorize_scopes
|
|
156
|
+
else []
|
|
207
157
|
)
|
|
208
158
|
|
|
209
|
-
# Validate required settings
|
|
210
|
-
if not settings.client_id:
|
|
211
|
-
msg = "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"
|
|
212
|
-
raise ValueError(msg)
|
|
213
|
-
if not settings.client_secret:
|
|
214
|
-
msg = "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET"
|
|
215
|
-
raise ValueError(msg)
|
|
216
|
-
|
|
217
|
-
# Validate tenant_id is provided
|
|
218
|
-
if not settings.tenant_id:
|
|
219
|
-
msg = (
|
|
220
|
-
"tenant_id is required - set via parameter or "
|
|
221
|
-
"FASTMCP_SERVER_AUTH_AZURE_TENANT_ID. Use your Azure tenant ID "
|
|
222
|
-
"(found in Azure Portal), 'organizations', or 'consumers'"
|
|
223
|
-
)
|
|
224
|
-
raise ValueError(msg)
|
|
225
|
-
|
|
226
|
-
# Validate required_scopes has at least one scope
|
|
227
|
-
if not settings.required_scopes:
|
|
228
|
-
msg = (
|
|
229
|
-
"required_scopes must include at least one scope - set via parameter or "
|
|
230
|
-
"FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES. Azure's OAuth API requires "
|
|
231
|
-
"the 'scope' parameter in authorization requests. Use the unprefixed scope "
|
|
232
|
-
"names from your Azure App registration (e.g., ['read', 'write'])"
|
|
233
|
-
)
|
|
234
|
-
raise ValueError(msg)
|
|
235
|
-
|
|
236
159
|
# Apply defaults
|
|
237
|
-
self.identifier_uri =
|
|
238
|
-
self.additional_authorize_scopes =
|
|
239
|
-
tenant_id_final = settings.tenant_id
|
|
160
|
+
self.identifier_uri = identifier_uri or f"api://{client_id}"
|
|
161
|
+
self.additional_authorize_scopes = parsed_additional_scopes
|
|
240
162
|
|
|
241
163
|
# Always validate tokens against the app's API client ID using JWT
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
jwks_uri = (
|
|
245
|
-
f"https://{base_authority_final}/{tenant_id_final}/discovery/v2.0/keys"
|
|
246
|
-
)
|
|
164
|
+
issuer = f"https://{base_authority}/{tenant_id}/v2.0"
|
|
165
|
+
jwks_uri = f"https://{base_authority}/{tenant_id}/discovery/v2.0/keys"
|
|
247
166
|
|
|
248
167
|
# Azure access tokens only include custom API scopes in the `scp` claim,
|
|
249
168
|
# NOT standard OIDC scopes (openid, profile, email, offline_access).
|
|
250
169
|
# Filter out OIDC scopes from validation - they'll still be sent to Azure
|
|
251
170
|
# during authorization (handled by _prefix_scopes_for_azure).
|
|
252
|
-
|
|
253
|
-
if settings.required_scopes:
|
|
171
|
+
if parsed_required_scopes:
|
|
254
172
|
validation_scopes = [
|
|
255
|
-
s for s in
|
|
173
|
+
s for s in parsed_required_scopes if s not in OIDC_SCOPES
|
|
256
174
|
]
|
|
257
175
|
# If all scopes were OIDC scopes, use None (no scope validation)
|
|
258
176
|
if not validation_scopes:
|
|
259
177
|
validation_scopes = None
|
|
178
|
+
else:
|
|
179
|
+
validation_scopes = None
|
|
260
180
|
|
|
261
181
|
token_verifier = JWTVerifier(
|
|
262
182
|
jwks_uri=jwks_uri,
|
|
263
183
|
issuer=issuer,
|
|
264
|
-
audience=
|
|
184
|
+
audience=client_id,
|
|
265
185
|
algorithm="RS256",
|
|
266
186
|
required_scopes=validation_scopes, # Only validate non-OIDC scopes
|
|
267
187
|
)
|
|
268
188
|
|
|
269
|
-
# Extract secret string from SecretStr
|
|
270
|
-
client_secret_str = (
|
|
271
|
-
settings.client_secret.get_secret_value() if settings.client_secret else ""
|
|
272
|
-
)
|
|
273
|
-
|
|
274
189
|
# Build Azure OAuth endpoints with tenant
|
|
275
190
|
authorization_endpoint = (
|
|
276
|
-
f"https://{
|
|
277
|
-
)
|
|
278
|
-
token_endpoint = (
|
|
279
|
-
f"https://{base_authority_final}/{tenant_id_final}/oauth2/v2.0/token"
|
|
191
|
+
f"https://{base_authority}/{tenant_id}/oauth2/v2.0/authorize"
|
|
280
192
|
)
|
|
193
|
+
token_endpoint = f"https://{base_authority}/{tenant_id}/oauth2/v2.0/token"
|
|
281
194
|
|
|
282
195
|
# Initialize OAuth proxy with Azure endpoints
|
|
283
196
|
super().__init__(
|
|
284
197
|
upstream_authorization_endpoint=authorization_endpoint,
|
|
285
198
|
upstream_token_endpoint=token_endpoint,
|
|
286
|
-
upstream_client_id=
|
|
287
|
-
upstream_client_secret=
|
|
199
|
+
upstream_client_id=client_id,
|
|
200
|
+
upstream_client_secret=client_secret,
|
|
288
201
|
token_verifier=token_verifier,
|
|
289
|
-
base_url=
|
|
290
|
-
redirect_path=
|
|
291
|
-
issuer_url=
|
|
292
|
-
|
|
293
|
-
allowed_client_redirect_uris=settings.allowed_client_redirect_uris,
|
|
202
|
+
base_url=base_url,
|
|
203
|
+
redirect_path=redirect_path,
|
|
204
|
+
issuer_url=issuer_url or base_url, # Default to base_url if not specified
|
|
205
|
+
allowed_client_redirect_uris=allowed_client_redirect_uris,
|
|
294
206
|
client_storage=client_storage,
|
|
295
|
-
jwt_signing_key=
|
|
207
|
+
jwt_signing_key=jwt_signing_key,
|
|
296
208
|
require_authorization_consent=require_authorization_consent,
|
|
297
209
|
# Advertise full scopes including OIDC (even though we only validate non-OIDC)
|
|
298
|
-
valid_scopes=
|
|
210
|
+
valid_scopes=parsed_required_scopes,
|
|
299
211
|
)
|
|
300
212
|
|
|
301
213
|
authority_info = ""
|
|
302
|
-
if
|
|
303
|
-
authority_info = f" using authority {
|
|
214
|
+
if base_authority != "login.microsoftonline.com":
|
|
215
|
+
authority_info = f" using authority {base_authority}"
|
|
304
216
|
logger.info(
|
|
305
217
|
"Initialized Azure OAuth provider for client %s with tenant %s%s%s",
|
|
306
|
-
|
|
307
|
-
|
|
218
|
+
client_id,
|
|
219
|
+
tenant_id,
|
|
308
220
|
f" and identifier_uri {self.identifier_uri}" if self.identifier_uri else "",
|
|
309
221
|
authority_info,
|
|
310
222
|
)
|
|
@@ -10,40 +10,18 @@ from __future__ import annotations
|
|
|
10
10
|
from urllib.parse import urlparse
|
|
11
11
|
|
|
12
12
|
import httpx
|
|
13
|
-
from pydantic import AnyHttpUrl
|
|
14
|
-
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
13
|
+
from pydantic import AnyHttpUrl
|
|
15
14
|
from starlette.responses import JSONResponse
|
|
16
15
|
from starlette.routing import Route
|
|
17
16
|
|
|
18
17
|
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
|
|
19
18
|
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
20
|
-
from fastmcp.settings import ENV_FILE
|
|
21
19
|
from fastmcp.utilities.auth import parse_scopes
|
|
22
20
|
from fastmcp.utilities.logging import get_logger
|
|
23
|
-
from fastmcp.utilities.types import NotSet, NotSetT
|
|
24
21
|
|
|
25
22
|
logger = get_logger(__name__)
|
|
26
23
|
|
|
27
24
|
|
|
28
|
-
class DescopeProviderSettings(BaseSettings):
|
|
29
|
-
model_config = SettingsConfigDict(
|
|
30
|
-
env_prefix="FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_",
|
|
31
|
-
env_file=ENV_FILE,
|
|
32
|
-
extra="ignore",
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
config_url: AnyHttpUrl | None = None
|
|
36
|
-
project_id: str | None = None
|
|
37
|
-
descope_base_url: AnyHttpUrl | str | None = None
|
|
38
|
-
base_url: AnyHttpUrl
|
|
39
|
-
required_scopes: list[str] | None = None
|
|
40
|
-
|
|
41
|
-
@field_validator("required_scopes", mode="before")
|
|
42
|
-
@classmethod
|
|
43
|
-
def _parse_scopes(cls, v):
|
|
44
|
-
return parse_scopes(v)
|
|
45
|
-
|
|
46
|
-
|
|
47
25
|
class DescopeProvider(RemoteAuthProvider):
|
|
48
26
|
"""Descope metadata provider for DCR (Dynamic Client Registration).
|
|
49
27
|
|
|
@@ -85,46 +63,37 @@ class DescopeProvider(RemoteAuthProvider):
|
|
|
85
63
|
def __init__(
|
|
86
64
|
self,
|
|
87
65
|
*,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
required_scopes: list[str] |
|
|
66
|
+
base_url: AnyHttpUrl | str,
|
|
67
|
+
config_url: AnyHttpUrl | str | None = None,
|
|
68
|
+
project_id: str | None = None,
|
|
69
|
+
descope_base_url: AnyHttpUrl | str | None = None,
|
|
70
|
+
required_scopes: list[str] | None = None,
|
|
93
71
|
token_verifier: TokenVerifier | None = None,
|
|
94
72
|
):
|
|
95
73
|
"""Initialize Descope metadata provider.
|
|
96
74
|
|
|
97
75
|
Args:
|
|
76
|
+
base_url: Public URL of this FastMCP server
|
|
98
77
|
config_url: Your Descope Well-Known URL (e.g., "https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration")
|
|
99
78
|
This is the new recommended way. If provided, project_id and descope_base_url are ignored.
|
|
100
79
|
project_id: Your Descope Project ID (e.g., "P2abc123"). Used with descope_base_url for backwards compatibility.
|
|
101
80
|
descope_base_url: Your Descope base URL (e.g., "https://api.descope.com"). Used with project_id for backwards compatibility.
|
|
102
|
-
base_url: Public URL of this FastMCP server
|
|
103
81
|
required_scopes: Optional list of scopes that must be present in validated tokens.
|
|
104
82
|
These scopes will be included in the protected resource metadata.
|
|
105
83
|
token_verifier: Optional token verifier. If None, creates JWT verifier for Descope
|
|
106
84
|
"""
|
|
107
|
-
|
|
108
|
-
{
|
|
109
|
-
k: v
|
|
110
|
-
for k, v in {
|
|
111
|
-
"config_url": config_url,
|
|
112
|
-
"project_id": project_id,
|
|
113
|
-
"descope_base_url": descope_base_url,
|
|
114
|
-
"base_url": base_url,
|
|
115
|
-
"required_scopes": required_scopes,
|
|
116
|
-
}.items()
|
|
117
|
-
if v is not NotSet
|
|
118
|
-
}
|
|
119
|
-
)
|
|
85
|
+
self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
|
|
120
86
|
|
|
121
|
-
|
|
87
|
+
# Parse scopes if provided as string
|
|
88
|
+
parsed_scopes = (
|
|
89
|
+
parse_scopes(required_scopes) if required_scopes is not None else None
|
|
90
|
+
)
|
|
122
91
|
|
|
123
92
|
# Determine which API is being used
|
|
124
|
-
if
|
|
93
|
+
if config_url is not None:
|
|
125
94
|
# New API: use config_url
|
|
126
95
|
# Strip /.well-known/openid-configuration from config_url if present
|
|
127
|
-
issuer_url = str(
|
|
96
|
+
issuer_url = str(config_url)
|
|
128
97
|
if issuer_url.endswith("/.well-known/openid-configuration"):
|
|
129
98
|
issuer_url = issuer_url[: -len("/.well-known/openid-configuration")]
|
|
130
99
|
|
|
@@ -150,10 +119,10 @@ class DescopeProvider(RemoteAuthProvider):
|
|
|
150
119
|
self.descope_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}".rstrip(
|
|
151
120
|
"/"
|
|
152
121
|
)
|
|
153
|
-
elif
|
|
122
|
+
elif project_id is not None and descope_base_url is not None:
|
|
154
123
|
# Old API: use project_id and descope_base_url
|
|
155
|
-
self.project_id =
|
|
156
|
-
descope_base_url_str = str(
|
|
124
|
+
self.project_id = project_id
|
|
125
|
+
descope_base_url_str = str(descope_base_url).rstrip("/")
|
|
157
126
|
# Ensure descope_base_url has a scheme
|
|
158
127
|
if not descope_base_url_str.startswith(("http://", "https://")):
|
|
159
128
|
descope_base_url_str = f"https://{descope_base_url_str}"
|
|
@@ -172,7 +141,7 @@ class DescopeProvider(RemoteAuthProvider):
|
|
|
172
141
|
issuer=issuer_url,
|
|
173
142
|
algorithm="RS256",
|
|
174
143
|
audience=self.project_id,
|
|
175
|
-
required_scopes=
|
|
144
|
+
required_scopes=parsed_scopes,
|
|
176
145
|
)
|
|
177
146
|
|
|
178
147
|
# Initialize RemoteAuthProvider with Descope as the authorization server
|
|
@@ -26,45 +26,17 @@ from datetime import datetime
|
|
|
26
26
|
|
|
27
27
|
import httpx
|
|
28
28
|
from key_value.aio.protocols import AsyncKeyValue
|
|
29
|
-
from pydantic import AnyHttpUrl
|
|
30
|
-
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
29
|
+
from pydantic import AnyHttpUrl
|
|
31
30
|
|
|
32
31
|
from fastmcp.server.auth import TokenVerifier
|
|
33
32
|
from fastmcp.server.auth.auth import AccessToken
|
|
34
33
|
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
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
39
|
|
|
43
|
-
class DiscordProviderSettings(BaseSettings):
|
|
44
|
-
"""Settings for Discord OAuth provider."""
|
|
45
|
-
|
|
46
|
-
model_config = SettingsConfigDict(
|
|
47
|
-
env_prefix="FASTMCP_SERVER_AUTH_DISCORD_",
|
|
48
|
-
env_file=ENV_FILE,
|
|
49
|
-
extra="ignore",
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
client_id: str | None = None
|
|
53
|
-
client_secret: SecretStr | None = None
|
|
54
|
-
base_url: AnyHttpUrl | str | None = None
|
|
55
|
-
issuer_url: AnyHttpUrl | str | None = None
|
|
56
|
-
redirect_path: str | None = None
|
|
57
|
-
required_scopes: list[str] | None = None
|
|
58
|
-
timeout_seconds: int | None = None
|
|
59
|
-
allowed_client_redirect_uris: list[str] | None = None
|
|
60
|
-
jwt_signing_key: str | None = None
|
|
61
|
-
|
|
62
|
-
@field_validator("required_scopes", mode="before")
|
|
63
|
-
@classmethod
|
|
64
|
-
def _parse_scopes(cls, v):
|
|
65
|
-
return parse_scopes(v)
|
|
66
|
-
|
|
67
|
-
|
|
68
40
|
class DiscordTokenVerifier(TokenVerifier):
|
|
69
41
|
"""Token verifier for Discord OAuth tokens.
|
|
70
42
|
|
|
@@ -200,16 +172,16 @@ class DiscordProvider(OAuthProxy):
|
|
|
200
172
|
def __init__(
|
|
201
173
|
self,
|
|
202
174
|
*,
|
|
203
|
-
client_id: str
|
|
204
|
-
client_secret: str
|
|
205
|
-
base_url: AnyHttpUrl | str
|
|
206
|
-
issuer_url: AnyHttpUrl | str |
|
|
207
|
-
redirect_path: str |
|
|
208
|
-
required_scopes: list[str] |
|
|
209
|
-
timeout_seconds: int
|
|
210
|
-
allowed_client_redirect_uris: list[str] |
|
|
175
|
+
client_id: str,
|
|
176
|
+
client_secret: str,
|
|
177
|
+
base_url: AnyHttpUrl | str,
|
|
178
|
+
issuer_url: AnyHttpUrl | str | None = None,
|
|
179
|
+
redirect_path: str | None = None,
|
|
180
|
+
required_scopes: list[str] | None = None,
|
|
181
|
+
timeout_seconds: int = 10,
|
|
182
|
+
allowed_client_redirect_uris: list[str] | None = None,
|
|
211
183
|
client_storage: AsyncKeyValue | None = None,
|
|
212
|
-
jwt_signing_key: str | bytes |
|
|
184
|
+
jwt_signing_key: str | bytes | None = None,
|
|
213
185
|
require_authorization_consent: bool = True,
|
|
214
186
|
):
|
|
215
187
|
"""Initialize Discord OAuth provider.
|
|
@@ -225,7 +197,7 @@ class DiscordProvider(OAuthProxy):
|
|
|
225
197
|
- "identify" for profile info (default)
|
|
226
198
|
- "email" for email access
|
|
227
199
|
- "guilds" for server membership info
|
|
228
|
-
timeout_seconds: HTTP request timeout for Discord API calls
|
|
200
|
+
timeout_seconds: HTTP request timeout for Discord API calls (defaults to 10)
|
|
229
201
|
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
230
202
|
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
231
203
|
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
|
|
@@ -239,70 +211,37 @@ class DiscordProvider(OAuthProxy):
|
|
|
239
211
|
When False, authorization proceeds directly without user confirmation.
|
|
240
212
|
SECURITY WARNING: Only disable for local development or testing environments.
|
|
241
213
|
"""
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
"client_id": client_id,
|
|
248
|
-
"client_secret": client_secret,
|
|
249
|
-
"base_url": base_url,
|
|
250
|
-
"issuer_url": issuer_url,
|
|
251
|
-
"redirect_path": redirect_path,
|
|
252
|
-
"required_scopes": required_scopes,
|
|
253
|
-
"timeout_seconds": timeout_seconds,
|
|
254
|
-
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
255
|
-
"jwt_signing_key": jwt_signing_key,
|
|
256
|
-
}.items()
|
|
257
|
-
if v is not NotSet
|
|
258
|
-
}
|
|
214
|
+
# Parse scopes if provided as string
|
|
215
|
+
required_scopes_final = (
|
|
216
|
+
parse_scopes(required_scopes)
|
|
217
|
+
if required_scopes is not None
|
|
218
|
+
else ["identify"]
|
|
259
219
|
)
|
|
260
220
|
|
|
261
|
-
# Validate required settings
|
|
262
|
-
if not settings.client_id:
|
|
263
|
-
raise ValueError(
|
|
264
|
-
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID"
|
|
265
|
-
)
|
|
266
|
-
if not settings.client_secret:
|
|
267
|
-
raise ValueError(
|
|
268
|
-
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET"
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
# Apply defaults
|
|
272
|
-
timeout_seconds_final = settings.timeout_seconds or 10
|
|
273
|
-
required_scopes_final = settings.required_scopes or ["identify"]
|
|
274
|
-
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
|
|
275
|
-
|
|
276
221
|
# Create Discord token verifier
|
|
277
222
|
token_verifier = DiscordTokenVerifier(
|
|
278
223
|
required_scopes=required_scopes_final,
|
|
279
|
-
timeout_seconds=
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
# Extract secret string from SecretStr
|
|
283
|
-
client_secret_str = (
|
|
284
|
-
settings.client_secret.get_secret_value() if settings.client_secret else ""
|
|
224
|
+
timeout_seconds=timeout_seconds,
|
|
285
225
|
)
|
|
286
226
|
|
|
287
227
|
# Initialize OAuth proxy with Discord endpoints
|
|
288
228
|
super().__init__(
|
|
289
229
|
upstream_authorization_endpoint="https://discord.com/oauth2/authorize",
|
|
290
230
|
upstream_token_endpoint="https://discord.com/api/oauth2/token",
|
|
291
|
-
upstream_client_id=
|
|
292
|
-
upstream_client_secret=
|
|
231
|
+
upstream_client_id=client_id,
|
|
232
|
+
upstream_client_secret=client_secret,
|
|
293
233
|
token_verifier=token_verifier,
|
|
294
|
-
base_url=
|
|
295
|
-
redirect_path=
|
|
296
|
-
issuer_url=
|
|
297
|
-
|
|
298
|
-
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
234
|
+
base_url=base_url,
|
|
235
|
+
redirect_path=redirect_path,
|
|
236
|
+
issuer_url=issuer_url or base_url, # Default to base_url if not specified
|
|
237
|
+
allowed_client_redirect_uris=allowed_client_redirect_uris,
|
|
299
238
|
client_storage=client_storage,
|
|
300
|
-
jwt_signing_key=
|
|
239
|
+
jwt_signing_key=jwt_signing_key,
|
|
301
240
|
require_authorization_consent=require_authorization_consent,
|
|
302
241
|
)
|
|
303
242
|
|
|
304
243
|
logger.debug(
|
|
305
244
|
"Initialized Discord OAuth provider for client %s with scopes: %s",
|
|
306
|
-
|
|
245
|
+
client_id,
|
|
307
246
|
required_scopes_final,
|
|
308
247
|
)
|