fastmcp 2.12.5__py3-none-any.whl → 2.13.0rc1__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/cli/cli.py +6 -6
- fastmcp/cli/install/claude_code.py +3 -3
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +7 -7
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/run.py +13 -8
- fastmcp/client/auth/oauth.py +100 -208
- fastmcp/client/client.py +11 -11
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +81 -171
- fastmcp/client/transports.py +76 -22
- fastmcp/contrib/component_manager/component_service.py +6 -6
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
- fastmcp/experimental/utilities/openapi/parser.py +23 -3
- fastmcp/prompts/prompt.py +13 -6
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/resource.py +13 -6
- fastmcp/resources/resource_manager.py +5 -164
- fastmcp/resources/template.py +107 -17
- fastmcp/server/auth/auth.py +40 -32
- fastmcp/server/auth/jwt_issuer.py +289 -0
- fastmcp/server/auth/oauth_proxy.py +1238 -234
- fastmcp/server/auth/oidc_proxy.py +8 -6
- fastmcp/server/auth/providers/auth0.py +12 -6
- fastmcp/server/auth/providers/aws.py +13 -2
- fastmcp/server/auth/providers/azure.py +137 -124
- fastmcp/server/auth/providers/descope.py +4 -6
- fastmcp/server/auth/providers/github.py +13 -7
- fastmcp/server/auth/providers/google.py +13 -7
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +8 -2
- fastmcp/server/auth/providers/scalekit.py +179 -0
- fastmcp/server/auth/providers/supabase.py +172 -0
- fastmcp/server/auth/providers/workos.py +16 -13
- fastmcp/server/context.py +89 -34
- fastmcp/server/http.py +53 -16
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/caching.py +469 -0
- fastmcp/server/middleware/error_handling.py +6 -2
- fastmcp/server/middleware/logging.py +48 -37
- fastmcp/server/middleware/middleware.py +28 -15
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/proxy.py +6 -6
- fastmcp/server/server.py +638 -183
- fastmcp/settings.py +22 -9
- fastmcp/tools/tool.py +7 -3
- fastmcp/tools/tool_manager.py +22 -108
- fastmcp/tools/tool_transform.py +3 -3
- fastmcp/utilities/cli.py +2 -2
- fastmcp/utilities/components.py +5 -0
- fastmcp/utilities/inspect.py +77 -21
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/tests.py +87 -4
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/ui.py +497 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/METADATA +8 -4
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/RECORD +66 -62
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/WHEEL +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,6 +12,7 @@ This implementation is based on:
|
|
|
12
12
|
from collections.abc import Sequence
|
|
13
13
|
|
|
14
14
|
import httpx
|
|
15
|
+
from key_value.aio.protocols import AsyncKeyValue
|
|
15
16
|
from pydantic import AnyHttpUrl, BaseModel, model_validator
|
|
16
17
|
from typing_extensions import Self
|
|
17
18
|
|
|
@@ -19,7 +20,6 @@ from fastmcp.server.auth import TokenVerifier
|
|
|
19
20
|
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
20
21
|
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
21
22
|
from fastmcp.utilities.logging import get_logger
|
|
22
|
-
from fastmcp.utilities.storage import KVStorage
|
|
23
23
|
|
|
24
24
|
logger = get_logger(__name__)
|
|
25
25
|
|
|
@@ -210,10 +210,11 @@ class OIDCProxy(OAuthProxy):
|
|
|
210
210
|
required_scopes: list[str] | None = None,
|
|
211
211
|
# FastMCP server configuration
|
|
212
212
|
base_url: AnyHttpUrl | str,
|
|
213
|
+
issuer_url: AnyHttpUrl | str | None = None,
|
|
213
214
|
redirect_path: str | None = None,
|
|
214
215
|
# Client configuration
|
|
215
216
|
allowed_client_redirect_uris: list[str] | None = None,
|
|
216
|
-
client_storage:
|
|
217
|
+
client_storage: AsyncKeyValue | None = None,
|
|
217
218
|
# Token validation configuration
|
|
218
219
|
token_endpoint_auth_method: str | None = None,
|
|
219
220
|
) -> None:
|
|
@@ -228,16 +229,16 @@ class OIDCProxy(OAuthProxy):
|
|
|
228
229
|
timeout_seconds: HTTP request timeout in seconds
|
|
229
230
|
algorithm: Token verifier algorithm
|
|
230
231
|
required_scopes: Required OAuth scopes
|
|
231
|
-
base_url: Public URL
|
|
232
|
-
|
|
232
|
+
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
|
|
233
|
+
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
|
|
234
|
+
to avoid 404s during discovery when mounting under a path.
|
|
233
235
|
redirect_path: Redirect path configured in upstream OAuth app (defaults to "/auth/callback")
|
|
234
236
|
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
235
237
|
Patterns support wildcards (e.g., "http://localhost:*", "https://*.example.com/*").
|
|
236
238
|
If None (default), only localhost redirect URIs are allowed.
|
|
237
239
|
If empty list, all redirect URIs are allowed (not recommended for production).
|
|
238
240
|
These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app.
|
|
239
|
-
client_storage:
|
|
240
|
-
Defaults to file-based storage if not specified.
|
|
241
|
+
client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
|
|
241
242
|
token_endpoint_auth_method: Token endpoint authentication method for upstream server.
|
|
242
243
|
Common values: "client_secret_basic", "client_secret_post", "none".
|
|
243
244
|
If None, authlib will use its default (typically "client_secret_basic").
|
|
@@ -290,6 +291,7 @@ class OIDCProxy(OAuthProxy):
|
|
|
290
291
|
"upstream_revocation_endpoint": revocation_endpoint,
|
|
291
292
|
"token_verifier": token_verifier,
|
|
292
293
|
"base_url": base_url,
|
|
294
|
+
"issuer_url": issuer_url or base_url,
|
|
293
295
|
"service_documentation_url": self.oidc_config.service_documentation,
|
|
294
296
|
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
295
297
|
"client_storage": client_storage,
|
|
@@ -21,13 +21,14 @@ Example:
|
|
|
21
21
|
```
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
from key_value.aio.protocols import AsyncKeyValue
|
|
24
25
|
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
|
25
26
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
26
27
|
|
|
27
28
|
from fastmcp.server.auth.oidc_proxy import OIDCProxy
|
|
29
|
+
from fastmcp.settings import ENV_FILE
|
|
28
30
|
from fastmcp.utilities.auth import parse_scopes
|
|
29
31
|
from fastmcp.utilities.logging import get_logger
|
|
30
|
-
from fastmcp.utilities.storage import KVStorage
|
|
31
32
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
32
33
|
|
|
33
34
|
logger = get_logger(__name__)
|
|
@@ -38,7 +39,7 @@ class Auth0ProviderSettings(BaseSettings):
|
|
|
38
39
|
|
|
39
40
|
model_config = SettingsConfigDict(
|
|
40
41
|
env_prefix="FASTMCP_SERVER_AUTH_AUTH0_",
|
|
41
|
-
env_file=
|
|
42
|
+
env_file=ENV_FILE,
|
|
42
43
|
extra="ignore",
|
|
43
44
|
)
|
|
44
45
|
|
|
@@ -47,6 +48,7 @@ class Auth0ProviderSettings(BaseSettings):
|
|
|
47
48
|
client_secret: SecretStr | None = None
|
|
48
49
|
audience: str | None = None
|
|
49
50
|
base_url: AnyHttpUrl | None = None
|
|
51
|
+
issuer_url: AnyHttpUrl | None = None
|
|
50
52
|
redirect_path: str | None = None
|
|
51
53
|
required_scopes: list[str] | None = None
|
|
52
54
|
allowed_client_redirect_uris: list[str] | None = None
|
|
@@ -89,10 +91,11 @@ class Auth0Provider(OIDCProxy):
|
|
|
89
91
|
client_secret: str | NotSetT = NotSet,
|
|
90
92
|
audience: str | NotSetT = NotSet,
|
|
91
93
|
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
94
|
+
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
92
95
|
required_scopes: list[str] | NotSetT = NotSet,
|
|
93
96
|
redirect_path: str | NotSetT = NotSet,
|
|
94
97
|
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
95
|
-
client_storage:
|
|
98
|
+
client_storage: AsyncKeyValue | None = None,
|
|
96
99
|
) -> None:
|
|
97
100
|
"""Initialize Auth0 OAuth provider.
|
|
98
101
|
|
|
@@ -101,13 +104,14 @@ class Auth0Provider(OIDCProxy):
|
|
|
101
104
|
client_id: Auth0 application client id
|
|
102
105
|
client_secret: Auth0 application client secret
|
|
103
106
|
audience: Auth0 API audience
|
|
104
|
-
base_url: Public URL
|
|
107
|
+
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
|
|
108
|
+
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
|
|
109
|
+
to avoid 404s during discovery when mounting under a path.
|
|
105
110
|
required_scopes: Required Auth0 scopes (defaults to ["openid"])
|
|
106
111
|
redirect_path: Redirect path configured in Auth0 application
|
|
107
112
|
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
108
113
|
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
109
|
-
client_storage:
|
|
110
|
-
Defaults to file-based storage if not specified.
|
|
114
|
+
client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
|
|
111
115
|
"""
|
|
112
116
|
settings = Auth0ProviderSettings.model_validate(
|
|
113
117
|
{
|
|
@@ -118,6 +122,7 @@ class Auth0Provider(OIDCProxy):
|
|
|
118
122
|
"client_secret": client_secret,
|
|
119
123
|
"audience": audience,
|
|
120
124
|
"base_url": base_url,
|
|
125
|
+
"issuer_url": issuer_url,
|
|
121
126
|
"required_scopes": required_scopes,
|
|
122
127
|
"redirect_path": redirect_path,
|
|
123
128
|
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
@@ -159,6 +164,7 @@ class Auth0Provider(OIDCProxy):
|
|
|
159
164
|
"client_secret": settings.client_secret.get_secret_value(),
|
|
160
165
|
"audience": settings.audience,
|
|
161
166
|
"base_url": settings.base_url,
|
|
167
|
+
"issuer_url": settings.issuer_url,
|
|
162
168
|
"redirect_path": settings.redirect_path,
|
|
163
169
|
"required_scopes": auth0_required_scopes,
|
|
164
170
|
"allowed_client_redirect_uris": settings.allowed_client_redirect_uris,
|
|
@@ -23,6 +23,7 @@ Example:
|
|
|
23
23
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
|
+
from key_value.aio.protocols import AsyncKeyValue
|
|
26
27
|
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
|
27
28
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
28
29
|
|
|
@@ -30,6 +31,7 @@ from fastmcp.server.auth import TokenVerifier
|
|
|
30
31
|
from fastmcp.server.auth.auth import AccessToken
|
|
31
32
|
from fastmcp.server.auth.oidc_proxy import OIDCProxy
|
|
32
33
|
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
34
|
+
from fastmcp.settings import ENV_FILE
|
|
33
35
|
from fastmcp.utilities.auth import parse_scopes
|
|
34
36
|
from fastmcp.utilities.logging import get_logger
|
|
35
37
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
@@ -42,7 +44,7 @@ class AWSCognitoProviderSettings(BaseSettings):
|
|
|
42
44
|
|
|
43
45
|
model_config = SettingsConfigDict(
|
|
44
46
|
env_prefix="FASTMCP_SERVER_AUTH_AWS_COGNITO_",
|
|
45
|
-
env_file=
|
|
47
|
+
env_file=ENV_FILE,
|
|
46
48
|
extra="ignore",
|
|
47
49
|
)
|
|
48
50
|
|
|
@@ -51,6 +53,7 @@ class AWSCognitoProviderSettings(BaseSettings):
|
|
|
51
53
|
client_id: str | None = None
|
|
52
54
|
client_secret: SecretStr | None = None
|
|
53
55
|
base_url: AnyHttpUrl | str | None = None
|
|
56
|
+
issuer_url: AnyHttpUrl | str | None = None
|
|
54
57
|
redirect_path: str | None = None
|
|
55
58
|
required_scopes: list[str] | None = None
|
|
56
59
|
allowed_client_redirect_uris: list[str] | None = None
|
|
@@ -127,9 +130,11 @@ class AWSCognitoProvider(OIDCProxy):
|
|
|
127
130
|
client_id: str | NotSetT = NotSet,
|
|
128
131
|
client_secret: str | NotSetT = NotSet,
|
|
129
132
|
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
133
|
+
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
130
134
|
redirect_path: str | NotSetT = NotSet,
|
|
131
135
|
required_scopes: list[str] | NotSetT = NotSet,
|
|
132
136
|
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
137
|
+
client_storage: AsyncKeyValue | None = None,
|
|
133
138
|
):
|
|
134
139
|
"""Initialize AWS Cognito OAuth provider.
|
|
135
140
|
|
|
@@ -138,11 +143,14 @@ class AWSCognitoProvider(OIDCProxy):
|
|
|
138
143
|
aws_region: AWS region where your User Pool is located (defaults to "eu-central-1")
|
|
139
144
|
client_id: Cognito app client ID
|
|
140
145
|
client_secret: Cognito app client secret
|
|
141
|
-
base_url: Public URL
|
|
146
|
+
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
|
|
147
|
+
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
|
|
148
|
+
to avoid 404s during discovery when mounting under a path.
|
|
142
149
|
redirect_path: Redirect path configured in Cognito app (defaults to "/auth/callback")
|
|
143
150
|
required_scopes: Required Cognito scopes (defaults to ["openid"])
|
|
144
151
|
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
145
152
|
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
153
|
+
client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
|
|
146
154
|
"""
|
|
147
155
|
|
|
148
156
|
settings = AWSCognitoProviderSettings.model_validate(
|
|
@@ -154,6 +162,7 @@ class AWSCognitoProvider(OIDCProxy):
|
|
|
154
162
|
"client_id": client_id,
|
|
155
163
|
"client_secret": client_secret,
|
|
156
164
|
"base_url": base_url,
|
|
165
|
+
"issuer_url": issuer_url,
|
|
157
166
|
"redirect_path": redirect_path,
|
|
158
167
|
"required_scopes": required_scopes,
|
|
159
168
|
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
@@ -202,8 +211,10 @@ class AWSCognitoProvider(OIDCProxy):
|
|
|
202
211
|
algorithm="RS256",
|
|
203
212
|
required_scopes=required_scopes_final,
|
|
204
213
|
base_url=settings.base_url,
|
|
214
|
+
issuer_url=settings.issuer_url,
|
|
205
215
|
redirect_path=redirect_path_final,
|
|
206
216
|
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
217
|
+
client_storage=client_storage,
|
|
207
218
|
)
|
|
208
219
|
|
|
209
220
|
logger.info(
|
|
@@ -6,17 +6,23 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from key_value.aio.protocols import AsyncKeyValue
|
|
10
12
|
from pydantic import SecretStr, field_validator
|
|
11
13
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
12
14
|
|
|
13
|
-
from fastmcp.server.auth import AccessToken, TokenVerifier
|
|
14
15
|
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
16
|
+
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
17
|
+
from fastmcp.settings import ENV_FILE
|
|
15
18
|
from fastmcp.utilities.auth import parse_scopes
|
|
16
19
|
from fastmcp.utilities.logging import get_logger
|
|
17
|
-
from fastmcp.utilities.storage import KVStorage
|
|
18
20
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
19
21
|
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from mcp.server.auth.provider import AuthorizationParams
|
|
24
|
+
from mcp.shared.auth import OAuthClientInformationFull
|
|
25
|
+
|
|
20
26
|
logger = get_logger(__name__)
|
|
21
27
|
|
|
22
28
|
|
|
@@ -25,94 +31,30 @@ class AzureProviderSettings(BaseSettings):
|
|
|
25
31
|
|
|
26
32
|
model_config = SettingsConfigDict(
|
|
27
33
|
env_prefix="FASTMCP_SERVER_AUTH_AZURE_",
|
|
28
|
-
env_file=
|
|
34
|
+
env_file=ENV_FILE,
|
|
29
35
|
extra="ignore",
|
|
30
36
|
)
|
|
31
37
|
|
|
32
38
|
client_id: str | None = None
|
|
33
39
|
client_secret: SecretStr | None = None
|
|
34
40
|
tenant_id: str | None = None
|
|
41
|
+
identifier_uri: str | None = None
|
|
35
42
|
base_url: str | None = None
|
|
43
|
+
issuer_url: str | None = None
|
|
36
44
|
redirect_path: str | None = None
|
|
37
45
|
required_scopes: list[str] | None = None
|
|
38
|
-
|
|
46
|
+
additional_authorize_scopes: list[str] | None = None
|
|
39
47
|
allowed_client_redirect_uris: list[str] | None = None
|
|
40
48
|
|
|
41
49
|
@field_validator("required_scopes", mode="before")
|
|
42
50
|
@classmethod
|
|
43
|
-
def _parse_scopes(cls, v):
|
|
51
|
+
def _parse_scopes(cls, v: object) -> list[str] | None:
|
|
44
52
|
return parse_scopes(v)
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Azure tokens are JWTs, but we verify them by calling the Microsoft Graph API
|
|
51
|
-
to get user information and validate the token.
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
def __init__(
|
|
55
|
-
self,
|
|
56
|
-
*,
|
|
57
|
-
required_scopes: list[str] | None = None,
|
|
58
|
-
timeout_seconds: int = 10,
|
|
59
|
-
):
|
|
60
|
-
"""Initialize the Azure token verifier.
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
required_scopes: Required OAuth scopes
|
|
64
|
-
timeout_seconds: HTTP request timeout
|
|
65
|
-
"""
|
|
66
|
-
super().__init__(required_scopes=required_scopes)
|
|
67
|
-
self.timeout_seconds = timeout_seconds
|
|
68
|
-
|
|
69
|
-
async def verify_token(self, token: str) -> AccessToken | None:
|
|
70
|
-
"""Verify Azure OAuth token by calling Microsoft Graph API."""
|
|
71
|
-
try:
|
|
72
|
-
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
|
|
73
|
-
# Use Microsoft Graph API to validate token and get user info
|
|
74
|
-
response = await client.get(
|
|
75
|
-
"https://graph.microsoft.com/v1.0/me",
|
|
76
|
-
headers={
|
|
77
|
-
"Authorization": f"Bearer {token}",
|
|
78
|
-
"User-Agent": "FastMCP-Azure-OAuth",
|
|
79
|
-
},
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
if response.status_code != 200:
|
|
83
|
-
logger.debug(
|
|
84
|
-
"Azure token verification failed: %d - %s",
|
|
85
|
-
response.status_code,
|
|
86
|
-
response.text[:200],
|
|
87
|
-
)
|
|
88
|
-
return None
|
|
89
|
-
|
|
90
|
-
user_data = response.json()
|
|
91
|
-
|
|
92
|
-
# Create AccessToken with Azure user info
|
|
93
|
-
return AccessToken(
|
|
94
|
-
token=token,
|
|
95
|
-
client_id=str(user_data.get("id", "unknown")),
|
|
96
|
-
scopes=self.required_scopes or [],
|
|
97
|
-
expires_at=None,
|
|
98
|
-
claims={
|
|
99
|
-
"sub": user_data.get("id"),
|
|
100
|
-
"email": user_data.get("mail")
|
|
101
|
-
or user_data.get("userPrincipalName"),
|
|
102
|
-
"name": user_data.get("displayName"),
|
|
103
|
-
"given_name": user_data.get("givenName"),
|
|
104
|
-
"family_name": user_data.get("surname"),
|
|
105
|
-
"job_title": user_data.get("jobTitle"),
|
|
106
|
-
"office_location": user_data.get("officeLocation"),
|
|
107
|
-
},
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
except httpx.RequestError as e:
|
|
111
|
-
logger.debug("Failed to verify Azure token: %s", e)
|
|
112
|
-
return None
|
|
113
|
-
except Exception as e:
|
|
114
|
-
logger.debug("Azure token verification error: %s", e)
|
|
115
|
-
return None
|
|
54
|
+
@field_validator("additional_authorize_scopes", mode="before")
|
|
55
|
+
@classmethod
|
|
56
|
+
def _parse_additional_authorize_scopes(cls, v: object) -> list[str] | None:
|
|
57
|
+
return parse_scopes(v)
|
|
116
58
|
|
|
117
59
|
|
|
118
60
|
class AzureProvider(OAuthProxy):
|
|
@@ -123,16 +65,17 @@ class AzureProvider(OAuthProxy):
|
|
|
123
65
|
Microsoft accounts depending on the tenant configuration.
|
|
124
66
|
|
|
125
67
|
Features:
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
68
|
+
- OAuth proxy to Azure/Microsoft identity platform
|
|
69
|
+
- JWT validation using tenant issuer and JWKS
|
|
70
|
+
- Supports tenant configurations: specific tenant ID, "organizations", or "consumers"
|
|
71
|
+
|
|
72
|
+
Setup:
|
|
73
|
+
1. Create an App registration in Azure Portal
|
|
74
|
+
2. Configure Web platform redirect URI: http://localhost:8000/auth/callback (or your custom path)
|
|
75
|
+
3. Add an Application ID URI. Either use the default (api://{client_id}) or set a custom one.
|
|
76
|
+
4. Add a custom scope.
|
|
77
|
+
5. Create a client secret.
|
|
78
|
+
6. Get Application (client) ID, Directory (tenant) ID, and client secret
|
|
136
79
|
|
|
137
80
|
Example:
|
|
138
81
|
```python
|
|
@@ -142,8 +85,10 @@ class AzureProvider(OAuthProxy):
|
|
|
142
85
|
auth = AzureProvider(
|
|
143
86
|
client_id="your-client-id",
|
|
144
87
|
client_secret="your-client-secret",
|
|
145
|
-
tenant_id="your-tenant-id",
|
|
146
|
-
|
|
88
|
+
tenant_id="your-tenant-id",
|
|
89
|
+
required_scopes=["your-scope"],
|
|
90
|
+
base_url="http://localhost:8000",
|
|
91
|
+
# identifier_uri defaults to api://{client_id}
|
|
147
92
|
)
|
|
148
93
|
|
|
149
94
|
mcp = FastMCP("My App", auth=auth)
|
|
@@ -156,27 +101,36 @@ class AzureProvider(OAuthProxy):
|
|
|
156
101
|
client_id: str | NotSetT = NotSet,
|
|
157
102
|
client_secret: str | NotSetT = NotSet,
|
|
158
103
|
tenant_id: str | NotSetT = NotSet,
|
|
104
|
+
identifier_uri: str | None | NotSetT = NotSet,
|
|
159
105
|
base_url: str | NotSetT = NotSet,
|
|
106
|
+
issuer_url: str | NotSetT = NotSet,
|
|
160
107
|
redirect_path: str | NotSetT = NotSet,
|
|
161
108
|
required_scopes: list[str] | None | NotSetT = NotSet,
|
|
162
|
-
|
|
109
|
+
additional_authorize_scopes: list[str] | None | NotSetT = NotSet,
|
|
163
110
|
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
164
|
-
client_storage:
|
|
165
|
-
):
|
|
111
|
+
client_storage: AsyncKeyValue | None = None,
|
|
112
|
+
) -> None:
|
|
166
113
|
"""Initialize Azure OAuth provider.
|
|
167
114
|
|
|
168
115
|
Args:
|
|
169
116
|
client_id: Azure application (client) ID
|
|
170
117
|
client_secret: Azure client secret
|
|
171
118
|
tenant_id: Azure tenant ID (your specific tenant ID, "organizations", or "consumers")
|
|
172
|
-
|
|
119
|
+
identifier_uri: Optional Application ID URI for your API. (defaults to api://{client_id})
|
|
120
|
+
Used only to prefix scopes in authorization requests. Tokens are always validated
|
|
121
|
+
against your app's client ID.
|
|
122
|
+
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
|
|
123
|
+
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
|
|
124
|
+
to avoid 404s during discovery when mounting under a path.
|
|
173
125
|
redirect_path: Redirect path configured in Azure (defaults to "/auth/callback")
|
|
174
|
-
required_scopes: Required scopes
|
|
175
|
-
|
|
126
|
+
required_scopes: Required scopes. These are validated on tokens and used as defaults
|
|
127
|
+
when the client does not request specific scopes.
|
|
128
|
+
additional_authorize_scopes: Additional scopes to include in the authorization request
|
|
129
|
+
without prefixing. Use this to request upstream scopes such as Microsoft Graph
|
|
130
|
+
permissions. These are not used for token validation.
|
|
176
131
|
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
177
132
|
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
178
|
-
client_storage:
|
|
179
|
-
Defaults to file-based storage if not specified.
|
|
133
|
+
client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
|
|
180
134
|
"""
|
|
181
135
|
settings = AzureProviderSettings.model_validate(
|
|
182
136
|
{
|
|
@@ -185,10 +139,12 @@ class AzureProvider(OAuthProxy):
|
|
|
185
139
|
"client_id": client_id,
|
|
186
140
|
"client_secret": client_secret,
|
|
187
141
|
"tenant_id": tenant_id,
|
|
142
|
+
"identifier_uri": identifier_uri,
|
|
188
143
|
"base_url": base_url,
|
|
144
|
+
"issuer_url": issuer_url,
|
|
189
145
|
"redirect_path": redirect_path,
|
|
190
146
|
"required_scopes": required_scopes,
|
|
191
|
-
"
|
|
147
|
+
"additional_authorize_scopes": additional_authorize_scopes,
|
|
192
148
|
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
193
149
|
}.items()
|
|
194
150
|
if v is not NotSet
|
|
@@ -197,45 +153,48 @@ class AzureProvider(OAuthProxy):
|
|
|
197
153
|
|
|
198
154
|
# Validate required settings
|
|
199
155
|
if not settings.client_id:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
156
|
+
msg = "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"
|
|
157
|
+
raise ValueError(msg)
|
|
203
158
|
if not settings.client_secret:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
159
|
+
msg = "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET"
|
|
160
|
+
raise ValueError(msg)
|
|
207
161
|
|
|
208
162
|
# Validate tenant_id is provided
|
|
209
163
|
if not settings.tenant_id:
|
|
210
|
-
|
|
211
|
-
"tenant_id is required - set via parameter or
|
|
212
|
-
"Use your Azure tenant ID
|
|
164
|
+
msg = (
|
|
165
|
+
"tenant_id is required - set via parameter or "
|
|
166
|
+
"FASTMCP_SERVER_AUTH_AZURE_TENANT_ID. Use your Azure tenant ID "
|
|
167
|
+
"(found in Azure Portal), 'organizations', or 'consumers'"
|
|
213
168
|
)
|
|
169
|
+
raise ValueError(msg)
|
|
170
|
+
|
|
171
|
+
if not settings.required_scopes:
|
|
172
|
+
raise ValueError("required_scopes is required")
|
|
214
173
|
|
|
215
174
|
# Apply defaults
|
|
175
|
+
self.identifier_uri = settings.identifier_uri or f"api://{settings.client_id}"
|
|
176
|
+
self.additional_authorize_scopes = settings.additional_authorize_scopes or []
|
|
216
177
|
tenant_id_final = settings.tenant_id
|
|
217
178
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
"
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
179
|
+
# Always validate tokens against the app's API client ID using JWT
|
|
180
|
+
issuer = f"https://login.microsoftonline.com/{tenant_id_final}/v2.0"
|
|
181
|
+
jwks_uri = (
|
|
182
|
+
f"https://login.microsoftonline.com/{tenant_id_final}/discovery/v2.0/keys"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
token_verifier = JWTVerifier(
|
|
186
|
+
jwks_uri=jwks_uri,
|
|
187
|
+
issuer=issuer,
|
|
188
|
+
audience=settings.client_id,
|
|
189
|
+
algorithm="RS256",
|
|
190
|
+
required_scopes=settings.required_scopes,
|
|
191
|
+
)
|
|
227
192
|
|
|
228
193
|
# Extract secret string from SecretStr
|
|
229
194
|
client_secret_str = (
|
|
230
195
|
settings.client_secret.get_secret_value() if settings.client_secret else ""
|
|
231
196
|
)
|
|
232
197
|
|
|
233
|
-
# Create Azure token verifier
|
|
234
|
-
token_verifier = AzureTokenVerifier(
|
|
235
|
-
required_scopes=scopes_final,
|
|
236
|
-
timeout_seconds=timeout_seconds_final,
|
|
237
|
-
)
|
|
238
|
-
|
|
239
198
|
# Build Azure OAuth endpoints with tenant
|
|
240
199
|
authorization_endpoint = (
|
|
241
200
|
f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/authorize"
|
|
@@ -253,13 +212,67 @@ class AzureProvider(OAuthProxy):
|
|
|
253
212
|
token_verifier=token_verifier,
|
|
254
213
|
base_url=settings.base_url,
|
|
255
214
|
redirect_path=settings.redirect_path,
|
|
256
|
-
issuer_url=settings.
|
|
257
|
-
|
|
215
|
+
issuer_url=settings.issuer_url
|
|
216
|
+
or settings.base_url, # Default to base_url if not specified
|
|
217
|
+
allowed_client_redirect_uris=settings.allowed_client_redirect_uris,
|
|
258
218
|
client_storage=client_storage,
|
|
259
219
|
)
|
|
260
220
|
|
|
261
221
|
logger.info(
|
|
262
|
-
"Initialized Azure OAuth provider for client %s with tenant %s",
|
|
222
|
+
"Initialized Azure OAuth provider for client %s with tenant %s%s",
|
|
263
223
|
settings.client_id,
|
|
264
224
|
tenant_id_final,
|
|
225
|
+
f" and identifier_uri {self.identifier_uri}" if self.identifier_uri else "",
|
|
265
226
|
)
|
|
227
|
+
|
|
228
|
+
async def authorize(
|
|
229
|
+
self,
|
|
230
|
+
client: OAuthClientInformationFull,
|
|
231
|
+
params: AuthorizationParams,
|
|
232
|
+
) -> str:
|
|
233
|
+
"""Start OAuth transaction and redirect to Azure AD.
|
|
234
|
+
|
|
235
|
+
Override parent's authorize method to filter out the 'resource' parameter
|
|
236
|
+
which is not supported by Azure AD v2.0 endpoints. The v2.0 endpoints use
|
|
237
|
+
scopes to determine the resource/audience instead of a separate parameter.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
client: OAuth client information
|
|
241
|
+
params: Authorization parameters from the client
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Authorization URL to redirect the user to Azure AD
|
|
245
|
+
"""
|
|
246
|
+
# Clear the resource parameter that Azure AD v2.0 doesn't support
|
|
247
|
+
# This parameter comes from RFC 8707 (OAuth 2.0 Resource Indicators)
|
|
248
|
+
# but Azure AD v2.0 uses scopes instead to determine the audience
|
|
249
|
+
params_to_use = params
|
|
250
|
+
if hasattr(params, "resource"):
|
|
251
|
+
original_resource = getattr(params, "resource", None)
|
|
252
|
+
if original_resource is not None:
|
|
253
|
+
params_to_use = params.model_copy(update={"resource": None})
|
|
254
|
+
if original_resource:
|
|
255
|
+
logger.debug(
|
|
256
|
+
"Filtering out 'resource' parameter '%s' for Azure AD v2.0 (use scopes instead)",
|
|
257
|
+
original_resource,
|
|
258
|
+
)
|
|
259
|
+
original_scopes = params_to_use.scopes or self.required_scopes
|
|
260
|
+
prefixed_scopes = (
|
|
261
|
+
self._add_prefix_to_scopes(original_scopes)
|
|
262
|
+
if self.identifier_uri
|
|
263
|
+
else original_scopes
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
final_scopes = list(prefixed_scopes)
|
|
267
|
+
if self.additional_authorize_scopes:
|
|
268
|
+
final_scopes.extend(self.additional_authorize_scopes)
|
|
269
|
+
|
|
270
|
+
modified_params = params_to_use.model_copy(update={"scopes": final_scopes})
|
|
271
|
+
|
|
272
|
+
auth_url = await super().authorize(client, modified_params)
|
|
273
|
+
separator = "&" if "?" in auth_url else "?"
|
|
274
|
+
return f"{auth_url}{separator}prompt=select_account"
|
|
275
|
+
|
|
276
|
+
def _add_prefix_to_scopes(self, scopes: list[str]) -> list[str]:
|
|
277
|
+
"""Add Application ID URI prefix for authorization request."""
|
|
278
|
+
return [f"{self.identifier_uri}/{scope}" for scope in scopes]
|
|
@@ -7,8 +7,6 @@ for seamless MCP client authentication.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
10
|
import httpx
|
|
13
11
|
from pydantic import AnyHttpUrl
|
|
14
12
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
@@ -17,6 +15,7 @@ from starlette.routing import Route
|
|
|
17
15
|
|
|
18
16
|
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
|
|
19
17
|
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
18
|
+
from fastmcp.settings import ENV_FILE
|
|
20
19
|
from fastmcp.utilities.logging import get_logger
|
|
21
20
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
22
21
|
|
|
@@ -26,7 +25,7 @@ logger = get_logger(__name__)
|
|
|
26
25
|
class DescopeProviderSettings(BaseSettings):
|
|
27
26
|
model_config = SettingsConfigDict(
|
|
28
27
|
env_prefix="FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_",
|
|
29
|
-
env_file=
|
|
28
|
+
env_file=ENV_FILE,
|
|
30
29
|
extra="ignore",
|
|
31
30
|
)
|
|
32
31
|
|
|
@@ -127,7 +126,6 @@ class DescopeProvider(RemoteAuthProvider):
|
|
|
127
126
|
def get_routes(
|
|
128
127
|
self,
|
|
129
128
|
mcp_path: str | None = None,
|
|
130
|
-
mcp_endpoint: Any | None = None,
|
|
131
129
|
) -> list[Route]:
|
|
132
130
|
"""Get OAuth routes including Descope authorization server metadata forwarding.
|
|
133
131
|
|
|
@@ -136,10 +134,10 @@ class DescopeProvider(RemoteAuthProvider):
|
|
|
136
134
|
|
|
137
135
|
Args:
|
|
138
136
|
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
|
|
139
|
-
|
|
137
|
+
This is used to advertise the resource URL in metadata.
|
|
140
138
|
"""
|
|
141
139
|
# Get the standard protected resource routes from RemoteAuthProvider
|
|
142
|
-
routes = super().get_routes(mcp_path
|
|
140
|
+
routes = super().get_routes(mcp_path)
|
|
143
141
|
|
|
144
142
|
async def oauth_authorization_server_metadata(request):
|
|
145
143
|
"""Forward Descope OAuth authorization server metadata with FastMCP customizations."""
|