fastmcp 2.13.0rc3__py3-none-any.whl → 2.13.0.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.
Files changed (65) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +2 -2
  3. fastmcp/client/__init__.py +9 -9
  4. fastmcp/client/auth/oauth.py +7 -6
  5. fastmcp/client/client.py +10 -10
  6. fastmcp/client/sampling.py +1 -1
  7. fastmcp/client/transports.py +34 -34
  8. fastmcp/contrib/component_manager/__init__.py +1 -1
  9. fastmcp/contrib/component_manager/component_manager.py +2 -2
  10. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  11. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  12. fastmcp/experimental/server/openapi/__init__.py +5 -8
  13. fastmcp/experimental/server/openapi/components.py +11 -7
  14. fastmcp/experimental/server/openapi/routing.py +2 -2
  15. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  16. fastmcp/experimental/utilities/openapi/director.py +1 -1
  17. fastmcp/experimental/utilities/openapi/json_schema_converter.py +2 -2
  18. fastmcp/experimental/utilities/openapi/models.py +3 -3
  19. fastmcp/experimental/utilities/openapi/parser.py +3 -5
  20. fastmcp/experimental/utilities/openapi/schemas.py +2 -2
  21. fastmcp/mcp_config.py +2 -3
  22. fastmcp/prompts/__init__.py +1 -1
  23. fastmcp/prompts/prompt.py +9 -13
  24. fastmcp/resources/__init__.py +5 -5
  25. fastmcp/resources/resource.py +1 -3
  26. fastmcp/resources/resource_manager.py +1 -1
  27. fastmcp/server/__init__.py +1 -1
  28. fastmcp/server/auth/__init__.py +5 -5
  29. fastmcp/server/auth/auth.py +2 -2
  30. fastmcp/server/auth/oidc_proxy.py +2 -2
  31. fastmcp/server/auth/providers/azure.py +48 -25
  32. fastmcp/server/auth/providers/bearer.py +1 -1
  33. fastmcp/server/auth/providers/in_memory.py +2 -2
  34. fastmcp/server/auth/providers/introspection.py +2 -2
  35. fastmcp/server/auth/providers/jwt.py +17 -18
  36. fastmcp/server/auth/providers/supabase.py +1 -1
  37. fastmcp/server/auth/providers/workos.py +2 -2
  38. fastmcp/server/context.py +8 -10
  39. fastmcp/server/dependencies.py +5 -6
  40. fastmcp/server/elicitation.py +1 -1
  41. fastmcp/server/http.py +2 -3
  42. fastmcp/server/middleware/__init__.py +1 -1
  43. fastmcp/server/middleware/caching.py +1 -1
  44. fastmcp/server/middleware/error_handling.py +8 -8
  45. fastmcp/server/middleware/middleware.py +1 -1
  46. fastmcp/server/openapi.py +10 -6
  47. fastmcp/server/proxy.py +5 -4
  48. fastmcp/server/server.py +27 -29
  49. fastmcp/tools/__init__.py +1 -1
  50. fastmcp/tools/tool.py +12 -12
  51. fastmcp/tools/tool_transform.py +6 -6
  52. fastmcp/utilities/cli.py +5 -6
  53. fastmcp/utilities/inspect.py +2 -2
  54. fastmcp/utilities/json_schema_type.py +4 -4
  55. fastmcp/utilities/logging.py +14 -18
  56. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  57. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  58. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  59. fastmcp/utilities/openapi.py +9 -9
  60. fastmcp/utilities/tests.py +2 -4
  61. {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/METADATA +3 -3
  62. {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/RECORD +65 -65
  63. {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/WHEEL +0 -0
  64. {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/entry_points.txt +0 -0
  65. {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/licenses/LICENSE +0 -0
@@ -235,7 +235,7 @@ class ResourceManager:
235
235
 
236
236
  # Then check templates (local and mounted) only if not found in concrete resources
237
237
  templates = await self.get_resource_templates()
238
- for template_key in templates.keys():
238
+ for template_key in templates:
239
239
  if match_uri_template(uri_str, template_key):
240
240
  return True
241
241
 
@@ -3,4 +3,4 @@ from .context import Context
3
3
  from . import dependencies
4
4
 
5
5
 
6
- __all__ = ["FastMCP", "Context"]
6
+ __all__ = ["Context", "FastMCP"]
@@ -10,14 +10,14 @@ from .oauth_proxy import OAuthProxy
10
10
 
11
11
 
12
12
  __all__ = [
13
+ "AccessToken",
13
14
  "AuthProvider",
14
- "OAuthProvider",
15
- "TokenVerifier",
16
15
  "JWTVerifier",
17
- "StaticTokenVerifier",
18
- "RemoteAuthProvider",
19
- "AccessToken",
16
+ "OAuthProvider",
20
17
  "OAuthProxy",
18
+ "RemoteAuthProvider",
19
+ "StaticTokenVerifier",
20
+ "TokenVerifier",
21
21
  ]
22
22
 
23
23
 
@@ -23,7 +23,7 @@ from mcp.server.auth.settings import (
23
23
  ClientRegistrationOptions,
24
24
  RevocationOptions,
25
25
  )
26
- from pydantic import AnyHttpUrl
26
+ from pydantic import AnyHttpUrl, Field
27
27
  from starlette.middleware import Middleware
28
28
  from starlette.middleware.authentication import AuthenticationMiddleware
29
29
  from starlette.routing import Route
@@ -32,7 +32,7 @@ from starlette.routing import Route
32
32
  class AccessToken(_SDKAccessToken):
33
33
  """AccessToken that includes all JWT claims."""
34
34
 
35
- claims: dict[str, Any] = {}
35
+ claims: dict[str, Any] = Field(default_factory=dict)
36
36
 
37
37
 
38
38
  class AuthProvider(TokenVerifierProtocol):
@@ -123,10 +123,10 @@ class OIDCConfiguration(BaseModel):
123
123
 
124
124
  try:
125
125
  AnyHttpUrl(value)
126
- except Exception:
126
+ except Exception as e:
127
127
  message = f"Invalid URL for configuration metadata: {attr}"
128
128
  logger.error(message)
129
- raise ValueError(message)
129
+ raise ValueError(message) from e
130
130
 
131
131
  enforce("issuer", True)
132
132
  enforce("authorization_endpoint", True)
@@ -6,7 +6,7 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from typing import TYPE_CHECKING
9
+ from typing import TYPE_CHECKING, Any
10
10
 
11
11
  from key_value.aio.protocols import AsyncKeyValue
12
12
  from pydantic import SecretStr, field_validator
@@ -113,12 +113,12 @@ class AzureProvider(OAuthProxy):
113
113
  client_id: str | NotSetT = NotSet,
114
114
  client_secret: str | NotSetT = NotSet,
115
115
  tenant_id: str | NotSetT = NotSet,
116
- identifier_uri: str | None | NotSetT = NotSet,
116
+ identifier_uri: str | NotSetT | None = NotSet,
117
117
  base_url: str | NotSetT = NotSet,
118
118
  issuer_url: str | NotSetT = NotSet,
119
119
  redirect_path: str | NotSetT = NotSet,
120
- required_scopes: list[str] | None | NotSetT = NotSet,
121
- additional_authorize_scopes: list[str] | None | NotSetT = NotSet,
120
+ required_scopes: list[str] | NotSetT | None = NotSet,
121
+ additional_authorize_scopes: list[str] | NotSetT | None = NotSet,
122
122
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
123
123
  client_storage: AsyncKeyValue | None = None,
124
124
  jwt_signing_key: str | bytes | NotSetT = NotSet,
@@ -202,32 +202,34 @@ class AzureProvider(OAuthProxy):
202
202
  )
203
203
  raise ValueError(msg)
204
204
 
205
+ # Validate required_scopes has at least one scope
205
206
  if not settings.required_scopes:
206
- raise ValueError("required_scopes is required")
207
+ msg = (
208
+ "required_scopes must include at least one scope - set via parameter or "
209
+ "FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES. Azure's OAuth API requires "
210
+ "the 'scope' parameter in authorization requests. Use the unprefixed scope "
211
+ "names from your Azure App registration (e.g., ['read', 'write'])"
212
+ )
213
+ raise ValueError(msg)
207
214
 
208
215
  # Apply defaults
209
216
  self.identifier_uri = settings.identifier_uri or f"api://{settings.client_id}"
210
217
  self.additional_authorize_scopes = settings.additional_authorize_scopes or []
211
218
  tenant_id_final = settings.tenant_id
212
219
 
213
- # Prefix required scopes with identifier_uri for Azure
214
- # Azure returns scopes as full URIs (e.g., "api://xxx/read") in tokens
215
- prefixed_required_scopes = [
216
- f"{self.identifier_uri}/{scope}" for scope in settings.required_scopes
217
- ]
218
-
219
220
  # Always validate tokens against the app's API client ID using JWT
220
221
  issuer = f"https://login.microsoftonline.com/{tenant_id_final}/v2.0"
221
222
  jwks_uri = (
222
223
  f"https://login.microsoftonline.com/{tenant_id_final}/discovery/v2.0/keys"
223
224
  )
224
225
 
226
+ # Azure returns unprefixed scopes in JWT tokens, so validate against unprefixed scopes
225
227
  token_verifier = JWTVerifier(
226
228
  jwks_uri=jwks_uri,
227
229
  issuer=issuer,
228
230
  audience=settings.client_id,
229
231
  algorithm="RS256",
230
- required_scopes=prefixed_required_scopes,
232
+ required_scopes=settings.required_scopes, # Unprefixed scopes for validation
231
233
  )
232
234
 
233
235
  # Extract secret string from SecretStr
@@ -298,19 +300,40 @@ class AzureProvider(OAuthProxy):
298
300
  "Filtering out 'resource' parameter '%s' for Azure AD v2.0 (use scopes instead)",
299
301
  original_resource,
300
302
  )
301
- # Scopes are already prefixed:
302
- # - self.required_scopes was prefixed during __init__
303
- # - Client scopes come from PRM which advertises prefixed scopes
304
- scopes = params_to_use.scopes or self.required_scopes
305
-
306
- final_scopes = list(scopes)
307
- # Add Microsoft Graph scopes separately - these use shorthand format (e.g., "User.Read")
308
- # and should not be prefixed with identifier_uri. Azure returns them as-is in tokens.
303
+ # Don't modify the scopes in params - they stay unprefixed for MCP clients
304
+ # We'll prefix them when building the Azure authorization URL (in _build_upstream_authorize_url)
305
+ auth_url = await super().authorize(client, params_to_use)
306
+ separator = "&" if "?" in auth_url else "?"
307
+ return f"{auth_url}{separator}prompt=select_account"
308
+
309
+ def _build_upstream_authorize_url(
310
+ self, txn_id: str, transaction: dict[str, Any]
311
+ ) -> str:
312
+ """Build Azure authorization URL with prefixed scopes.
313
+
314
+ Overrides parent to prefix scopes with identifier_uri before sending to Azure,
315
+ while keeping unprefixed scopes in the transaction for MCP clients.
316
+ """
317
+ # Get unprefixed scopes from transaction
318
+ unprefixed_scopes = transaction.get("scopes") or self.required_scopes or []
319
+
320
+ # Prefix scopes for Azure authorization request
321
+ prefixed_scopes = []
322
+ for scope in unprefixed_scopes:
323
+ if "://" in scope or "/" in scope:
324
+ # Already a full URI or path (e.g., "api://xxx/read" or "User.Read")
325
+ prefixed_scopes.append(scope)
326
+ else:
327
+ # Unprefixed scope name - prefix it with identifier_uri
328
+ prefixed_scopes.append(f"{self.identifier_uri}/{scope}")
329
+
330
+ # Add Microsoft Graph scopes (not validated, not prefixed)
309
331
  if self.additional_authorize_scopes:
310
- final_scopes.extend(self.additional_authorize_scopes)
332
+ prefixed_scopes.extend(self.additional_authorize_scopes)
311
333
 
312
- modified_params = params_to_use.model_copy(update={"scopes": final_scopes})
334
+ # Temporarily modify transaction dict for parent's URL building
335
+ modified_transaction = transaction.copy()
336
+ modified_transaction["scopes"] = prefixed_scopes
313
337
 
314
- auth_url = await super().authorize(client, modified_params)
315
- separator = "&" if "?" in auth_url else "?"
316
- return f"{auth_url}{separator}prompt=select_account"
338
+ # Let parent build the URL with prefixed scopes
339
+ return super()._build_upstream_authorize_url(txn_id, modified_transaction)
@@ -11,7 +11,7 @@ from fastmcp.server.auth.providers.jwt import JWKData, JWKSData, RSAKeyPair
11
11
  from fastmcp.server.auth.providers.jwt import JWTVerifier as BearerAuthProvider
12
12
 
13
13
  # Re-export for backwards compatibility
14
- __all__ = ["BearerAuthProvider", "RSAKeyPair", "JWKData", "JWKSData"]
14
+ __all__ = ["BearerAuthProvider", "JWKData", "JWKSData", "RSAKeyPair"]
15
15
 
16
16
  # Deprecated in 2.11
17
17
  if fastmcp.settings.deprecation_warnings:
@@ -96,10 +96,10 @@ class InMemoryOAuthProvider(OAuthProvider):
96
96
  # or if params.redirect_uri is None and client has a default.
97
97
  # However, the AuthorizationHandler handles the primary validation.
98
98
  pass # Let's assume AuthorizationHandler did its job.
99
- except Exception: # Replace with specific validation error if client.validate_redirect_uri existed
99
+ except Exception as e: # Replace with specific validation error if client.validate_redirect_uri existed
100
100
  raise AuthorizeError(
101
101
  error="invalid_request", error_description="Invalid redirect_uri."
102
- )
102
+ ) from e
103
103
 
104
104
  auth_code_value = f"test_auth_code_{secrets.token_hex(16)}"
105
105
  expires_at = time.time() + DEFAULT_AUTH_CODE_EXPIRY_SECONDS
@@ -97,8 +97,8 @@ class IntrospectionTokenVerifier(TokenVerifier):
97
97
  client_id: str | NotSetT = NotSet,
98
98
  client_secret: str | NotSetT = NotSet,
99
99
  timeout_seconds: int | NotSetT = NotSet,
100
- required_scopes: list[str] | None | NotSetT = NotSet,
101
- base_url: AnyHttpUrl | str | None | NotSetT = NotSet,
100
+ required_scopes: list[str] | NotSetT | None = NotSet,
101
+ base_url: AnyHttpUrl | str | NotSetT | None = NotSet,
102
102
  ):
103
103
  """
104
104
  Initialize the introspection token verifier.
@@ -184,13 +184,13 @@ class JWTVerifier(TokenVerifier):
184
184
  def __init__(
185
185
  self,
186
186
  *,
187
- public_key: str | None | NotSetT = NotSet,
188
- jwks_uri: str | None | NotSetT = NotSet,
189
- issuer: str | None | NotSetT = NotSet,
190
- audience: str | list[str] | None | NotSetT = NotSet,
191
- algorithm: str | None | NotSetT = NotSet,
192
- required_scopes: list[str] | None | NotSetT = NotSet,
193
- base_url: AnyHttpUrl | str | None | NotSetT = NotSet,
187
+ public_key: str | NotSetT | None = NotSet,
188
+ jwks_uri: str | NotSetT | None = NotSet,
189
+ issuer: str | NotSetT | None = NotSet,
190
+ audience: str | list[str] | NotSetT | None = NotSet,
191
+ algorithm: str | NotSetT | None = NotSet,
192
+ required_scopes: list[str] | NotSetT | None = NotSet,
193
+ base_url: AnyHttpUrl | str | NotSetT | None = NotSet,
194
194
  ):
195
195
  """
196
196
  Initialize the JWT token verifier.
@@ -283,7 +283,7 @@ class JWTVerifier(TokenVerifier):
283
283
  return await self._get_jwks_key(kid)
284
284
 
285
285
  except Exception as e:
286
- raise ValueError(f"Failed to extract key ID from token: {e}")
286
+ raise ValueError(f"Failed to extract key ID from token: {e}") from e
287
287
 
288
288
  async def _get_jwks_key(self, kid: str | None) -> str:
289
289
  """Fetch key from JWKS with simple caching."""
@@ -342,10 +342,10 @@ class JWTVerifier(TokenVerifier):
342
342
  raise ValueError("No keys found in JWKS")
343
343
 
344
344
  except httpx.HTTPError as e:
345
- raise ValueError(f"Failed to fetch JWKS: {e}")
345
+ raise ValueError(f"Failed to fetch JWKS: {e}") from e
346
346
  except Exception as e:
347
347
  self.logger.debug(f"JWKS fetch failed: {e}")
348
- raise ValueError(f"Failed to fetch JWKS: {e}")
348
+ raise ValueError(f"Failed to fetch JWKS: {e}") from e
349
349
 
350
350
  def _extract_scopes(self, claims: dict[str, Any]) -> list[str]:
351
351
  """
@@ -400,14 +400,13 @@ class JWTVerifier(TokenVerifier):
400
400
 
401
401
  # Validate issuer - note we use issuer instead of issuer_url here because
402
402
  # issuer is optional, allowing users to make this check optional
403
- if self.issuer:
404
- if claims.get("iss") != self.issuer:
405
- self.logger.debug(
406
- "Token validation failed: issuer mismatch for client %s",
407
- client_id,
408
- )
409
- self.logger.info("Bearer token rejected for client %s", client_id)
410
- return None
403
+ if self.issuer and claims.get("iss") != self.issuer:
404
+ self.logger.debug(
405
+ "Token validation failed: issuer mismatch for client %s",
406
+ client_id,
407
+ )
408
+ self.logger.info("Bearer token rejected for client %s", client_id)
409
+ return None
411
410
 
412
411
  # Validate audience if configured
413
412
  if self.audience:
@@ -83,7 +83,7 @@ class SupabaseProvider(RemoteAuthProvider):
83
83
  *,
84
84
  project_url: AnyHttpUrl | str | NotSetT = NotSet,
85
85
  base_url: AnyHttpUrl | str | NotSetT = NotSet,
86
- required_scopes: list[str] | None | NotSetT = NotSet,
86
+ required_scopes: list[str] | NotSetT | None = NotSet,
87
87
  token_verifier: TokenVerifier | None = None,
88
88
  ):
89
89
  """Initialize Supabase metadata provider.
@@ -169,7 +169,7 @@ class WorkOSProvider(OAuthProxy):
169
169
  base_url: AnyHttpUrl | str | NotSetT = NotSet,
170
170
  issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
171
171
  redirect_path: str | NotSetT = NotSet,
172
- required_scopes: list[str] | None | NotSetT = NotSet,
172
+ required_scopes: list[str] | NotSetT | None = NotSet,
173
173
  timeout_seconds: int | NotSetT = NotSet,
174
174
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
175
175
  client_storage: AsyncKeyValue | None = None,
@@ -338,7 +338,7 @@ class AuthKitProvider(RemoteAuthProvider):
338
338
  *,
339
339
  authkit_domain: AnyHttpUrl | str | NotSetT = NotSet,
340
340
  base_url: AnyHttpUrl | str | NotSetT = NotSet,
341
- required_scopes: list[str] | None | NotSetT = NotSet,
341
+ required_scopes: list[str] | NotSetT | None = NotSet,
342
342
  token_verifier: TokenVerifier | None = None,
343
343
  ):
