fastmcp 2.12.5__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.
Files changed (108) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +11 -11
  3. fastmcp/cli/install/claude_code.py +6 -6
  4. fastmcp/cli/install/claude_desktop.py +3 -3
  5. fastmcp/cli/install/cursor.py +18 -12
  6. fastmcp/cli/install/gemini_cli.py +3 -3
  7. fastmcp/cli/install/mcp_json.py +3 -3
  8. fastmcp/cli/run.py +13 -8
  9. fastmcp/client/__init__.py +9 -9
  10. fastmcp/client/auth/oauth.py +115 -217
  11. fastmcp/client/client.py +105 -39
  12. fastmcp/client/logging.py +18 -14
  13. fastmcp/client/oauth_callback.py +85 -171
  14. fastmcp/client/sampling.py +1 -1
  15. fastmcp/client/transports.py +80 -25
  16. fastmcp/contrib/component_manager/__init__.py +1 -1
  17. fastmcp/contrib/component_manager/component_manager.py +2 -2
  18. fastmcp/contrib/component_manager/component_service.py +6 -6
  19. fastmcp/contrib/mcp_mixin/README.md +32 -1
  20. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  21. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  22. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  23. fastmcp/experimental/server/openapi/__init__.py +5 -8
  24. fastmcp/experimental/server/openapi/components.py +11 -7
  25. fastmcp/experimental/server/openapi/routing.py +2 -2
  26. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  27. fastmcp/experimental/utilities/openapi/director.py +14 -15
  28. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  29. fastmcp/experimental/utilities/openapi/models.py +3 -3
  30. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  31. fastmcp/experimental/utilities/openapi/schemas.py +2 -2
  32. fastmcp/mcp_config.py +3 -4
  33. fastmcp/prompts/__init__.py +1 -1
  34. fastmcp/prompts/prompt.py +22 -19
  35. fastmcp/prompts/prompt_manager.py +16 -101
  36. fastmcp/resources/__init__.py +5 -5
  37. fastmcp/resources/resource.py +14 -9
  38. fastmcp/resources/resource_manager.py +9 -168
  39. fastmcp/resources/template.py +107 -17
  40. fastmcp/resources/types.py +30 -24
  41. fastmcp/server/__init__.py +1 -1
  42. fastmcp/server/auth/__init__.py +9 -5
  43. fastmcp/server/auth/auth.py +70 -43
  44. fastmcp/server/auth/handlers/authorize.py +326 -0
  45. fastmcp/server/auth/jwt_issuer.py +236 -0
  46. fastmcp/server/auth/middleware.py +96 -0
  47. fastmcp/server/auth/oauth_proxy.py +1510 -289
  48. fastmcp/server/auth/oidc_proxy.py +84 -20
  49. fastmcp/server/auth/providers/auth0.py +40 -21
  50. fastmcp/server/auth/providers/aws.py +29 -3
  51. fastmcp/server/auth/providers/azure.py +312 -131
  52. fastmcp/server/auth/providers/bearer.py +1 -1
  53. fastmcp/server/auth/providers/debug.py +114 -0
  54. fastmcp/server/auth/providers/descope.py +86 -29
  55. fastmcp/server/auth/providers/discord.py +308 -0
  56. fastmcp/server/auth/providers/github.py +29 -8
  57. fastmcp/server/auth/providers/google.py +48 -9
  58. fastmcp/server/auth/providers/in_memory.py +27 -3
  59. fastmcp/server/auth/providers/introspection.py +281 -0
  60. fastmcp/server/auth/providers/jwt.py +48 -31
  61. fastmcp/server/auth/providers/oci.py +233 -0
  62. fastmcp/server/auth/providers/scalekit.py +238 -0
  63. fastmcp/server/auth/providers/supabase.py +188 -0
  64. fastmcp/server/auth/providers/workos.py +35 -17
  65. fastmcp/server/context.py +177 -51
  66. fastmcp/server/dependencies.py +39 -12
  67. fastmcp/server/elicitation.py +1 -1
  68. fastmcp/server/http.py +56 -17
  69. fastmcp/server/low_level.py +121 -2
  70. fastmcp/server/middleware/__init__.py +1 -1
  71. fastmcp/server/middleware/caching.py +476 -0
  72. fastmcp/server/middleware/error_handling.py +14 -10
  73. fastmcp/server/middleware/logging.py +50 -39
  74. fastmcp/server/middleware/middleware.py +29 -16
  75. fastmcp/server/middleware/rate_limiting.py +3 -3
  76. fastmcp/server/middleware/tool_injection.py +116 -0
  77. fastmcp/server/openapi.py +10 -6
  78. fastmcp/server/proxy.py +22 -11
  79. fastmcp/server/server.py +725 -242
  80. fastmcp/settings.py +24 -10
  81. fastmcp/tools/__init__.py +1 -1
  82. fastmcp/tools/tool.py +70 -23
  83. fastmcp/tools/tool_manager.py +30 -112
  84. fastmcp/tools/tool_transform.py +12 -10
  85. fastmcp/utilities/cli.py +67 -28
  86. fastmcp/utilities/components.py +7 -2
  87. fastmcp/utilities/inspect.py +79 -23
  88. fastmcp/utilities/json_schema.py +4 -4
  89. fastmcp/utilities/json_schema_type.py +4 -4
  90. fastmcp/utilities/logging.py +118 -8
  91. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  92. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  93. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  94. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +4 -4
  95. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  96. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  97. fastmcp/utilities/openapi.py +11 -11
  98. fastmcp/utilities/tests.py +85 -4
  99. fastmcp/utilities/types.py +78 -16
  100. fastmcp/utilities/ui.py +626 -0
  101. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
  102. fastmcp-2.13.2.dist-info/RECORD +144 -0
  103. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  104. fastmcp/cli/claude.py +0 -135
  105. fastmcp/utilities/storage.py +0 -204
  106. fastmcp-2.12.5.dist-info/RECORD +0 -134
  107. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  108. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
