fastmcp 2.10.6__py3-none-any.whl → 2.11.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/cli/cli.py +128 -33
- fastmcp/cli/install/claude_code.py +42 -1
- fastmcp/cli/install/claude_desktop.py +42 -1
- fastmcp/cli/install/cursor.py +42 -1
- fastmcp/cli/install/mcp_json.py +41 -0
- fastmcp/cli/run.py +127 -1
- fastmcp/client/__init__.py +2 -0
- fastmcp/client/auth/oauth.py +68 -99
- fastmcp/client/oauth_callback.py +18 -0
- fastmcp/client/transports.py +69 -15
- fastmcp/contrib/component_manager/example.py +2 -2
- fastmcp/experimental/server/openapi/README.md +266 -0
- fastmcp/experimental/server/openapi/__init__.py +38 -0
- fastmcp/experimental/server/openapi/components.py +348 -0
- fastmcp/experimental/server/openapi/routing.py +132 -0
- fastmcp/experimental/server/openapi/server.py +466 -0
- fastmcp/experimental/utilities/openapi/README.md +239 -0
- fastmcp/experimental/utilities/openapi/__init__.py +68 -0
- fastmcp/experimental/utilities/openapi/director.py +208 -0
- fastmcp/experimental/utilities/openapi/formatters.py +355 -0
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
- fastmcp/experimental/utilities/openapi/models.py +85 -0
- fastmcp/experimental/utilities/openapi/parser.py +618 -0
- fastmcp/experimental/utilities/openapi/schemas.py +538 -0
- fastmcp/mcp_config.py +125 -88
- fastmcp/prompts/prompt.py +11 -1
- fastmcp/resources/resource.py +21 -1
- fastmcp/resources/template.py +20 -1
- fastmcp/server/auth/__init__.py +18 -2
- fastmcp/server/auth/auth.py +225 -7
- fastmcp/server/auth/providers/bearer.py +25 -473
- fastmcp/server/auth/providers/in_memory.py +4 -2
- fastmcp/server/auth/providers/jwt.py +538 -0
- fastmcp/server/auth/providers/workos.py +151 -0
- fastmcp/server/auth/registry.py +52 -0
- fastmcp/server/context.py +107 -26
- fastmcp/server/dependencies.py +9 -2
- fastmcp/server/http.py +48 -57
- fastmcp/server/middleware/middleware.py +3 -23
- fastmcp/server/openapi.py +1 -1
- fastmcp/server/proxy.py +50 -11
- fastmcp/server/server.py +168 -59
- fastmcp/settings.py +73 -6
- fastmcp/tools/tool.py +36 -3
- fastmcp/tools/tool_manager.py +38 -2
- fastmcp/tools/tool_transform.py +112 -3
- fastmcp/utilities/components.py +41 -3
- fastmcp/utilities/json_schema.py +136 -98
- fastmcp/utilities/json_schema_type.py +1 -3
- fastmcp/utilities/mcp_config.py +28 -0
- fastmcp/utilities/openapi.py +243 -57
- fastmcp/utilities/tests.py +54 -6
- fastmcp/utilities/types.py +94 -11
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/METADATA +4 -3
- fastmcp-2.11.1.dist-info/RECORD +108 -0
- fastmcp/server/auth/providers/bearer_env.py +0 -63
- fastmcp/utilities/cache.py +0 -26
- fastmcp-2.10.6.dist-info/RECORD +0 -93
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,473 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
RevocationOptions,
|
|
27
|
-
)
|
|
28
|
-
from fastmcp.utilities.logging import get_logger
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class JWKData(TypedDict, total=False):
|
|
32
|
-
"""JSON Web Key data structure."""
|
|
33
|
-
|
|
34
|
-
kty: str # Key type (e.g., "RSA") - required
|
|
35
|
-
kid: str # Key ID (optional but recommended)
|
|
36
|
-
use: str # Usage (e.g., "sig")
|
|
37
|
-
alg: str # Algorithm (e.g., "RS256")
|
|
38
|
-
n: str # Modulus (for RSA keys)
|
|
39
|
-
e: str # Exponent (for RSA keys)
|
|
40
|
-
x5c: list[str] # X.509 certificate chain (for JWKs)
|
|
41
|
-
x5t: str # X.509 certificate thumbprint (for JWKs)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class JWKSData(TypedDict):
|
|
45
|
-
"""JSON Web Key Set data structure."""
|
|
46
|
-
|
|
47
|
-
keys: list[JWKData]
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@dataclass(frozen=True, kw_only=True, repr=False)
|
|
51
|
-
class RSAKeyPair:
|
|
52
|
-
private_key: SecretStr
|
|
53
|
-
public_key: str
|
|
54
|
-
|
|
55
|
-
@classmethod
|
|
56
|
-
def generate(cls) -> "RSAKeyPair":
|
|
57
|
-
"""
|
|
58
|
-
Generate an RSA key pair for testing.
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
tuple: (private_key_pem, public_key_pem)
|
|
62
|
-
"""
|
|
63
|
-
# Generate private key
|
|
64
|
-
private_key = rsa.generate_private_key(
|
|
65
|
-
public_exponent=65537,
|
|
66
|
-
key_size=2048,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
# Get public key
|
|
70
|
-
public_key = private_key.public_key()
|
|
71
|
-
|
|
72
|
-
# Serialize private key to PEM format
|
|
73
|
-
private_pem = private_key.private_bytes(
|
|
74
|
-
encoding=serialization.Encoding.PEM,
|
|
75
|
-
format=serialization.PrivateFormat.PKCS8,
|
|
76
|
-
encryption_algorithm=serialization.NoEncryption(),
|
|
77
|
-
).decode("utf-8")
|
|
78
|
-
|
|
79
|
-
# Serialize public key to PEM format
|
|
80
|
-
public_pem = public_key.public_bytes(
|
|
81
|
-
encoding=serialization.Encoding.PEM,
|
|
82
|
-
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
83
|
-
).decode("utf-8")
|
|
84
|
-
|
|
85
|
-
return cls(
|
|
86
|
-
private_key=SecretStr(private_pem),
|
|
87
|
-
public_key=public_pem,
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
def create_token(
|
|
91
|
-
self,
|
|
92
|
-
subject: str = "fastmcp-user",
|
|
93
|
-
issuer: str = "https://fastmcp.example.com",
|
|
94
|
-
audience: str | list[str] | None = None,
|
|
95
|
-
scopes: list[str] | None = None,
|
|
96
|
-
expires_in_seconds: int = 3600,
|
|
97
|
-
additional_claims: dict[str, Any] | None = None,
|
|
98
|
-
kid: str | None = None,
|
|
99
|
-
) -> str:
|
|
100
|
-
"""
|
|
101
|
-
Generate a test JWT token for testing purposes.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
private_key_pem: RSA private key in PEM format
|
|
105
|
-
subject: Subject claim (usually user ID)
|
|
106
|
-
issuer: Issuer claim
|
|
107
|
-
audience: Audience claim - can be a string or list of strings (optional)
|
|
108
|
-
scopes: List of scopes to include
|
|
109
|
-
expires_in_seconds: Token expiration time in seconds
|
|
110
|
-
additional_claims: Any additional claims to include
|
|
111
|
-
kid: Key ID for JWKS lookup (optional)
|
|
112
|
-
|
|
113
|
-
Returns:
|
|
114
|
-
Signed JWT token string
|
|
115
|
-
"""
|
|
116
|
-
# TODO : Add support for configurable algorithms
|
|
117
|
-
jwt = JsonWebToken(["RS256"])
|
|
118
|
-
|
|
119
|
-
now = int(time.time())
|
|
120
|
-
|
|
121
|
-
# Build payload
|
|
122
|
-
payload = {
|
|
123
|
-
"iss": issuer,
|
|
124
|
-
"sub": subject,
|
|
125
|
-
"iat": now,
|
|
126
|
-
"exp": now + expires_in_seconds,
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if audience:
|
|
130
|
-
payload["aud"] = audience
|
|
131
|
-
|
|
132
|
-
if scopes:
|
|
133
|
-
payload["scope"] = " ".join(scopes)
|
|
134
|
-
|
|
135
|
-
if additional_claims:
|
|
136
|
-
payload.update(additional_claims)
|
|
137
|
-
|
|
138
|
-
# Create header
|
|
139
|
-
header = {"alg": "RS256"}
|
|
140
|
-
if kid:
|
|
141
|
-
header["kid"] = kid
|
|
142
|
-
|
|
143
|
-
# Sign and return token
|
|
144
|
-
token_bytes = jwt.encode(
|
|
145
|
-
header,
|
|
146
|
-
payload,
|
|
147
|
-
key=self.private_key.get_secret_value(),
|
|
148
|
-
)
|
|
149
|
-
return token_bytes.decode("utf-8")
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
class BearerAuthProvider(OAuthProvider):
|
|
153
|
-
"""
|
|
154
|
-
Simple JWT Bearer Token validator for hosted MCP servers.
|
|
155
|
-
Uses RS256 asymmetric encryption by default but supports all JWA algorithms. Supports either static public key
|
|
156
|
-
or JWKS URI for key rotation.
|
|
157
|
-
|
|
158
|
-
Note that this provider DOES NOT permit client registration or revocation, or any OAuth flows.
|
|
159
|
-
It is intended to be used with a control plane that manages clients and tokens.
|
|
160
|
-
"""
|
|
161
|
-
|
|
162
|
-
def __init__(
|
|
163
|
-
self,
|
|
164
|
-
public_key: str | None = None,
|
|
165
|
-
jwks_uri: str | None = None,
|
|
166
|
-
issuer: str | None = None,
|
|
167
|
-
algorithm: str | None = None,
|
|
168
|
-
audience: str | list[str] | None = None,
|
|
169
|
-
required_scopes: list[str] | None = None,
|
|
170
|
-
):
|
|
171
|
-
"""
|
|
172
|
-
Initialize the provider. Either public_key or jwks_uri must be provided.
|
|
173
|
-
|
|
174
|
-
Args:
|
|
175
|
-
public_key: RSA public key in PEM format (for static key)
|
|
176
|
-
jwks_uri: URI to fetch keys from (for key rotation)
|
|
177
|
-
issuer: Expected issuer claim (optional)
|
|
178
|
-
algorithm: Algorithm to use for verification (optional, defaults to RS256)
|
|
179
|
-
audience: Expected audience claim - can be a string or list of strings (optional)
|
|
180
|
-
required_scopes: List of required scopes for access (optional)
|
|
181
|
-
"""
|
|
182
|
-
if not (public_key or jwks_uri):
|
|
183
|
-
raise ValueError("Either public_key or jwks_uri must be provided")
|
|
184
|
-
if public_key and jwks_uri:
|
|
185
|
-
raise ValueError("Provide either public_key or jwks_uri, not both")
|
|
186
|
-
|
|
187
|
-
if not algorithm:
|
|
188
|
-
algorithm = "RS256"
|
|
189
|
-
if algorithm not in {
|
|
190
|
-
"HS256",
|
|
191
|
-
"HS384",
|
|
192
|
-
"HS512",
|
|
193
|
-
"RS256",
|
|
194
|
-
"RS384",
|
|
195
|
-
"RS512",
|
|
196
|
-
"ES256",
|
|
197
|
-
"ES384",
|
|
198
|
-
"ES512",
|
|
199
|
-
"PS256",
|
|
200
|
-
"PS384",
|
|
201
|
-
"PS512",
|
|
202
|
-
}:
|
|
203
|
-
raise ValueError(f"Unsupported algorithm: {algorithm}.")
|
|
204
|
-
|
|
205
|
-
# Only pass issuer to parent if it's a valid URL, otherwise use default
|
|
206
|
-
# This allows the issuer claim validation to work with string issuers per RFC 7519
|
|
207
|
-
try:
|
|
208
|
-
issuer_url = AnyHttpUrl(issuer) if issuer else "https://fastmcp.example.com"
|
|
209
|
-
except ValidationError:
|
|
210
|
-
# Issuer is not a valid URL, use default for parent class
|
|
211
|
-
issuer_url = "https://fastmcp.example.com"
|
|
212
|
-
|
|
213
|
-
super().__init__(
|
|
214
|
-
issuer_url=issuer_url,
|
|
215
|
-
client_registration_options=ClientRegistrationOptions(enabled=False),
|
|
216
|
-
revocation_options=RevocationOptions(enabled=False),
|
|
217
|
-
required_scopes=required_scopes,
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
self.algorithm = algorithm
|
|
221
|
-
self.issuer = issuer
|
|
222
|
-
self.audience = audience
|
|
223
|
-
self.public_key = public_key
|
|
224
|
-
self.jwks_uri = jwks_uri
|
|
225
|
-
self.jwt = JsonWebToken([self.algorithm]) # Use RS256 by default
|
|
226
|
-
self.logger = get_logger(__name__)
|
|
227
|
-
|
|
228
|
-
# Simple JWKS cache
|
|
229
|
-
self._jwks_cache: dict[str, str] = {}
|
|
230
|
-
self._jwks_cache_time: float = 0
|
|
231
|
-
self._cache_ttl = 3600 # 1 hour
|
|
232
|
-
|
|
233
|
-
async def _get_verification_key(self, token: str) -> str:
|
|
234
|
-
"""Get the verification key for the token."""
|
|
235
|
-
if self.public_key:
|
|
236
|
-
return self.public_key
|
|
237
|
-
|
|
238
|
-
# Extract kid from token header for JWKS lookup
|
|
239
|
-
try:
|
|
240
|
-
import base64
|
|
241
|
-
import json
|
|
242
|
-
|
|
243
|
-
header_b64 = token.split(".")[0]
|
|
244
|
-
header_b64 += "=" * (4 - len(header_b64) % 4) # Add padding
|
|
245
|
-
header = json.loads(base64.urlsafe_b64decode(header_b64))
|
|
246
|
-
kid = header.get("kid")
|
|
247
|
-
|
|
248
|
-
return await self._get_jwks_key(kid)
|
|
249
|
-
|
|
250
|
-
except Exception as e:
|
|
251
|
-
raise ValueError(f"Failed to extract key ID from token: {e}")
|
|
252
|
-
|
|
253
|
-
async def _get_jwks_key(self, kid: str | None) -> str:
|
|
254
|
-
"""Fetch key from JWKS with simple caching."""
|
|
255
|
-
if not self.jwks_uri:
|
|
256
|
-
raise ValueError("JWKS URI not configured")
|
|
257
|
-
|
|
258
|
-
current_time = time.time()
|
|
259
|
-
|
|
260
|
-
# Check cache first
|
|
261
|
-
if current_time - self._jwks_cache_time < self._cache_ttl:
|
|
262
|
-
if kid and kid in self._jwks_cache:
|
|
263
|
-
return self._jwks_cache[kid]
|
|
264
|
-
elif not kid and len(self._jwks_cache) == 1:
|
|
265
|
-
# If no kid but only one key cached, use it
|
|
266
|
-
return next(iter(self._jwks_cache.values()))
|
|
267
|
-
|
|
268
|
-
# Fetch JWKS
|
|
269
|
-
try:
|
|
270
|
-
async with httpx.AsyncClient() as client:
|
|
271
|
-
response = await client.get(self.jwks_uri)
|
|
272
|
-
response.raise_for_status()
|
|
273
|
-
jwks_data = response.json()
|
|
274
|
-
|
|
275
|
-
# Cache all keys
|
|
276
|
-
self._jwks_cache = {}
|
|
277
|
-
for key_data in jwks_data.get("keys", []):
|
|
278
|
-
key_kid = key_data.get("kid")
|
|
279
|
-
jwk = JsonWebKey.import_key(key_data)
|
|
280
|
-
public_key = jwk.get_public_key() # type: ignore
|
|
281
|
-
|
|
282
|
-
if key_kid:
|
|
283
|
-
self._jwks_cache[key_kid] = public_key
|
|
284
|
-
else:
|
|
285
|
-
# Key without kid - use a default identifier
|
|
286
|
-
self._jwks_cache["_default"] = public_key
|
|
287
|
-
|
|
288
|
-
self._jwks_cache_time = current_time
|
|
289
|
-
|
|
290
|
-
# Select the appropriate key
|
|
291
|
-
if kid:
|
|
292
|
-
if kid not in self._jwks_cache:
|
|
293
|
-
self.logger.debug(
|
|
294
|
-
"JWKS key lookup failed: key ID '%s' not found", kid
|
|
295
|
-
)
|
|
296
|
-
raise ValueError(f"Key ID '{kid}' not found in JWKS")
|
|
297
|
-
return self._jwks_cache[kid]
|
|
298
|
-
else:
|
|
299
|
-
# No kid in token - only allow if there's exactly one key
|
|
300
|
-
if len(self._jwks_cache) == 1:
|
|
301
|
-
return next(iter(self._jwks_cache.values()))
|
|
302
|
-
elif len(self._jwks_cache) > 1:
|
|
303
|
-
raise ValueError(
|
|
304
|
-
"Multiple keys in JWKS but no key ID (kid) in token"
|
|
305
|
-
)
|
|
306
|
-
else:
|
|
307
|
-
raise ValueError("No keys found in JWKS")
|
|
308
|
-
|
|
309
|
-
except Exception as e:
|
|
310
|
-
self.logger.debug("JWKS fetch failed: %s", str(e))
|
|
311
|
-
raise ValueError(f"Failed to fetch JWKS: {e}")
|
|
312
|
-
|
|
313
|
-
async def load_access_token(self, token: str) -> AccessToken | None:
|
|
314
|
-
"""
|
|
315
|
-
Validates the provided JWT bearer token.
|
|
316
|
-
|
|
317
|
-
Args:
|
|
318
|
-
token: The JWT token string to validate
|
|
319
|
-
|
|
320
|
-
Returns:
|
|
321
|
-
AccessToken object if valid, None if invalid or expired
|
|
322
|
-
"""
|
|
323
|
-
try:
|
|
324
|
-
# Get verification key (static or from JWKS)
|
|
325
|
-
verification_key = await self._get_verification_key(token)
|
|
326
|
-
|
|
327
|
-
# Decode and verify the JWT token
|
|
328
|
-
claims = self.jwt.decode(token, verification_key)
|
|
329
|
-
|
|
330
|
-
# Extract client ID early for logging
|
|
331
|
-
client_id = claims.get("client_id") or claims.get("sub") or "unknown"
|
|
332
|
-
|
|
333
|
-
# Validate expiration
|
|
334
|
-
exp = claims.get("exp")
|
|
335
|
-
if exp and exp < time.time():
|
|
336
|
-
self.logger.debug(
|
|
337
|
-
"Token validation failed: expired token for client %s", client_id
|
|
338
|
-
)
|
|
339
|
-
self.logger.info("Bearer token rejected for client %s", client_id)
|
|
340
|
-
return None
|
|
341
|
-
|
|
342
|
-
# Validate issuer - note we use issuer instead of issuer_url here because
|
|
343
|
-
# issuer is optional, allowing users to make this check optional
|
|
344
|
-
if self.issuer:
|
|
345
|
-
if claims.get("iss") != self.issuer:
|
|
346
|
-
self.logger.debug(
|
|
347
|
-
"Token validation failed: issuer mismatch for client %s",
|
|
348
|
-
client_id,
|
|
349
|
-
)
|
|
350
|
-
self.logger.info("Bearer token rejected for client %s", client_id)
|
|
351
|
-
return None
|
|
352
|
-
|
|
353
|
-
# Validate audience if configured
|
|
354
|
-
if self.audience:
|
|
355
|
-
aud = claims.get("aud")
|
|
356
|
-
|
|
357
|
-
# Handle different combinations of audience types
|
|
358
|
-
audience_valid = False
|
|
359
|
-
if isinstance(self.audience, list):
|
|
360
|
-
# self.audience is a list - check if any expected audience is present
|
|
361
|
-
if isinstance(aud, list):
|
|
362
|
-
# Both are lists - check for intersection
|
|
363
|
-
audience_valid = any(
|
|
364
|
-
expected in aud for expected in self.audience
|
|
365
|
-
)
|
|
366
|
-
else:
|
|
367
|
-
# aud is a string - check if it's in our expected list
|
|
368
|
-
audience_valid = aud in self.audience
|
|
369
|
-
else:
|
|
370
|
-
# self.audience is a string - use original logic
|
|
371
|
-
if isinstance(aud, list):
|
|
372
|
-
audience_valid = self.audience in aud
|
|
373
|
-
else:
|
|
374
|
-
audience_valid = aud == self.audience
|
|
375
|
-
|
|
376
|
-
if not audience_valid:
|
|
377
|
-
self.logger.debug(
|
|
378
|
-
"Token validation failed: audience mismatch for client %s",
|
|
379
|
-
client_id,
|
|
380
|
-
)
|
|
381
|
-
self.logger.info("Bearer token rejected for client %s", client_id)
|
|
382
|
-
return None
|
|
383
|
-
|
|
384
|
-
# Extract scopes
|
|
385
|
-
scopes = self._extract_scopes(claims)
|
|
386
|
-
|
|
387
|
-
return AccessToken(
|
|
388
|
-
token=token,
|
|
389
|
-
client_id=str(client_id),
|
|
390
|
-
scopes=scopes,
|
|
391
|
-
expires_at=int(exp) if exp else None,
|
|
392
|
-
)
|
|
393
|
-
|
|
394
|
-
except JoseError:
|
|
395
|
-
self.logger.debug("Token validation failed: JWT signature/format invalid")
|
|
396
|
-
return None
|
|
397
|
-
except Exception as e:
|
|
398
|
-
self.logger.debug("Token validation failed: %s", str(e))
|
|
399
|
-
return None
|
|
400
|
-
|
|
401
|
-
def _extract_scopes(self, claims: dict[str, Any]) -> list[str]:
|
|
402
|
-
"""
|
|
403
|
-
Extract scopes from JWT claims. Supports both 'scope' and 'scp'
|
|
404
|
-
claims.
|
|
405
|
-
|
|
406
|
-
Checks the `scope` claim first (standard OAuth2 claim), then the `scp`
|
|
407
|
-
claim (used by some Identity Providers).
|
|
408
|
-
"""
|
|
409
|
-
|
|
410
|
-
for claim in ["scope", "scp"]:
|
|
411
|
-
if claim in claims:
|
|
412
|
-
if isinstance(claims[claim], str):
|
|
413
|
-
return claims[claim].split()
|
|
414
|
-
elif isinstance(claims[claim], list):
|
|
415
|
-
return claims[claim]
|
|
416
|
-
|
|
417
|
-
return []
|
|
418
|
-
|
|
419
|
-
async def verify_token(self, token: str) -> AccessToken | None:
|
|
420
|
-
"""
|
|
421
|
-
Verify a bearer token and return access info if valid.
|
|
422
|
-
|
|
423
|
-
This method implements the TokenVerifier protocol by delegating
|
|
424
|
-
to our existing load_access_token method.
|
|
425
|
-
|
|
426
|
-
Args:
|
|
427
|
-
token: The JWT token string to validate
|
|
428
|
-
|
|
429
|
-
Returns:
|
|
430
|
-
AccessToken object if valid, None if invalid or expired
|
|
431
|
-
"""
|
|
432
|
-
return await self.load_access_token(token)
|
|
433
|
-
|
|
434
|
-
# --- Unused OAuth server methods ---
|
|
435
|
-
async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
|
|
436
|
-
raise NotImplementedError("Client management not supported")
|
|
437
|
-
|
|
438
|
-
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
|
|
439
|
-
raise NotImplementedError("Client registration not supported")
|
|
440
|
-
|
|
441
|
-
async def authorize(
|
|
442
|
-
self, client: OAuthClientInformationFull, params: AuthorizationParams
|
|
443
|
-
) -> str:
|
|
444
|
-
raise NotImplementedError("Authorization flow not supported")
|
|
445
|
-
|
|
446
|
-
async def load_authorization_code(
|
|
447
|
-
self, client: OAuthClientInformationFull, authorization_code: str
|
|
448
|
-
) -> AuthorizationCode | None:
|
|
449
|
-
raise NotImplementedError("Authorization code flow not supported")
|
|
450
|
-
|
|
451
|
-
async def exchange_authorization_code(
|
|
452
|
-
self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
|
|
453
|
-
) -> OAuthToken:
|
|
454
|
-
raise NotImplementedError("Authorization code exchange not supported")
|
|
455
|
-
|
|
456
|
-
async def load_refresh_token(
|
|
457
|
-
self, client: OAuthClientInformationFull, refresh_token: str
|
|
458
|
-
) -> RefreshToken | None:
|
|
459
|
-
raise NotImplementedError("Refresh token flow not supported")
|
|
460
|
-
|
|
461
|
-
async def exchange_refresh_token(
|
|
462
|
-
self,
|
|
463
|
-
client: OAuthClientInformationFull,
|
|
464
|
-
refresh_token: RefreshToken,
|
|
465
|
-
scopes: list[str],
|
|
466
|
-
) -> OAuthToken:
|
|
467
|
-
raise NotImplementedError("Refresh token exchange not supported")
|
|
468
|
-
|
|
469
|
-
async def revoke_token(
|
|
470
|
-
self,
|
|
471
|
-
token: AccessToken | RefreshToken,
|
|
472
|
-
) -> None:
|
|
473
|
-
raise NotImplementedError("Token revocation not supported")
|
|
1
|
+
"""Backwards compatibility shim for BearerAuthProvider.
|
|
2
|
+
|
|
3
|
+
The BearerAuthProvider class has been moved to fastmcp.server.auth.providers.jwt.JWTVerifier
|
|
4
|
+
for better organization. This module provides a backwards-compatible import.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
import fastmcp
|
|
10
|
+
from fastmcp.server.auth.providers.jwt import JWKData, JWKSData, RSAKeyPair
|
|
11
|
+
from fastmcp.server.auth.providers.jwt import JWTVerifier as BearerAuthProvider
|
|
12
|
+
|
|
13
|
+
# Re-export for backwards compatibility
|
|
14
|
+
__all__ = ["BearerAuthProvider", "RSAKeyPair", "JWKData", "JWKSData"]
|
|
15
|
+
|
|
16
|
+
# Deprecated in 2.11
|
|
17
|
+
if fastmcp.settings.deprecation_warnings:
|
|
18
|
+
warnings.warn(
|
|
19
|
+
"The `fastmcp.server.auth.providers.bearer` module is deprecated "
|
|
20
|
+
"and will be removed in a future version. "
|
|
21
|
+
"Please use `fastmcp.server.auth.providers.jwt.JWTVerifier` "
|
|
22
|
+
"instead of this module's BearerAuthProvider.",
|
|
23
|
+
DeprecationWarning,
|
|
24
|
+
stacklevel=2,
|
|
25
|
+
)
|
|
@@ -36,18 +36,20 @@ class InMemoryOAuthProvider(OAuthProvider):
|
|
|
36
36
|
|
|
37
37
|
def __init__(
|
|
38
38
|
self,
|
|
39
|
-
|
|
39
|
+
base_url: AnyHttpUrl | str | None = None,
|
|
40
40
|
service_documentation_url: AnyHttpUrl | str | None = None,
|
|
41
41
|
client_registration_options: ClientRegistrationOptions | None = None,
|
|
42
42
|
revocation_options: RevocationOptions | None = None,
|
|
43
43
|
required_scopes: list[str] | None = None,
|
|
44
|
+
resource_server_url: AnyHttpUrl | str | None = None,
|
|
44
45
|
):
|
|
45
46
|
super().__init__(
|
|
46
|
-
|
|
47
|
+
base_url=base_url or "http://fastmcp.example.com",
|
|
47
48
|
service_documentation_url=service_documentation_url,
|
|
48
49
|
client_registration_options=client_registration_options,
|
|
49
50
|
revocation_options=revocation_options,
|
|
50
51
|
required_scopes=required_scopes,
|
|
52
|
+
resource_server_url=resource_server_url,
|
|
51
53
|
)
|
|
52
54
|
self.clients: dict[str, OAuthClientInformationFull] = {}
|
|
53
55
|
self.auth_codes: dict[str, AuthorizationCode] = {}
|