fastmcp 2.12.1__py3-none-any.whl → 2.13.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +56 -36
  3. fastmcp/cli/install/__init__.py +2 -0
  4. fastmcp/cli/install/claude_code.py +7 -16
  5. fastmcp/cli/install/claude_desktop.py +4 -12
  6. fastmcp/cli/install/cursor.py +20 -30
  7. fastmcp/cli/install/gemini_cli.py +241 -0
  8. fastmcp/cli/install/mcp_json.py +4 -12
  9. fastmcp/cli/run.py +15 -94
  10. fastmcp/client/__init__.py +9 -9
  11. fastmcp/client/auth/oauth.py +117 -206
  12. fastmcp/client/client.py +123 -47
  13. fastmcp/client/elicitation.py +6 -1
  14. fastmcp/client/logging.py +18 -14
  15. fastmcp/client/oauth_callback.py +85 -171
  16. fastmcp/client/sampling.py +1 -1
  17. fastmcp/client/transports.py +81 -26
  18. fastmcp/contrib/component_manager/__init__.py +1 -1
  19. fastmcp/contrib/component_manager/component_manager.py +2 -2
  20. fastmcp/contrib/component_manager/component_service.py +7 -7
  21. fastmcp/contrib/mcp_mixin/README.md +35 -4
  22. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  23. fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
  24. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  25. fastmcp/experimental/server/openapi/__init__.py +5 -8
  26. fastmcp/experimental/server/openapi/components.py +11 -7
  27. fastmcp/experimental/server/openapi/routing.py +2 -2
  28. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  29. fastmcp/experimental/utilities/openapi/director.py +16 -10
  30. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  31. fastmcp/experimental/utilities/openapi/models.py +3 -3
  32. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  33. fastmcp/experimental/utilities/openapi/schemas.py +33 -7
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +32 -27
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +28 -20
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +119 -27
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -5
  45. fastmcp/server/auth/auth.py +80 -47
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1556 -265
  50. fastmcp/server/auth/oidc_proxy.py +412 -0
  51. fastmcp/server/auth/providers/auth0.py +193 -0
  52. fastmcp/server/auth/providers/aws.py +263 -0
  53. fastmcp/server/auth/providers/azure.py +314 -129
  54. fastmcp/server/auth/providers/bearer.py +1 -1
  55. fastmcp/server/auth/providers/debug.py +114 -0
  56. fastmcp/server/auth/providers/descope.py +229 -0
  57. fastmcp/server/auth/providers/discord.py +308 -0
  58. fastmcp/server/auth/providers/github.py +31 -6
  59. fastmcp/server/auth/providers/google.py +50 -7
  60. fastmcp/server/auth/providers/in_memory.py +27 -3
  61. fastmcp/server/auth/providers/introspection.py +281 -0
  62. fastmcp/server/auth/providers/jwt.py +48 -31
  63. fastmcp/server/auth/providers/oci.py +233 -0
  64. fastmcp/server/auth/providers/scalekit.py +238 -0
  65. fastmcp/server/auth/providers/supabase.py +188 -0
  66. fastmcp/server/auth/providers/workos.py +37 -15
  67. fastmcp/server/context.py +194 -67
  68. fastmcp/server/dependencies.py +56 -16
  69. fastmcp/server/elicitation.py +1 -1
  70. fastmcp/server/http.py +57 -18
  71. fastmcp/server/low_level.py +121 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +158 -116
  76. fastmcp/server/middleware/middleware.py +30 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi.py +15 -7
  80. fastmcp/server/proxy.py +22 -11
  81. fastmcp/server/server.py +744 -254
  82. fastmcp/settings.py +65 -15
  83. fastmcp/tools/__init__.py +1 -1
  84. fastmcp/tools/tool.py +173 -108
  85. fastmcp/tools/tool_manager.py +30 -112
  86. fastmcp/tools/tool_transform.py +13 -11
  87. fastmcp/utilities/cli.py +67 -28
  88. fastmcp/utilities/components.py +7 -2
  89. fastmcp/utilities/inspect.py +79 -23
  90. fastmcp/utilities/json_schema.py +21 -4
  91. fastmcp/utilities/json_schema_type.py +4 -4
  92. fastmcp/utilities/logging.py +182 -10
  93. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  94. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  95. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
  96. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
  97. fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
  98. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  99. fastmcp/utilities/openapi.py +11 -11
  100. fastmcp/utilities/tests.py +93 -10
  101. fastmcp/utilities/types.py +87 -21
  102. fastmcp/utilities/ui.py +626 -0
  103. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
  104. fastmcp-2.13.2.dist-info/RECORD +144 -0
  105. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  106. fastmcp/cli/claude.py +0 -144
  107. fastmcp-2.12.1.dist-info/RECORD +0 -128
  108. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  109. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