@@ -6,9 +6,12 @@ import traceback
6
6
  from collections.abc import Callable
7
7
  from typing import Any
8
8
 
9
+ import anyio
9
10
  from mcp import McpError
10
11
  from mcp.types import ErrorData
11
12
 
13
+ from fastmcp.exceptions import NotFoundError
14
+
12
15
  from .middleware import CallNext, Middleware, MiddlewareContext
13
16
 
14
17
 
@@ -61,7 +64,7 @@ class ErrorHandlingMiddleware(Middleware):
61
64
  error_key = f"{error_type}:{method}"
62
65
  self.error_counts[error_key] = self.error_counts.get(error_key, 0) + 1
63
66
 
64
- base_message = f"Error in {method}: {error_type}: {str(error)}"
67
+ base_message = f"Error in {method}: {error_type}: {error!s}"
65
68
 
66
69
  if self.include_traceback:
67
70
  self.logger.error(f"{base_message}\n{traceback.format_exc()}")
@@ -88,23 +91,24 @@ class ErrorHandlingMiddleware(Middleware):
88
91
 
89
92
  if error_type in (ValueError, TypeError):
90
93
  return McpError(
91
- ErrorData(code=-32602, message=f"Invalid params: {str(error)}")
94
+ ErrorData(code=-32602, message=f"Invalid params: {error!s}")
92
95
  )
93
- elif error_type in (FileNotFoundError, KeyError):
96
+ elif error_type in (FileNotFoundError, KeyError, NotFoundError):
94
97
  return McpError(
95
- ErrorData(code=-32001, message=f"Resource not found: {str(error)}")
98
+ ErrorData(code=-32001, message=f"Resource not found: {error!s}")
96
99
  )
97
100
  elif error_type is PermissionError:
98
101
  return McpError(
99
- ErrorData(code=-32000, message=f"Permission denied: {str(error)}")
102
+ ErrorData(code=-32000, message=f"Permission denied: {error!s}")
100
103
  )
104
+ # asyncio.TimeoutError is a subclass of TimeoutError in Python 3.10, alias in 3.11+
101
105
  elif error_type in (TimeoutError, asyncio.TimeoutError):
102
106
  return McpError(
103
- ErrorData(code=-32000, message=f"Request timeout: {str(error)}")
107
+ ErrorData(code=-32000, message=f"Request timeout: {error!s}")
104
108
  )
105
109
  else:
106
110
  return McpError(
107
- ErrorData(code=-32603, message=f"Internal error: {str(error)}")
111
+ ErrorData(code=-32603, message=f"Internal error: {error!s}")
108
112
  )
109
113
 
110
114
  async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
