fastmcp 2.12.4__py3-none-any.whl → 2.13.0rc1__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 (68) hide show
  1. fastmcp/cli/cli.py +6 -6
  2. fastmcp/cli/install/claude_code.py +3 -3
  3. fastmcp/cli/install/claude_desktop.py +3 -3
  4. fastmcp/cli/install/cursor.py +7 -7
  5. fastmcp/cli/install/gemini_cli.py +3 -3
  6. fastmcp/cli/install/mcp_json.py +3 -3
  7. fastmcp/cli/run.py +13 -8
  8. fastmcp/client/auth/oauth.py +100 -208
  9. fastmcp/client/client.py +11 -11
  10. fastmcp/client/logging.py +18 -14
  11. fastmcp/client/oauth_callback.py +81 -171
  12. fastmcp/client/transports.py +76 -22
  13. fastmcp/contrib/component_manager/component_service.py +6 -6
  14. fastmcp/contrib/mcp_mixin/README.md +32 -1
  15. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  16. fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
  17. fastmcp/experimental/utilities/openapi/parser.py +23 -3
  18. fastmcp/prompts/prompt.py +13 -6
  19. fastmcp/prompts/prompt_manager.py +16 -101
  20. fastmcp/resources/resource.py +13 -6
  21. fastmcp/resources/resource_manager.py +5 -164
  22. fastmcp/resources/template.py +107 -17
  23. fastmcp/server/auth/auth.py +40 -32
  24. fastmcp/server/auth/jwt_issuer.py +289 -0
  25. fastmcp/server/auth/oauth_proxy.py +1238 -234
  26. fastmcp/server/auth/oidc_proxy.py +8 -6
  27. fastmcp/server/auth/providers/auth0.py +12 -6
  28. fastmcp/server/auth/providers/aws.py +13 -2
  29. fastmcp/server/auth/providers/azure.py +137 -124
  30. fastmcp/server/auth/providers/descope.py +4 -6
  31. fastmcp/server/auth/providers/github.py +13 -7
  32. fastmcp/server/auth/providers/google.py +13 -7
  33. fastmcp/server/auth/providers/introspection.py +281 -0
  34. fastmcp/server/auth/providers/jwt.py +8 -2
  35. fastmcp/server/auth/providers/scalekit.py +179 -0
  36. fastmcp/server/auth/providers/supabase.py +172 -0
  37. fastmcp/server/auth/providers/workos.py +16 -13
  38. fastmcp/server/context.py +89 -34
  39. fastmcp/server/http.py +53 -16
  40. fastmcp/server/low_level.py +121 -2
  41. fastmcp/server/middleware/caching.py +469 -0
  42. fastmcp/server/middleware/error_handling.py +6 -2
  43. fastmcp/server/middleware/logging.py +48 -37
  44. fastmcp/server/middleware/middleware.py +28 -15
  45. fastmcp/server/middleware/rate_limiting.py +3 -3
  46. fastmcp/server/proxy.py +6 -6
  47. fastmcp/server/server.py +638 -183
  48. fastmcp/settings.py +22 -9
  49. fastmcp/tools/tool.py +7 -3
  50. fastmcp/tools/tool_manager.py +22 -108
  51. fastmcp/tools/tool_transform.py +3 -3
  52. fastmcp/utilities/cli.py +2 -2
  53. fastmcp/utilities/components.py +5 -0
  54. fastmcp/utilities/inspect.py +77 -21
  55. fastmcp/utilities/logging.py +118 -8
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  59. fastmcp/utilities/tests.py +87 -4
  60. fastmcp/utilities/types.py +1 -1
  61. fastmcp/utilities/ui.py +497 -0
  62. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/METADATA +8 -4
  63. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/RECORD +66 -62
  64. fastmcp/cli/claude.py +0 -135
  65. fastmcp/utilities/storage.py +0 -204
  66. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/WHEEL +0 -0
  67. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/entry_points.txt +0 -0
  68. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,289 @@
