fastmcp 2.12.4__py3-none-any.whl → 2.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/cli/cli.py +7 -6
- fastmcp/cli/install/claude_code.py +6 -6
- 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 +85 -171
- fastmcp/client/transports.py +77 -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/resources/types.py +30 -24
- fastmcp/server/auth/auth.py +40 -32
- fastmcp/server/auth/handlers/authorize.py +324 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1256 -242
- fastmcp/server/auth/oidc_proxy.py +23 -6
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +178 -127
- fastmcp/server/auth/providers/descope.py +4 -6
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +30 -9
- 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 +32 -14
- fastmcp/server/context.py +122 -36
- fastmcp/server/http.py +58 -18
- 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/middleware/tool_injection.py +116 -0
- fastmcp/server/proxy.py +6 -6
- fastmcp/server/server.py +683 -207
- fastmcp/settings.py +24 -10
- fastmcp/tools/tool.py +7 -3
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +3 -3
- fastmcp/utilities/cli.py +62 -22
- 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 +617 -0
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/METADATA +10 -6
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/RECORD +70 -63
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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,31 @@ 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
|
|
48
|
+
jwt_signing_key: str | None = None
|
|
40
49
|
|
|
41
50
|
@field_validator("required_scopes", mode="before")
|
|
42
51
|
@classmethod
|
|
43
|
-
def _parse_scopes(cls, v):
|
|
52
|
+
def _parse_scopes(cls, v: object) -> list[str] | None:
|
|
44
53
|
return parse_scopes(v)
|
|
45
54
|
|
|
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
|
|
55
|
+
@field_validator("additional_authorize_scopes", mode="before")
|
|
56
|
+
@classmethod
|
|
57
|
+
def _parse_additional_authorize_scopes(cls, v: object) -> list[str] | None:
|
|
58
|
+
return parse_scopes(v)
|
|
116
59
|
|
|
117
60
|
|
|
118
61
|
class AzureProvider(OAuthProxy):
|
|
@@ -122,17 +65,28 @@ class AzureProvider(OAuthProxy):
|
|
|
122
65
|
OAuth Proxy pattern. It supports both organizational accounts and personal
|
|
123
66
|
Microsoft accounts depending on the tenant configuration.
|
|
124
67
|
|
|
125
|
-
|
|
126
|
-
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
-
|
|
68
|
+
Scope Handling:
|
|
69
|
+
- required_scopes: Provide unprefixed scope names (e.g., ["read", "write"])
|
|
70
|
+
→ Automatically prefixed with identifier_uri during initialization
|
|
71
|
+
→ Validated on all tokens and advertised to MCP clients
|
|
72
|
+
- additional_authorize_scopes: Provide full format (e.g., ["User.Read"])
|
|
73
|
+
→ NOT prefixed, NOT validated, NOT advertised to clients
|
|
74
|
+
→ Used to request Microsoft Graph or other upstream API permissions
|
|
130
75
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
76
|
+
Features:
|
|
77
|
+
- OAuth proxy to Azure/Microsoft identity platform
|
|
78
|
+
- JWT validation using tenant issuer and JWKS
|
|
79
|
+
- Supports tenant configurations: specific tenant ID, "organizations", or "consumers"
|
|
80
|
+
- Custom API scopes and Microsoft Graph scopes in a single provider
|
|
81
|
+
|
|
82
|
+
Setup:
|
|
83
|
+
1. Create an App registration in Azure Portal
|
|
84
|
+
2. Configure Web platform redirect URI: http://localhost:8000/auth/callback (or your custom path)
|
|
85
|
+
3. Add an Application ID URI under "Expose an API" (defaults to api://{client_id})
|
|
86
|
+
4. Add custom scopes (e.g., "read", "write") under "Expose an API"
|
|
87
|
+
5. Set access token version to 2 in the App manifest: "requestedAccessTokenVersion": 2
|
|
88
|
+
6. Create a client secret
|
|
89
|
+
7. Get Application (client) ID, Directory (tenant) ID, and client secret
|
|
136
90
|
|
|
137
91
|
Example:
|
|
138
92
|
```python
|
|
@@ -142,8 +96,11 @@ class AzureProvider(OAuthProxy):
|
|
|
142
96
|
auth = AzureProvider(
|
|
143
97
|
client_id="your-client-id",
|
|
144
98
|
client_secret="your-client-secret",
|
|
145
|
-
tenant_id="your-tenant-id",
|
|
146
|
-
|
|
99
|
+
tenant_id="your-tenant-id",
|
|
100
|
+
required_scopes=["read", "write"], # Unprefixed scope names
|
|
101
|
+
additional_authorize_scopes=["User.Read", "Mail.Read"], # Optional Graph scopes
|
|
102
|
+
base_url="http://localhost:8000",
|
|
103
|
+
# identifier_uri defaults to api://{client_id}
|
|
147
104
|
)
|
|
148
105
|
|
|
149
106
|
mcp = FastMCP("My App", auth=auth)
|
|
@@ -156,27 +113,57 @@ class AzureProvider(OAuthProxy):
|
|
|
156
113
|
client_id: str | NotSetT = NotSet,
|
|
157
114
|
client_secret: str | NotSetT = NotSet,
|
|
158
115
|
tenant_id: str | NotSetT = NotSet,
|
|
116
|
+
identifier_uri: str | None | NotSetT = NotSet,
|
|
159
117
|
base_url: str | NotSetT = NotSet,
|
|
118
|
+
issuer_url: str | NotSetT = NotSet,
|
|
160
119
|
redirect_path: str | NotSetT = NotSet,
|
|
161
120
|
required_scopes: list[str] | None | NotSetT = NotSet,
|
|
162
|
-
|
|
121
|
+
additional_authorize_scopes: list[str] | None | NotSetT = NotSet,
|
|
163
122
|
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
164
|
-
client_storage:
|
|
165
|
-
|
|
123
|
+
client_storage: AsyncKeyValue | None = None,
|
|
124
|
+
jwt_signing_key: str | bytes | NotSetT = NotSet,
|
|
125
|
+
require_authorization_consent: bool = True,
|
|
126
|
+
) -> None:
|
|
166
127
|
"""Initialize Azure OAuth provider.
|
|
167
128
|
|
|
168
129
|
Args:
|
|
169
|
-
client_id: Azure application (client) ID
|
|
170
|
-
client_secret: Azure client secret
|
|
171
|
-
tenant_id: Azure tenant ID (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
130
|
+
client_id: Azure application (client) ID from your App registration
|
|
131
|
+
client_secret: Azure client secret from your App registration
|
|
132
|
+
tenant_id: Azure tenant ID (specific tenant GUID, "organizations", or "consumers")
|
|
133
|
+
identifier_uri: Optional Application ID URI for your custom API (defaults to api://{client_id}).
|
|
134
|
+
This URI is automatically prefixed to all required_scopes during initialization.
|
|
135
|
+
Example: identifier_uri="api://my-api" + required_scopes=["read"]
|
|
136
|
+
→ tokens validated for "api://my-api/read"
|
|
137
|
+
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
|
|
138
|
+
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
|
|
139
|
+
to avoid 404s during discovery when mounting under a path.
|
|
140
|
+
redirect_path: Redirect path configured in Azure App registration (defaults to "/auth/callback")
|
|
141
|
+
required_scopes: Custom API scope names WITHOUT prefix (e.g., ["read", "write"]).
|
|
142
|
+
- Automatically prefixed with identifier_uri during initialization
|
|
143
|
+
- Validated on all tokens
|
|
144
|
+
- Advertised in Protected Resource Metadata
|
|
145
|
+
- Must match scope names defined in Azure Portal under "Expose an API"
|
|
146
|
+
Example: ["read", "write"] → validates tokens containing ["api://xxx/read", "api://xxx/write"]
|
|
147
|
+
additional_authorize_scopes: Microsoft Graph or other upstream scopes in full format.
|
|
148
|
+
- NOT prefixed with identifier_uri
|
|
149
|
+
- NOT validated on tokens
|
|
150
|
+
- NOT advertised to MCP clients
|
|
151
|
+
- Used to request additional permissions from Azure (e.g., Graph API access)
|
|
152
|
+
Example: ["User.Read", "Mail.Read", "offline_access"]
|
|
153
|
+
These scopes allow your FastMCP server to call Microsoft Graph APIs using the
|
|
154
|
+
upstream Azure token, but MCP clients are unaware of them.
|
|
176
155
|
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
177
156
|
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
178
|
-
client_storage: Storage
|
|
179
|
-
|
|
157
|
+
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
|
|
158
|
+
If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
|
|
159
|
+
disk store will be encrypted using a key derived from the JWT Signing Key.
|
|
160
|
+
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
|
|
161
|
+
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
|
|
162
|
+
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
|
|
163
|
+
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
|
164
|
+
When True, users see a consent screen before being redirected to Azure.
|
|
165
|
+
When False, authorization proceeds directly without user confirmation.
|
|
166
|
+
SECURITY WARNING: Only disable for local development or testing environments.
|
|
180
167
|
"""
|
|
181
168
|
settings = AzureProviderSettings.model_validate(
|
|
182
169
|
{
|
|
@@ -185,11 +172,14 @@ class AzureProvider(OAuthProxy):
|
|
|
185
172
|
"client_id": client_id,
|
|
186
173
|
"client_secret": client_secret,
|
|
187
174
|
"tenant_id": tenant_id,
|
|
175
|
+
"identifier_uri": identifier_uri,
|
|
188
176
|
"base_url": base_url,
|
|
177
|
+
"issuer_url": issuer_url,
|
|
189
178
|
"redirect_path": redirect_path,
|
|
190
179
|
"required_scopes": required_scopes,
|
|
191
|
-
"
|
|
180
|
+
"additional_authorize_scopes": additional_authorize_scopes,
|
|
192
181
|
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
182
|
+
"jwt_signing_key": jwt_signing_key,
|
|
193
183
|
}.items()
|
|
194
184
|
if v is not NotSet
|
|
195
185
|
}
|
|
@@ -197,45 +187,54 @@ class AzureProvider(OAuthProxy):
|
|
|
197
187
|
|
|
198
188
|
# Validate required settings
|
|
199
189
|
if not settings.client_id:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
190
|
+
msg = "client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"
|
|
191
|
+
raise ValueError(msg)
|
|
203
192
|
if not settings.client_secret:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
193
|
+
msg = "client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET"
|
|
194
|
+
raise ValueError(msg)
|
|
207
195
|
|
|
208
196
|
# Validate tenant_id is provided
|
|
209
197
|
if not settings.tenant_id:
|
|
210
|
-
|
|
211
|
-
"tenant_id is required - set via parameter or
|
|
212
|
-
"Use your Azure tenant ID
|
|
198
|
+
msg = (
|
|
199
|
+
"tenant_id is required - set via parameter or "
|
|
200
|
+
"FASTMCP_SERVER_AUTH_AZURE_TENANT_ID. Use your Azure tenant ID "
|
|
201
|
+
"(found in Azure Portal), 'organizations', or 'consumers'"
|
|
213
202
|
)
|
|
203
|
+
raise ValueError(msg)
|
|
204
|
+
|
|
205
|
+
if not settings.required_scopes:
|
|
206
|
+
raise ValueError("required_scopes is required")
|
|
214
207
|
|
|
215
208
|
# Apply defaults
|
|
209
|
+
self.identifier_uri = settings.identifier_uri or f"api://{settings.client_id}"
|
|
210
|
+
self.additional_authorize_scopes = settings.additional_authorize_scopes or []
|
|
216
211
|
tenant_id_final = settings.tenant_id
|
|
217
212
|
|
|
218
|
-
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
"
|
|
222
|
-
"email",
|
|
223
|
-
"openid",
|
|
224
|
-
"profile",
|
|
213
|
+
# Prefix required scopes with identifier_uri for Azure
|
|
214
|
+
# Azure returns scopes as full URIs (e.g., "api://xxx/read") in tokens
|
|
215
|
+
prefixed_required_scopes = [
|
|
216
|
+
f"{self.identifier_uri}/{scope}" for scope in settings.required_scopes
|
|
225
217
|
]
|
|
226
|
-
|
|
218
|
+
|
|
219
|
+
# Always validate tokens against the app's API client ID using JWT
|
|
220
|
+
issuer = f"https://login.microsoftonline.com/{tenant_id_final}/v2.0"
|
|
221
|
+
jwks_uri = (
|
|
222
|
+
f"https://login.microsoftonline.com/{tenant_id_final}/discovery/v2.0/keys"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
token_verifier = JWTVerifier(
|
|
226
|
+
jwks_uri=jwks_uri,
|
|
227
|
+
issuer=issuer,
|
|
228
|
+
audience=settings.client_id,
|
|
229
|
+
algorithm="RS256",
|
|
230
|
+
required_scopes=prefixed_required_scopes,
|
|
231
|
+
)
|
|
227
232
|
|
|
228
233
|
# Extract secret string from SecretStr
|
|
229
234
|
client_secret_str = (
|
|
230
235
|
settings.client_secret.get_secret_value() if settings.client_secret else ""
|
|
231
236
|
)
|
|
232
237
|
|
|
233
|
-
# Create Azure token verifier
|
|
234
|
-
token_verifier = AzureTokenVerifier(
|
|
235
|
-
required_scopes=scopes_final,
|
|
236
|
-
timeout_seconds=timeout_seconds_final,
|
|
237
|
-
)
|
|
238
|
-
|
|
239
238
|
# Build Azure OAuth endpoints with tenant
|
|
240
239
|
authorization_endpoint = (
|
|
241
240
|
f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/authorize"
|
|
@@ -253,13 +252,65 @@ class AzureProvider(OAuthProxy):
|
|
|
253
252
|
token_verifier=token_verifier,
|
|
254
253
|
base_url=settings.base_url,
|
|
255
254
|
redirect_path=settings.redirect_path,
|
|
256
|
-
issuer_url=settings.
|
|
257
|
-
|
|
255
|
+
issuer_url=settings.issuer_url
|
|
256
|
+
or settings.base_url, # Default to base_url if not specified
|
|
257
|
+
allowed_client_redirect_uris=settings.allowed_client_redirect_uris,
|
|
258
258
|
client_storage=client_storage,
|
|
259
|
+
jwt_signing_key=settings.jwt_signing_key,
|
|
260
|
+
require_authorization_consent=require_authorization_consent,
|
|
259
261
|
)
|
|
260
262
|
|
|
261
263
|
logger.info(
|
|
262
|
-
"Initialized Azure OAuth provider for client %s with tenant %s",
|
|
264
|
+
"Initialized Azure OAuth provider for client %s with tenant %s%s",
|
|
263
265
|
settings.client_id,
|
|
264
266
|
tenant_id_final,
|
|
267
|
+
f" and identifier_uri {self.identifier_uri}" if self.identifier_uri else "",
|
|
265
268
|
)
|
|
269
|
+
|
|
270
|
+
async def authorize(
|
|
271
|
+
self,
|
|
272
|
+
client: OAuthClientInformationFull,
|
|
273
|
+
params: AuthorizationParams,
|
|
274
|
+
) -> str:
|
|
275
|
+
"""Start OAuth transaction and redirect to Azure AD.
|
|
276
|
+
|
|
277
|
+
Override parent's authorize method to filter out the 'resource' parameter
|
|
278
|
+
which is not supported by Azure AD v2.0 endpoints. The v2.0 endpoints use
|
|
279
|
+
scopes to determine the resource/audience instead of a separate parameter.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
client: OAuth client information
|
|
283
|
+
params: Authorization parameters from the client
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Authorization URL to redirect the user to Azure AD
|
|
287
|
+
"""
|
|
288
|
+
# Clear the resource parameter that Azure AD v2.0 doesn't support
|
|
289
|
+
# This parameter comes from RFC 8707 (OAuth 2.0 Resource Indicators)
|
|
290
|
+
# but Azure AD v2.0 uses scopes instead to determine the audience
|
|
291
|
+
params_to_use = params
|
|
292
|
+
if hasattr(params, "resource"):
|
|
293
|
+
original_resource = getattr(params, "resource", None)
|
|
294
|
+
if original_resource is not None:
|
|
295
|
+
params_to_use = params.model_copy(update={"resource": None})
|
|
296
|
+
if original_resource:
|
|
297
|
+
logger.debug(
|
|
298
|
+
"Filtering out 'resource' parameter '%s' for Azure AD v2.0 (use scopes instead)",
|
|
299
|
+
original_resource,
|
|
300
|
+
)
|
|
301
|
+
# Scopes are already prefixed:
|
|
302
|
+
# - self.required_scopes was prefixed during __init__
|
|
303
|
+
# - Client scopes come from PRM which advertises prefixed scopes
|
|
304
|
+
scopes = params_to_use.scopes or self.required_scopes
|
|
305
|
+
|
|
306
|
+
final_scopes = list(scopes)
|
|
307
|
+
# Add Microsoft Graph scopes separately - these use shorthand format (e.g., "User.Read")
|
|
308
|
+
# and should not be prefixed with identifier_uri. Azure returns them as-is in tokens.
|
|
309
|
+
if self.additional_authorize_scopes:
|
|
310
|
+
final_scopes.extend(self.additional_authorize_scopes)
|
|
311
|
+
|
|
312
|
+
modified_params = params_to_use.model_copy(update={"scopes": final_scopes})
|
|
313
|
+
|
|
314
|
+
auth_url = await super().authorize(client, modified_params)
|
|
315
|
+
separator = "&" if "?" in auth_url else "?"
|
|
316
|
+
return f"{auth_url}{separator}prompt=select_account"
|
|
@@ -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."""
|
|
@@ -22,15 +22,16 @@ Example:
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
24
|
import httpx
|
|
25
|
+
from key_value.aio.protocols import AsyncKeyValue
|
|
25
26
|
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
|
26
27
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
27
28
|
|
|
28
29
|
from fastmcp.server.auth import TokenVerifier
|
|
29
30
|
from fastmcp.server.auth.auth import AccessToken
|
|
30
31
|
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
32
|
+
from fastmcp.settings import ENV_FILE
|
|
31
33
|
from fastmcp.utilities.auth import parse_scopes
|
|
32
34
|
from fastmcp.utilities.logging import get_logger
|
|
33
|
-
from fastmcp.utilities.storage import KVStorage
|
|
34
35
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
35
36
|
|
|
36
37
|
logger = get_logger(__name__)
|
|
@@ -41,17 +42,19 @@ class GitHubProviderSettings(BaseSettings):
|
|
|
41
42
|
|
|
42
43
|
model_config = SettingsConfigDict(
|
|
43
44
|
env_prefix="FASTMCP_SERVER_AUTH_GITHUB_",
|
|
44
|
-
env_file=
|
|
45
|
+
env_file=ENV_FILE,
|
|
45
46
|
extra="ignore",
|
|
46
47
|
)
|
|
47
48
|
|
|
48
49
|
client_id: str | None = None
|
|
49
50
|
client_secret: SecretStr | None = None
|
|
50
51
|
base_url: AnyHttpUrl | str | None = None
|
|
52
|
+
issuer_url: AnyHttpUrl | str | None = None
|
|
51
53
|
redirect_path: str | None = None
|
|
52
54
|
required_scopes: list[str] | None = None
|
|
53
55
|
timeout_seconds: int | None = None
|
|
54
56
|
allowed_client_redirect_uris: list[str] | None = None
|
|
57
|
+
jwt_signing_key: str | None = None
|
|
55
58
|
|
|
56
59
|
@field_validator("required_scopes", mode="before")
|
|
57
60
|
@classmethod
|
|
@@ -198,25 +201,38 @@ class GitHubProvider(OAuthProxy):
|
|
|
198
201
|
client_id: str | NotSetT = NotSet,
|
|
199
202
|
client_secret: str | NotSetT = NotSet,
|
|
200
203
|
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
204
|
+
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
201
205
|
redirect_path: str | NotSetT = NotSet,
|
|
202
206
|
required_scopes: list[str] | NotSetT = NotSet,
|
|
203
207
|
timeout_seconds: int | NotSetT = NotSet,
|
|
204
208
|
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
205
|
-
client_storage:
|
|
209
|
+
client_storage: AsyncKeyValue | None = None,
|
|
210
|
+
jwt_signing_key: str | bytes | NotSetT = NotSet,
|
|
211
|
+
require_authorization_consent: bool = True,
|
|
206
212
|
):
|
|
207
213
|
"""Initialize GitHub OAuth provider.
|
|
208
214
|
|
|
209
215
|
Args:
|
|
210
216
|
client_id: GitHub OAuth app client ID (e.g., "Ov23li...")
|
|
211
217
|
client_secret: GitHub OAuth app client secret
|
|
212
|
-
base_url: Public URL
|
|
218
|
+
base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
|
|
219
|
+
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
|
|
220
|
+
to avoid 404s during discovery when mounting under a path.
|
|
213
221
|
redirect_path: Redirect path configured in GitHub OAuth app (defaults to "/auth/callback")
|
|
214
222
|
required_scopes: Required GitHub scopes (defaults to ["user"])
|
|
215
223
|
timeout_seconds: HTTP request timeout for GitHub API calls
|
|
216
224
|
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
217
225
|
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
218
|
-
client_storage: Storage
|
|
219
|
-
|
|
226
|
+
client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
|
|
227
|
+
If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
|
|
228
|
+
disk store will be encrypted using a key derived from the JWT Signing Key.
|
|
229
|
+
jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
|
|
230
|
+
they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
|
|
231
|
+
provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
|
|
232
|
+
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
|
233
|
+
When True, users see a consent screen before being redirected to GitHub.
|
|
234
|
+
When False, authorization proceeds directly without user confirmation.
|
|
235
|
+
SECURITY WARNING: Only disable for local development or testing environments.
|
|
220
236
|
"""
|
|
221
237
|
|
|
222
238
|
settings = GitHubProviderSettings.model_validate(
|
|
@@ -226,10 +242,12 @@ class GitHubProvider(OAuthProxy):
|
|
|
226
242
|
"client_id": client_id,
|
|
227
243
|
"client_secret": client_secret,
|
|
228
244
|
"base_url": base_url,
|
|
245
|
+
"issuer_url": issuer_url,
|
|
229
246
|
"redirect_path": redirect_path,
|
|
230
247
|
"required_scopes": required_scopes,
|
|
231
248
|
"timeout_seconds": timeout_seconds,
|
|
232
249
|
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
250
|
+
"jwt_signing_key": jwt_signing_key,
|
|
233
251
|
}.items()
|
|
234
252
|
if v is not NotSet
|
|
235
253
|
}
|
|
@@ -271,12 +289,15 @@ class GitHubProvider(OAuthProxy):
|
|
|
271
289
|
token_verifier=token_verifier,
|
|
272
290
|
base_url=settings.base_url,
|
|
273
291
|
redirect_path=settings.redirect_path,
|
|
274
|
-
issuer_url=settings.
|
|
292
|
+
issuer_url=settings.issuer_url
|
|
293
|
+
or settings.base_url, # Default to base_url if not specified
|
|
275
294
|
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
276
295
|
client_storage=client_storage,
|
|
296
|
+
jwt_signing_key=settings.jwt_signing_key,
|
|
297
|
+
require_authorization_consent=require_authorization_consent,
|
|
277
298
|
)
|
|
278
299
|
|
|
279
|
-
logger.
|
|
300
|
+
logger.debug(
|
|
280
301
|
"Initialized GitHub OAuth provider for client %s with scopes: %s",
|
|
281
302
|
settings.client_id,
|
|
282
303
|
required_scopes_final,
|