@@ -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
@@ -16,7 +17,135 @@ def default_serializer(data: Any) -> str:
16
17
  return pydantic_core.to_json(data, fallback=str).decode()
17
18
 
18
19
 
19
- class LoggingMiddleware(Middleware):
20
+ class BaseLoggingMiddleware(Middleware):
21
+ """Base class for logging middleware."""
22
+
23
+ logger: Logger
24
+ log_level: int
25
+ include_payloads: bool
26
+ include_payload_length: bool
27
+ estimate_payload_tokens: bool
28
+ max_payload_length: int | None
29
+ methods: list[str] | None
30
+ structured_logging: bool
31
+ payload_serializer: Callable[[Any], str] | None
32
+
33
+ def _serialize_payload(self, context: MiddlewareContext[Any]) -> str:
34
+ payload: str
35
+
36
+ if not self.payload_serializer:
37
+ payload = default_serializer(context.message)
38
+ else:
39
+ try:
40
+ payload = self.payload_serializer(context.message)
41
+ except Exception as e:
42
+ self.logger.warning(
43
+ f"Failed to serialize payload due to {e}: {context.type} {context.method} {context.source}."
44
+ )
45
+ payload = default_serializer(context.message)
46
+
47
+ return payload
48
+
49
+ def _format_message(self, message: dict[str, str | int | float]) -> str:
50
+ """Format a message for logging."""
51
+ if self.structured_logging:
52
+ return json.dumps(message)
53
+ else:
54
+ return " ".join([f"{k}={v}" for k, v in message.items()])
55
+
56
+ def _create_before_message(
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
+ }
64
+
65
+ if (
66
+ self.include_payloads
67
+ or self.include_payload_length
68
+ or self.estimate_payload_tokens
69
+ ):
70
+ payload = self._serialize_payload(context)
71
+
72
+ if self.include_payload_length or self.estimate_payload_tokens:
73
+ payload_length = len(payload)
74
+ payload_tokens = payload_length // 4
75
+ if self.estimate_payload_tokens:
76
+ message["payload_tokens"] = payload_tokens
77
+ if self.include_payload_length:
78
+ message["payload_length"] = payload_length
79
+
80
+ if self.max_payload_length and len(payload) > self.max_payload_length:
81
+ payload = payload[: self.max_payload_length] + "..."
82
+
83
+ if self.include_payloads:
84
+ message["payload"] = payload
85
+ message["payload_type"] = type(context.message).__name__
86
+
87
+ return message
88
+
89
+ def _create_error_message(
90
+ self,
91
+ context: MiddlewareContext[Any],
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
104
+
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",
113
+ "method": context.method or "unknown",
114
+ "source": context.source,
115
+ "duration_ms": duration_ms,
116
+ }
117
+ return message
118
+
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))
123
+
124
+ async def on_message(
125
+ self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
126
+ ) -> Any:
127
+ """Log messages for configured methods."""
128
+
129
+ if self.methods and context.method not in self.methods:
130
+ return await call_next(context)
131
+
132
+ self._log_message(self._create_before_message(context))
133
+
134
+ start_time = time.perf_counter()
135
+ try:
136
+ result = await call_next(context)
137
+
138
+ self._log_message(self._create_after_message(context, start_time))
139
+
140
+ return result
141
+ except Exception as e:
142
+ self._log_message(
143
+ self._create_error_message(context, start_time, e), logging.ERROR
144
+ )
145
+ raise
146
+
147
+
148
+ class LoggingMiddleware(BaseLoggingMiddleware):
20
149
  """Middleware that provides comprehensive request and response logging.
21
150
 
22
151
  Logs all MCP messages with configurable detail levels. Useful for debugging,
@@ -37,9 +166,12 @@ class LoggingMiddleware(Middleware):
37
166
 
38
167
  def __init__(
39
168
  self,
169
+ *,
40
170
  logger: logging.Logger | None = None,
41
171
  log_level: int = logging.INFO,
42
172
  include_payloads: bool = False,
173
+ include_payload_length: bool = False,
174
+ estimate_payload_tokens: bool = False,
43
175
  max_payload_length: int = 1000,
44
176
  methods: list[str] | None = None,
45
177
  payload_serializer: Callable[[Any], str] | None = None,
@@ -50,68 +182,25 @@ class LoggingMiddleware(Middleware):
50
182
  logger: Logger instance to use. If None, creates a logger named 'fastmcp.requests'
51
183
  log_level: Log level for messages (default: INFO)
52
184
  include_payloads: Whether to include message payloads in logs
185
+ include_payload_length: Whether to include response size in logs
186
+ estimate_payload_tokens: Whether to estimate response tokens
53
187
  max_payload_length: Maximum length of payload to log (prevents huge logs)
54
188
  methods: List of methods to log. If None, logs all methods.
189
+ payload_serializer: Callable that converts objects to a JSON string for the
190
+ payload. If not provided, uses FastMCP's default tool serializer.
55
191
  """
