fastmcp 2.12.1__py3-none-any.whl → 2.13.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 +56 -36
- fastmcp/cli/install/__init__.py +2 -0
- fastmcp/cli/install/claude_code.py +7 -16
- fastmcp/cli/install/claude_desktop.py +4 -12
- fastmcp/cli/install/cursor.py +20 -30
- fastmcp/cli/install/gemini_cli.py +241 -0
- fastmcp/cli/install/mcp_json.py +4 -12
- fastmcp/cli/run.py +15 -94
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +117 -206
- fastmcp/client/client.py +123 -47
- fastmcp/client/elicitation.py +6 -1
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +81 -26
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +7 -7
- fastmcp/contrib/mcp_mixin/README.md +35 -4
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
- 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 +16 -10
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +33 -7
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +32 -27
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +28 -20
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +119 -27
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +80 -47
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1556 -265
- fastmcp/server/auth/oidc_proxy.py +412 -0
- fastmcp/server/auth/providers/auth0.py +193 -0
- fastmcp/server/auth/providers/aws.py +263 -0
- fastmcp/server/auth/providers/azure.py +314 -129
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +229 -0
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +31 -6
- fastmcp/server/auth/providers/google.py +50 -7
- fastmcp/server/auth/providers/in_memory.py +27 -3
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +37 -15
- fastmcp/server/context.py +194 -67
- fastmcp/server/dependencies.py +56 -16
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +57 -18
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +158 -116
- fastmcp/server/middleware/middleware.py +30 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +15 -7
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +744 -254
- fastmcp/settings.py +65 -15
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +173 -108
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +13 -11
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +21 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +182 -10
- 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/environments/uv.py +10 -45
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
- fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +93 -10
- fastmcp/utilities/types.py +87 -21
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -144
- fastmcp-2.12.1.dist-info/RECORD +0 -128
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
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, overload
|
|
13
|
+
|
|
14
|
+
from authlib.jose import JsonWebToken
|
|
15
|
+
from authlib.jose.errors import JoseError
|
|
16
|
+
from cryptography.hazmat.primitives import hashes
|
|
17
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
18
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
19
|
+
|
|
20
|
+
from fastmcp.utilities.logging import get_logger
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
KDF_ITERATIONS = 1000000
|
|
25
|
+
|
|
26
|
+
|
|
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
|
+
|
|
31
|
+
|
|
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."""
|
|
35
|
+
|
|
36
|
+
|
|
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
|
+
)
|
|
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())
|
|
56
|
+
|
|
57
|
+
return base64.urlsafe_b64encode(derived_key)
|
|
58
|
+
|
|
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())
|
|
66
|
+
|
|
67
|
+
return base64.urlsafe_b64encode(pbkdf2)
|
|
68
|
+
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"Either high_entropy_material or low_entropy_material must be provided"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class JWTIssuer:
|
|
75
|
+
"""Issues and validates FastMCP-signed JWT tokens using HS256.
|
|
76
|
+
|
|
77
|
+
This issuer creates JWT tokens for MCP clients with proper audience claims,
|
|
78
|
+
maintaining OAuth 2.0 token boundaries. Tokens are signed with HS256 using
|
|
79
|
+
a key derived from the upstream client secret.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
issuer: str,
|
|
85
|
+
audience: str,
|
|
86
|
+
signing_key: bytes,
|
|
87
|
+
):
|
|
88
|
+
"""Initialize JWT issuer.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
issuer: Token issuer (FastMCP server base URL)
|
|
92
|
+
audience: Token audience (typically {base_url}/mcp)
|
|
93
|
+
signing_key: HS256 signing key (32 bytes)
|
|
94
|
+
"""
|
|
95
|
+
self.issuer = issuer
|
|
96
|
+
self.audience = audience
|
|
97
|
+
self._signing_key = signing_key
|
|
98
|
+
self._jwt = JsonWebToken(["HS256"])
|
|
99
|
+
|
|
100
|
+
def issue_access_token(
|
|
101
|
+
self,
|
|
102
|
+
client_id: str,
|
|
103
|
+
scopes: list[str],
|
|
104
|
+
jti: str,
|
|
105
|
+
expires_in: int = 3600,
|
|
106
|
+
) -> str:
|
|
107
|
+
"""Issue a minimal FastMCP access token.
|
|
108
|
+
|
|
109
|
+
FastMCP tokens are reference tokens containing only the minimal claims
|
|
110
|
+
needed for validation and lookup. The JTI maps to the upstream token
|
|
111
|
+
which contains actual user identity and authorization data.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
client_id: MCP client ID
|
|
115
|
+
scopes: Token scopes
|
|
116
|
+
jti: Unique token identifier (maps to upstream token)
|
|
117
|
+
expires_in: Token lifetime in seconds
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Signed JWT token
|
|
121
|
+
"""
|
|
122
|
+
now = int(time.time())
|
|
123
|
+
|
|
124
|
+
header = {"alg": "HS256", "typ": "JWT"}
|
|
125
|
+
payload = {
|
|
126
|
+
"iss": self.issuer,
|
|
127
|
+
"aud": self.audience,
|
|
128
|
+
"client_id": client_id,
|
|
129
|
+
"scope": " ".join(scopes),
|
|
130
|
+
"exp": now + expires_in,
|
|
131
|
+
"iat": now,
|
|
132
|
+
"jti": jti,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
token_bytes = self._jwt.encode(header, payload, self._signing_key)
|
|
136
|
+
token = token_bytes.decode("utf-8")
|
|
137
|
+
|
|
138
|
+
logger.debug(
|
|
139
|
+
"Issued access token for client=%s jti=%s exp=%d",
|
|
140
|
+
client_id,
|
|
141
|
+
jti[:8],
|
|
142
|
+
payload["exp"],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return token
|
|
146
|
+
|
|
147
|
+
def issue_refresh_token(
|
|
148
|
+
self,
|
|
149
|
+
client_id: str,
|
|
150
|
+
scopes: list[str],
|
|
151
|
+
jti: str,
|
|
152
|
+
expires_in: int,
|
|
153
|
+
) -> str:
|
|
154
|
+
"""Issue a minimal FastMCP refresh token.
|
|
155
|
+
|
|
156
|
+
FastMCP refresh tokens are reference tokens containing only the minimal
|
|
157
|
+
claims needed for validation and lookup. The JTI maps to the upstream
|
|
158
|
+
token which contains actual user identity and authorization data.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
client_id: MCP client ID
|
|
162
|
+
scopes: Token scopes
|
|
163
|
+
jti: Unique token identifier (maps to upstream token)
|
|
164
|
+
expires_in: Token lifetime in seconds (should match upstream refresh expiry)
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Signed JWT token
|
|
168
|
+
"""
|
|
169
|
+
now = int(time.time())
|
|
170
|
+
|
|
171
|
+
header = {"alg": "HS256", "typ": "JWT"}
|
|
172
|
+
payload = {
|
|
173
|
+
"iss": self.issuer,
|
|
174
|
+
"aud": self.audience,
|
|
175
|
+
"client_id": client_id,
|
|
176
|
+
"scope": " ".join(scopes),
|
|
177
|
+
"exp": now + expires_in,
|
|
178
|
+
"iat": now,
|
|
179
|
+
"jti": jti,
|
|
180
|
+
"token_use": "refresh",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
token_bytes = self._jwt.encode(header, payload, self._signing_key)
|
|
184
|
+
token = token_bytes.decode("utf-8")
|
|
185
|
+
|
|
186
|
+
logger.debug(
|
|
187
|
+
"Issued refresh token for client=%s jti=%s exp=%d",
|
|
188
|
+
client_id,
|
|
189
|
+
jti[:8],
|
|
190
|
+
payload["exp"],
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return token
|
|
194
|
+
|
|
195
|
+
def verify_token(self, token: str) -> dict[str, Any]:
|
|
196
|
+
"""Verify and decode a FastMCP token.
|
|
197
|
+
|
|
198
|
+
Validates JWT signature, expiration, issuer, and audience.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
token: JWT token to verify
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Decoded token payload
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
JoseError: If token is invalid, expired, or has wrong claims
|
|
208
|
+
"""
|
|
209
|
+
try:
|
|
210
|
+
# Decode and verify signature
|
|
211
|
+
payload = self._jwt.decode(token, self._signing_key)
|
|
212
|
+
|
|
213
|
+
# Validate expiration
|
|
214
|
+
exp = payload.get("exp")
|
|
215
|
+
if exp and exp < time.time():
|
|
216
|
+
logger.debug("Token expired")
|
|
217
|
+
raise JoseError("Token has expired")
|
|
218
|
+
|
|
219
|
+
# Validate issuer
|
|
220
|
+
if payload.get("iss") != self.issuer:
|
|
221
|
+
logger.debug("Token has invalid issuer")
|
|
222
|
+
raise JoseError("Invalid token issuer")
|
|
223
|
+
|
|
224
|
+
# Validate audience
|
|
225
|
+
if payload.get("aud") != self.audience:
|
|
226
|
+
logger.debug("Token has invalid audience")
|
|
227
|
+
raise JoseError("Invalid token audience")
|
|
228
|
+
|
|
229
|
+
logger.debug(
|
|
230
|
+
"Token verified successfully for subject=%s", payload.get("sub")
|
|
231
|
+
)
|
|
232
|
+
return payload
|
|
233
|
+
|
|
234
|
+
except JoseError as e:
|
|
235
|
+
logger.debug("Token validation failed: %s", e)
|
|
236
|
+
raise
|
|
@@ -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
|
+
)
|