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.
Files changed (81) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +3 -2
  3. fastmcp/cli/install/claude_code.py +3 -3
  4. fastmcp/client/__init__.py +9 -9
  5. fastmcp/client/auth/oauth.py +7 -6
  6. fastmcp/client/client.py +10 -10
  7. fastmcp/client/oauth_callback.py +6 -2
  8. fastmcp/client/sampling.py +1 -1
  9. fastmcp/client/transports.py +35 -34
  10. fastmcp/contrib/component_manager/__init__.py +1 -1
  11. fastmcp/contrib/component_manager/component_manager.py +2 -2
  12. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  13. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  14. fastmcp/experimental/server/openapi/__init__.py +5 -8
  15. fastmcp/experimental/server/openapi/components.py +11 -7
  16. fastmcp/experimental/server/openapi/routing.py +2 -2
  17. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  18. fastmcp/experimental/utilities/openapi/director.py +1 -1
  19. fastmcp/experimental/utilities/openapi/json_schema_converter.py +2 -2
  20. fastmcp/experimental/utilities/openapi/models.py +3 -3
  21. fastmcp/experimental/utilities/openapi/parser.py +3 -5
  22. fastmcp/experimental/utilities/openapi/schemas.py +2 -2
  23. fastmcp/mcp_config.py +2 -3
  24. fastmcp/prompts/__init__.py +1 -1
  25. fastmcp/prompts/prompt.py +9 -13
  26. fastmcp/resources/__init__.py +5 -5
  27. fastmcp/resources/resource.py +1 -3
  28. fastmcp/resources/resource_manager.py +1 -1
  29. fastmcp/resources/types.py +30 -24
  30. fastmcp/server/__init__.py +1 -1
  31. fastmcp/server/auth/__init__.py +5 -5
  32. fastmcp/server/auth/auth.py +2 -2
  33. fastmcp/server/auth/handlers/authorize.py +324 -0
  34. fastmcp/server/auth/jwt_issuer.py +39 -92
  35. fastmcp/server/auth/middleware.py +96 -0
  36. fastmcp/server/auth/oauth_proxy.py +236 -217
  37. fastmcp/server/auth/oidc_proxy.py +18 -3
  38. fastmcp/server/auth/providers/auth0.py +28 -15
  39. fastmcp/server/auth/providers/aws.py +16 -1
  40. fastmcp/server/auth/providers/azure.py +101 -40
  41. fastmcp/server/auth/providers/bearer.py +1 -1
  42. fastmcp/server/auth/providers/github.py +16 -1
  43. fastmcp/server/auth/providers/google.py +16 -1
  44. fastmcp/server/auth/providers/in_memory.py +2 -2
  45. fastmcp/server/auth/providers/introspection.py +2 -2
  46. fastmcp/server/auth/providers/jwt.py +17 -18
  47. fastmcp/server/auth/providers/supabase.py +1 -1
  48. fastmcp/server/auth/providers/workos.py +18 -3
  49. fastmcp/server/context.py +41 -12
  50. fastmcp/server/dependencies.py +5 -6
  51. fastmcp/server/elicitation.py +1 -1
  52. fastmcp/server/http.py +3 -4
  53. fastmcp/server/middleware/__init__.py +1 -1
  54. fastmcp/server/middleware/caching.py +1 -1
  55. fastmcp/server/middleware/error_handling.py +8 -8
  56. fastmcp/server/middleware/middleware.py +1 -1
  57. fastmcp/server/middleware/tool_injection.py +116 -0
  58. fastmcp/server/openapi.py +10 -6
  59. fastmcp/server/proxy.py +5 -4
  60. fastmcp/server/server.py +74 -55
  61. fastmcp/settings.py +2 -1
  62. fastmcp/tools/__init__.py +1 -1
  63. fastmcp/tools/tool.py +12 -12
  64. fastmcp/tools/tool_manager.py +8 -4
  65. fastmcp/tools/tool_transform.py +6 -6
  66. fastmcp/utilities/cli.py +50 -21
  67. fastmcp/utilities/inspect.py +2 -2
  68. fastmcp/utilities/json_schema_type.py +4 -4
  69. fastmcp/utilities/logging.py +14 -18
  70. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  71. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  72. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  73. fastmcp/utilities/openapi.py +9 -9
  74. fastmcp/utilities/tests.py +2 -4
  75. fastmcp/utilities/ui.py +126 -6
  76. {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/METADATA +5 -5
  77. fastmcp-2.13.0.1.dist-info/RECORD +141 -0
  78. fastmcp-2.13.0rc2.dist-info/RECORD +0 -138
  79. {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/WHEEL +0 -0
  80. {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/entry_points.txt +0 -0
  81. {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
- Uses HKDF (RFC 5869) to derive a cryptographically secure signing key from
29
- the upstream OAuth client secret combined with a server-specific salt.
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
- Returns:
36
- 32-byte key suitable for HS256 JWT signing
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
- Returns:
56
- 32-byte Fernet key (base64url-encoded)
57
- """
58
- key_material = HKDF(
59
- algorithm=hashes.SHA256(),
60
- length=32,
61
- salt=b"fastmcp-token-encryption-v1",
62
- info=b"Fernet",
63
- ).derive(upstream_secret.encode())
64
- return base64.urlsafe_b64encode(key_material)
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
- def derive_key_from_secret(secret: str | bytes, salt: str, info: bytes) -> bytes:
68
- """Derive 32-byte key from user-provided secret (string or bytes).
57
+ return base64.urlsafe_b64encode(derived_key)
69
58
 
70
- Accepts any length input and derives a proper cryptographic key.
71
- Uses HKDF to stretch weak inputs into strong keys.
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
- Args:
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
- Returns:
79
- 32-byte key suitable for HS256 JWT signing or Fernet encryption
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
+ )