fastmcp 2.13.0rc3__py3-none-any.whl → 2.13.0.2__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/__init__.py +2 -2
- fastmcp/cli/cli.py +2 -2
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +7 -6
- fastmcp/client/client.py +10 -10
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +34 -34
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +1 -1
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +2 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +3 -5
- fastmcp/experimental/utilities/openapi/schemas.py +2 -2
- fastmcp/mcp_config.py +2 -3
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +9 -13
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +1 -3
- fastmcp/resources/resource_manager.py +1 -1
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +5 -5
- fastmcp/server/auth/auth.py +2 -2
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/azure.py +48 -25
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/in_memory.py +2 -2
- fastmcp/server/auth/providers/introspection.py +2 -2
- fastmcp/server/auth/providers/jwt.py +17 -18
- fastmcp/server/auth/providers/supabase.py +1 -1
- fastmcp/server/auth/providers/workos.py +2 -2
- fastmcp/server/context.py +8 -10
- fastmcp/server/dependencies.py +5 -6
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +2 -3
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +1 -1
- fastmcp/server/middleware/error_handling.py +8 -8
- fastmcp/server/middleware/middleware.py +1 -1
- fastmcp/server/openapi.py +10 -6
- fastmcp/server/proxy.py +5 -4
- fastmcp/server/server.py +27 -29
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +12 -12
- fastmcp/tools/tool_transform.py +6 -6
- fastmcp/utilities/cli.py +5 -6
- fastmcp/utilities/inspect.py +2 -2
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +14 -18
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +9 -9
- fastmcp/utilities/tests.py +2 -4
- {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/METADATA +3 -3
- {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/RECORD +65 -65
- {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/WHEEL +0 -0
- {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -235,7 +235,7 @@ class ResourceManager:
|
|
|
235
235
|
|
|
236
236
|
# Then check templates (local and mounted) only if not found in concrete resources
|
|
237
237
|
templates = await self.get_resource_templates()
|
|
238
|
-
for template_key in templates
|
|
238
|
+
for template_key in templates:
|
|
239
239
|
if match_uri_template(uri_str, template_key):
|
|
240
240
|
return True
|
|
241
241
|
|
fastmcp/server/__init__.py
CHANGED
fastmcp/server/auth/__init__.py
CHANGED
|
@@ -10,14 +10,14 @@ from .oauth_proxy import OAuthProxy
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
13
|
+
"AccessToken",
|
|
13
14
|
"AuthProvider",
|
|
14
|
-
"OAuthProvider",
|
|
15
|
-
"TokenVerifier",
|
|
16
15
|
"JWTVerifier",
|
|
17
|
-
"
|
|
18
|
-
"RemoteAuthProvider",
|
|
19
|
-
"AccessToken",
|
|
16
|
+
"OAuthProvider",
|
|
20
17
|
"OAuthProxy",
|
|
18
|
+
"RemoteAuthProvider",
|
|
19
|
+
"StaticTokenVerifier",
|
|
20
|
+
"TokenVerifier",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
|
fastmcp/server/auth/auth.py
CHANGED
|
@@ -23,7 +23,7 @@ from mcp.server.auth.settings import (
|
|
|
23
23
|
ClientRegistrationOptions,
|
|
24
24
|
RevocationOptions,
|
|
25
25
|
)
|
|
26
|
-
from pydantic import AnyHttpUrl
|
|
26
|
+
from pydantic import AnyHttpUrl, Field
|
|
27
27
|
from starlette.middleware import Middleware
|
|
28
28
|
from starlette.middleware.authentication import AuthenticationMiddleware
|
|
29
29
|
from starlette.routing import Route
|
|
@@ -32,7 +32,7 @@ from starlette.routing import Route
|
|
|
32
32
|
class AccessToken(_SDKAccessToken):
|
|
33
33
|
"""AccessToken that includes all JWT claims."""
|
|
34
34
|
|
|
35
|
-
claims: dict[str, Any] =
|
|
35
|
+
claims: dict[str, Any] = Field(default_factory=dict)
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
class AuthProvider(TokenVerifierProtocol):
|
|
@@ -123,10 +123,10 @@ class OIDCConfiguration(BaseModel):
|
|
|
123
123
|
|
|
124
124
|
try:
|
|
125
125
|
AnyHttpUrl(value)
|
|
126
|
-
except Exception:
|
|
126
|
+
except Exception as e:
|
|
127
127
|
message = f"Invalid URL for configuration metadata: {attr}"
|
|
128
128
|
logger.error(message)
|
|
129
|
-
raise ValueError(message)
|
|
129
|
+
raise ValueError(message) from e
|
|
130
130
|
|
|
131
131
|
enforce("issuer", True)
|
|
132
132
|
enforce("authorization_endpoint", True)
|
|
@@ -6,7 +6,7 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from typing import TYPE_CHECKING
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
11
|
from key_value.aio.protocols import AsyncKeyValue
|
|
12
12
|
from pydantic import SecretStr, field_validator
|
|
@@ -113,12 +113,12 @@ class AzureProvider(OAuthProxy):
|
|
|
113
113
|
client_id: str | NotSetT = NotSet,
|
|
114
114
|
client_secret: str | NotSetT = NotSet,
|
|
115
115
|
tenant_id: str | NotSetT = NotSet,
|
|
116
|
-
identifier_uri: str |
|
|
116
|
+
identifier_uri: str | NotSetT | None = NotSet,
|
|
117
117
|
base_url: str | NotSetT = NotSet,
|
|
118
118
|
issuer_url: str | NotSetT = NotSet,
|
|
119
119
|
redirect_path: str | NotSetT = NotSet,
|
|
120
|
-
required_scopes: list[str] |
|
|
121
|
-
additional_authorize_scopes: list[str] |
|
|
120
|
+
required_scopes: list[str] | NotSetT | None = NotSet,
|
|
121
|
+
additional_authorize_scopes: list[str] | NotSetT | None = NotSet,
|
|
122
122
|
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
123
123
|
client_storage: AsyncKeyValue | None = None,
|
|
124
124
|
jwt_signing_key: str | bytes | NotSetT = NotSet,
|
|
@@ -202,32 +202,34 @@ class AzureProvider(OAuthProxy):
|
|
|
202
202
|
)
|
|
203
203
|
raise ValueError(msg)
|
|
204
204
|
|
|
205
|
+
# Validate required_scopes has at least one scope
|
|
205
206
|
if not settings.required_scopes:
|
|
206
|
-
|
|
207
|
+
msg = (
|
|
208
|
+
"required_scopes must include at least one scope - set via parameter or "
|
|
209
|
+
"FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES. Azure's OAuth API requires "
|
|
210
|
+
"the 'scope' parameter in authorization requests. Use the unprefixed scope "
|
|
211
|
+
"names from your Azure App registration (e.g., ['read', 'write'])"
|
|
212
|
+
)
|
|
213
|
+
raise ValueError(msg)
|
|
207
214
|
|
|
208
215
|
# Apply defaults
|
|
209
216
|
self.identifier_uri = settings.identifier_uri or f"api://{settings.client_id}"
|
|
210
217
|
self.additional_authorize_scopes = settings.additional_authorize_scopes or []
|
|
211
218
|
tenant_id_final = settings.tenant_id
|
|
212
219
|
|
|
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
|
|
217
|
-
]
|
|
218
|
-
|
|
219
220
|
# Always validate tokens against the app's API client ID using JWT
|
|
220
221
|
issuer = f"https://login.microsoftonline.com/{tenant_id_final}/v2.0"
|
|
221
222
|
jwks_uri = (
|
|
222
223
|
f"https://login.microsoftonline.com/{tenant_id_final}/discovery/v2.0/keys"
|
|
223
224
|
)
|
|
224
225
|
|
|
226
|
+
# Azure returns unprefixed scopes in JWT tokens, so validate against unprefixed scopes
|
|
225
227
|
token_verifier = JWTVerifier(
|
|
226
228
|
jwks_uri=jwks_uri,
|
|
227
229
|
issuer=issuer,
|
|
228
230
|
audience=settings.client_id,
|
|
229
231
|
algorithm="RS256",
|
|
230
|
-
required_scopes=
|
|
232
|
+
required_scopes=settings.required_scopes, # Unprefixed scopes for validation
|
|
231
233
|
)
|
|
232
234
|
|
|
233
235
|
# Extract secret string from SecretStr
|
|
@@ -298,19 +300,40 @@ class AzureProvider(OAuthProxy):
|
|
|
298
300
|
"Filtering out 'resource' parameter '%s' for Azure AD v2.0 (use scopes instead)",
|
|
299
301
|
original_resource,
|
|
300
302
|
)
|
|
301
|
-
#
|
|
302
|
-
#
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
303
|
+
# Don't modify the scopes in params - they stay unprefixed for MCP clients
|
|
304
|
+
# We'll prefix them when building the Azure authorization URL (in _build_upstream_authorize_url)
|
|
305
|
+
auth_url = await super().authorize(client, params_to_use)
|
|
306
|
+
separator = "&" if "?" in auth_url else "?"
|
|
307
|
+
return f"{auth_url}{separator}prompt=select_account"
|
|
308
|
+
|
|
309
|
+
def _build_upstream_authorize_url(
|
|
310
|
+
self, txn_id: str, transaction: dict[str, Any]
|
|
311
|
+
) -> str:
|
|
312
|
+
"""Build Azure authorization URL with prefixed scopes.
|
|
313
|
+
|
|
314
|
+
Overrides parent to prefix scopes with identifier_uri before sending to Azure,
|
|
315
|
+
while keeping unprefixed scopes in the transaction for MCP clients.
|
|
316
|
+
"""
|
|
317
|
+
# Get unprefixed scopes from transaction
|
|
318
|
+
unprefixed_scopes = transaction.get("scopes") or self.required_scopes or []
|
|
319
|
+
|
|
320
|
+
# Prefix scopes for Azure authorization request
|
|
321
|
+
prefixed_scopes = []
|
|
322
|
+
for scope in unprefixed_scopes:
|
|
323
|
+
if "://" in scope or "/" in scope:
|
|
324
|
+
# Already a full URI or path (e.g., "api://xxx/read" or "User.Read")
|
|
325
|
+
prefixed_scopes.append(scope)
|
|
326
|
+
else:
|
|
327
|
+
# Unprefixed scope name - prefix it with identifier_uri
|
|
328
|
+
prefixed_scopes.append(f"{self.identifier_uri}/{scope}")
|
|
329
|
+
|
|
330
|
+
# Add Microsoft Graph scopes (not validated, not prefixed)
|
|
309
331
|
if self.additional_authorize_scopes:
|
|
310
|
-
|
|
332
|
+
prefixed_scopes.extend(self.additional_authorize_scopes)
|
|
311
333
|
|
|
312
|
-
|
|
334
|
+
# Temporarily modify transaction dict for parent's URL building
|
|
335
|
+
modified_transaction = transaction.copy()
|
|
336
|
+
modified_transaction["scopes"] = prefixed_scopes
|
|
313
337
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return f"{auth_url}{separator}prompt=select_account"
|
|
338
|
+
# Let parent build the URL with prefixed scopes
|
|
339
|
+
return super()._build_upstream_authorize_url(txn_id, modified_transaction)
|
|
@@ -11,7 +11,7 @@ from fastmcp.server.auth.providers.jwt import JWKData, JWKSData, RSAKeyPair
|
|
|
11
11
|
from fastmcp.server.auth.providers.jwt import JWTVerifier as BearerAuthProvider
|
|
12
12
|
|
|
13
13
|
# Re-export for backwards compatibility
|
|
14
|
-
__all__ = ["BearerAuthProvider", "
|
|
14
|
+
__all__ = ["BearerAuthProvider", "JWKData", "JWKSData", "RSAKeyPair"]
|
|
15
15
|
|
|
16
16
|
# Deprecated in 2.11
|
|
17
17
|
if fastmcp.settings.deprecation_warnings:
|
|
@@ -96,10 +96,10 @@ class InMemoryOAuthProvider(OAuthProvider):
|
|
|
96
96
|
# or if params.redirect_uri is None and client has a default.
|
|
97
97
|
# However, the AuthorizationHandler handles the primary validation.
|
|
98
98
|
pass # Let's assume AuthorizationHandler did its job.
|
|
99
|
-
except Exception: # Replace with specific validation error if client.validate_redirect_uri existed
|
|
99
|
+
except Exception as e: # Replace with specific validation error if client.validate_redirect_uri existed
|
|
100
100
|
raise AuthorizeError(
|
|
101
101
|
error="invalid_request", error_description="Invalid redirect_uri."
|
|
102
|
-
)
|
|
102
|
+
) from e
|
|
103
103
|
|
|
104
104
|
auth_code_value = f"test_auth_code_{secrets.token_hex(16)}"
|
|
105
105
|
expires_at = time.time() + DEFAULT_AUTH_CODE_EXPIRY_SECONDS
|
|
@@ -97,8 +97,8 @@ class IntrospectionTokenVerifier(TokenVerifier):
|
|
|
97
97
|
client_id: str | NotSetT = NotSet,
|
|
98
98
|
client_secret: str | NotSetT = NotSet,
|
|
99
99
|
timeout_seconds: int | NotSetT = NotSet,
|
|
100
|
-
required_scopes: list[str] |
|
|
101
|
-
base_url: AnyHttpUrl | str |
|
|
100
|
+
required_scopes: list[str] | NotSetT | None = NotSet,
|
|
101
|
+
base_url: AnyHttpUrl | str | NotSetT | None = NotSet,
|
|
102
102
|
):
|
|
103
103
|
"""
|
|
104
104
|
Initialize the introspection token verifier.
|
|
@@ -184,13 +184,13 @@ class JWTVerifier(TokenVerifier):
|
|
|
184
184
|
def __init__(
|
|
185
185
|
self,
|
|
186
186
|
*,
|
|
187
|
-
public_key: str |
|
|
188
|
-
jwks_uri: str |
|
|
189
|
-
issuer: str |
|
|
190
|
-
audience: str | list[str] |
|
|
191
|
-
algorithm: str |
|
|
192
|
-
required_scopes: list[str] |
|
|
193
|
-
base_url: AnyHttpUrl | str |
|
|
187
|
+
public_key: str | NotSetT | None = NotSet,
|
|
188
|
+
jwks_uri: str | NotSetT | None = NotSet,
|
|
189
|
+
issuer: str | NotSetT | None = NotSet,
|
|
190
|
+
audience: str | list[str] | NotSetT | None = NotSet,
|
|
191
|
+
algorithm: str | NotSetT | None = NotSet,
|
|
192
|
+
required_scopes: list[str] | NotSetT | None = NotSet,
|
|
193
|
+
base_url: AnyHttpUrl | str | NotSetT | None = NotSet,
|
|
194
194
|
):
|
|
195
195
|
"""
|
|
196
196
|
Initialize the JWT token verifier.
|
|
@@ -283,7 +283,7 @@ class JWTVerifier(TokenVerifier):
|
|
|
283
283
|
return await self._get_jwks_key(kid)
|
|
284
284
|
|
|
285
285
|
except Exception as e:
|
|
286
|
-
raise ValueError(f"Failed to extract key ID from token: {e}")
|
|
286
|
+
raise ValueError(f"Failed to extract key ID from token: {e}") from e
|
|
287
287
|
|
|
288
288
|
async def _get_jwks_key(self, kid: str | None) -> str:
|
|
289
289
|
"""Fetch key from JWKS with simple caching."""
|
|
@@ -342,10 +342,10 @@ class JWTVerifier(TokenVerifier):
|
|
|
342
342
|
raise ValueError("No keys found in JWKS")
|
|
343
343
|
|
|
344
344
|
except httpx.HTTPError as e:
|
|
345
|
-
raise ValueError(f"Failed to fetch JWKS: {e}")
|
|
345
|
+
raise ValueError(f"Failed to fetch JWKS: {e}") from e
|
|
346
346
|
except Exception as e:
|
|
347
347
|
self.logger.debug(f"JWKS fetch failed: {e}")
|
|
348
|
-
raise ValueError(f"Failed to fetch JWKS: {e}")
|
|
348
|
+
raise ValueError(f"Failed to fetch JWKS: {e}") from e
|
|
349
349
|
|
|
350
350
|
def _extract_scopes(self, claims: dict[str, Any]) -> list[str]:
|
|
351
351
|
"""
|
|
@@ -400,14 +400,13 @@ class JWTVerifier(TokenVerifier):
|
|
|
400
400
|
|
|
401
401
|
# Validate issuer - note we use issuer instead of issuer_url here because
|
|
402
402
|
# issuer is optional, allowing users to make this check optional
|
|
403
|
-
if self.issuer:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
return None
|
|
403
|
+
if self.issuer and claims.get("iss") != self.issuer:
|
|
404
|
+
self.logger.debug(
|
|
405
|
+
"Token validation failed: issuer mismatch for client %s",
|
|
406
|
+
client_id,
|
|
407
|
+
)
|
|
408
|
+
self.logger.info("Bearer token rejected for client %s", client_id)
|
|
409
|
+
return None
|
|
411
410
|
|
|
412
411
|
# Validate audience if configured
|
|
413
412
|
if self.audience:
|
|
@@ -83,7 +83,7 @@ class SupabaseProvider(RemoteAuthProvider):
|
|
|
83
83
|
*,
|
|
84
84
|
project_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
85
85
|
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
86
|
-
required_scopes: list[str] |
|
|
86
|
+
required_scopes: list[str] | NotSetT | None = NotSet,
|
|
87
87
|
token_verifier: TokenVerifier | None = None,
|
|
88
88
|
):
|
|
89
89
|
"""Initialize Supabase metadata provider.
|
|
@@ -169,7 +169,7 @@ class WorkOSProvider(OAuthProxy):
|
|
|
169
169
|
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
170
170
|
issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
171
171
|
redirect_path: str | NotSetT = NotSet,
|
|
172
|
-
required_scopes: list[str] |
|
|
172
|
+
required_scopes: list[str] | NotSetT | None = NotSet,
|
|
173
173
|
timeout_seconds: int | NotSetT = NotSet,
|
|
174
174
|
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
|
175
175
|
client_storage: AsyncKeyValue | None = None,
|
|
@@ -338,7 +338,7 @@ class AuthKitProvider(RemoteAuthProvider):
|
|
|
338
338
|
*,
|
|
339
339
|
authkit_domain: AnyHttpUrl | str | NotSetT = NotSet,
|
|
340
340
|
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
|
341
|
-
required_scopes: list[str] |
|
|
341
|
+
required_scopes: list[str] | NotSetT | None = NotSet,
|
|
342
342
|
token_verifier: TokenVerifier | None = None,
|
|
343
343
|
):
|
|
344
344
|
"""Initialize AuthKit metadata provider.
|
fastmcp/server/context.py
CHANGED
|
@@ -188,8 +188,8 @@ class Context:
|
|
|
188
188
|
"""
|
|
189
189
|
try:
|
|
190
190
|
return request_ctx.get()
|
|
191
|
-
except LookupError:
|
|
192
|
-
raise ValueError("Context is not available outside of a request")
|
|
191
|
+
except LookupError as e:
|
|
192
|
+
raise ValueError("Context is not available outside of a request") from e
|
|
193
193
|
|
|
194
194
|
async def report_progress(
|
|
195
195
|
self, progress: float, total: float | None = None, message: str | None = None
|
|
@@ -342,7 +342,7 @@ class Context:
|
|
|
342
342
|
session_id = str(uuid4())
|
|
343
343
|
|
|
344
344
|
# Save the session id to the session attributes
|
|
345
|
-
|
|
345
|
+
session._fastmcp_id = session_id
|
|
346
346
|
return session_id
|
|
347
347
|
|
|
348
348
|
@property
|
|
@@ -595,13 +595,11 @@ class Context:
|
|
|
595
595
|
choice_literal = Literal[tuple(response_type)] # type: ignore
|
|
596
596
|
response_type = ScalarElicitationType[choice_literal] # type: ignore
|
|
597
597
|
# if the user provided a primitive scalar, wrap it in an object schema
|
|
598
|
-
elif
|
|
599
|
-
response_type
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
# if the user provided an Enum type, wrap it in an object schema
|
|
604
|
-
elif isinstance(response_type, type) and issubclass(response_type, Enum):
|
|
598
|
+
elif (
|
|
599
|
+
response_type in {bool, int, float, str}
|
|
600
|
+
or get_origin(response_type) is Literal
|
|
601
|
+
or (isinstance(response_type, type) and issubclass(response_type, Enum))
|
|
602
|
+
):
|
|
605
603
|
response_type = ScalarElicitationType[response_type] # type: ignore
|
|
606
604
|
|
|
607
605
|
response_type = cast(type[T], response_type)
|
fastmcp/server/dependencies.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from mcp.server.auth.middleware.auth_context import (
|
|
@@ -16,11 +17,11 @@ if TYPE_CHECKING:
|
|
|
16
17
|
from fastmcp.server.context import Context
|
|
17
18
|
|
|
18
19
|
__all__ = [
|
|
20
|
+
"AccessToken",
|
|
21
|
+
"get_access_token",
|
|
19
22
|
"get_context",
|
|
20
|
-
"get_http_request",
|
|
21
23
|
"get_http_headers",
|
|
22
|
-
"
|
|
23
|
-
"AccessToken",
|
|
24
|
+
"get_http_request",
|
|
24
25
|
]
|
|
25
26
|
|
|
26
27
|
|
|
@@ -43,10 +44,8 @@ def get_http_request() -> Request:
|
|
|
43
44
|
from mcp.server.lowlevel.server import request_ctx
|
|
44
45
|
|
|
45
46
|
request = None
|
|
46
|
-
|
|
47
|
+
with contextlib.suppress(LookupError):
|
|
47
48
|
request = request_ctx.get().request
|
|
48
|
-
except LookupError:
|
|
49
|
-
pass
|
|
50
49
|
|
|
51
50
|
if request is None:
|
|
52
51
|
raise RuntimeError("No active HTTP request found.")
|
fastmcp/server/elicitation.py
CHANGED
fastmcp/server/http.py
CHANGED
|
@@ -342,9 +342,8 @@ def create_streamable_http_app(
|
|
|
342
342
|
# Create a lifespan manager to start and stop the session manager
|
|
343
343
|
@asynccontextmanager
|
|
344
344
|
async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
|
|
345
|
-
async with server._lifespan_manager():
|
|
346
|
-
|
|
347
|
-
yield
|
|
345
|
+
async with server._lifespan_manager(), session_manager.run():
|
|
346
|
+
yield
|
|
348
347
|
|
|
349
348
|
# Create and return the app with lifespan
|
|
350
349
|
app = create_base_app(
|
|
@@ -46,7 +46,7 @@ class CachableReadResourceContents(BaseModel):
|
|
|
46
46
|
|
|
47
47
|
@classmethod
|
|
48
48
|
def get_sizes(cls, values: Sequence[Self]) -> int:
|
|
49
|
-
return sum(
|
|
49
|
+
return sum(item.get_size() for item in values)
|
|
50
50
|
|
|
51
51
|
@classmethod
|
|
52
52
|
def wrap(cls, values: Sequence[ReadResourceContents]) -> list[Self]:
|
|
@@ -64,7 +64,7 @@ class ErrorHandlingMiddleware(Middleware):
|
|
|
64
64
|
error_key = f"{error_type}:{method}"
|
|
65
65
|
self.error_counts[error_key] = self.error_counts.get(error_key, 0) + 1
|
|
66
66
|
|
|
67
|
-
base_message = f"Error in {method}: {error_type}: {
|
|
67
|
+
base_message = f"Error in {method}: {error_type}: {error!s}"
|
|
68
68
|
|
|
69
69
|
if self.include_traceback:
|
|
70
70
|
self.logger.error(f"{base_message}\n{traceback.format_exc()}")
|
|
@@ -91,24 +91,24 @@ class ErrorHandlingMiddleware(Middleware):
|
|
|
91
91
|
|
|
92
92
|
if error_type in (ValueError, TypeError):
|
|
93
93
|
return McpError(
|
|
94
|
-
ErrorData(code=-32602, message=f"Invalid params: {
|
|
94
|
+
ErrorData(code=-32602, message=f"Invalid params: {error!s}")
|
|
95
95
|
)
|
|
96
96
|
elif error_type in (FileNotFoundError, KeyError, NotFoundError):
|
|
97
97
|
return McpError(
|
|
98
|
-
ErrorData(code=-32001, message=f"Resource not found: {
|
|
98
|
+
ErrorData(code=-32001, message=f"Resource not found: {error!s}")
|
|
99
99
|
)
|
|
100
100
|
elif error_type is PermissionError:
|
|
101
101
|
return McpError(
|
|
102
|
-
ErrorData(code=-32000, message=f"Permission denied: {
|
|
102
|
+
ErrorData(code=-32000, message=f"Permission denied: {error!s}")
|
|
103
103
|
)
|
|
104
104
|
# asyncio.TimeoutError is a subclass of TimeoutError in Python 3.10, alias in 3.11+
|
|
105
105
|
elif error_type in (TimeoutError, asyncio.TimeoutError):
|
|
106
106
|
return McpError(
|
|
107
|
-
ErrorData(code=-32000, message=f"Request timeout: {
|
|
107
|
+
ErrorData(code=-32000, message=f"Request timeout: {error!s}")
|
|
108
108
|
)
|
|
109
109
|
else:
|
|
110
110
|
return McpError(
|
|
111
|
-
ErrorData(code=-32603, message=f"Internal error: {
|
|
111
|
+
ErrorData(code=-32603, message=f"Internal error: {error!s}")
|
|
112
112
|
)
|
|
113
113
|
|
|
114
114
|
async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
|
|
@@ -120,7 +120,7 @@ class ErrorHandlingMiddleware(Middleware):
|
|
|
120
120
|
|
|
121
121
|
# Transform and re-raise
|
|
122
122
|
transformed_error = self._transform_error(error)
|
|
123
|
-
raise transformed_error
|
|
123
|
+
raise transformed_error from error
|
|
124
124
|
|
|
125
125
|
def get_error_stats(self) -> dict[str, int]:
|
|
126
126
|
"""Get error statistics for monitoring."""
|
|
@@ -200,7 +200,7 @@ class RetryMiddleware(Middleware):
|
|
|
200
200
|
delay = self._calculate_delay(attempt)
|
|
201
201
|
self.logger.warning(
|
|
202
202
|
f"Request {context.method} failed (attempt {attempt + 1}/{self.max_retries + 1}): "
|
|
203
|
-
f"{type(error).__name__}: {
|
|
203
|
+
f"{type(error).__name__}: {error!s}. Retrying in {delay:.1f}s..."
|
|
204
204
|
)
|
|
205
205
|
|
|
206
206
|
await anyio.sleep(delay)
|
fastmcp/server/openapi.py
CHANGED
|
@@ -513,11 +513,11 @@ class OpenAPITool(Tool):
|
|
|
513
513
|
if e.response.text:
|
|
514
514
|
error_message += f" - {e.response.text}"
|
|
515
515
|
|
|
516
|
-
raise ValueError(error_message)
|
|
516
|
+
raise ValueError(error_message) from e
|
|
517
517
|
|
|
518
518
|
except httpx.RequestError as e:
|
|
519
519
|
# Handle request errors (connection, timeout, etc.)
|
|
520
|
-
raise ValueError(f"Request error: {
|
|
520
|
+
raise ValueError(f"Request error: {e!s}") from e
|
|
521
521
|
|
|
522
522
|
|
|
523
523
|
class OpenAPIResource(Resource):
|
|
@@ -531,9 +531,11 @@ class OpenAPIResource(Resource):
|
|
|
531
531
|
name: str,
|
|
532
532
|
description: str,
|
|
533
533
|
mime_type: str = "application/json",
|
|
534
|
-
tags: set[str] =
|
|
534
|
+
tags: set[str] | None = None,
|
|
535
535
|
timeout: float | None = None,
|
|
536
536
|
):
|
|
537
|
+
if tags is None:
|
|
538
|
+
tags = set()
|
|
537
539
|
super().__init__(
|
|
538
540
|
uri=AnyUrl(uri), # Convert string to AnyUrl
|
|
539
541
|
name=name,
|
|
@@ -632,11 +634,11 @@ class OpenAPIResource(Resource):
|
|
|
632
634
|
if e.response.text:
|
|
633
635
|
error_message += f" - {e.response.text}"
|
|
634
636
|
|
|
635
|
-
raise ValueError(error_message)
|
|
637
|
+
raise ValueError(error_message) from e
|
|
636
638
|
|
|
637
639
|
except httpx.RequestError as e:
|
|
638
640
|
# Handle request errors (connection, timeout, etc.)
|
|
639
|
-
raise ValueError(f"Request error: {
|
|
641
|
+
raise ValueError(f"Request error: {e!s}") from e
|
|
640
642
|
|
|
641
643
|
|
|
642
644
|
class OpenAPIResourceTemplate(ResourceTemplate):
|
|
@@ -650,9 +652,11 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
650
652
|
name: str,
|
|
651
653
|
description: str,
|
|
652
654
|
parameters: dict[str, Any],
|
|
653
|
-
tags: set[str] =
|
|
655
|
+
tags: set[str] | None = None,
|
|
654
656
|
timeout: float | None = None,
|
|
655
657
|
):
|
|
658
|
+
if tags is None:
|
|
659
|
+
tags = set()
|
|
656
660
|
super().__init__(
|
|
657
661
|
uri_template=uri_template,
|
|
658
662
|
name=name,
|
fastmcp/server/proxy.py
CHANGED
|
@@ -198,7 +198,9 @@ class ProxyResourceManager(ResourceManager, ProxyManagerMixin):
|
|
|
198
198
|
elif isinstance(result[0], BlobResourceContents):
|
|
199
199
|
return result[0].blob
|
|
200
200
|
else:
|
|
201
|
-
raise ResourceError(
|
|
201
|
+
raise ResourceError(
|
|
202
|
+
f"Unsupported content type: {type(result[0])}"
|
|
203
|
+
) from None
|
|
202
204
|
|
|
203
205
|
|
|
204
206
|
class ProxyPromptManager(PromptManager, ProxyManagerMixin):
|
|
@@ -558,7 +560,7 @@ class ProxyClient(Client[ClientTransportT]):
|
|
|
558
560
|
kwargs["log_handler"] = ProxyClient.default_log_handler
|
|
559
561
|
if "progress_handler" not in kwargs:
|
|
560
562
|
kwargs["progress_handler"] = ProxyClient.default_progress_handler
|
|
561
|
-
super().__init__(**kwargs |
|
|
563
|
+
super().__init__(**kwargs | {"transport": transport})
|
|
562
564
|
|
|
563
565
|
@classmethod
|
|
564
566
|
async def default_sampling_handler(
|
|
@@ -572,7 +574,7 @@ class ProxyClient(Client[ClientTransportT]):
|
|
|
572
574
|
"""
|
|
573
575
|
ctx = get_context()
|
|
574
576
|
content = await ctx.sample(
|
|
575
|
-
|
|
577
|
+
list(messages),
|
|
576
578
|
system_prompt=params.systemPrompt,
|
|
577
579
|
temperature=params.temperature,
|
|
578
580
|
max_tokens=params.maxTokens,
|
|
@@ -649,7 +651,6 @@ class StatefulProxyClient(ProxyClient[ClientTransportT]):
|
|
|
649
651
|
The stateful proxy client will be forced disconnected when the session is exited.
|
|
650
652
|
So we do nothing here.
|
|
651
653
|
"""
|
|
652
|
-
pass
|
|
653
654
|
|
|
654
655
|
async def clear(self):
|
|
655
656
|
"""
|