fastmcp 2.12.5__py3-none-any.whl → 2.14.0__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 (133) hide show
  1. fastmcp/__init__.py +2 -23
  2. fastmcp/cli/__init__.py +0 -3
  3. fastmcp/cli/__main__.py +5 -0
  4. fastmcp/cli/cli.py +19 -33
  5. fastmcp/cli/install/claude_code.py +6 -6
  6. fastmcp/cli/install/claude_desktop.py +3 -3
  7. fastmcp/cli/install/cursor.py +18 -12
  8. fastmcp/cli/install/gemini_cli.py +3 -3
  9. fastmcp/cli/install/mcp_json.py +3 -3
  10. fastmcp/cli/install/shared.py +0 -15
  11. fastmcp/cli/run.py +13 -8
  12. fastmcp/cli/tasks.py +110 -0
  13. fastmcp/client/__init__.py +9 -9
  14. fastmcp/client/auth/oauth.py +123 -225
  15. fastmcp/client/client.py +697 -95
  16. fastmcp/client/elicitation.py +11 -5
  17. fastmcp/client/logging.py +18 -14
  18. fastmcp/client/messages.py +7 -5
  19. fastmcp/client/oauth_callback.py +85 -171
  20. fastmcp/client/roots.py +2 -1
  21. fastmcp/client/sampling.py +1 -1
  22. fastmcp/client/tasks.py +614 -0
  23. fastmcp/client/transports.py +117 -30
  24. fastmcp/contrib/component_manager/__init__.py +1 -1
  25. fastmcp/contrib/component_manager/component_manager.py +2 -2
  26. fastmcp/contrib/component_manager/component_service.py +10 -26
  27. fastmcp/contrib/mcp_mixin/README.md +32 -1
  28. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  29. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  30. fastmcp/dependencies.py +25 -0
  31. fastmcp/experimental/sampling/handlers/openai.py +3 -3
  32. fastmcp/experimental/server/openapi/__init__.py +20 -21
  33. fastmcp/experimental/utilities/openapi/__init__.py +16 -47
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +54 -51
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +43 -21
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +161 -61
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -14
  45. fastmcp/server/auth/auth.py +197 -46
  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 +1469 -298
  50. fastmcp/server/auth/oidc_proxy.py +91 -20
  51. fastmcp/server/auth/providers/auth0.py +40 -21
  52. fastmcp/server/auth/providers/aws.py +29 -3
  53. fastmcp/server/auth/providers/azure.py +312 -131
  54. fastmcp/server/auth/providers/debug.py +114 -0
  55. fastmcp/server/auth/providers/descope.py +86 -29
  56. fastmcp/server/auth/providers/discord.py +308 -0
  57. fastmcp/server/auth/providers/github.py +29 -8
  58. fastmcp/server/auth/providers/google.py +48 -9
  59. fastmcp/server/auth/providers/in_memory.py +29 -5
  60. fastmcp/server/auth/providers/introspection.py +281 -0
  61. fastmcp/server/auth/providers/jwt.py +48 -31
  62. fastmcp/server/auth/providers/oci.py +233 -0
  63. fastmcp/server/auth/providers/scalekit.py +238 -0
  64. fastmcp/server/auth/providers/supabase.py +188 -0
  65. fastmcp/server/auth/providers/workos.py +35 -17
  66. fastmcp/server/context.py +236 -116
  67. fastmcp/server/dependencies.py +503 -18
  68. fastmcp/server/elicitation.py +286 -48
  69. fastmcp/server/event_store.py +177 -0
  70. fastmcp/server/http.py +71 -20
  71. fastmcp/server/low_level.py +165 -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 +50 -39
  76. fastmcp/server/middleware/middleware.py +29 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi/__init__.py +35 -0
  80. fastmcp/{experimental/server → server}/openapi/components.py +15 -10
  81. fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
  82. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  83. fastmcp/server/proxy.py +72 -48
  84. fastmcp/server/server.py +1415 -733
  85. fastmcp/server/tasks/__init__.py +21 -0
  86. fastmcp/server/tasks/capabilities.py +22 -0
  87. fastmcp/server/tasks/config.py +89 -0
  88. fastmcp/server/tasks/converters.py +205 -0
  89. fastmcp/server/tasks/handlers.py +356 -0
  90. fastmcp/server/tasks/keys.py +93 -0
  91. fastmcp/server/tasks/protocol.py +355 -0
  92. fastmcp/server/tasks/subscriptions.py +205 -0
  93. fastmcp/settings.py +125 -113
  94. fastmcp/tools/__init__.py +1 -1
  95. fastmcp/tools/tool.py +138 -55
  96. fastmcp/tools/tool_manager.py +30 -112
  97. fastmcp/tools/tool_transform.py +12 -21
  98. fastmcp/utilities/cli.py +67 -28
  99. fastmcp/utilities/components.py +10 -5
  100. fastmcp/utilities/inspect.py +79 -23
  101. fastmcp/utilities/json_schema.py +4 -4
  102. fastmcp/utilities/json_schema_type.py +8 -8
  103. fastmcp/utilities/logging.py +118 -8
  104. fastmcp/utilities/mcp_config.py +1 -2
  105. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  106. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  107. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  108. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
  109. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  110. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  111. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  112. fastmcp/utilities/openapi/__init__.py +63 -0
  113. fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
  114. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  115. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
  116. fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
  117. fastmcp/utilities/tests.py +92 -5
  118. fastmcp/utilities/types.py +86 -16
  119. fastmcp/utilities/ui.py +626 -0
  120. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
  121. fastmcp-2.14.0.dist-info/RECORD +156 -0
  122. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
  123. fastmcp/cli/claude.py +0 -135
  124. fastmcp/server/auth/providers/bearer.py +0 -25
  125. fastmcp/server/openapi.py +0 -1083
  126. fastmcp/utilities/openapi.py +0 -1568
  127. fastmcp/utilities/storage.py +0 -204
  128. fastmcp-2.12.5.dist-info/RECORD +0 -134
  129. fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  130. fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
  131. fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
  132. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
  133. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.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
