fastmcp 2.12.3__py3-none-any.whl → 2.12.4__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/install/gemini_cli.py +0 -1
- fastmcp/cli/run.py +2 -2
- fastmcp/client/auth/oauth.py +49 -36
- fastmcp/client/client.py +12 -2
- fastmcp/contrib/mcp_mixin/README.md +2 -2
- fastmcp/experimental/utilities/openapi/schemas.py +31 -5
- fastmcp/server/auth/auth.py +3 -3
- fastmcp/server/auth/oauth_proxy.py +42 -12
- fastmcp/server/auth/oidc_proxy.py +348 -0
- fastmcp/server/auth/providers/auth0.py +174 -0
- fastmcp/server/auth/providers/aws.py +237 -0
- fastmcp/server/auth/providers/azure.py +6 -2
- fastmcp/server/auth/providers/descope.py +172 -0
- fastmcp/server/auth/providers/github.py +6 -2
- fastmcp/server/auth/providers/google.py +6 -2
- fastmcp/server/auth/providers/workos.py +6 -2
- fastmcp/server/context.py +7 -6
- fastmcp/server/http.py +1 -1
- fastmcp/server/middleware/logging.py +147 -116
- fastmcp/server/middleware/middleware.py +3 -2
- fastmcp/server/openapi.py +5 -1
- fastmcp/server/server.py +36 -31
- fastmcp/settings.py +27 -5
- fastmcp/tools/tool.py +4 -2
- fastmcp/utilities/json_schema.py +18 -1
- fastmcp/utilities/logging.py +66 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +2 -1
- fastmcp/utilities/storage.py +204 -0
- fastmcp/utilities/tests.py +8 -6
- {fastmcp-2.12.3.dist-info → fastmcp-2.12.4.dist-info}/METADATA +120 -47
- {fastmcp-2.12.3.dist-info → fastmcp-2.12.4.dist-info}/RECORD +34 -29
- {fastmcp-2.12.3.dist-info → fastmcp-2.12.4.dist-info}/WHEEL +0 -0
- {fastmcp-2.12.3.dist-info → fastmcp-2.12.4.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.3.dist-info → fastmcp-2.12.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""AWS Cognito OAuth provider for FastMCP.
|
|
2
|
+
|
|
3
|
+
This module provides a complete AWS Cognito OAuth integration that's ready to use
|
|
4
|
+
with a user pool ID, domain prefix, client ID and client secret. It handles all
|
|
5
|
+
the complexity of AWS Cognito's OAuth flow, token validation, and user management.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
```python
|
|
9
|
+
from fastmcp import FastMCP
|
|
10
|
+
from fastmcp.server.auth.providers.aws_cognito import AWSCognitoProvider
|
|
11
|
+
|
|
12
|
+
# Simple AWS Cognito OAuth protection
|
|
13
|
+
auth = AWSCognitoProvider(
|
|
14
|
+
user_pool_id="your-user-pool-id",
|
|
15
|
+
aws_region="eu-central-1",
|
|
16
|
+
client_id="your-cognito-client-id",
|
|
17
|
+
client_secret="your-cognito-client-secret"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
mcp = FastMCP("My Protected Server", auth=auth)
|
|
21
|
+
```
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from pydantic import AnyHttpUrl, SecretStr, field_validator
|
|
27
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
28
|
+
|
|
29
|
+
from fastmcp.server.auth import TokenVerifier
|
|
30
|
+
from fastmcp.server.auth.auth import AccessToken
|
|
31
|
+
from fastmcp.server.auth.oidc_proxy import OIDCProxy
|
|
32
|
+
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
33
|
+
from fastmcp.utilities.auth import parse_scopes
|
|
34
|
+
from fastmcp.utilities.logging import get_logger
|
|
35
|
+
from fastmcp.utilities.types import NotSet, NotSetT
|
|
36
|
+
|
|
37
|
+
logger = get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AWSCognitoProviderSettings(BaseSettings):
|
|
41
|
+
"""Settings for AWS Cognito OAuth provider."""
|
|
42
|
+
|
|
43
|
+
model_config = SettingsConfigDict(
|
|
44
|
+
env_prefix="FASTMCP_SERVER_AUTH_AWS_COGNITO_",
|
|
45
|
+
env_file=".env",
|
|
46
|
+
extra="ignore",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
user_pool_id: str | None = None
|
|
50
|
+
aws_region: str | None = None
|
|
51
|
+
client_id: str | None = None
|
|
52
|
+
client_secret: SecretStr | None = None
|
|
53
|
+
base_url: AnyHttpUrl | str | None = None
|
|
54
|
+
redirect_path: str | None = None
|
|
55
|
+
required_scopes: list[str] | None = None
|
|
56
|
+
allowed_client_redirect_uris: list[str] | None = None
|
|
57
|
+
|
|
58
|
+
@field_validator("required_scopes", mode="before")
|
|
59
|
+
@classmethod
|
|
60
|
+
def _parse_scopes(cls, v):
|
|
61
|
+
return parse_scopes(v)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AWSCognitoTokenVerifier(JWTVerifier):
|
|
65
|
+
"""Token verifier that filters claims to Cognito-specific subset."""
|
|
66
|
+
|
|
67
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
68
|
+
"""Verify token and filter claims to Cognito-specific subset."""
|
|
69
|
+
# Use base JWT verification
|
|
70
|
+
access_token = await super().verify_token(token)
|
|
71
|
+
if not access_token:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
# Filter claims to Cognito-specific subset
|
|
75
|
+
cognito_claims = {
|
|
76
|
+
"sub": access_token.claims.get("sub"),
|
|
77
|
+
"username": access_token.claims.get("username"),
|
|
78
|
+
"cognito:groups": access_token.claims.get("cognito:groups", []),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Return new AccessToken with filtered claims
|
|
82
|
+
return AccessToken(
|
|
83
|
+
token=access_token.token,
|
|
84
|
+
client_id=access_token.client_id,
|
|
85
|
+
scopes=access_token.scopes,
|
|
86
|
+
expires_at=access_token.expires_at,
|
|
87
|
+
claims=cognito_claims,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AWSCognitoProvider(OIDCProxy):
|
|
92
|
+
"""Complete AWS Cognito OAuth provider for FastMCP.
|
|
93
|
+
|
|
94
|
+
This provider makes it trivial to add AWS Cognito OAuth protection to any
|
|
95
|
+
FastMCP server using OIDC Discovery. Just provide your Cognito User Pool details,
|
|
96
|
+
client credentials, and a base URL, and you're ready to go.
|
|
97
|
+
|
|
98
|
+
Features:
|
|
99
|
+
- Automatic OIDC Discovery from AWS Cognito User Pool
|
|
100
|
+
- Automatic JWT token validation via Cognito's public keys
|
|
101
|
+
- Cognito-specific claim filtering (sub, username, cognito:groups)
|
|
102
|
+
- Support for Cognito User Pools
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
```python
|
|
106
|
+
from fastmcp import FastMCP
|
|
107
|
+
from fastmcp.server.auth.providers.aws_cognito import AWSCognitoProvider
|
|
108
|
+
|
|
109
|
+
auth = AWSCognitoProvider(
|
|
110
|
+
user_pool_id="eu-central-1_XXXXXXXXX",
|
|
111
|
+
aws_region="eu-central-1",
|
|
112
|
+
client_id="your-cognito-client-id",
|
|
113
|
+
client_secret="your-cognito-client-secret",
|
|
114
|
+
base_url="https://my-server.com",
|
|
115
|
+
redirect_path="/custom/callback",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
mcp = FastMCP("My App", auth=auth)
|
|
119
|
+
```
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
*,
|
|
125
|
+
user_pool_id: str | NotSetT = NotSet,
|
|
126
|
+
aws_region: str | NotSetT = NotSet,
|
|
127
|
+
client_id: str | NotSetT = NotSet,
|
|
128
|
+
client_secret: str | NotSetT = NotSet,
|
|
129
|
+
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
130
|
+
redirect_path: str | NotSetT = NotSet,
|
|
131
|
+
required_scopes: list[str] | NotSetT = NotSet,
|
|
132
|
+
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
133
|
+
):
|
|
134
|
+
"""Initialize AWS Cognito OAuth provider.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
user_pool_id: Your Cognito User Pool ID (e.g., "eu-central-1_XXXXXXXXX")
|
|
138
|
+
aws_region: AWS region where your User Pool is located (defaults to "eu-central-1")
|
|
139
|
+
client_id: Cognito app client ID
|
|
140
|
+
client_secret: Cognito app client secret
|
|
141
|
+
base_url: Public URL of your FastMCP server (for OAuth callbacks)
|
|
142
|
+
redirect_path: Redirect path configured in Cognito app (defaults to "/auth/callback")
|
|
143
|
+
required_scopes: Required Cognito scopes (defaults to ["openid"])
|
|
144
|
+
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
145
|
+
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
settings = AWSCognitoProviderSettings.model_validate(
|
|
149
|
+
{
|
|
150
|
+
k: v
|
|
151
|
+
for k, v in {
|
|
152
|
+
"user_pool_id": user_pool_id,
|
|
153
|
+
"aws_region": aws_region,
|
|
154
|
+
"client_id": client_id,
|
|
155
|
+
"client_secret": client_secret,
|
|
156
|
+
"base_url": base_url,
|
|
157
|
+
"redirect_path": redirect_path,
|
|
158
|
+
"required_scopes": required_scopes,
|
|
159
|
+
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
|
160
|
+
}.items()
|
|
161
|
+
if v is not NotSet
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Validate required settings
|
|
166
|
+
if not settings.user_pool_id:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
"user_pool_id is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID"
|
|
169
|
+
)
|
|
170
|
+
if not settings.client_id:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
"client_id is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID"
|
|
173
|
+
)
|
|
174
|
+
if not settings.client_secret:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
"client_secret is required - set via parameter or FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Apply defaults
|
|
180
|
+
required_scopes_final = settings.required_scopes or ["openid"]
|
|
181
|
+
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
|
|
182
|
+
aws_region_final = settings.aws_region or "eu-central-1"
|
|
183
|
+
redirect_path_final = settings.redirect_path or "/auth/callback"
|
|
184
|
+
|
|
185
|
+
# Construct OIDC discovery URL
|
|
186
|
+
config_url = f"https://cognito-idp.{aws_region_final}.amazonaws.com/{settings.user_pool_id}/.well-known/openid-configuration"
|
|
187
|
+
|
|
188
|
+
# Extract secret string from SecretStr
|
|
189
|
+
client_secret_str = (
|
|
190
|
+
settings.client_secret.get_secret_value() if settings.client_secret else ""
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Store Cognito-specific info for claim filtering
|
|
194
|
+
self.user_pool_id = settings.user_pool_id
|
|
195
|
+
self.aws_region = aws_region_final
|
|
196
|
+
|
|
197
|
+
# Initialize OIDC proxy with Cognito discovery
|
|
198
|
+
super().__init__(
|
|
199
|
+
config_url=config_url,
|
|
200
|
+
client_id=settings.client_id,
|
|
201
|
+
client_secret=client_secret_str,
|
|
202
|
+
algorithm="RS256",
|
|
203
|
+
required_scopes=required_scopes_final,
|
|
204
|
+
base_url=settings.base_url,
|
|
205
|
+
redirect_path=redirect_path_final,
|
|
206
|
+
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
logger.info(
|
|
210
|
+
"Initialized AWS Cognito OAuth provider for client %s with scopes: %s",
|
|
211
|
+
settings.client_id,
|
|
212
|
+
required_scopes_final,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def get_token_verifier(
|
|
216
|
+
self,
|
|
217
|
+
*,
|
|
218
|
+
algorithm: str | None = None,
|
|
219
|
+
audience: str | None = None,
|
|
220
|
+
required_scopes: list[str] | None = None,
|
|
221
|
+
timeout_seconds: int | None = None,
|
|
222
|
+
) -> TokenVerifier:
|
|
223
|
+
"""Creates a Cognito-specific token verifier with claim filtering.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
algorithm: Optional token verifier algorithm
|
|
227
|
+
audience: Optional token verifier audience
|
|
228
|
+
required_scopes: Optional token verifier required_scopes
|
|
229
|
+
timeout_seconds: HTTP request timeout in seconds
|
|
230
|
+
"""
|
|
231
|
+
return AWSCognitoTokenVerifier(
|
|
232
|
+
issuer=str(self.oidc_config.issuer),
|
|
233
|
+
audience=audience,
|
|
234
|
+
algorithm=algorithm,
|
|
235
|
+
jwks_uri=str(self.oidc_config.jwks_uri),
|
|
236
|
+
required_scopes=required_scopes,
|
|
237
|
+
)
|
|
@@ -14,6 +14,7 @@ from fastmcp.server.auth import AccessToken, TokenVerifier
|
|
|
14
14
|
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
15
15
|
from fastmcp.utilities.auth import parse_scopes
|
|
16
16
|
from fastmcp.utilities.logging import get_logger
|
|
17
|
+
from fastmcp.utilities.storage import KVStorage
|
|
17
18
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
18
19
|
|
|
19
20
|
logger = get_logger(__name__)
|
|
@@ -160,6 +161,7 @@ class AzureProvider(OAuthProxy):
|
|
|
160
161
|
required_scopes: list[str] | None | NotSetT = NotSet,
|
|
161
162
|
timeout_seconds: int | NotSetT = NotSet,
|
|
162
163
|
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
164
|
+
client_storage: KVStorage | None = None,
|
|
163
165
|
):
|
|
164
166
|
"""Initialize Azure OAuth provider.
|
|
165
167
|
|
|
@@ -173,6 +175,8 @@ class AzureProvider(OAuthProxy):
|
|
|
173
175
|
timeout_seconds: HTTP request timeout for Azure API calls
|
|
174
176
|
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
175
177
|
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
178
|
+
client_storage: Storage implementation for OAuth client registrations.
|
|
179
|
+
Defaults to file-based storage if not specified.
|
|
176
180
|
"""
|
|
177
181
|
settings = AzureProviderSettings.model_validate(
|
|
178
182
|
{
|
|
@@ -211,7 +215,6 @@ class AzureProvider(OAuthProxy):
|
|
|
211
215
|
# Apply defaults
|
|
212
216
|
tenant_id_final = settings.tenant_id
|
|
213
217
|
|
|
214
|
-
redirect_path_final = settings.redirect_path or "/auth/callback"
|
|
215
218
|
timeout_seconds_final = settings.timeout_seconds or 10
|
|
216
219
|
# Default scopes for Azure - User.Read gives us access to user info via Graph API
|
|
217
220
|
scopes_final = settings.required_scopes or [
|
|
@@ -249,9 +252,10 @@ class AzureProvider(OAuthProxy):
|
|
|
249
252
|
upstream_client_secret=client_secret_str,
|
|
250
253
|
token_verifier=token_verifier,
|
|
251
254
|
base_url=settings.base_url,
|
|
252
|
-
redirect_path=
|
|
255
|
+
redirect_path=settings.redirect_path,
|
|
253
256
|
issuer_url=settings.base_url,
|
|
254
257
|
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
258
|
+
client_storage=client_storage,
|
|
255
259
|
)
|
|
256
260
|
|
|
257
261
|
logger.info(
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Descope authentication provider for FastMCP.
|
|
2
|
+
|
|
3
|
+
This module provides DescopeProvider - a complete authentication solution that integrates
|
|
4
|
+
with Descope's OAuth 2.1 and OpenID Connect services, supporting Dynamic Client Registration (DCR)
|
|
5
|
+
for seamless MCP client authentication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from pydantic import AnyHttpUrl
|
|
14
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
15
|
+
from starlette.responses import JSONResponse
|
|
16
|
+
from starlette.routing import Route
|
|
17
|
+
|
|
18
|
+
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
|
|
19
|
+
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
20
|
+
from fastmcp.utilities.logging import get_logger
|
|
21
|
+
from fastmcp.utilities.types import NotSet, NotSetT
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DescopeProviderSettings(BaseSettings):
|
|
27
|
+
model_config = SettingsConfigDict(
|
|
28
|
+
env_prefix="FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_",
|
|
29
|
+
env_file=".env",
|
|
30
|
+
extra="ignore",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
project_id: str
|
|
34
|
+
base_url: AnyHttpUrl
|
|
35
|
+
descope_base_url: AnyHttpUrl = AnyHttpUrl("https://api.descope.com")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DescopeProvider(RemoteAuthProvider):
|
|
39
|
+
"""Descope metadata provider for DCR (Dynamic Client Registration).
|
|
40
|
+
|
|
41
|
+
This provider implements Descope integration using metadata forwarding.
|
|
42
|
+
This is the recommended approach for Descope DCR
|
|
43
|
+
as it allows Descope to handle the OAuth flow directly while FastMCP acts
|
|
44
|
+
as a resource server.
|
|
45
|
+
|
|
46
|
+
IMPORTANT SETUP REQUIREMENTS:
|
|
47
|
+
|
|
48
|
+
1. Enable Dynamic Client Registration in Descope Console:
|
|
49
|
+
- Go to the [Inbound Apps page](https://app.descope.com/apps/inbound) of the Descope Console
|
|
50
|
+
- Click **DCR Settings**
|
|
51
|
+
- Enable **Dynamic Client Registration (DCR)**
|
|
52
|
+
- Define allowed scopes
|
|
53
|
+
|
|
54
|
+
2. Note your Project ID:
|
|
55
|
+
- Save your Project ID from [Project Settings](https://app.descope.com/settings/project)
|
|
56
|
+
- Example: P2abc...123
|
|
57
|
+
|
|
58
|
+
For detailed setup instructions, see:
|
|
59
|
+
https://docs.descope.com/identity-federation/inbound-apps/creating-inbound-apps#method-2-dynamic-client-registration-dcr
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
```python
|
|
63
|
+
from fastmcp.server.auth.providers.descope import DescopeProvider
|
|
64
|
+
|
|
65
|
+
# Create Descope metadata provider (JWT verifier created automatically)
|
|
66
|
+
descope_auth = DescopeProvider(
|
|
67
|
+
project_id="P2abc...123",
|
|
68
|
+
base_url="https://your-fastmcp-server.com",
|
|
69
|
+
descope_base_url="https://api.descope.com",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Use with FastMCP
|
|
73
|
+
mcp = FastMCP("My App", auth=descope_auth)
|
|
74
|
+
```
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
*,
|
|
80
|
+
project_id: str | NotSetT = NotSet,
|
|
81
|
+
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
82
|
+
descope_base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
83
|
+
token_verifier: TokenVerifier | None = None,
|
|
84
|
+
):
|
|
85
|
+
"""Initialize Descope metadata provider.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
project_id: Your Descope Project ID (e.g., "P2abc...123")
|
|
89
|
+
base_url: Public URL of this FastMCP server
|
|
90
|
+
descope_base_url: Descope API base URL (defaults to https://api.descope.com)
|
|
91
|
+
token_verifier: Optional token verifier. If None, creates JWT verifier for Descope
|
|
92
|
+
"""
|
|
93
|
+
settings = DescopeProviderSettings.model_validate(
|
|
94
|
+
{
|
|
95
|
+
k: v
|
|
96
|
+
for k, v in {
|
|
97
|
+
"project_id": project_id,
|
|
98
|
+
"base_url": base_url,
|
|
99
|
+
"descope_base_url": descope_base_url,
|
|
100
|
+
}.items()
|
|
101
|
+
if v is not NotSet
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self.project_id = settings.project_id
|
|
106
|
+
self.base_url = str(settings.base_url).rstrip("/")
|
|
107
|
+
self.descope_base_url = str(settings.descope_base_url).rstrip("/")
|
|
108
|
+
|
|
109
|
+
# Create default JWT verifier if none provided
|
|
110
|
+
if token_verifier is None:
|
|
111
|
+
token_verifier = JWTVerifier(
|
|
112
|
+
jwks_uri=f"{self.descope_base_url}/{self.project_id}/.well-known/jwks.json",
|
|
113
|
+
issuer=f"{self.descope_base_url}/v1/apps/{self.project_id}",
|
|
114
|
+
algorithm="RS256",
|
|
115
|
+
audience=self.project_id,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Initialize RemoteAuthProvider with Descope as the authorization server
|
|
119
|
+
super().__init__(
|
|
120
|
+
token_verifier=token_verifier,
|
|
121
|
+
authorization_servers=[
|
|
122
|
+
AnyHttpUrl(f"{self.descope_base_url}/v1/apps/{self.project_id}")
|
|
123
|
+
],
|
|
124
|
+
base_url=self.base_url,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def get_routes(
|
|
128
|
+
self,
|
|
129
|
+
mcp_path: str | None = None,
|
|
130
|
+
mcp_endpoint: Any | None = None,
|
|
131
|
+
) -> list[Route]:
|
|
132
|
+
"""Get OAuth routes including Descope authorization server metadata forwarding.
|
|
133
|
+
|
|
134
|
+
This returns the standard protected resource routes plus an authorization server
|
|
135
|
+
metadata endpoint that forwards Descope's OAuth metadata to clients.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
|
|
139
|
+
mcp_endpoint: The MCP endpoint handler to protect with auth
|
|
140
|
+
"""
|
|
141
|
+
# Get the standard protected resource routes from RemoteAuthProvider
|
|
142
|
+
routes = super().get_routes(mcp_path, mcp_endpoint)
|
|
143
|
+
|
|
144
|
+
async def oauth_authorization_server_metadata(request):
|
|
145
|
+
"""Forward Descope OAuth authorization server metadata with FastMCP customizations."""
|
|
146
|
+
try:
|
|
147
|
+
async with httpx.AsyncClient() as client:
|
|
148
|
+
response = await client.get(
|
|
149
|
+
f"{self.descope_base_url}/v1/apps/{self.project_id}/.well-known/oauth-authorization-server"
|
|
150
|
+
)
|
|
151
|
+
response.raise_for_status()
|
|
152
|
+
metadata = response.json()
|
|
153
|
+
return JSONResponse(metadata)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
return JSONResponse(
|
|
156
|
+
{
|
|
157
|
+
"error": "server_error",
|
|
158
|
+
"error_description": f"Failed to fetch Descope metadata: {e}",
|
|
159
|
+
},
|
|
160
|
+
status_code=500,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Add Descope authorization server metadata forwarding
|
|
164
|
+
routes.append(
|
|
165
|
+
Route(
|
|
166
|
+
"/.well-known/oauth-authorization-server",
|
|
167
|
+
endpoint=oauth_authorization_server_metadata,
|
|
168
|
+
methods=["GET"],
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return routes
|
|
@@ -30,6 +30,7 @@ from fastmcp.server.auth.auth import AccessToken
|
|
|
30
30
|
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
31
31
|
from fastmcp.utilities.auth import parse_scopes
|
|
32
32
|
from fastmcp.utilities.logging import get_logger
|
|
33
|
+
from fastmcp.utilities.storage import KVStorage
|
|
33
34
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
34
35
|
|
|
35
36
|
logger = get_logger(__name__)
|
|
@@ -201,6 +202,7 @@ class GitHubProvider(OAuthProxy):
|
|
|
201
202
|
required_scopes: list[str] | NotSetT = NotSet,
|
|
202
203
|
timeout_seconds: int | NotSetT = NotSet,
|
|
203
204
|
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
205
|
+
client_storage: KVStorage | None = None,
|
|
204
206
|
):
|
|
205
207
|
"""Initialize GitHub OAuth provider.
|
|
206
208
|
|
|
@@ -213,6 +215,8 @@ class GitHubProvider(OAuthProxy):
|
|
|
213
215
|
timeout_seconds: HTTP request timeout for GitHub API calls
|
|
214
216
|
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
215
217
|
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
218
|
+
client_storage: Storage implementation for OAuth client registrations.
|
|
219
|
+
Defaults to file-based storage if not specified.
|
|
216
220
|
"""
|
|
217
221
|
|
|
218
222
|
settings = GitHubProviderSettings.model_validate(
|
|
@@ -243,7 +247,6 @@ class GitHubProvider(OAuthProxy):
|
|
|
243
247
|
|
|
244
248
|
# Apply defaults
|
|
245
249
|
|
|
246
|
-
redirect_path_final = settings.redirect_path or "/auth/callback"
|
|
247
250
|
timeout_seconds_final = settings.timeout_seconds or 10
|
|
248
251
|
required_scopes_final = settings.required_scopes or ["user"]
|
|
249
252
|
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
|
|
@@ -267,9 +270,10 @@ class GitHubProvider(OAuthProxy):
|
|
|
267
270
|
upstream_client_secret=client_secret_str,
|
|
268
271
|
token_verifier=token_verifier,
|
|
269
272
|
base_url=settings.base_url,
|
|
270
|
-
redirect_path=
|
|
273
|
+
redirect_path=settings.redirect_path,
|
|
271
274
|
issuer_url=settings.base_url, # We act as the issuer for client registration
|
|
272
275
|
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
276
|
+
client_storage=client_storage,
|
|
273
277
|
)
|
|
274
278
|
|
|
275
279
|
logger.info(
|
|
@@ -32,6 +32,7 @@ from fastmcp.server.auth.auth import AccessToken
|
|
|
32
32
|
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
33
33
|
from fastmcp.utilities.auth import parse_scopes
|
|
34
34
|
from fastmcp.utilities.logging import get_logger
|
|
35
|
+
from fastmcp.utilities.storage import KVStorage
|
|
35
36
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
36
37
|
|
|
37
38
|
logger = get_logger(__name__)
|
|
@@ -217,6 +218,7 @@ class GoogleProvider(OAuthProxy):
|
|
|
217
218
|
required_scopes: list[str] | NotSetT = NotSet,
|
|
218
219
|
timeout_seconds: int | NotSetT = NotSet,
|
|
219
220
|
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
221
|
+
client_storage: KVStorage | None = None,
|
|
220
222
|
):
|
|
221
223
|
"""Initialize Google OAuth provider.
|
|
222
224
|
|
|
@@ -232,6 +234,8 @@ class GoogleProvider(OAuthProxy):
|
|
|
232
234
|
timeout_seconds: HTTP request timeout for Google API calls
|
|
233
235
|
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
234
236
|
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
237
|
+
client_storage: Storage implementation for OAuth client registrations.
|
|
238
|
+
Defaults to file-based storage if not specified.
|
|
235
239
|
"""
|
|
236
240
|
|
|
237
241
|
settings = GoogleProviderSettings.model_validate(
|
|
@@ -261,7 +265,6 @@ class GoogleProvider(OAuthProxy):
|
|
|
261
265
|
)
|
|
262
266
|
|
|
263
267
|
# Apply defaults
|
|
264
|
-
redirect_path_final = settings.redirect_path or "/auth/callback"
|
|
265
268
|
timeout_seconds_final = settings.timeout_seconds or 10
|
|
266
269
|
# Google requires at least one scope - openid is the minimal OIDC scope
|
|
267
270
|
required_scopes_final = settings.required_scopes or ["openid"]
|
|
@@ -286,9 +289,10 @@ class GoogleProvider(OAuthProxy):
|
|
|
286
289
|
upstream_client_secret=client_secret_str,
|
|
287
290
|
token_verifier=token_verifier,
|
|
288
291
|
base_url=settings.base_url,
|
|
289
|
-
redirect_path=
|
|
292
|
+
redirect_path=settings.redirect_path,
|
|
290
293
|
issuer_url=settings.base_url, # We act as the issuer for client registration
|
|
291
294
|
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
295
|
+
client_storage=client_storage,
|
|
292
296
|
)
|
|
293
297
|
|
|
294
298
|
logger.info(
|
|
@@ -23,6 +23,7 @@ from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
|
|
23
23
|
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
24
24
|
from fastmcp.utilities.auth import parse_scopes
|
|
25
25
|
from fastmcp.utilities.logging import get_logger
|
|
26
|
+
from fastmcp.utilities.storage import KVStorage
|
|
26
27
|
from fastmcp.utilities.types import NotSet, NotSetT
|
|
27
28
|
|
|
28
29
|
logger = get_logger(__name__)
|
|
@@ -169,6 +170,7 @@ class WorkOSProvider(OAuthProxy):
|
|
|
169
170
|
required_scopes: list[str] | None | NotSetT = NotSet,
|
|
170
171
|
timeout_seconds: int | NotSetT = NotSet,
|
|
171
172
|
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
173
|
+
client_storage: KVStorage | None = None,
|
|
172
174
|
):
|
|
173
175
|
"""Initialize WorkOS OAuth provider.
|
|
174
176
|
|
|
@@ -182,6 +184,8 @@ class WorkOSProvider(OAuthProxy):
|
|
|
182
184
|
timeout_seconds: HTTP request timeout for WorkOS API calls
|
|
183
185
|
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
|
184
186
|
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
|
187
|
+
client_storage: Storage implementation for OAuth client registrations.
|
|
188
|
+
Defaults to file-based storage if not specified.
|
|
185
189
|
"""
|
|
186
190
|
|
|
187
191
|
settings = WorkOSProviderSettings.model_validate(
|
|
@@ -220,7 +224,6 @@ class WorkOSProvider(OAuthProxy):
|
|
|
220
224
|
if not authkit_domain_str.startswith(("http://", "https://")):
|
|
221
225
|
authkit_domain_str = f"https://{authkit_domain_str}"
|
|
222
226
|
authkit_domain_final = authkit_domain_str.rstrip("/")
|
|
223
|
-
redirect_path_final = settings.redirect_path or "/auth/callback"
|
|
224
227
|
timeout_seconds_final = settings.timeout_seconds or 10
|
|
225
228
|
scopes_final = settings.required_scopes or []
|
|
226
229
|
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
|
|
@@ -245,9 +248,10 @@ class WorkOSProvider(OAuthProxy):
|
|
|
245
248
|
upstream_client_secret=client_secret_str,
|
|
246
249
|
token_verifier=token_verifier,
|
|
247
250
|
base_url=settings.base_url,
|
|
248
|
-
redirect_path=
|
|
251
|
+
redirect_path=settings.redirect_path,
|
|
249
252
|
issuer_url=settings.base_url,
|
|
250
253
|
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
|
254
|
+
client_storage=client_storage,
|
|
251
255
|
)
|
|
252
256
|
|
|
253
257
|
logger.info(
|
fastmcp/server/context.py
CHANGED
|
@@ -5,7 +5,7 @@ import copy
|
|
|
5
5
|
import inspect
|
|
6
6
|
import warnings
|
|
7
7
|
import weakref
|
|
8
|
-
from collections.abc import Generator, Mapping
|
|
8
|
+
from collections.abc import Generator, Mapping, Sequence
|
|
9
9
|
from contextlib import contextmanager
|
|
10
10
|
from contextvars import ContextVar, Token
|
|
11
11
|
from dataclasses import dataclass
|
|
@@ -17,9 +17,10 @@ from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
|
17
17
|
from mcp.server.lowlevel.server import request_ctx
|
|
18
18
|
from mcp.shared.context import RequestContext
|
|
19
19
|
from mcp.types import (
|
|
20
|
+
AudioContent,
|
|
20
21
|
ClientCapabilities,
|
|
21
|
-
ContentBlock,
|
|
22
22
|
CreateMessageResult,
|
|
23
|
+
ImageContent,
|
|
23
24
|
IncludeContext,
|
|
24
25
|
ModelHint,
|
|
25
26
|
ModelPreferences,
|
|
@@ -359,13 +360,13 @@ class Context:
|
|
|
359
360
|
|
|
360
361
|
async def sample(
|
|
361
362
|
self,
|
|
362
|
-
messages: str |
|
|
363
|
+
messages: str | Sequence[str | SamplingMessage],
|
|
363
364
|
system_prompt: str | None = None,
|
|
364
365
|
include_context: IncludeContext | None = None,
|
|
365
366
|
temperature: float | None = None,
|
|
366
367
|
max_tokens: int | None = None,
|
|
367
368
|
model_preferences: ModelPreferences | str | list[str] | None = None,
|
|
368
|
-
) ->
|
|
369
|
+
) -> TextContent | ImageContent | AudioContent:
|
|
369
370
|
"""
|
|
370
371
|
Send a sampling request to the client and await the response.
|
|
371
372
|
|
|
@@ -383,7 +384,7 @@ class Context:
|
|
|
383
384
|
content=TextContent(text=messages, type="text"), role="user"
|
|
384
385
|
)
|
|
385
386
|
]
|
|
386
|
-
elif isinstance(messages,
|
|
387
|
+
elif isinstance(messages, Sequence):
|
|
387
388
|
sampling_messages = [
|
|
388
389
|
SamplingMessage(content=TextContent(text=m, type="text"), role="user")
|
|
389
390
|
if isinstance(m, str)
|
|
@@ -573,7 +574,7 @@ class Context:
|
|
|
573
574
|
warnings.warn(
|
|
574
575
|
"Context.get_http_request() is deprecated and will be removed in a future version. "
|
|
575
576
|
"Use get_http_request() from fastmcp.server.dependencies instead. "
|
|
576
|
-
"See https://gofastmcp.com/
|
|
577
|
+
"See https://gofastmcp.com/servers/context#http-requests for more details.",
|
|
577
578
|
DeprecationWarning,
|
|
578
579
|
stacklevel=2,
|
|
579
580
|
)
|
fastmcp/server/http.py
CHANGED
|
@@ -113,7 +113,7 @@ def create_base_app(
|
|
|
113
113
|
A Starlette application
|
|
114
114
|
"""
|
|
115
115
|
# Always add RequestContextMiddleware as the outermost middleware
|
|
116
|
-
middleware.
|
|
116
|
+
middleware.insert(0, Middleware(RequestContextMiddleware))
|
|
117
117
|
|
|
118
118
|
return StarletteWithLifespan(
|
|
119
119
|
routes=routes,
|