@@ -116,7 +120,7 @@ class ErrorHandlingMiddleware(Middleware):
116
120
 
117
121
  # Transform and re-raise
118
122
  transformed_error = self._transform_error(error)
119
- raise transformed_error
123
+ raise transformed_error from error
120
124
 
121
125
  def get_error_stats(self) -> dict[str, int]:
122
126
  """Get error statistics for monitoring."""
@@ -196,10 +200,10 @@ class RetryMiddleware(Middleware):
196
200
  delay = self._calculate_delay(attempt)
197
201
  self.logger.warning(
198
202
  f"Request {context.method} failed (attempt {attempt + 1}/{self.max_retries + 1}): "
199
- f"{type(error).__name__}: {str(error)}. Retrying in {delay:.1f}s..."
203
+ f"{type(error).__name__}: {error!s}. Retrying in {delay:.1f}s..."
200
204
  )
201
205
 
202
- await asyncio.sleep(delay)
206
+ await anyio.sleep(delay)
203
207
 
204
208
  # Re-raise the last error if all retries failed
205
209
  if last_error:
@@ -2,6 +2,7 @@
2
2
 
3
3
  import json
4
4
  import logging
5
+ import time
5
6
  from collections.abc import Callable
6
7
  from logging import Logger
7
8
  from typing import Any
@@ -45,21 +46,21 @@ class BaseLoggingMiddleware(Middleware):
45
46
 
46
47
  return payload
47
48
 
48
- def _format_message(self, message: dict[str, str | int]) -> str:
49
+ def _format_message(self, message: dict[str, str | int | float]) -> str:
49
50
  """Format a message for logging."""
50
51
  if self.structured_logging:
51
52
  return json.dumps(message)
52
53
  else:
53
54
  return " ".join([f"{k}={v}" for k, v in message.items()])
54
55
 
55
- def _get_timestamp_from_context(self, context: MiddlewareContext[Any]) -> str:
56
- """Get a timestamp from the context."""
57
- return context.timestamp.isoformat()
58
-
59
56
  def _create_before_message(
60
- self, context: MiddlewareContext[Any], event: str
61
- ) -> dict[str, str | int]:
62
- message = self._create_base_message(context, event)
57
+ self, context: MiddlewareContext[Any]
58
+ ) -> dict[str, str | int | float]:
59
+ message = {
60
+ "event": context.type + "_start",
61
+ "method": context.method or "unknown",
62
+ "source": context.source,
63
+ }
63
64
 
