fastmcp 2.8.1__py3-none-any.whl → 2.9.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 (38) hide show
  1. fastmcp/cli/cli.py +99 -1
  2. fastmcp/cli/run.py +1 -3
  3. fastmcp/client/auth/oauth.py +1 -2
  4. fastmcp/client/client.py +21 -5
  5. fastmcp/client/transports.py +17 -2
  6. fastmcp/contrib/mcp_mixin/README.md +79 -2
  7. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -0
  8. fastmcp/prompts/prompt.py +91 -11
  9. fastmcp/prompts/prompt_manager.py +119 -43
  10. fastmcp/resources/resource.py +11 -1
  11. fastmcp/resources/resource_manager.py +249 -76
  12. fastmcp/resources/template.py +27 -1
  13. fastmcp/server/auth/providers/bearer.py +32 -10
  14. fastmcp/server/context.py +41 -2
  15. fastmcp/server/http.py +8 -0
  16. fastmcp/server/middleware/__init__.py +6 -0
  17. fastmcp/server/middleware/error_handling.py +206 -0
  18. fastmcp/server/middleware/logging.py +165 -0
  19. fastmcp/server/middleware/middleware.py +236 -0
  20. fastmcp/server/middleware/rate_limiting.py +231 -0
  21. fastmcp/server/middleware/timing.py +156 -0
  22. fastmcp/server/proxy.py +250 -140
  23. fastmcp/server/server.py +320 -242
  24. fastmcp/settings.py +2 -2
  25. fastmcp/tools/tool.py +6 -2
  26. fastmcp/tools/tool_manager.py +114 -45
  27. fastmcp/utilities/components.py +22 -2
  28. fastmcp/utilities/inspect.py +326 -0
  29. fastmcp/utilities/json_schema.py +67 -23
  30. fastmcp/utilities/mcp_config.py +13 -7
  31. fastmcp/utilities/openapi.py +5 -3
  32. fastmcp/utilities/tests.py +1 -1
  33. fastmcp/utilities/types.py +90 -1
  34. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/METADATA +2 -2
  35. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/RECORD +38 -31
  36. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/WHEEL +0 -0
  37. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/entry_points.txt +0 -0
  38. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/proxy.py CHANGED
@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Any, cast
4
4
  from urllib.parse import quote
5
5
 
6
6
  import mcp.types
7
- from mcp.server.lowlevel.helper_types import ReadResourceContents
8
7
  from mcp.shared.exceptions import McpError