@@ -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, mt.InitializeResult | None],
154
+ ) -> mt.InitializeResult | 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)
@@ -0,0 +1,35 @@
1
+ """OpenAPI server implementation for FastMCP - refactored for better maintainability."""
2
+
3
+ # Import from server
4
+ from .server import FastMCPOpenAPI
5
+
6
+ # Import from routing
7
+ from .routing import (
8
+ MCPType,
9
+ RouteMap,
10
+ RouteMapFn,
11
+ ComponentFn,
12
+ DEFAULT_ROUTE_MAPPINGS,
13
+ _determine_route_type,
14
+ )
15
+
16
+ # Import from components
17
+ from .components import (
18
+ OpenAPITool,
19
+ OpenAPIResource,
20
+ OpenAPIResourceTemplate,
21
+ )
22
+
23
+ # Export public symbols - maintaining backward compatibility
24
+ __all__ = [
25
+ "DEFAULT_ROUTE_MAPPINGS",
26
+ "ComponentFn",
27
+ "FastMCPOpenAPI",
28
+ "MCPType",
29
+ "OpenAPIResource",
30
+ "OpenAPIResourceTemplate",
31
+ "OpenAPITool",
32
+ "RouteMap",
33
+ "RouteMapFn",
34
+ "_determine_route_type",
35
+ ]
@@ -9,14 +9,15 @@ import httpx
9
9
  from mcp.types import ToolAnnotations
10
10
  from pydantic.networks import AnyUrl
11
11
 
12
- # Import from our new utilities
13
- from fastmcp.experimental.utilities.openapi import HTTPRoute
14
- from fastmcp.experimental.utilities.openapi.director import RequestDirector
15
12
  from fastmcp.resources import Resource, ResourceTemplate
16
13
  from fastmcp.server.dependencies import get_http_headers
17
14
  from fastmcp.tools.tool import Tool, ToolResult
18
15
  from fastmcp.utilities.logging import get_logger
19
16
 
17
+ # Import from our new utilities
18
+ from fastmcp.utilities.openapi import HTTPRoute
19
+ from fastmcp.utilities.openapi.director import RequestDirector
20
+
20
21
  if TYPE_CHECKING:
21
22
  from fastmcp.server import Context
22
23
 
@@ -146,11 +147,11 @@ class OpenAPITool(Tool):
146
147
  if e.response.text:
147
148
  error_message += f" - {e.response.text}"
148
149
 
149
- raise ValueError(error_message)
150
+ raise ValueError(error_message) from e
150
151
 
151
152
  except httpx.RequestError as e:
152
153
  # Handle request errors (connection, timeout, etc.)
153
- raise ValueError(f"Request error: {str(e)}")
154
+ raise ValueError(f"Request error: {e!s}") from e
154
155
 