56
- self.logger: Logger = logger or logging.getLogger("fastmcp.requests")
57
- self.log_level: int = log_level
192
+ self.logger: Logger = logger or logging.getLogger("fastmcp.middleware.logging")
193
+ self.log_level = log_level
58
194
  self.include_payloads: bool = include_payloads
195
+ self.include_payload_length: bool = include_payload_length
196
+ self.estimate_payload_tokens: bool = estimate_payload_tokens
59
197
  self.max_payload_length: int = max_payload_length
60
198
  self.methods: list[str] | None = methods
61
199
  self.payload_serializer: Callable[[Any], str] | None = payload_serializer
200
+ self.structured_logging: bool = False
62
201
 
63
- def _format_message(self, context: MiddlewareContext[Any]) -> str:
64
- """Format a message for logging."""
65
- parts = [
66
- f"source={context.source}",
67
- f"type={context.type}",
68
- f"method={context.method or 'unknown'}",
69
- ]
70
-
71
- if self.include_payloads:
72
- payload: str
73
202
 
74
- if not self.payload_serializer:
75
- payload = default_serializer(context.message)
76
- else:
77
- try:
78
- payload = self.payload_serializer(context.message)
79
- except Exception as e:
80
- self.logger.warning(
81
- f"Failed {e} to serialize payload: {context.type} {context.method} {context.source}."
82
- )
83
- payload = default_serializer(context.message)
84
-
85
- if len(payload) > self.max_payload_length:
86
- payload = payload[: self.max_payload_length] + "..."
87
-
88
- parts.append(f"payload={payload}")
89
- return " ".join(parts)
90
-
91
- async def on_message(
92
- self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
93
- ) -> Any:
94
- """Log all messages."""
95
- message_info = self._format_message(context)
96
- if self.methods and context.method not in self.methods:
97
- return await call_next(context)
98
-
99
- self.logger.log(self.log_level, f"Processing message: {message_info}")
100
-
101
- try:
102
- result = await call_next(context)
103
- self.logger.log(
104
- self.log_level, f"Completed message: {context.method or 'unknown'}"
105
- )
106
- return result
107
- except Exception as e:
108
- self.logger.log(
109
- logging.ERROR, f"Failed message: {context.method or 'unknown'} - {e}"
110
- )
111
- raise
112
-
113
-
114
- class StructuredLoggingMiddleware(Middleware):
203
+ class StructuredLoggingMiddleware(BaseLoggingMiddleware):
115
204
  """Middleware that provides structured JSON logging for better log analysis.
116
205
 
117
206
  Outputs structured logs that are easier to parse and analyze with log
@@ -129,9 +218,12 @@ class StructuredLoggingMiddleware(Middleware):
129
218
 
130
219
  def __init__(
131
220
  self,
221
+ *,
132
222
  logger: logging.Logger | None = None,
133
223
  log_level: int = logging.INFO,
134
224
  include_payloads: bool = False,
225
+ include_payload_length: bool = False,
226
+ estimate_payload_tokens: bool = False,
135
227
  methods: list[str] | None = None,
136
228
  payload_serializer: Callable[[Any], str] | None = None,
137
229
  ):
@@ -141,74 +233,24 @@ class StructuredLoggingMiddleware(Middleware):
141
233
  logger: Logger instance to use. If None, creates a logger named 'fastmcp.structured'
142
234
  log_level: Log level for messages (default: INFO)
143
235
  include_payloads: Whether to include message payloads in logs
236
+ include_payload_length: Whether to include payload size in logs
237
+ estimate_payload_tokens: Whether to estimate token count using length // 4
144
238
  methods: List of methods to log. If None, logs all methods.
145
- serializer: Callable that converts objects to a JSON string for the
239
+ payload_serializer: Callable that converts objects to a JSON string for the
146
240
  payload. If not provided, uses FastMCP's default tool serializer.
147
241
  """