64
65
  if (
65
66
  self.include_payloads
@@ -85,57 +86,61 @@ class BaseLoggingMiddleware(Middleware):
85
86
 
86
87
  return message
87
88
 
88
- def _create_after_message(
89
- self, context: MiddlewareContext[Any], event: str
90
- ) -> dict[str, str | int]:
91
- return self._create_base_message(context, event)
92
-
93
- def _create_base_message(
89
+ def _create_error_message(
94
90
  self,
95
91
  context: MiddlewareContext[Any],
96
- event: str,
97
- ) -> dict[str, str | int]:
98
- """Format a message for logging."""
92
+ start_time: float,
93
+ error: Exception,
94
+ ) -> dict[str, str | int | float]:
95
+ duration_ms: float = _get_duration_ms(start_time)
96
+ message = {
97
+ "event": context.type + "_error",
98
+ "method": context.method or "unknown",
99
+ "source": context.source,
100
+ "duration_ms": duration_ms,
101
+ "error": str(object=error),
102
+ }
103
+ return message
99
104
 
100
- parts: dict[str, str | int] = {
101
- "event": event,
102
- "timestamp": self._get_timestamp_from_context(context),
105
+ def _create_after_message(
106
+ self,
107
+ context: MiddlewareContext[Any],
108
+ start_time: float,
109
+ ) -> dict[str, str | int | float]:
110
+ duration_ms: float = _get_duration_ms(start_time)
111
+ message = {
112
+ "event": context.type + "_success",
103
113
  "method": context.method or "unknown",
104
- "type": context.type,
105
114
  "source": context.source,
115
+ "duration_ms": duration_ms,
106
116
  }
117
+ return message
107
118
 
108
- return parts
119
+ def _log_message(
120
+ self, message: dict[str, str | int | float], log_level: int | None = None
121
+ ):
122
+ self.logger.log(log_level or self.log_level, self._format_message(message))
109
123
 
110
124
  async def on_message(
111
125
  self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
112
126
  ) -> Any:
113
- """Log all messages."""
127
+ """Log messages for configured methods."""
114
128
 
115
129
  if self.methods and context.method not in self.methods:
116
130
  return await call_next(context)
117
131
 
118
- request_start_log_message = self._create_before_message(
119
- context, "request_start"
120
- )
121
-
122
- formatted_message = self._format_message(request_start_log_message)
123
- self.logger.log(self.log_level, f"Processing message: {formatted_message}")
132
+ self._log_message(self._create_before_message(context))
124
133
 
134
+ start_time = time.perf_counter()
125
135
  try:
126
136
  result = await call_next(context)
127
137
 
128
- request_success_log_message = self._create_after_message(
129
- context, "request_success"
130
- )
131
-
132
- formatted_message = self._format_message(request_success_log_message)
133
- self.logger.log(self.log_level, f"Completed message: {formatted_message}")
138
+ self._log_message(self._create_after_message(context, start_time))
134
139
 
135
140
  return result
136
141
  except Exception as e:
137
- self.logger.log(
138
- logging.ERROR, f"Failed message: {context.method or 'unknown'} - {e}"
142
+ self._log_message(
143
+ self._create_error_message(context, start_time, e), logging.ERROR
139
144
  )
140
145
  raise
141
146
 
@@ -184,7 +189,7 @@ class LoggingMiddleware(BaseLoggingMiddleware):
184
189
  payload_serializer: Callable that converts objects to a JSON string for the
185
190
  payload. If not provided, uses FastMCP's default tool serializer.
186
191
  """
187
- self.logger: Logger = logger or logging.getLogger("fastmcp.requests")
192
+ self.logger: Logger = logger or logging.getLogger("fastmcp.middleware.logging")
188
193
  self.log_level = log_level
189
194
  self.include_payloads: bool = include_payloads
190
195
  self.include_payload_length: bool = include_payload_length
@@ -234,7 +239,9 @@ class StructuredLoggingMiddleware(BaseLoggingMiddleware):
234
239
  payload_serializer: Callable that converts objects to a JSON string for the
235
240
  payload. If not provided, uses FastMCP's default tool serializer.
236
241
  """
237
- self.logger: Logger = logger or logging.getLogger("fastmcp.structured")
242
+ self.logger: Logger = logger or logging.getLogger(
243
+ "fastmcp.middleware.structured_logging"
244
+ )
238
245
  self.log_level: int = log_level
239
246
  self.include_payloads: bool = include_payloads
240
247
  self.include_payload_length: bool = include_payload_length
@@ -243,3 +250,7 @@ class StructuredLoggingMiddleware(BaseLoggingMiddleware):
243
250
  self.payload_serializer: Callable[[Any], str] | None = payload_serializer
244
251
  self.max_payload_length: int | None = None
245
252
  self.structured_logging: bool = True
253
+
254
+
255
+ def _get_duration_ms(start_time: float, /) -> float:
256
+ return round(number=(time.perf_counter() - start_time) * 1000, ndigits=2)
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from collections.abc import Awaitable
4
+ from collections.abc import Awaitable, Sequence
5
5
  from dataclasses import dataclass, field, replace
6
6
  from datetime import datetime, timezone
7
7
  from functools import partial
@@ -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__)
@@ -99,6 +99,8 @@ class Middleware:
99
99
  handler = call_next
100
100
 
101
101
  match context.method:
102
+ case "initialize":
103
+ handler = partial(self.on_initialize, call_next=handler)
102
104
  case "tools/call":
103
105
  handler = partial(self.on_call_tool, call_next=handler)
104
106
  case "resources/read":
@@ -133,18 +135,25 @@ class Middleware:
133
135
 
134
136
  async def on_request(
135
137
  self,
136
- context: MiddlewareContext[mt.Request],
137
- call_next: CallNext[mt.Request, Any],
138
+ context: MiddlewareContext[mt.Request[Any, Any]],
139
+ call_next: CallNext[mt.Request[Any, Any], Any],
138
140
  ) -> Any:
139
141
  return await call_next(context)
140
142
 
141
143
  async def on_notification(
142
144
  self,
143
- context: MiddlewareContext[mt.Notification],
144
- call_next: CallNext[mt.Notification, Any],
145
+ context: MiddlewareContext[mt.Notification[Any, Any]],
146
+ call_next: CallNext[mt.Notification[Any, Any], Any],
145
147
  ) -> Any:
146
148
  return await call_next(context)
147
149
 
150
+ async def on_initialize(
151
+ self,
152
+ context: MiddlewareContext[mt.InitializeRequest],
153
+ call_next: CallNext[mt.InitializeRequest, None],
154
+ ) -> None:
155
+ return await call_next(context)
156
+
148
157
  async def on_call_tool(
149
158
  self,
150
159
  context: MiddlewareContext[mt.CallToolRequestParams],
@@ -155,8 +164,10 @@ class Middleware:
155
164
  async def on_read_resource(
156
165
  self,
157
166
  context: MiddlewareContext[mt.ReadResourceRequestParams],
158
- call_next: CallNext[mt.ReadResourceRequestParams, list[ReadResourceContents]],
159
- ) -> list[ReadResourceContents]:
167
+ call_next: CallNext[
168
+ mt.ReadResourceRequestParams, Sequence[ReadResourceContents]
169
+ ],
170
+ ) -> Sequence[ReadResourceContents]:
160
171
  return await call_next(context)
161
172
 
162
173
  async def on_get_prompt(
@@ -169,27 +180,29 @@ class Middleware:
169
180
  async def on_list_tools(
170
181
  self,
171
182
  context: MiddlewareContext[mt.ListToolsRequest],
172
- call_next: CallNext[mt.ListToolsRequest, list[Tool]],
173
- ) -> list[Tool]:
183
+ call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],
184
+ ) -> Sequence[Tool]:
174
185
  return await call_next(context)
175
186
 
176
187
  async def on_list_resources(
177
188
  self,
178
189
  context: MiddlewareContext[mt.ListResourcesRequest],
179
- call_next: CallNext[mt.ListResourcesRequest, list[Resource]],
180
- ) -> list[Resource]:
190
+ call_next: CallNext[mt.ListResourcesRequest, Sequence[Resource]],
191
+ ) -> Sequence[Resource]:
181
192
  return await call_next(context)