9
8
  from mcp.types import (
10
9
  METHOD_NOT_FOUND,
@@ -17,10 +16,14 @@ from pydantic.networks import AnyUrl
17
16
  from fastmcp.client import Client
18
17
  from fastmcp.exceptions import NotFoundError, ResourceError, ToolError
19
18
  from fastmcp.prompts import Prompt, PromptMessage
19
+ from fastmcp.prompts.prompt import PromptArgument
20
+ from fastmcp.prompts.prompt_manager import PromptManager
20
21
  from fastmcp.resources import Resource, ResourceTemplate
22
+ from fastmcp.resources.resource_manager import ResourceManager
21
23
  from fastmcp.server.context import Context
22
24
  from fastmcp.server.server import FastMCP
23
25
  from fastmcp.tools.tool import Tool
26
+ from fastmcp.tools.tool_manager import ToolManager
24
27
  from fastmcp.utilities.logging import get_logger
25
28
  from fastmcp.utilities.types import MCPContent
26
29
 
@@ -30,18 +33,197 @@ if TYPE_CHECKING:
30
33
  logger = get_logger(__name__)
31
34
 
32
35
 
36
+ class ProxyToolManager(ToolManager):
37
+ """A ToolManager that sources its tools from a remote client in addition to local and mounted tools."""
38
+
39
+ def __init__(self, client: Client, **kwargs):
40
+ super().__init__(**kwargs)
41
+ self.client = client
42
+
43
+ async def get_tools(self) -> dict[str, Tool]:
44
+ """Gets the unfiltered tool inventory including local, mounted, and proxy tools."""
45
+ # First get local and mounted tools from parent
46
+ all_tools = await super().get_tools()
47
+
48
+ # Then add proxy tools, but don't overwrite existing ones
49
+ try:
50
+ async with self.client:
51
+ client_tools = await self.client.list_tools()
52
+ for tool in client_tools:
53
+ if tool.name not in all_tools:
54
+ all_tools[tool.name] = ProxyTool.from_mcp_tool(
55
+ self.client, tool
56
+ )
57
+ except McpError as e:
58
+ if e.error.code == METHOD_NOT_FOUND:
59
+ pass # No tools available from proxy
60
+ else:
61
+ raise e
62
+
63
+ return all_tools
64
+
65
+ async def list_tools(self) -> list[Tool]:
66
+ """Gets the filtered list of tools including local, mounted, and proxy tools."""
67
+ tools_dict = await self.get_tools()
68
+ return list(tools_dict.values())
69
+
70
+ async def call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
71
+ """Calls a tool, trying local/mounted first, then proxy if not found."""
72
+ try:
73
+ # First try local and mounted tools
74
+ return await super().call_tool(key, arguments)
75
+ except NotFoundError:
76
+ # If not found locally, try proxy
77
+ async with self.client:
78
+ return await self.client.call_tool(key, arguments)
79
+
80
+
81
+ class ProxyResourceManager(ResourceManager):
82
+ """A ResourceManager that sources its resources from a remote client in addition to local and mounted resources."""
83
+
84
+ def __init__(self, client: Client, **kwargs):
85
+ super().__init__(**kwargs)
86
+ self.client = client
87
+
88
+ async def get_resources(self) -> dict[str, Resource]:
89
+ """Gets the unfiltered resource inventory including local, mounted, and proxy resources."""
90
+ # First get local and mounted resources from parent
91
+ all_resources = await super().get_resources()
92
+
93
+ # Then add proxy resources, but don't overwrite existing ones
94
+ try:
95
+ async with self.client:
96
+ client_resources = await self.client.list_resources()
97
+ for resource in client_resources:
98
+ if str(resource.uri) not in all_resources:
99
+ all_resources[str(resource.uri)] = (
100
+ ProxyResource.from_mcp_resource(self.client, resource)
101
+ )
102
+ except McpError as e:
103
+ if e.error.code == METHOD_NOT_FOUND:
104
+ pass # No resources available from proxy
105
+ else:
106
+ raise e
107
+
108
+ return all_resources
109
+
110
+ async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
111
+ """Gets the unfiltered template inventory including local, mounted, and proxy templates."""
112
+ # First get local and mounted templates from parent
113
+ all_templates = await super().get_resource_templates()
114
+
115
+ # Then add proxy templates, but don't overwrite existing ones
116
+ try:
117
+ async with self.client:
118
+ client_templates = await self.client.list_resource_templates()
119
+ for template in client_templates:
120
+ if template.uriTemplate not in all_templates:
121
+ all_templates[template.uriTemplate] = (
122
+ ProxyTemplate.from_mcp_template(self.client, template)
123
+ )
124
+ except McpError as e:
125
+ if e.error.code == METHOD_NOT_FOUND:
126
+ pass # No templates available from proxy
127
+ else:
128
+ raise e
129
+
130
+ return all_templates
131
+
132
+ async def list_resources(self) -> list[Resource]:
133
+ """Gets the filtered list of resources including local, mounted, and proxy resources."""
134
+ resources_dict = await self.get_resources()
135
+ return list(resources_dict.values())
136
+
137
+ async def list_resource_templates(self) -> list[ResourceTemplate]:
138
+ """Gets the filtered list of templates including local, mounted, and proxy templates."""
139
+ templates_dict = await self.get_resource_templates()
140
+ return list(templates_dict.values())
141
+
142
+ async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
143
+ """Reads a resource, trying local/mounted first, then proxy if not found."""
144
+ try:
145
+ # First try local and mounted resources
146
+ return await super().read_resource(uri)
147
+ except NotFoundError:
148
+ # If not found locally, try proxy
149
+ async with self.client:
150
+ result = await self.client.read_resource(uri)
151
+ if isinstance(result[0], TextResourceContents):
152
+ return result[0].text
153
+ elif isinstance(result[0], BlobResourceContents):
154
+ return result[0].blob
155
+ else:
156
+ raise ResourceError(f"Unsupported content type: {type(result[0])}")
157
+
158
+
159
+ class ProxyPromptManager(PromptManager):
160
+ """A PromptManager that sources its prompts from a remote client in addition to local and mounted prompts."""
161
+
162
+ def __init__(self, client: Client, **kwargs):
163
+ super().__init__(**kwargs)
164
+ self.client = client
165
+
166
+ async def get_prompts(self) -> dict[str, Prompt]:
167
+ """Gets the unfiltered prompt inventory including local, mounted, and proxy prompts."""
168
+ # First get local and mounted prompts from parent
169
+ all_prompts = await super().get_prompts()
170
+
171
+ # Then add proxy prompts, but don't overwrite existing ones
172
+ try:
173
+ async with self.client:
174
+ client_prompts = await self.client.list_prompts()
175
+ for prompt in client_prompts:
176
+ if prompt.name not in all_prompts:
177
+ all_prompts[prompt.name] = ProxyPrompt.from_mcp_prompt(
178
+ self.client, prompt
179
+ )
180
+ except McpError as e:
181
+ if e.error.code == METHOD_NOT_FOUND:
182
+ pass # No prompts available from proxy
183
+ else:
184
+ raise e
185
+
186
+ return all_prompts
187
+
188
+ async def list_prompts(self) -> list[Prompt]:
189
+ """Gets the filtered list of prompts including local, mounted, and proxy prompts."""
190
+ prompts_dict = await self.get_prompts()
191
+ return list(prompts_dict.values())
192
+
193
+ async def render_prompt(
194
+ self,
195
+ name: str,
196
+ arguments: dict[str, Any] | None = None,
197
+ ) -> GetPromptResult:
198
+ """Renders a prompt, trying local/mounted first, then proxy if not found."""
199
+ try:
200
+ # First try local and mounted prompts
201
+ return await super().render_prompt(name, arguments)
202
+ except NotFoundError:
203
+ # If not found locally, try proxy
204
+ async with self.client:
205
+ result = await self.client.get_prompt(name, arguments)
206
+ return result
207
+
208
+
33
209
  class ProxyTool(Tool):
210
+ """
211
+ A Tool that represents and executes a tool on a remote server.
212
+ """
213
+
34
214
  def __init__(self, client: Client, **kwargs):
35
215
  super().__init__(**kwargs)
36
216
  self._client = client
37
217
 
38
218
  @classmethod
39
- async def from_client(cls, client: Client, tool: mcp.types.Tool) -> ProxyTool:
219
+ def from_mcp_tool(cls, client: Client, mcp_tool: mcp.types.Tool) -> ProxyTool:
220
+ """Factory method to create a ProxyTool from a raw MCP tool schema."""
40
221
  return cls(
41
222
  client=client,
42
- name=tool.name,
43
- description=tool.description,
44
- parameters=tool.inputSchema,
223
+ name=mcp_tool.name,
224
+ description=mcp_tool.description,
225
+ parameters=mcp_tool.inputSchema,
226
+ annotations=mcp_tool.annotations,
45
227
  )
46
228
 
47
229
  async def run(
@@ -49,8 +231,8 @@ class ProxyTool(Tool):
49
231
  arguments: dict[str, Any],
50
232
  context: Context | None = None,
51
233
  ) -> list[MCPContent]:
52
- # the client context manager will swallow any exceptions inside a TaskGroup
53
- # so we return the raw result and raise an exception ourselves
234
+ """Executes the tool by making a call through the client."""
235
+ # This is where the remote execution logic lives.
54
236
  async with self._client:
55
237
  result = await self._client.call_tool_mcp(
56
238
  name=self.name,
@@ -62,6 +244,10 @@ class ProxyTool(Tool):
62
244
 
63
245
 
64
246
  class ProxyResource(Resource):
247
+ """
248
+ A Resource that represents and reads a resource from a remote server.
249
+ """
250
+
65
251
  _client: Client
66
252
  _value: str | bytes | None = None
67
253
 
@@ -71,18 +257,20 @@ class ProxyResource(Resource):
71
257
  self._value = _value
72
258
 
73
259
  @classmethod
74
- async def from_client(
75
- cls, client: Client, resource: mcp.types.Resource
260
+ def from_mcp_resource(
261
+ cls, client: Client, mcp_resource: mcp.types.Resource
76
262
  ) -> ProxyResource:
263
+ """Factory method to create a ProxyResource from a raw MCP resource schema."""
77
264
  return cls(
78
265
  client=client,
79
- uri=resource.uri,
80
- name=resource.name,
81
- description=resource.description,
82
- mime_type=resource.mimeType,
266
+ uri=mcp_resource.uri,
267
+ name=mcp_resource.name,
268
+ description=mcp_resource.description,
269
+ mime_type=mcp_resource.mimeType or "text/plain",
83
270
  )
84
271
 
85
272
  async def read(self) -> str | bytes:
273
+ """Read the resource content from the remote server."""
86
274
  if self._value is not None:
87
275
  return self._value
88
276
 
@@ -97,20 +285,26 @@ class ProxyResource(Resource):
97
285
 
98
286
 
99
287
  class ProxyTemplate(ResourceTemplate):
288
+ """
289
+ A ResourceTemplate that represents and creates resources from a remote server template.
290
+ """
291
+
100
292
  def __init__(self, client: Client, **kwargs):
101
293
  super().__init__(**kwargs)
102
294
  self._client = client
103
295
 
104
296
  @classmethod
105
- async def from_client(
106
- cls, client: Client, template: mcp.types.ResourceTemplate
297
+ def from_mcp_template(
298
+ cls, client: Client, mcp_template: mcp.types.ResourceTemplate
107
299
  ) -> ProxyTemplate:
300
+ """Factory method to create a ProxyTemplate from a raw MCP template schema."""
108
301
  return cls(
109
302
  client=client,
110
- uri_template=template.uriTemplate,
111
- name=template.name,
112
- description=template.description,
113
- parameters={},
303
+ uri_template=mcp_template.uriTemplate,
304
+ name=mcp_template.name,
305
+ description=mcp_template.description,
306
+ mime_type=mcp_template.mimeType or "text/plain",
307
+ parameters={}, # Remote templates don't have local parameters
114
308
  )
115
309
 
116
310
  async def create_resource(
@@ -119,6 +313,7 @@ class ProxyTemplate(ResourceTemplate):
119
313
  params: dict[str, Any],
120
314
  context: Context | None = None,
121
315
  ) -> ProxyResource:
316
+ """Create a resource from the template by calling the remote server."""
122
317
  # don't use the provided uri, because it may not be the same as the
123
318
  # uri_template on the remote server.
124
319
  # quote params to ensure they are valid for the uri_template
@@ -146,6 +341,10 @@ class ProxyTemplate(ResourceTemplate):
146
341
 
147
342
 
148
343
  class ProxyPrompt(Prompt):
344
+ """
345
+ A Prompt that represents and renders a prompt from a remote server.
346
+ """
347
+
149
348
  _client: Client
150
349
 
151
350
  def __init__(self, client: Client, **kwargs):
@@ -153,139 +352,50 @@ class ProxyPrompt(Prompt):
153
352
  self._client = client
154
353
 
155
354
  @classmethod
156
- async def from_client(cls, client: Client, prompt: mcp.types.Prompt) -> ProxyPrompt:
355
+ def from_mcp_prompt(
356
+ cls, client: Client, mcp_prompt: mcp.types.Prompt
357
+ ) -> ProxyPrompt:
358
+ """Factory method to create a ProxyPrompt from a raw MCP prompt schema."""
359
+ arguments = [
360
+ PromptArgument(
361
+ name=arg.name,
362
+ description=arg.description,
363
+ required=arg.required or False,
364
+ )
365
+ for arg in mcp_prompt.arguments or []
366
+ ]
157
367
  return cls(
158
368
  client=client,
159
- name=prompt.name,
160
- description=prompt.description,
161
- arguments=[a.model_dump() for a in prompt.arguments or []],
369
+ name=mcp_prompt.name,
370
+ description=mcp_prompt.description,
371
+ arguments=arguments,
162
372
  )
163
373
 
164
374
  async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
375
+ """Render the prompt by making a call through the client."""
165
376
  async with self._client:
166
377
  result = await self._client.get_prompt(self.name, arguments)
167
378
  return result.messages
168
379
 
169
380
 
170
381
  class FastMCPProxy(FastMCP):
382
+ """
383
+ A FastMCP server that acts as a proxy to a remote MCP-compliant server.
384
+ It uses specialized managers that fulfill requests via an HTTP client.
385
+ """
386
+
171
387
  def __init__(self, client: Client, **kwargs):
388
+ """
389
+ Initializes the proxy server.
390
+
391
+ Args:
392
+ client: The FastMCP client connected to the backend server.
393
+ **kwargs: Additional settings for the FastMCP server.
394
+ """
172
395
  super().__init__(**kwargs)
173
396
  self.client = client
174
397
 
175
- async def get_tools(self) -> dict[str, Tool]:
176
- tools = await super().get_tools()
177
-
178
- async with self.client:
179
- try:
180
- client_tools = await self.client.list_tools()
181
- except McpError as e:
182
- if e.error.code == METHOD_NOT_FOUND:
183
- client_tools = []
184
- else:
185
- raise e
186
- for tool in client_tools:
187
- # don't overwrite tools defined in the server
188
- if tool.name not in tools:
189
- tool_proxy = await ProxyTool.from_client(self.client, tool)
190
- tools[tool_proxy.name] = tool_proxy
191
-
192
- return tools
193
-
194
- async def get_resources(self) -> dict[str, Resource]:
195
- resources = await super().get_resources()
196
-
197
- async with self.client:
198
- try:
199
- client_resources = await self.client.list_resources()
200
- except McpError as e:
201
- if e.error.code == METHOD_NOT_FOUND:
202
- client_resources = []
203
- else:
204
- raise e
205
- for resource in client_resources:
206
- # don't overwrite resources defined in the server
207
- if str(resource.uri) not in resources:
208
- resource_proxy = await ProxyResource.from_client(
209
- self.client, resource
210
- )
211
- resources[str(resource_proxy.uri)] = resource_proxy
212
-
213
- return resources
214
-
215
- async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
216
- templates = await super().get_resource_templates()
217
-
218
- async with self.client:
219
- try:
220
- client_templates = await self.client.list_resource_templates()
221
- except McpError as e:
222
- if e.error.code == METHOD_NOT_FOUND:
223
- client_templates = []
224
- else:
225
- raise e
226
- for template in client_templates:
227
- # don't overwrite templates defined in the server
228
- if template.uriTemplate not in templates:
229
- template_proxy = await ProxyTemplate.from_client(
230
- self.client, template
231
- )
232
- templates[template_proxy.uri_template] = template_proxy
233
-
234
- return templates
235
-
236
- async def get_prompts(self) -> dict[str, Prompt]:
237
- prompts = await super().get_prompts()
238
-
239
- async with self.client:
240
- try:
241
- client_prompts = await self.client.list_prompts()
242
- except McpError as e:
243
- if e.error.code == METHOD_NOT_FOUND:
244
- client_prompts = []
245
- else:
246
- raise e
247
- for prompt in client_prompts:
248
- # don't overwrite prompts defined in the server
249
- if prompt.name not in prompts:
250
- prompt_proxy = await ProxyPrompt.from_client(self.client, prompt)
251
- prompts[prompt_proxy.name] = prompt_proxy
252
-
253
- return prompts
254
-
255
- async def _call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
256
- try:
257
- result = await super()._call_tool(key, arguments)
258
- return result
259
- except NotFoundError:
260
- async with self.client:
261
- result = await self.client.call_tool(key, arguments)
262
- return result
263
-
264
- async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
265
- try:
266
- result = await super()._read_resource(uri)
267
- return result
268
- except NotFoundError:
269
- async with self.client:
270
- resource = await self.client.read_resource(uri)
271
- if isinstance(resource[0], TextResourceContents):
272
- content = resource[0].text
273
- elif isinstance(resource[0], BlobResourceContents):
274
- content = resource[0].blob
275
- else:
276
- raise ValueError(f"Unsupported content type: {type(resource[0])}")
277
-
278
- return [
279
- ReadResourceContents(content=content, mime_type=resource[0].mimeType)
280
- ]
281
-
282
- async def _get_prompt(
283
- self, name: str, arguments: dict[str, Any] | None = None
284
- ) -> GetPromptResult:
285
- try:
286
- result = await super()._get_prompt(name, arguments)
287
- return result
288
- except NotFoundError:
289
- async with self.client:
290
- result = await self.client.get_prompt(name, arguments)
291
- return result
398
+ # Replace the default managers with our specialized proxy managers.
399
+ self._tool_manager = ProxyToolManager(client=self.client)
400
+ self._resource_manager = ProxyResourceManager(client=self.client)
401
+ self._prompt_manager = ProxyPromptManager(client=self.client)