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
@@ -46,6 +46,7 @@ class WorkOSProviderSettings(BaseSettings):
46
46
  required_scopes: list[str] | None = None
47
47
  timeout_seconds: int | None = None
48
48
  allowed_client_redirect_uris: list[str] | None = None
49
+ jwt_signing_key: str | None = None
49
50
 
50
51
  @field_validator("required_scopes", mode="before")
51
52
  @classmethod
@@ -168,10 +169,12 @@ class WorkOSProvider(OAuthProxy):
168
169
  base_url: AnyHttpUrl | str | NotSetT = NotSet,
169
170
  issuer_url: AnyHttpUrl | str | NotSetT = NotSet,
170
171
  redirect_path: str | NotSetT = NotSet,
171
- required_scopes: list[str] | None | NotSetT = NotSet,
172
+ required_scopes: list[str] | NotSetT | None = NotSet,
172
173
  timeout_seconds: int | NotSetT = NotSet,
173
174
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
174
175
  client_storage: AsyncKeyValue | None = None,
176
+ jwt_signing_key: str | bytes | NotSetT = NotSet,
177
+ require_authorization_consent: bool = True,
175
178
  ):
176
179
  """Initialize WorkOS OAuth provider.
177
180
 
@@ -187,7 +190,16 @@ class WorkOSProvider(OAuthProxy):
187
190
  timeout_seconds: HTTP request timeout for WorkOS API calls
188
191
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
189
192
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
190
- client_storage: An AsyncKeyValue-compatible store for client registrations, registrations are stored in memory if not provided
193
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
194
+ If None, a DiskStore will be created in the data directory (derived from `platformdirs`). The
195
+ disk store will be encrypted using a key derived from the JWT Signing Key.
196
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
197
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
198
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
199
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
200
+ When True, users see a consent screen before being redirected to WorkOS.
201
+ When False, authorization proceeds directly without user confirmation.
202
+ SECURITY WARNING: Only disable for local development or testing environments.
191
203
  """
192
204
 
193
205
  settings = WorkOSProviderSettings.model_validate(
@@ -203,6 +215,7 @@ class WorkOSProvider(OAuthProxy):
203
215
  "required_scopes": required_scopes,
204
216
  "timeout_seconds": timeout_seconds,
205
217
  "allowed_client_redirect_uris": allowed_client_redirect_uris,
218
+ "jwt_signing_key": jwt_signing_key,
206
219
  }.items()
207
220
  if v is not NotSet
208
221
  }
@@ -256,6 +269,8 @@ class WorkOSProvider(OAuthProxy):
256
269
  or settings.base_url, # Default to base_url if not specified
257
270
  allowed_client_redirect_uris=allowed_client_redirect_uris_final,
258
271
  client_storage=client_storage,
272
+ jwt_signing_key=settings.jwt_signing_key,
273
+ require_authorization_consent=require_authorization_consent,
259
274
  )
260
275
 
261
276
  logger.debug(
@@ -323,7 +338,7 @@ class AuthKitProvider(RemoteAuthProvider):
323
338
  *,
324
339
  authkit_domain: AnyHttpUrl | str | NotSetT = NotSet,
325
340
  base_url: AnyHttpUrl | str | NotSetT = NotSet,
326
- required_scopes: list[str] | None | NotSetT = NotSet,
341
+ required_scopes: list[str] | NotSetT | None = NotSet,
327
342
  token_verifier: TokenVerifier | None = None,
328
343
  ):
329
344
  """Initialize AuthKit metadata provider.
fastmcp/server/context.py CHANGED
@@ -22,6 +22,7 @@ from mcp.types import (
22
22
  AudioContent,
23
23
  ClientCapabilities,
24
24
  CreateMessageResult,
25
+ GetPromptResult,
25
26
  ImageContent,
26
27
  IncludeContext,
27
28
  ModelHint,
@@ -32,6 +33,8 @@ from mcp.types import (
32
33
  TextContent,
33
34
  )
34
35
  from mcp.types import CreateMessageRequestParams as SamplingParams
36
+ from mcp.types import Prompt as MCPPrompt
37
+ from mcp.types import Resource as MCPResource
35
38
  from pydantic.networks import AnyUrl
36
39
  from starlette.requests import Request
37
40
  from typing_extensions import TypeVar
@@ -185,8 +188,8 @@ class Context:
185
188
  """