148
- self.logger: Logger = logger or logging.getLogger("fastmcp.structured")
242
+ self.logger: Logger = logger or logging.getLogger(
243
+ "fastmcp.middleware.structured_logging"
244
+ )
149
245
  self.log_level: int = log_level
150
246
  self.include_payloads: bool = include_payloads
247
+ self.include_payload_length: bool = include_payload_length
248
+ self.estimate_payload_tokens: bool = estimate_payload_tokens
151
249
  self.methods: list[str] | None = methods
152
250
  self.payload_serializer: Callable[[Any], str] | None = payload_serializer
251
+ self.max_payload_length: int | None = None
252
+ self.structured_logging: bool = True
153
253
 
154
- def _create_log_entry(
155
- self, context: MiddlewareContext[Any], event: str, **extra_fields: Any
156
- ) -> dict[str, Any]:
157
- """Create a structured log entry."""
158
- entry = {
159
- "event": event,
160
- "timestamp": context.timestamp.isoformat(),
161
- "source": context.source,
162
- "type": context.type,
163
- "method": context.method,
164
- **extra_fields,
165
- }
166
254
 
167
- if self.include_payloads:
168
- payload: str
169
-
170
- if not self.payload_serializer:
171
- payload = default_serializer(context.message)
172
- else:
173
- try:
174
- payload = self.payload_serializer(context.message)
175
- except Exception as e:
176
- self.logger.warning(
177
- f"Failed {str(e)} to serialize payload: {context.type} {context.method} {context.source}."
178
- )
179
- payload = default_serializer(context.message)
180
-
181
- entry["payload"] = payload
182
-
183
- return entry
184
-
185
- async def on_message(
186
- self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
187
- ) -> Any:
188
- """Log structured message information."""
189
- start_entry = self._create_log_entry(context, "request_start")
190
- if self.methods and context.method not in self.methods:
191
- return await call_next(context)
192
-
193
- self.logger.log(self.log_level, json.dumps(start_entry))
194
-
195
- try:
196
- result = await call_next(context)
197
-
198
- success_entry = self._create_log_entry(
199
- context,
200
- "request_success",
201
- result_type=type(result).__name__ if result else None,
202
- )
203
- self.logger.log(self.log_level, json.dumps(success_entry))
204
-
205
- return result
206
- except Exception as e:
207
- error_entry = self._create_log_entry(
208
- context,
209
- "request_error",
210
- error_type=type(e).__name__,
211
- error_message=str(e),
212
- )
213
- self.logger.log(logging.ERROR, json.dumps(error_entry))
214
- raise
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
@@ -15,6 +15,7 @@ from typing import (
15
15
  )
16
16
 
17
17
  import mcp.types as mt
18
+ from mcp.server.lowlevel.helper_types import ReadResourceContents
18
19
  from typing_extensions import TypeVar
19
20
 
20
21
  from fastmcp.prompts.prompt import Prompt
@@ -26,9 +27,9 @@ if TYPE_CHECKING:
26
27
  from fastmcp.server.context import Context
27
28
 
28
29
  __all__ = [
30
+ "CallNext",
29
31
  "Middleware",
30
32
  "MiddlewareContext",
31
- "CallNext",
32
33
  ]
33
34
 
34
35
  logger = logging.getLogger(__name__)
@@ -98,6 +99,8 @@ class Middleware:
98
99
  handler = call_next
99
100
 
100
101
  match context.method:
102
+ case "initialize":
103
+ handler = partial(self.on_initialize, call_next=handler)
101
104
  case "tools/call":
102
105
  handler = partial(self.on_call_tool, call_next=handler)
103
106
  case "resources/read":
@@ -132,18 +135,25 @@ class Middleware:
132
135
 