344
344
  """Initialize AuthKit metadata provider.
fastmcp/server/context.py CHANGED
@@ -188,8 +188,8 @@ class Context:
188
188
  """
189
189
  try:
190
190
  return request_ctx.get()
191
- except LookupError:
192
- raise ValueError("Context is not available outside of a request")
191
+ except LookupError as e:
192
+ raise ValueError("Context is not available outside of a request") from e
193
193
 
194
194
  async def report_progress(
195
195
  self, progress: float, total: float | None = None, message: str | None = None
@@ -342,7 +342,7 @@ class Context:
342
342
  session_id = str(uuid4())
343
343
 
344
344
  # Save the session id to the session attributes
345
- setattr(session, "_fastmcp_id", session_id)
345
+ session._fastmcp_id = session_id
346
346
  return session_id
347
347
 
348
348
  @property
@@ -595,13 +595,11 @@ class Context:
595
595
  choice_literal = Literal[tuple(response_type)] # type: ignore
596
596
  response_type = ScalarElicitationType[choice_literal] # type: ignore
597
597
  # if the user provided a primitive scalar, wrap it in an object schema
598
- elif response_type in {bool, int, float, str}:
599
- response_type = ScalarElicitationType[response_type] # type: ignore
600
- # if the user provided a Literal type, wrap it in an object schema
601
- elif get_origin(response_type) is Literal:
602
- response_type = ScalarElicitationType[response_type] # type: ignore
603
- # if the user provided an Enum type, wrap it in an object schema
604
- elif isinstance(response_type, type) and issubclass(response_type, Enum):
598
+ elif (
599
+ response_type in {bool, int, float, str}
600
+ or get_origin(response_type) is Literal
601
+ or (isinstance(response_type, type) and issubclass(response_type, Enum))
602
+ ):
605
603
  response_type = ScalarElicitationType[response_type] # type: ignore
606
604
 
607
605
  response_type = cast(type[T], response_type)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  from typing import TYPE_CHECKING
4
5
 
5
6
  from mcp.server.auth.middleware.auth_context import (
@@ -16,11 +17,11 @@ if TYPE_CHECKING:
16
17
  from fastmcp.server.context import Context
17
18
 
18
19
  __all__ = [
20
+ "AccessToken",
21
+ "get_access_token",
19
22
  "get_context",
20
- "get_http_request",
21
23
  "get_http_headers",
22
- "get_access_token",
23
- "AccessToken",
24
+ "get_http_request",
24
25
  ]
25
26
 
26
27
 
@@ -43,10 +44,8 @@ def get_http_request() -> Request:
43
44
  from mcp.server.lowlevel.server import request_ctx
44
45
 
45
46
  request = None
46
- try:
47
+ with contextlib.suppress(LookupError):
47
48
  request = request_ctx.get().request
48
- except LookupError:
49
- pass
50
49
 
51
50
  if request is None:
52
51
  raise RuntimeError("No active HTTP request found.")
@@ -20,8 +20,8 @@ __all__ = [
20
20
  "AcceptedElicitation",
21
21
  "CancelledElicitation",
22
22
  "DeclinedElicitation",
23
- "get_elicitation_schema",
24
23
  "ScalarElicitationType",
24
+ "get_elicitation_schema",
25
25
  ]
26
26
 
27
27
  logger = get_logger(__name__)
fastmcp/server/http.py CHANGED
@@ -342,9 +342,8 @@ def create_streamable_http_app(
342
342
  # Create a lifespan manager to start and stop the session manager
343
343
  @asynccontextmanager
344
344
  async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
345
- async with server._lifespan_manager():
346
- async with session_manager.run():
347
- yield
345
+ async with server._lifespan_manager(), session_manager.run():
346
+ yield
348
347
 
349
348
  # Create and return the app with lifespan
350
349
  app = create_base_app(
@@ -5,7 +5,7 @@ from .middleware import (
5
5
  )
6
6
 
7
7
  __all__ = [
8
+ "CallNext",
8
9
  "Middleware",
9
10
  "MiddlewareContext",
10
- "CallNext",
11
11
  ]
@@ -46,7 +46,7 @@ class CachableReadResourceContents(BaseModel):
46
46
 
47
47
  @classmethod
48
48
  def get_sizes(cls, values: Sequence[Self]) -> int:
49
- return sum([item.get_size() for item in values])
49
+ return sum(item.get_size() for item in values)
50
50
 
51
51
  @classmethod
52
52
  def wrap(cls, values: Sequence[ReadResourceContents]) -> list[Self]:
@@ -64,7 +64,7 @@ class ErrorHandlingMiddleware(Middleware):
64
64
  error_key = f"{error_type}:{method}"
65
65
  self.error_counts[error_key] = self.error_counts.get(error_key, 0) + 1
66
66
 
67
- base_message = f"Error in {method}: {error_type}: {str(error)}"
67
+ base_message = f"Error in {method}: {error_type}: {error!s}"
68
68
 
69
69
  if self.include_traceback:
70
70
  self.logger.error(f"{base_message}\n{traceback.format_exc()}")
@@ -91,24 +91,24 @@ class ErrorHandlingMiddleware(Middleware):
91
91
 
92
92
  if error_type in (ValueError, TypeError):
93
93
  return McpError(
94
- ErrorData(code=-32602, message=f"Invalid params: {str(error)}")
94
+ ErrorData(code=-32602, message=f"Invalid params: {error!s}")
95
95
  )
96
96
  elif error_type in (FileNotFoundError, KeyError, NotFoundError):
97
97
  return McpError(
98
- ErrorData(code=-32001, message=f"Resource not found: {str(error)}")
98
+ ErrorData(code=-32001, message=f"Resource not found: {error!s}")
99
99
  )
100
100
  elif error_type is PermissionError:
101
101
  return McpError(
102
- ErrorData(code=-32000, message=f"Permission denied: {str(error)}")
102
+ ErrorData(code=-32000, message=f"Permission denied: {error!s}")
103
103
  )
104
104
  # asyncio.TimeoutError is a subclass of TimeoutError in Python 3.10, alias in 3.11+
105
105
  elif error_type in (TimeoutError, asyncio.TimeoutError):
106
106
  return McpError(
107
- ErrorData(code=-32000, message=f"Request timeout: {str(error)}")
107
+ ErrorData(code=-32000, message=f"Request timeout: {error!s}")
108
108
  )
109
109
  else:
110
110
  return McpError(
111
- ErrorData(code=-32603, message=f"Internal error: {str(error)}")
111
+ ErrorData(code=-32603, message=f"Internal error: {error!s}")
112
112
  )
113
113
 
114
114
  async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
@@ -120,7 +120,7 @@ class ErrorHandlingMiddleware(Middleware):
120
120
 
121
121
  # Transform and re-raise
122
122
  transformed_error = self._transform_error(error)
123
- raise transformed_error
123
+ raise transformed_error from error
124
124
 
125
125
  def get_error_stats(self) -> dict[str, int]:
126
126
  """Get error statistics for monitoring."""
@@ -200,7 +200,7 @@ class RetryMiddleware(Middleware):
200
200
  delay = self._calculate_delay(attempt)
201
201
  self.logger.warning(
202
202
  f"Request {context.method} failed (attempt {attempt + 1}/{self.max_retries + 1}): "
203
- f"{type(error).__name__}: {str(error)}. Retrying in {delay:.1f}s..."
203
+ f"{type(error).__name__}: {error!s}. Retrying in {delay:.1f}s..."
204
204
  )
205
205
 
206
206
  await anyio.sleep(delay)
@@ -27,9 +27,9 @@ if TYPE_CHECKING:
27
27
  from fastmcp.server.context import Context
28
28
 
29
29
  __all__ = [
30
+ "CallNext",
30
31
  "Middleware",
31
32
  "MiddlewareContext",
32
- "CallNext",
33
33
  ]
34
34
 
35
35
  logger = logging.getLogger(__name__)
fastmcp/server/openapi.py CHANGED
@@ -513,11 +513,11 @@ class OpenAPITool(Tool):
513
513
  if e.response.text:
514
514
  error_message += f" - {e.response.text}"
515
515
 
516
- raise ValueError(error_message)
516
+ raise ValueError(error_message) from e
517
517
 
518
518
  except httpx.RequestError as e:
519
519
  # Handle request errors (connection, timeout, etc.)
520
- raise ValueError(f"Request error: {str(e)}")
520
+ raise ValueError(f"Request error: {e!s}") from e
521
521
 
522
522
 
523
523
  class OpenAPIResource(Resource):
@@ -531,9 +531,11 @@ class OpenAPIResource(Resource):
531
531
  name: str,
532
532
  description: str,
533
533
  mime_type: str = "application/json",
534
- tags: set[str] = set(),
534
+ tags: set[str] | None = None,
535
535
  timeout: float | None = None,
536
536
  ):
537
+ if tags is None:
538
+ tags = set()
537
539
  super().__init__(
538
540
  uri=AnyUrl(uri), # Convert string to AnyUrl
539
541
  name=name,
@@ -632,11 +634,11 @@ class OpenAPIResource(Resource):
632
634
  if e.response.text:
633
635
  error_message += f" - {e.response.text}"
634
636
 
635
- raise ValueError(error_message)
637
+ raise ValueError(error_message) from e
636
638
 
637
639
  except httpx.RequestError as e:
638
640
  # Handle request errors (connection, timeout, etc.)
639
- raise ValueError(f"Request error: {str(e)}")
641
+ raise ValueError(f"Request error: {e!s}") from e
640
642
 
641
643
 
642
644
  class OpenAPIResourceTemplate(ResourceTemplate):
@@ -650,9 +652,11 @@ class OpenAPIResourceTemplate(ResourceTemplate):
650
652
  name: str,
651
653
  description: str,
652
654
  parameters: dict[str, Any],
653
- tags: set[str] = set(),
655
+ tags: set[str] | None = None,
654
656
  timeout: float | None = None,
655
657
  ):
658
+ if tags is None:
659
+ tags = set()
656
660
  super().__init__(
657
661
  uri_template=uri_template,
658
662
  name=name,
fastmcp/server/proxy.py CHANGED
@@ -198,7 +198,9 @@ class ProxyResourceManager(ResourceManager, ProxyManagerMixin):
198
198
  elif isinstance(result[0], BlobResourceContents):
199
199
  return result[0].blob
200
200
  else:
201
- raise ResourceError(f"Unsupported content type: {type(result[0])}")
201
+ raise ResourceError(
202
+ f"Unsupported content type: {type(result[0])}"
203
+ ) from None
202
204
 
203
205
 
204
206
  class ProxyPromptManager(PromptManager, ProxyManagerMixin):
@@ -558,7 +560,7 @@ class ProxyClient(Client[ClientTransportT]):
558
560
  kwargs["log_handler"] = ProxyClient.default_log_handler
559
561
  if "progress_handler" not in kwargs:
560
562
  kwargs["progress_handler"] = ProxyClient.default_progress_handler
561
- super().__init__(**kwargs | dict(transport=transport))
563
+ super().__init__(**kwargs | {"transport": transport})
562
564
 
563
565
  @classmethod
564
566
  async def default_sampling_handler(
@@ -572,7 +574,7 @@ class ProxyClient(Client[ClientTransportT]):
572
574
  """
573
575
  ctx = get_context()
574
576
  content = await ctx.sample(
575
- [msg for msg in messages],
577
+ list(messages),
576
578
  system_prompt=params.systemPrompt,
577
579
  temperature=params.temperature,
578
580
  max_tokens=params.maxTokens,
@@ -649,7 +651,6 @@ class StatefulProxyClient(ProxyClient[ClientTransportT]):
649
651
  The stateful proxy client will be forced disconnected when the session is exited.
650
652
  So we do nothing here.
651
653
  """
652
- pass
653
654
 
654
655
  async def clear(self):
655
656
  """