186
189
  try:
187
190
  return request_ctx.get()
188
- except LookupError:
189
- 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
190
193
 
191
194
  async def report_progress(
192
195
  self, progress: float, total: float | None = None, message: str | None = None
@@ -215,6 +218,36 @@ class Context:
215
218
  related_request_id=self.request_id,
216
219
  )
217
220
 
221
+ async def list_resources(self) -> list[MCPResource]:
222
+ """List all available resources from the server.
223
+
224
+ Returns:
225
+ List of Resource objects available on the server
226
+ """
227
+ return await self.fastmcp._list_resources_mcp()
228
+
229
+ async def list_prompts(self) -> list[MCPPrompt]:
230
+ """List all available prompts from the server.
231
+
232
+ Returns:
233
+ List of Prompt objects available on the server
234
+ """
235
+ return await self.fastmcp._list_prompts_mcp()
236
+
237
+ async def get_prompt(
238
+ self, name: str, arguments: dict[str, Any] | None = None
239
+ ) -> GetPromptResult:
240
+ """Get a prompt by name with optional arguments.
241
+
242
+ Args:
243
+ name: The name of the prompt to get
244
+ arguments: Optional arguments to pass to the prompt
245
+
246
+ Returns:
247
+ The prompt result
248
+ """
249
+ return await self.fastmcp._get_prompt_mcp(name, arguments)
250
+
218
251
  async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
219
252
  """Read a resource by URI.
220
253
 
@@ -224,8 +257,6 @@ class Context:
224
257
  Returns:
225
258
  The resource content as either text or bytes
226
259
  """
227
- if self.fastmcp is None:
228
- raise ValueError("Context is not available outside of a request")
229
260
  return await self.fastmcp._read_resource_mcp(uri)
230
261
 
231
262
  async def log(
@@ -311,7 +342,7 @@ class Context:
311
342
  session_id = str(uuid4())
312
343
 
313
344
  # Save the session id to the session attributes
314
- setattr(session, "_fastmcp_id", session_id)
345
+ session._fastmcp_id = session_id
315
346
  return session_id
316
347
 
317
348
  @property
@@ -564,13 +595,11 @@ class Context:
564
595
  choice_literal = Literal[tuple(response_type)] # type: ignore
565
596
  response_type = ScalarElicitationType[choice_literal] # type: ignore
566
597
  # if the user provided a primitive scalar, wrap it in an object schema
567
- elif response_type in {bool, int, float, str}:
568
- response_type = ScalarElicitationType[response_type] # type: ignore
569
- # if the user provided a Literal type, wrap it in an object schema
570
- elif get_origin(response_type) is Literal:
571
- response_type = ScalarElicitationType[response_type] # type: ignore
572
- # if the user provided an Enum type, wrap it in an object schema
573
- 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
+ ):
574
603
  response_type = ScalarElicitationType[response_type] # type: ignore
575
604
 
576
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
@@ -5,7 +5,6 @@ from contextlib import asynccontextmanager, contextmanager
5
5
  from contextvars import ContextVar
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- from mcp.server.auth.middleware.bearer_auth import RequireAuthMiddleware
9
8
  from mcp.server.auth.routes import build_resource_metadata_url
10
9
  from mcp.server.lowlevel.server import LifespanResultT
11
10
  from mcp.server.sse import SseServerTransport
@@ -21,6 +20,7 @@ from starlette.routing import BaseRoute, Mount, Route
21
20
  from starlette.types import Lifespan, Receive, Scope, Send
22
21
 
23
22
  from fastmcp.server.auth import AuthProvider
23
+ from fastmcp.server.auth.middleware import RequireAuthMiddleware
24
24
  from fastmcp.utilities.logging import get_logger
25
25
 
26
26
  if TYPE_CHECKING:
@@ -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__)
@@ -0,0 +1,116 @@
1
+ """A middleware for injecting tools into the MCP server context."""
2
+
3
+ from collections.abc import Sequence
4
+ from logging import Logger
5
+ from typing import Annotated, Any
6
+
7
+ import mcp.types
8
+ from mcp.server.lowlevel.helper_types import ReadResourceContents
9
+ from mcp.types import Prompt
10
+ from pydantic import AnyUrl
11
+ from typing_extensions import override
12
+
13
+ from fastmcp.server.context import Context
14
+ from fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext
15
+ from fastmcp.tools.tool import Tool, ToolResult
16
+ from fastmcp.utilities.logging import get_logger
17
+
18
+ logger: Logger = get_logger(name=__name__)
19
+
20
+
21
+ class ToolInjectionMiddleware(Middleware):
22
+ """A middleware for injecting tools into the context."""
23
+
24
+ def __init__(self, tools: Sequence[Tool]):
25
+ """Initialize the tool injection middleware."""
26
+ self._tools_to_inject: Sequence[Tool] = tools
27
+ self._tools_to_inject_by_name: dict[str, Tool] = {
28
+ tool.name: tool for tool in tools
29
+ }
30
+
31
+ @override
32
+ async def on_list_tools(
33
+ self,
34
+ context: MiddlewareContext[mcp.types.ListToolsRequest],
35
+ call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]],
36
+ ) -> Sequence[Tool]:
37
+ """Inject tools into the response."""
38
+ return [*self._tools_to_inject, *await call_next(context)]
39
+
40
+ @override
41
+ async def on_call_tool(
42
+ self,
43
+ context: MiddlewareContext[mcp.types.CallToolRequestParams],
44
+ call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult],
45
+ ) -> ToolResult:
46
+ """Intercept tool calls to injected tools."""
47
+ if context.message.name in self._tools_to_inject_by_name:
48
+ tool = self._tools_to_inject_by_name[context.message.name]
49
+ return await tool.run(arguments=context.message.arguments or {})
50
+
51
+ return await call_next(context)
52
+
53
+
54
+ async def list_prompts(context: Context) -> list[Prompt]:
55
+ """List prompts available on the server."""
56
+ return await context.list_prompts()
57
+
58
+
59
+ list_prompts_tool = Tool.from_function(
60
+ fn=list_prompts,
61
+ )
62
+
63
+
64
+ async def get_prompt(
65
+ context: Context,
66
+ name: Annotated[str, "The name of the prompt to render."],
67
+ arguments: Annotated[
68
+ dict[str, Any] | None, "The arguments to pass to the prompt."
69
+ ] = None,
70
+ ) -> mcp.types.GetPromptResult:
71
+ """Render a prompt available on the server."""
72
+ return await context.get_prompt(name=name, arguments=arguments)
73
+
74
+
75
+ get_prompt_tool = Tool.from_function(
76
+ fn=get_prompt,
77
+ )
78
+
79
+
80
+ class PromptToolMiddleware(ToolInjectionMiddleware):
81
+ """A middleware for injecting prompts as tools into the context."""
82
+
83
+ def __init__(self) -> None:
84
+ tools: list[Tool] = [list_prompts_tool, get_prompt_tool]
85
+ super().__init__(tools=tools)
86
+
87
+
88
+ async def list_resources(context: Context) -> list[mcp.types.Resource]:
89
+ """List resources available on the server."""
90
+ return await context.list_resources()
91
+
92
+
93
+ list_resources_tool = Tool.from_function(
94
+ fn=list_resources,
95
+ )
96
+
97
+
98
+ async def read_resource(
99
+ context: Context,
100
+ uri: Annotated[AnyUrl | str, "The URI of the resource to read."],
101
+ ) -> list[ReadResourceContents]:
102
+ """Read a resource available on the server."""
103
+ return await context.read_resource(uri=uri)
104
+
105
+
106
+ read_resource_tool = Tool.from_function(
107
+ fn=read_resource,
108
+ )
109
+
110
+
111
+ class ResourceToolMiddleware(ToolInjectionMiddleware):
112
+ """A middleware for injecting resources as tools into the context."""
113
+
114
+ def __init__(self) -> None:
115
+ tools: list[Tool] = [list_resources_tool, read_resource_tool]
116
+ super().__init__(tools=tools)
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
  """