182
193
 
183
194
  async def on_list_resource_templates(
184
195
  self,
185
196
  context: MiddlewareContext[mt.ListResourceTemplatesRequest],
186
- call_next: CallNext[mt.ListResourceTemplatesRequest, list[ResourceTemplate]],
187
- ) -> list[ResourceTemplate]:
197
+ call_next: CallNext[
198
+ mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]
199
+ ],
200
+ ) -> Sequence[ResourceTemplate]:
188
201
  return await call_next(context)
189
202
 
190
203
  async def on_list_prompts(
191
204
  self,
192
205
  context: MiddlewareContext[mt.ListPromptsRequest],
193
- call_next: CallNext[mt.ListPromptsRequest, list[Prompt]],
194
- ) -> list[Prompt]:
206
+ call_next: CallNext[mt.ListPromptsRequest, Sequence[Prompt]],
207
+ ) -> Sequence[Prompt]:
195
208
  return await call_next(context)
@@ -1,11 +1,11 @@
1
1
  """Rate limiting middleware for protecting FastMCP servers from abuse."""
2
2
 
3
- import asyncio
4
3
  import time
5
4
  from collections import defaultdict, deque
6
5
  from collections.abc import Callable
7
6
  from typing import Any
8
7
 
8
+ import anyio
9
9
  from mcp import McpError
10
10
  from mcp.types import ErrorData
11
11
 
@@ -33,7 +33,7 @@ class TokenBucketRateLimiter:
33
33
  self.refill_rate = refill_rate
34
34
  self.tokens = capacity
35
35
  self.last_refill = time.time()
36
- self._lock = asyncio.Lock()
36
+ self._lock = anyio.Lock()
37
37
 
38
38
  async def consume(self, tokens: int = 1) -> bool:
39
39
  """Try to consume tokens from the bucket.
@@ -71,7 +71,7 @@ class SlidingWindowRateLimiter:
71
71
  self.max_requests = max_requests
72
72
  self.window_seconds = window_seconds
73
73
  self.requests = deque()
74
- self._lock = asyncio.Lock()
74
+ self._lock = anyio.Lock()
75
75
 
76
76
  async def is_allowed(self) -> bool:
77
77
  """Check if a request is allowed."""
@@ -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,