155
156
 
156
157
  class OpenAPIResource(Resource):
@@ -165,9 +166,11 @@ class OpenAPIResource(Resource):
165
166
  name: str,
166
167
  description: str,
167
168
  mime_type: str = "application/json",
168
- tags: set[str] = set(),
169
+ tags: set[str] | None = None,
169
170
  timeout: float | None = None,
170
171
  ):
172
+ if tags is None:
173
+ tags = set()
171
174
  super().__init__(
172
175
  uri=AnyUrl(uri), # Convert string to AnyUrl
173
176
  name=name,
@@ -276,11 +279,11 @@ class OpenAPIResource(Resource):
276
279
  if e.response.text:
277
280
  error_message += f" - {e.response.text}"
278
281
 
279
- raise ValueError(error_message)
282
+ raise ValueError(error_message) from e
280
283
 
281
284
  except httpx.RequestError as e:
282
285
  # Handle request errors (connection, timeout, etc.)
283
- raise ValueError(f"Request error: {str(e)}")
286
+ raise ValueError(f"Request error: {e!s}") from e
284
287
 
285
288
 
286
289
  class OpenAPIResourceTemplate(ResourceTemplate):
@@ -295,9 +298,11 @@ class OpenAPIResourceTemplate(ResourceTemplate):
295
298
  name: str,
296
299
  description: str,
297
300
  parameters: dict[str, Any],
298
- tags: set[str] = set(),
301
+ tags: set[str] | None = None,
299
302
  timeout: float | None = None,
300
303
  ):
304
+ if tags is None:
305
+ tags = set()
301
306
  super().__init__(
302
307
  uri_template=uri_template,
303
308
  name=name,
@@ -342,7 +347,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
342
347
 
343
348
  # Export public symbols
344
349
  __all__ = [
345
- "OpenAPITool",
346
350
  "OpenAPIResource",
347
351
  "OpenAPIResourceTemplate",
352
+ "OpenAPITool",
348
353
  ]
@@ -14,8 +14,8 @@ if TYPE_CHECKING:
14
14
  OpenAPITool,
15
15
  )
16
16
  # Import from our new utilities
17
- from fastmcp.experimental.utilities.openapi import HttpMethod, HTTPRoute
18
17
  from fastmcp.utilities.logging import get_logger
18
+ from fastmcp.utilities.openapi import HttpMethod, HTTPRoute
19
19
 
20
20
  logger = get_logger(__name__)
21
21
 
@@ -121,10 +121,10 @@ def _determine_route_type(
121
121
 
122
122
  # Export public symbols
123
123
  __all__ = [
124
+ "DEFAULT_ROUTE_MAPPINGS",
125
+ "ComponentFn",
124
126
  "MCPType",
125
127
  "RouteMap",
126
128
  "RouteMapFn",
127
- "ComponentFn",
128
- "DEFAULT_ROUTE_MAPPINGS",
129
129
  "_determine_route_type",
130
130
  ]
@@ -7,16 +7,17 @@ from typing import Any, Literal
7
7
  import httpx
8
8
  from jsonschema_path import SchemaPath
9
9
 
10
+ from fastmcp.server.server import FastMCP
11
+ from fastmcp.utilities.logging import get_logger
12
+
10
13
  # Import from our new utilities and components
11
- from fastmcp.experimental.utilities.openapi import (
14
+ from fastmcp.utilities.openapi import (
12
15
  HTTPRoute,
13
16
  extract_output_schema_from_responses,
14
17
  format_simple_description,
15
18
  parse_openapi_to_http_routes,
16
19
  )
17
- from fastmcp.experimental.utilities.openapi.director import RequestDirector
18
- from fastmcp.server.server import FastMCP
19
- from fastmcp.utilities.logging import get_logger
20
+ from fastmcp.utilities.openapi.director import RequestDirector
20
21
 
21
22
  from .components import (
22
23
  OpenAPIResource,
@@ -247,7 +248,7 @@ class FastMCPOpenAPI(FastMCP):
247
248
  # Create the new name
248
249
  new_name = f"{name}_{self._used_names[component_type][name]}"
249
250
  logger.debug(
250
- f"Name collision detected: '{name}' already exists as a {component_type[:-1]}. "
251
+ f"Name collision detected: '{name}' already exists as a {component_type}. "
251
252
  f"Using '{new_name}' instead."
252
253
  )
253
254