133
136
  async def on_request(
134
137
  self,
135
- context: MiddlewareContext[mt.Request],
136
- call_next: CallNext[mt.Request, Any],
138
+ context: MiddlewareContext[mt.Request[Any, Any]],
139
+ call_next: CallNext[mt.Request[Any, Any], Any],
137
140
  ) -> Any:
138
141
  return await call_next(context)
139
142
 
140
143
  async def on_notification(
141
144
  self,
142
- context: MiddlewareContext[mt.Notification],
143
- call_next: CallNext[mt.Notification, Any],
145
+ context: MiddlewareContext[mt.Notification[Any, Any]],
146
+ call_next: CallNext[mt.Notification[Any, Any], Any],
144
147
  ) -> Any:
145
148
  return await call_next(context)
146
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
+
147
157
  async def on_call_tool(
148
158
  self,
149
159
  context: MiddlewareContext[mt.CallToolRequestParams],
@@ -154,8 +164,10 @@ class Middleware:
154
164
  async def on_read_resource(
155
165
  self,
156
166
  context: MiddlewareContext[mt.ReadResourceRequestParams],
157
- call_next: CallNext[mt.ReadResourceRequestParams, mt.ReadResourceResult],
158
- ) -> mt.ReadResourceResult:
167
+ call_next: CallNext[
168
+ mt.ReadResourceRequestParams, Sequence[ReadResourceContents]
169
+ ],
170
+ ) -> Sequence[ReadResourceContents]:
159
171
  return await call_next(context)
160
172
 
161
173
  async def on_get_prompt(
@@ -168,27 +180,29 @@ class Middleware:
168
180
  async def on_list_tools(
169
181
  self,
170
182
  context: MiddlewareContext[mt.ListToolsRequest],
171
- call_next: CallNext[mt.ListToolsRequest, list[Tool]],
172
- ) -> list[Tool]:
183
+ call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],
184
+ ) -> Sequence[Tool]:
173
185
  return await call_next(context)
174
186
 
175
187
  async def on_list_resources(
176
188
  self,
177
189
  context: MiddlewareContext[mt.ListResourcesRequest],
178
- call_next: CallNext[mt.ListResourcesRequest, list[Resource]],
179
- ) -> list[Resource]:
190
+ call_next: CallNext[mt.ListResourcesRequest, Sequence[Resource]],
191
+ ) -> Sequence[Resource]:
180
192
  return await call_next(context)
181
193
 
182
194
  async def on_list_resource_templates(
183
195
  self,
184
196
  context: MiddlewareContext[mt.ListResourceTemplatesRequest],
185
- call_next: CallNext[mt.ListResourceTemplatesRequest, list[ResourceTemplate]],
186
- ) -> list[ResourceTemplate]:
197
+ call_next: CallNext[
198
+ mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]
199
+ ],
200
+ ) -> Sequence[ResourceTemplate]:
187
201
  return await call_next(context)
188
202
 
189
203
  async def on_list_prompts(
190
204
  self,
191
205
  context: MiddlewareContext[mt.ListPromptsRequest],
192
- call_next: CallNext[mt.ListPromptsRequest, list[Prompt]],
193
- ) -> list[Prompt]:
206
+ call_next: CallNext[mt.ListPromptsRequest, Sequence[Prompt]],
207
+ ) -> Sequence[Prompt]:
194
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,
@@ -785,6 +789,7 @@ class FastMCPOpenAPI(FastMCP):
785
789
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
786
790
 
787
791
  # Process routes
792
+ num_excluded = 0
788
793
  route_maps = (route_maps or []) + DEFAULT_ROUTE_MAPPINGS
789
794
  for route in http_routes:
790
795
  # Determine route type based on mappings or default rules
@@ -823,8 +828,11 @@ class FastMCPOpenAPI(FastMCP):
823
828
  self._create_openapi_template(route, component_name, tags=route_tags)
824
829
  elif route_type == MCPType.EXCLUDE:
825
830
  logger.info(f"Excluding route: {route.method} {route.path}")
831
+ num_excluded += 1
826
832
 
827
- logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
833
+ logger.info(
834
+ f"Created FastMCP OpenAPI server with {len(http_routes) - num_excluded} routes"
835
+ )
828
836
 
829
837
  def _generate_default_name(
830
838
  self, route: openapi.HTTPRoute, mcp_names_map: dict[str, str] | None = None