1
+ """JWT token issuance and verification for FastMCP OAuth Proxy.
2
+
3
+ This module implements the token factory pattern for OAuth proxies, where the proxy
4
+ issues its own JWT tokens to clients instead of forwarding upstream provider tokens.
5
+ This maintains proper OAuth 2.0 token audience boundaries.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import time
12
+ from typing import Any
13
+
14
+ from authlib.jose import JsonWebToken
15
+ from authlib.jose.errors import JoseError
16
+ from cryptography.fernet import Fernet
17
+ from cryptography.hazmat.primitives import hashes
18
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
19
+
20
+ from fastmcp.utilities.logging import get_logger
21
+
22
+ logger = get_logger(__name__)
23
+
24
+
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
+
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.
30
+
31
+ Args:
32
+ upstream_secret: The OAuth client secret from upstream provider
33
+ server_salt: Random salt unique to this server instance
34
+
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.
51
+
52
+ Args:
53
+ upstream_secret: The OAuth client secret from upstream provider
54
+
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)
65
+
66
+
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).
69
+
70
+ Accepts any length input and derives a proper cryptographic key.
71
+ Uses HKDF to stretch weak inputs into strong keys.
72
+
73
+ Args:
74
+ secret: User-provided secret (any string or bytes)
75
+ salt: Application-specific salt string
76
+ info: Key purpose identifier
77
+
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)
88
+
89
+
90
+ class JWTIssuer:
91
+ """Issues and validates FastMCP-signed JWT tokens using HS256.
92
+
93
+ This issuer creates JWT tokens for MCP clients with proper audience claims,
94
+ maintaining OAuth 2.0 token boundaries. Tokens are signed with HS256 using
95
+ a key derived from the upstream client secret.
96
+ """
97
+
98
+ def __init__(
99
+ self,
100
+ issuer: str,
101
+ audience: str,
102
+ signing_key: bytes,
103
+ ):
104
+ """Initialize JWT issuer.
105
+
106
+ Args:
107
+ issuer: Token issuer (FastMCP server base URL)
108
+ audience: Token audience (typically {base_url}/mcp)
109
+ signing_key: HS256 signing key (32 bytes)
110
+ """
111
+ self.issuer = issuer
112
+ self.audience = audience
113
+ self._signing_key = signing_key
114
+ self._jwt = JsonWebToken(["HS256"])
115
+
116
+ def issue_access_token(
117
+ self,
118
+ client_id: str,
119
+ scopes: list[str],
120
+ jti: str,
121
+ expires_in: int = 3600,
122
+ ) -> str:
123
+ """Issue a minimal FastMCP access token.
124
+
125
+ FastMCP tokens are reference tokens containing only the minimal claims
126
+ needed for validation and lookup. The JTI maps to the upstream token
127
+ which contains actual user identity and authorization data.
128
+
129
+ Args:
130
+ client_id: MCP client ID
131
+ scopes: Token scopes
132
+ jti: Unique token identifier (maps to upstream token)
133
+ expires_in: Token lifetime in seconds
134
+
135
+ Returns:
136
+ Signed JWT token
137
+ """
138
+ now = int(time.time())
139
+
140
+ header = {"alg": "HS256", "typ": "JWT"}
141
+ payload = {
142
+ "iss": self.issuer,
143
+ "aud": self.audience,
144
+ "client_id": client_id,
145
+ "scope": " ".join(scopes),
146
+ "exp": now + expires_in,
147
+ "iat": now,
148
+ "jti": jti,
149
+ }
150
+
151
+ token_bytes = self._jwt.encode(header, payload, self._signing_key)
152
+ token = token_bytes.decode("utf-8")
153
+
154
+ logger.debug(
155
+ "Issued access token for client=%s jti=%s exp=%d",
156
+ client_id,
157
+ jti[:8],
158
+ payload["exp"],
159
+ )
160
+
161
+ return token
162
+
163
+ def issue_refresh_token(
164
+ self,
165
+ client_id: str,
166
+ scopes: list[str],
167
+ jti: str,
168
+ expires_in: int,
169
+ ) -> str:
170
+ """Issue a minimal FastMCP refresh token.
171
+
172
+ FastMCP refresh tokens are reference tokens containing only the minimal
173
+ claims needed for validation and lookup. The JTI maps to the upstream
174
+ token which contains actual user identity and authorization data.
175
+
176
+ Args:
177
+ client_id: MCP client ID
178
+ scopes: Token scopes
179
+ jti: Unique token identifier (maps to upstream token)
180
+ expires_in: Token lifetime in seconds (should match upstream refresh expiry)
181
+
182
+ Returns:
183
+ Signed JWT token
184
+ """
185
+ now = int(time.time())
186
+
187
+ header = {"alg": "HS256", "typ": "JWT"}
188
+ payload = {
189
+ "iss": self.issuer,
190
+ "aud": self.audience,
191
+ "client_id": client_id,
192
+ "scope": " ".join(scopes),
193
+ "exp": now + expires_in,
194
+ "iat": now,
195
+ "jti": jti,
196
+ "token_use": "refresh",
197
+ }
198
+
199
+ token_bytes = self._jwt.encode(header, payload, self._signing_key)
200
+ token = token_bytes.decode("utf-8")
201
+
202
+ logger.debug(
203
+ "Issued refresh token for client=%s jti=%s exp=%d",
204
+ client_id,
205
+ jti[:8],
206
+ payload["exp"],
207
+ )
208
+
209
+ return token
210
+
211
+ def verify_token(self, token: str) -> dict[str, Any]:
212
+ """Verify and decode a FastMCP token.
213
+
214
+ Validates JWT signature, expiration, issuer, and audience.
215
+
216
+ Args:
217
+ token: JWT token to verify
218
+
219
+ Returns:
220
+ Decoded token payload
221
+
222
+ Raises:
223
+ JoseError: If token is invalid, expired, or has wrong claims
224
+ """
225
+ try:
226
+ # Decode and verify signature
227
+ payload = self._jwt.decode(token, self._signing_key)
228
+
229
+ # Validate expiration
230
+ exp = payload.get("exp")
231
+ if exp and exp < time.time():
232
+ logger.debug("Token expired")
233
+ raise JoseError("Token has expired")
234
+
235
+ # Validate issuer
236
+ if payload.get("iss") != self.issuer:
237
+ logger.debug("Token has invalid issuer")
238
+ raise JoseError("Invalid token issuer")
239
+
240
+ # Validate audience
241
+ if payload.get("aud") != self.audience:
242
+ logger.debug("Token has invalid audience")
243
+ raise JoseError("Invalid token audience")
244
+
245
+ logger.debug(
246
+ "Token verified successfully for subject=%s", payload.get("sub")
247
+ )
248
+ return payload
249
+
250
+ except JoseError as e:
251
+ logger.debug("Token validation failed: %s", e)
252
+ 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()