fastmcp 2.13.0rc2__py3-none-any.whl → 2.13.0.1__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 +3 -2
- fastmcp/cli/install/claude_code.py +3 -3
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +7 -6
- fastmcp/client/client.py +10 -10
- fastmcp/client/oauth_callback.py +6 -2
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +35 -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/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +5 -5
- fastmcp/server/auth/auth.py +2 -2
- fastmcp/server/auth/handlers/authorize.py +324 -0
- fastmcp/server/auth/jwt_issuer.py +39 -92
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +236 -217
- fastmcp/server/auth/oidc_proxy.py +18 -3
- fastmcp/server/auth/providers/auth0.py +28 -15
- fastmcp/server/auth/providers/aws.py +16 -1
- fastmcp/server/auth/providers/azure.py +101 -40
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/github.py +16 -1
- fastmcp/server/auth/providers/google.py +16 -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 +18 -3
- fastmcp/server/context.py +41 -12
- fastmcp/server/dependencies.py +5 -6
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +3 -4
- 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/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +10 -6
- fastmcp/server/proxy.py +5 -4
- fastmcp/server/server.py +74 -55
- fastmcp/settings.py +2 -1
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +12 -12
- fastmcp/tools/tool_manager.py +8 -4
- fastmcp/tools/tool_transform.py +6 -6
- fastmcp/utilities/cli.py +50 -21
- 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/utilities/ui.py +126 -6
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/METADATA +5 -5
- fastmcp-2.13.0.1.dist-info/RECORD +141 -0
- fastmcp-2.13.0rc2.dist-info/RECORD +0 -138
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,82 +9,66 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import base64
|
|
11
11
|
import time
|
|
12
|
-
from typing import Any
|
|
12
|
+
from typing import Any, overload
|
|
13
13
|
|
|
14
14
|
from authlib.jose import JsonWebToken
|
|
15
15
|
from authlib.jose.errors import JoseError
|
|
16
|
-
from cryptography.fernet import Fernet
|
|
17
16
|
from cryptography.hazmat.primitives import hashes
|
|
18
17
|
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
18
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
19
19
|
|
|
20
20
|
from fastmcp.utilities.logging import get_logger
|
|
21
21
|
|
|
22
22
|
logger = get_logger(__name__)
|
|
23
23
|
|
|
24
|
+
KDF_ITERATIONS = 1000000
|
|
24
25
|
|
|
25
|
-
def derive_jwt_key(upstream_secret: str, server_salt: str) -> bytes:
|
|
26
|
-
"""Derive JWT signing key from upstream client secret and server salt.
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
@overload
|
|
28
|
+
def derive_jwt_key(*, high_entropy_material: str, salt: str) -> bytes:
|
|
29
|
+
"""Derive JWT signing key from a high-entropy key material and server salt."""
|
|
30
30
|
|
|
31
|
-
Args:
|
|
32
|
-
upstream_secret: The OAuth client secret from upstream provider
|
|
33
|
-
server_salt: Random salt unique to this server instance
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"""
|
|
38
|
-
return HKDF(
|
|
39
|
-
algorithm=hashes.SHA256(),
|
|
40
|
-
length=32,
|
|
41
|
-
salt=f"fastmcp-jwt-signing-v1-{server_salt}".encode(),
|
|
42
|
-
info=b"HS256",
|
|
43
|
-
).derive(upstream_secret.encode())
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def derive_encryption_key(upstream_secret: str) -> bytes:
|
|
47
|
-
"""Derive Fernet encryption key from upstream client secret.
|
|
48
|
-
|
|
49
|
-
Uses HKDF to derive a cryptographically secure encryption key for
|
|
50
|
-
encrypting upstream tokens at rest.
|
|
32
|
+
@overload
|
|
33
|
+
def derive_jwt_key(*, low_entropy_material: str, salt: str) -> bytes:
|
|
34
|
+
"""Derive JWT signing key from a low-entropy key material and server salt."""
|
|
51
35
|
|
|
52
|
-
Args:
|
|
53
|
-
upstream_secret: The OAuth client secret from upstream provider
|
|
54
36
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
37
|
+
def derive_jwt_key(
|
|
38
|
+
*,
|
|
39
|
+
high_entropy_material: str | None = None,
|
|
40
|
+
low_entropy_material: str | None = None,
|
|
41
|
+
salt: str,
|
|
42
|
+
) -> bytes:
|
|
43
|
+
"""Derive JWT signing key from a high-entropy or low-entropy key material and server salt."""
|
|
44
|
+
if high_entropy_material is not None and low_entropy_material is not None:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"Either high_entropy_material or low_entropy_material must be provided, but not both"
|
|
47
|
+
)
|
|
65
48
|
|
|
49
|
+
if high_entropy_material is not None:
|
|
50
|
+
derived_key = HKDF(
|
|
51
|
+
algorithm=hashes.SHA256(),
|
|
52
|
+
length=32,
|
|
53
|
+
salt=salt.encode(),
|
|
54
|
+
info=b"Fernet",
|
|
55
|
+
).derive(key_material=high_entropy_material.encode())
|
|
66
56
|
|
|
67
|
-
|
|
68
|
-
"""Derive 32-byte key from user-provided secret (string or bytes).
|
|
57
|
+
return base64.urlsafe_b64encode(derived_key)
|
|
69
58
|
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
if low_entropy_material is not None:
|
|
60
|
+
pbkdf2 = PBKDF2HMAC(
|
|
61
|
+
algorithm=hashes.SHA256(),
|
|
62
|
+
length=32,
|
|
63
|
+
salt=salt.encode(),
|
|
64
|
+
iterations=KDF_ITERATIONS,
|
|
65
|
+
).derive(key_material=low_entropy_material.encode())
|
|
72
66
|
|
|
73
|
-
|
|
74
|
-
secret: User-provided secret (any string or bytes)
|
|
75
|
-
salt: Application-specific salt string
|
|
76
|
-
info: Key purpose identifier
|
|
67
|
+
return base64.urlsafe_b64encode(pbkdf2)
|
|
77
68
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
secret_bytes = secret.encode() if isinstance(secret, str) else secret
|
|
82
|
-
return HKDF(
|
|
83
|
-
algorithm=hashes.SHA256(),
|
|
84
|
-
length=32,
|
|
85
|
-
salt=salt.encode(),
|
|
86
|
-
info=info,
|
|
87
|
-
).derive(secret_bytes)
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"Either high_entropy_material or low_entropy_material must be provided"
|
|
71
|
+
)
|
|
88
72
|
|
|
89
73
|
|
|
90
74
|
class JWTIssuer:
|
|
@@ -250,40 +234,3 @@ class JWTIssuer:
|
|
|
250
234
|
except JoseError as e:
|
|
251
235
|
logger.debug("Token validation failed: %s", e)
|
|
252
236
|
raise
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
class TokenEncryption:
|
|
256
|
-
"""Handles encryption/decryption of upstream OAuth tokens at rest."""
|
|
257
|
-
|
|
258
|
-
def __init__(self, encryption_key: bytes):
|
|
259
|
-
"""Initialize token encryption.
|
|
260
|
-
|
|
261
|
-
Args:
|
|
262
|
-
encryption_key: Fernet encryption key (32 bytes, base64url-encoded)
|
|
263
|
-
"""
|
|
264
|
-
self._fernet = Fernet(encryption_key)
|
|
265
|
-
|
|
266
|
-
def encrypt(self, token: str) -> bytes:
|
|
267
|
-
"""Encrypt a token for storage.
|
|
268
|
-
|
|
269
|
-
Args:
|
|
270
|
-
token: Plain text token
|
|
271
|
-
|
|
272
|
-
Returns:
|
|
273
|
-
Encrypted token bytes
|
|
274
|
-
"""
|
|
275
|
-
return self._fernet.encrypt(token.encode())
|
|
276
|
-
|
|
277
|
-
def decrypt(self, encrypted_token: bytes) -> str:
|
|
278
|
-
"""Decrypt a token from storage.
|
|
279
|
-
|
|
280
|
-
Args:
|
|
281
|
-
encrypted_token: Encrypted token bytes
|
|
282
|
-
|
|
283
|
-
Returns:
|
|
284
|
-
Plain text token
|
|
285
|
-
|
|
286
|
-
Raises:
|
|
287
|
-
cryptography.fernet.InvalidToken: If token is corrupted or key is wrong
|
|
288
|
-
"""
|
|
289
|
-
return self._fernet.decrypt(encrypted_token).decode()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Enhanced authentication middleware with better error messages.
|
|
2
|
+
|
|
3
|
+
This module provides enhanced versions of MCP SDK authentication middleware
|
|
4
|
+
that return more helpful error messages for developers troubleshooting
|
|
5
|
+
authentication issues.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
from mcp.server.auth.middleware.bearer_auth import (
|
|
13
|
+
RequireAuthMiddleware as SDKRequireAuthMiddleware,
|
|
14
|
+
)
|
|
15
|
+
from starlette.types import Send
|
|
16
|
+
|
|
17
|
+
from fastmcp.utilities.logging import get_logger
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RequireAuthMiddleware(SDKRequireAuthMiddleware):
|
|
23
|
+
"""Enhanced authentication middleware with detailed error messages.
|
|
24
|
+
|
|
25
|
+
Extends the SDK's RequireAuthMiddleware to provide more actionable
|
|
26
|
+
error messages when authentication fails. This helps developers
|
|
27
|
+
understand what went wrong and how to fix it.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
async def _send_auth_error(
|
|
31
|
+
self, send: Send, status_code: int, error: str, description: str
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Send an authentication error response with enhanced error messages.
|
|
34
|
+
|
|
35
|
+
Overrides the SDK's _send_auth_error to provide more detailed
|
|
36
|
+
error descriptions that help developers troubleshoot authentication
|
|
37
|
+
issues.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
send: ASGI send callable
|
|
41
|
+
status_code: HTTP status code (401 or 403)
|
|
42
|
+
error: OAuth error code
|
|
43
|
+
description: Base error description
|
|
44
|
+
"""
|
|
45
|
+
# Enhance error descriptions based on error type
|
|
46
|
+
enhanced_description = description
|
|
47
|
+
|
|
48
|
+
if error == "invalid_token" and status_code == 401:
|
|
49
|
+
# This is the "Authentication required" error
|
|
50
|
+
enhanced_description = (
|
|
51
|
+
"Authentication failed. The provided bearer token is invalid, expired, or no longer recognized by the server. "
|
|
52
|
+
"To resolve: clear authentication tokens in your MCP client and reconnect. "
|
|
53
|
+
"Your client should automatically re-register and obtain new tokens."
|
|
54
|
+
)
|
|
55
|
+
elif error == "insufficient_scope":
|
|
56
|
+
# Scope error - already has good detail from SDK
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
# Build WWW-Authenticate header value
|
|
60
|
+
www_auth_parts = [
|
|
61
|
+
f'error="{error}"',
|
|
62
|
+
f'error_description="{enhanced_description}"',
|
|
63
|
+
]
|
|
64
|
+
if self.resource_metadata_url:
|
|
65
|
+
www_auth_parts.append(f'resource_metadata="{self.resource_metadata_url}"')
|
|
66
|
+
|
|
67
|
+
www_authenticate = f"Bearer {', '.join(www_auth_parts)}"
|
|
68
|
+
|
|
69
|
+
# Send response
|
|
70
|
+
body = {"error": error, "error_description": enhanced_description}
|
|
71
|
+
body_bytes = json.dumps(body).encode()
|
|
72
|
+
|
|
73
|
+
await send(
|
|
74
|
+
{
|
|
75
|
+
"type": "http.response.start",
|
|
76
|
+
"status": status_code,
|
|
77
|
+
"headers": [
|
|
78
|
+
(b"content-type", b"application/json"),
|
|
79
|
+
(b"content-length", str(len(body_bytes)).encode()),
|
|
80
|
+
(b"www-authenticate", www_authenticate.encode()),
|
|
81
|
+
],
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
await send(
|
|
86
|
+
{
|
|
87
|
+
"type": "http.response.body",
|
|
88
|
+
"body": body_bytes,
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
logger.info(
|
|
93
|
+
"Auth error returned: %s (status=%d)",
|
|
94
|
+
error,
|
|
95
|
+
status_code,
|
|
96
|
+
)
|