fastmcp 2.1.1__py3-none-any.whl → 2.2.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.
fastmcp/server/server.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """FastMCP - A more ergonomic interface for MCP servers."""
2
2
 
3
- import json
3
+ import datetime
4
4
  from collections.abc import AsyncIterator, Awaitable, Callable
5
5
  from contextlib import (
6
6
  AbstractAsyncContextManager,
@@ -13,7 +13,6 @@ import anyio
13
13
  import httpx
14
14
  import pydantic_core
15
15
  import uvicorn
16
- from fastapi import FastAPI
17
16
  from mcp.server.lowlevel.helper_types import ReadResourceContents
18
17
  from mcp.server.lowlevel.server import LifespanResultT
19
18
  from mcp.server.lowlevel.server import Server as MCPServer
@@ -28,7 +27,6 @@ from mcp.types import (
28
27
  TextContent,
29
28
  )
30
29
  from mcp.types import Prompt as MCPPrompt
31
- from mcp.types import PromptArgument as MCPPromptArgument
32
30
  from mcp.types import Resource as MCPResource
33
31
  from mcp.types import ResourceTemplate as MCPResourceTemplate
34
32
  from mcp.types import Tool as MCPTool
@@ -39,24 +37,115 @@ from starlette.routing import Mount, Route
39
37
 
40
38
  import fastmcp
41
39
  import fastmcp.settings
42
- from fastmcp.exceptions import ResourceError
40
+ from fastmcp.exceptions import NotFoundError, ResourceError
43
41
  from fastmcp.prompts import Prompt, PromptManager
44
- from fastmcp.prompts.prompt import Message, PromptResult
42
+ from fastmcp.prompts.prompt import PromptResult
45
43
  from fastmcp.resources import Resource, ResourceManager
46
44
  from fastmcp.resources.template import ResourceTemplate
47
45
  from fastmcp.tools import ToolManager
48
46
  from fastmcp.tools.tool import Tool
49
47
  from fastmcp.utilities.decorators import DecoratedFunction
50
48
  from fastmcp.utilities.logging import configure_logging, get_logger
51
- from fastmcp.utilities.types import Image
52
49
 
53
50
  if TYPE_CHECKING:
54
51
  from fastmcp.client import Client
55
52
  from fastmcp.server.context import Context
56
53
  from fastmcp.server.openapi import FastMCPOpenAPI
57
54
  from fastmcp.server.proxy import FastMCPProxy
55
+
58
56
  logger = get_logger(__name__)
59
57
 
58
+ NOT_FOUND = object()
59
+
60
+
61
+ class MountedServer:
62
+ def __init__(
63
+ self,
64
+ prefix: str,
65
+ server: "FastMCP",
66
+ tool_separator: str | None = None,
67
+ resource_separator: str | None = None,
68
+ prompt_separator: str | None = None,
69
+ ):
70
+ if tool_separator is None:
71
+ tool_separator = "_"
72
+ if resource_separator is None:
73
+ resource_separator = "+"
74
+ if prompt_separator is None:
75
+ prompt_separator = "_"
76
+
77
+ self.server = server
78
+ self.prefix = prefix
79
+ self.tool_separator = tool_separator
80
+ self.resource_separator = resource_separator
81
+ self.prompt_separator = prompt_separator
82
+
83
+ async def get_tools(self) -> dict[str, Tool]:
84
+ tools = await self.server.get_tools()
85
+ return {
86
+ f"{self.prefix}{self.tool_separator}{key}": tool
87
+ for key, tool in tools.items()
88
+ }
89
+
90
+ async def get_resources(self) -> dict[str, Resource]:
91
+ resources = await self.server.get_resources()
92
+ return {
93
+ f"{self.prefix}{self.resource_separator}{key}": resource
94
+ for key, resource in resources.items()
95
+ }
96
+
97
+ async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
98
+ templates = await self.server.get_resource_templates()
99
+ return {
100
+ f"{self.prefix}{self.resource_separator}{key}": template
101
+ for key, template in templates.items()
102
+ }
103
+
104
+ async def get_prompts(self) -> dict[str, Prompt]:
105
+ prompts = await self.server.get_prompts()
106
+ return {
107
+ f"{self.prefix}{self.prompt_separator}{key}": prompt
108
+ for key, prompt in prompts.items()
109
+ }
110
+
111
+ def match_tool(self, key: str) -> bool:
112
+ return key.startswith(f"{self.prefix}{self.tool_separator}")
113
+
114
+ def strip_tool_prefix(self, key: str) -> str:
115
+ return key.removeprefix(f"{self.prefix}{self.tool_separator}")
116
+
117
+ def match_resource(self, key: str) -> bool:
118
+ return key.startswith(f"{self.prefix}{self.resource_separator}")
119
+
120
+ def strip_resource_prefix(self, key: str) -> str:
121
+ return key.removeprefix(f"{self.prefix}{self.resource_separator}")
122
+
123
+ def match_prompt(self, key: str) -> bool:
124
+ return key.startswith(f"{self.prefix}{self.prompt_separator}")
125
+
126
+ def strip_prompt_prefix(self, key: str) -> str:
127
+ return key.removeprefix(f"{self.prefix}{self.prompt_separator}")
128
+
129
+
130
+ class TimedCache:
131
+ def __init__(self, expiration: datetime.timedelta):
132
+ self.expiration = expiration
133
+ self.cache: dict[Any, tuple[Any, datetime.datetime]] = {}
134
+
135
+ def set(self, key: Any, value: Any) -> None:
136
+ expires = datetime.datetime.now() + self.expiration
137
+ self.cache[key] = (value, expires)
138
+
139
+ def get(self, key: Any) -> Any:
140
+ value = self.cache.get(key)
141
+ if value is not None and value[1] > datetime.datetime.now():
142
+ return value[0]
143
+ else:
144
+ return NOT_FOUND
145
+
146
+ def clear(self) -> None:
147
+ self.cache.clear()
148
+
60
149
 
61
150
  @asynccontextmanager
62
151
  async def default_lifespan(server: "FastMCP") -> AsyncIterator[Any]:
@@ -71,7 +160,7 @@ async def default_lifespan(server: "FastMCP") -> AsyncIterator[Any]:
71
160
  yield {}
72
161
 
73
162
 
74
- def lifespan_wrapper(
163
+ def _lifespan_wrapper(
75
164
  app: "FastMCP",
76
165
  lifespan: Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]],
77
166
  ) -> Callable[
@@ -80,17 +169,7 @@ def lifespan_wrapper(
80
169
  @asynccontextmanager
81
170
  async def wrap(s: MCPServer[LifespanResultT]) -> AsyncIterator[LifespanResultT]:
82
171
  async with AsyncExitStack() as stack:
83
- # enter main app's lifespan
84
172
  context = await stack.enter_async_context(lifespan(app))
85
-
86
- # Enter all mounted app lifespans
87
- for prefix, mounted_app in app._mounted_apps.items():
88
- mounted_context = mounted_app._mcp_server.lifespan(
89
- mounted_app._mcp_server
90
- )
91
- await stack.enter_async_context(mounted_context)
92
- logger.debug(f"Prepared lifespan for mounted app '{prefix}'")
93
-
94
173
  yield context
95
174
 
96
175
  return wrap
@@ -109,9 +188,13 @@ class FastMCP(Generic[LifespanResultT]):
109
188
  ):
110
189
  self.tags: set[str] = tags or set()
111
190
  self.settings = fastmcp.settings.ServerSettings(**settings)
191
+ self._cache = TimedCache(
192
+ expiration=datetime.timedelta(
193
+ seconds=self.settings.cache_expiration_seconds
194
+ )
195
+ )
112
196
 
113
- # Setup for mounted apps - must be initialized before _mcp_server
114
- self._mounted_apps: dict[str, FastMCP] = {}
197
+ self._mounted_servers: dict[str, MountedServer] = {}
115
198
 
116
199
  if lifespan is None:
117
200
  lifespan = default_lifespan
@@ -119,7 +202,7 @@ class FastMCP(Generic[LifespanResultT]):
119
202
  self._mcp_server = MCPServer[LifespanResultT](
120
203
  name=name or "FastMCP",
121
204
  instructions=instructions,
122
- lifespan=lifespan_wrapper(self, lifespan),
205
+ lifespan=_lifespan_wrapper(self, lifespan),
123
206
  )
124
207
  self._tool_manager = ToolManager(
125
208
  duplicate_behavior=self.settings.on_duplicate_tools
@@ -138,6 +221,9 @@ class FastMCP(Generic[LifespanResultT]):
138
221
  # Configure logging
139
222
  configure_logging(self.settings.log_level)
140
223
 
224
+ def __repr__(self) -> str:
225
+ return f"{type(self).__name__}({self.name!r})"
226
+
141
227
  @property
142
228
  def name(self) -> str:
143
229
  return self._mcp_server.name
@@ -146,7 +232,9 @@ class FastMCP(Generic[LifespanResultT]):
146
232
  def instructions(self) -> str | None:
147
233
  return self._mcp_server.instructions
148
234
 
149
- async def run_async(self, transport: Literal["stdio", "sse"] | None = None) -> None:
235
+ async def run_async(
236
+ self, transport: Literal["stdio", "sse"] | None = None, **transport_kwargs: Any
237
+ ) -> None:
150
238
  """Run the FastMCP server asynchronously.
151
239
 
152
240
  Args:
@@ -158,51 +246,31 @@ class FastMCP(Generic[LifespanResultT]):
158
246
  raise ValueError(f"Unknown transport: {transport}")
159
247
 
160
248
  if transport == "stdio":
161
- await self.run_stdio_async()
249
+ await self.run_stdio_async(**transport_kwargs)
162
250
  else: # transport == "sse"
163
- await self.run_sse_async()
251
+ await self.run_sse_async(**transport_kwargs)
164
252
 
165
- def run(self, transport: Literal["stdio", "sse"] | None = None) -> None:
253
+ def run(
254
+ self, transport: Literal["stdio", "sse"] | None = None, **transport_kwargs: Any
255
+ ) -> None:
166
256
  """Run the FastMCP server. Note this is a synchronous function.
167
257
 
168
258
  Args:
169
259
  transport: Transport protocol to use ("stdio" or "sse")
170
260
  """
171
261
  logger.info(f'Starting server "{self.name}"...')
172
- anyio.run(self.run_async, transport)
262
+ anyio.run(self.run_async, transport, **transport_kwargs)
173
263
 
174
264
  def _setup_handlers(self) -> None:
175
265
  """Set up core MCP protocol handlers."""
176
266
  self._mcp_server.list_tools()(self._mcp_list_tools)
177
- self._mcp_server.call_tool()(self.call_tool)
267
+ self._mcp_server.call_tool()(self._mcp_call_tool)
178
268
  self._mcp_server.list_resources()(self._mcp_list_resources)
179
269
  self._mcp_server.read_resource()(self._mcp_read_resource)
180
270
  self._mcp_server.list_prompts()(self._mcp_list_prompts)
181
271
  self._mcp_server.get_prompt()(self._mcp_get_prompt)
182
272
  self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
183
273
 
184
- def list_tools(self) -> list[Tool]:
185
- return self._tool_manager.list_tools()
186
-
187
- async def _mcp_list_tools(self) -> list[MCPTool]:
188
- """
189
- List all available tools, in the format expected by the low-level MCP
190
- server.
191
-
192
- See `list_tools` for a more ergonomic way to list tools.
193
- """
194
-
195
- tools = self.list_tools()
196
-
197
- return [
198
- MCPTool(
199
- name=info.name,
200
- description=info.description,
201
- inputSchema=info.parameters,
202
- )
203
- for info in tools
204
- ]
205
-
206
274
  def get_context(self) -> "Context[ServerSession, LifespanResultT]":
207
275
  """
208
276
  Returns a Context object. Note that the context will only be valid
@@ -217,83 +285,152 @@ class FastMCP(Generic[LifespanResultT]):
217
285
 
218
286
  return Context(request_context=request_context, fastmcp=self)
219
287
 
220
- async def call_tool(
221
- self, name: str, arguments: dict[str, Any]
222
- ) -> list[TextContent | ImageContent | EmbeddedResource]:
223
- """Call a tool by name with arguments."""
224
- context = self.get_context()
225
- result = await self._tool_manager.call_tool(name, arguments, context=context)
226
- converted_result = _convert_to_content(result)
227
- return converted_result
288
+ async def get_tools(self) -> dict[str, Tool]:
289
+ """Get all registered tools, indexed by registered key."""
290
+ if (tools := self._cache.get("tools")) is NOT_FOUND:
291
+ tools = {}
292
+ for server in self._mounted_servers.values():
293
+ server_tools = await server.get_tools()
294
+ tools.update(server_tools)
295
+ tools.update(self._tool_manager.get_tools())
296
+ self._cache.set("tools", tools)
297
+ return tools
298
+
299
+ async def get_resources(self) -> dict[str, Resource]:
300
+ """Get all registered resources, indexed by registered key."""
301
+ if (resources := self._cache.get("resources")) is NOT_FOUND:
302
+ resources = {}
303
+ for server in self._mounted_servers.values():
304
+ server_resources = await server.get_resources()
305
+ resources.update(server_resources)
306
+ resources.update(self._resource_manager.get_resources())
307
+ self._cache.set("resources", resources)
308
+ return resources
309
+
310
+ async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
311
+ """Get all registered resource templates, indexed by registered key."""
312
+ if (templates := self._cache.get("resource_templates")) is NOT_FOUND:
313
+ templates = {}
314
+ for server in self._mounted_servers.values():
315
+ server_templates = await server.get_resource_templates()
316
+ templates.update(server_templates)
317
+ templates.update(self._resource_manager.get_templates())
318
+ self._cache.set("resource_templates", templates)
319
+ return templates
320
+
321
+ async def get_prompts(self) -> dict[str, Prompt]:
322
+ """
323
+ List all available prompts.
324
+ """
325
+ if (prompts := self._cache.get("prompts")) is NOT_FOUND:
326
+ prompts = {}
327
+ for server in self._mounted_servers.values():
328
+ server_prompts = await server.get_prompts()
329
+ prompts.update(server_prompts)
330
+ prompts.update(self._prompt_manager.get_prompts())
331
+ self._cache.set("prompts", prompts)
332
+ return prompts
228
333
 
229
- def list_resources(self) -> list[Resource]:
230
- return self._resource_manager.list_resources()
334
+ async def _mcp_list_tools(self) -> list[MCPTool]:
335
+ """
336
+ List all available tools, in the format expected by the low-level MCP
337
+ server.
338
+
339
+ """
340
+ tools = await self.get_tools()
341
+ return [tool.to_mcp_tool(name=key) for key, tool in tools.items()]
231
342
 
232
343
  async def _mcp_list_resources(self) -> list[MCPResource]:
233
344
  """
234
345
  List all available resources, in the format expected by the low-level MCP
235
346
  server.
236
347
 
237
- See `list_resources` for a more ergonomic way to list resources.
238
348
  """
239
-
240
- resources = self.list_resources()
349
+ resources = await self.get_resources()
241
350
  return [
242
- MCPResource(
243
- uri=resource.uri,
244
- name=resource.name or "",
245
- description=resource.description,
246
- mimeType=resource.mime_type,
247
- )
248
- for resource in resources
351
+ resource.to_mcp_resource(uri=key) for key, resource in resources.items()
249
352
  ]
250
353
 
251
- def list_resource_templates(self) -> list[ResourceTemplate]:
252
- return self._resource_manager.list_templates()
253
-
254
354
  async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
255
355
  """
256
356
  List all available resource templates, in the format expected by the low-level
257
357
  MCP server.
258
358
 
259
- See `list_resource_templates` for a more ergonomic way to list resource
260
- templates.
261
359
  """
262
- templates = self.list_resource_templates()
360
+ templates = await self.get_resource_templates()
263
361
  return [
264
- MCPResourceTemplate(
265
- uriTemplate=template.uri_template,
266
- name=template.name,
267
- description=template.description,
268
- )
269
- for template in templates
362
+ template.to_mcp_template(uriTemplate=key)
363
+ for key, template in templates.items()
270
364
  ]
271
365
 
272
- async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
273
- """Read a resource by URI."""
274
- resource = await self._resource_manager.get_resource(uri)
275
- if not resource:
276
- raise ResourceError(f"Unknown resource: {uri}")
277
- return await resource.read()
366
+ async def _mcp_list_prompts(self) -> list[MCPPrompt]:
367
+ """
368
+ List all available prompts, in the format expected by the low-level MCP
369
+ server.
370
+
371
+ """
372
+ prompts = await self.get_prompts()
373
+ return [prompt.to_mcp_prompt(name=key) for key, prompt in prompts.items()]
374
+
375
+ async def _mcp_call_tool(
376
+ self, key: str, arguments: dict[str, Any]
377
+ ) -> list[TextContent | ImageContent | EmbeddedResource]:
378
+ """Call a tool by name with arguments."""
379
+ if self._tool_manager.has_tool(key):
380
+ context = self.get_context()
381
+ result = await self._tool_manager.call_tool(key, arguments, context=context)
382
+
383
+ else:
384
+ for server in self._mounted_servers.values():
385
+ if server.match_tool(key):
386
+ new_key = server.strip_tool_prefix(key)
387
+ result = await server.server._mcp_call_tool(new_key, arguments)
388
+ break
389
+ else:
390
+ raise NotFoundError(f"Unknown tool: {key}")
391
+ return result
278
392
 
279
393
  async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
280
394
  """
281
395
  Read a resource by URI, in the format expected by the low-level MCP
282
396
  server.
283
-
284
- See `read_resource` for a more ergonomic way to read resources.
285
397
  """
398
+ if self._resource_manager.has_resource(uri):
399
+ resource = await self._resource_manager.get_resource(uri)
400
+ try:
401
+ content = await resource.read()
402
+ return [
403
+ ReadResourceContents(content=content, mime_type=resource.mime_type)
404
+ ]
405
+ except Exception as e:
406
+ logger.error(f"Error reading resource {uri}: {e}")
407
+ raise ResourceError(str(e))
408
+ else:
409
+ for server in self._mounted_servers.values():
410
+ if server.match_resource(str(uri)):
411
+ new_uri = server.strip_resource_prefix(str(uri))
412
+ return await server.server._mcp_read_resource(new_uri)
413
+ else:
414
+ raise NotFoundError(f"Unknown resource: {uri}")
286
415
 
287
- resource = await self._resource_manager.get_resource(uri)
288
- if not resource:
289
- raise ResourceError(f"Unknown resource: {uri}")
416
+ async def _mcp_get_prompt(
417
+ self, name: str, arguments: dict[str, Any] | None = None
418
+ ) -> GetPromptResult:
419
+ """
420
+ Get a prompt by name with arguments, in the format expected by the low-level
421
+ MCP server.
290
422
 
291
- try:
292
- content = await self.read_resource(uri)
293
- return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
294
- except Exception as e:
295
- logger.error(f"Error reading resource {uri}: {e}")
296
- raise ResourceError(str(e))
423
+ """
424
+ if self._prompt_manager.has_prompt(name):
425
+ messages = await self._prompt_manager.render_prompt(name, arguments)
426
+ return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
427
+ else:
428
+ for server in self._mounted_servers.values():
429
+ if server.match_prompt(name):
430
+ new_key = server.strip_prompt_prefix(name)
431
+ return await server.server._mcp_get_prompt(new_key, arguments)
432
+ else:
433
+ raise NotFoundError(f"Unknown prompt: {name}")
297
434
 
298
435
  def add_tool(
299
436
  self,
@@ -316,6 +453,7 @@ class FastMCP(Generic[LifespanResultT]):
316
453
  self._tool_manager.add_tool_from_fn(
317
454
  fn, name=name, description=description, tags=tags
318
455
  )
456
+ self._cache.clear()
319
457
 
320
458
  def tool(
321
459
  self,
@@ -359,18 +497,19 @@ class FastMCP(Generic[LifespanResultT]):
359
497
 
360
498
  def decorator(fn: AnyFunction) -> AnyFunction:
361
499
  self.add_tool(fn, name=name, description=description, tags=tags)
362
- return DecoratedFunction(fn)
500
+ return fn
363
501
 
364
502
  return decorator
365
503
 
366
- def add_resource(self, resource: Resource) -> None:
504
+ def add_resource(self, resource: Resource, key: str | None = None) -> None:
367
505
  """Add a resource to the server.
368
506
 
369
507
  Args:
370
508
  resource: A Resource instance to add
371
509
  """
372
510
 
373
- self._resource_manager.add_resource(resource)
511
+ self._resource_manager.add_resource(resource, key=key)
512
+ self._cache.clear()
374
513
 
375
514
  def add_resource_fn(
376
515
  self,
@@ -402,6 +541,7 @@ class FastMCP(Generic[LifespanResultT]):
402
541
  mime_type=mime_type,
403
542
  tags=tags,
404
543
  )
544
+ self._cache.clear()
405
545
 
406
546
  def resource(
407
547
  self,
@@ -457,7 +597,7 @@ class FastMCP(Generic[LifespanResultT]):
457
597
  )
458
598
 
459
599
  def decorator(fn: AnyFunction) -> AnyFunction:
460
- self._resource_manager.add_resource_or_template_from_fn(
600
+ self.add_resource_fn(
461
601
  fn=fn,
462
602
  uri=uri,
463
603
  name=name,
@@ -465,7 +605,7 @@ class FastMCP(Generic[LifespanResultT]):
465
605
  mime_type=mime_type,
466
606
  tags=tags,
467
607
  )
468
- return DecoratedFunction(fn)
608
+ return fn
469
609
 
470
610
  return decorator
471
611
 
@@ -487,6 +627,7 @@ class FastMCP(Generic[LifespanResultT]):
487
627
  description=description,
488
628
  tags=tags,
489
629
  )
630
+ self._cache.clear()
490
631
 
491
632
  def prompt(
492
633
  self,
@@ -592,87 +733,72 @@ class FastMCP(Generic[LifespanResultT]):
592
733
  ],
593
734
  )
594
735
 
595
- def list_prompts(self) -> list[Prompt]:
596
- """
597
- List all available prompts.
598
- """
599
- return self._prompt_manager.list_prompts()
600
-
601
- async def _mcp_list_prompts(self) -> list[MCPPrompt]:
602
- """
603
- List all available prompts, in the format expected by the low-level MCP
604
- server.
605
-
606
- See `list_prompts` for a more ergonomic way to list prompts.
736
+ def mount(
737
+ self,
738
+ prefix: str,
739
+ server: "FastMCP",
740
+ tool_separator: str | None = None,
741
+ resource_separator: str | None = None,
742
+ prompt_separator: str | None = None,
743
+ ) -> None:
607
744
  """
608
- prompts = self.list_prompts()
609
- return [
610
- MCPPrompt(
611
- name=prompt.name,
612
- description=prompt.description,
613
- arguments=[
614
- MCPPromptArgument(
615
- name=arg.name,
616
- description=arg.description,
617
- required=arg.required,
618
- )
619
- for arg in (prompt.arguments or [])
620
- ],
621
- )
622
- for prompt in prompts
623
- ]
624
-
625
- async def get_prompt(
626
- self, name: str, arguments: dict[str, Any] | None = None
627
- ) -> list[Message]:
628
- """Get a prompt by name with arguments."""
629
- return await self._prompt_manager.render_prompt(name, arguments)
630
-
631
- async def _mcp_get_prompt(
632
- self, name: str, arguments: dict[str, Any] | None = None
633
- ) -> GetPromptResult:
745
+ Mount another FastMCP server on a given prefix.
634
746
  """
635
- Get a prompt by name with arguments, in the format expected by the low-level
636
- MCP server.
747
+ mounted_server = MountedServer(
748
+ server=server,
749
+ prefix=prefix,
750
+ tool_separator=tool_separator,
751
+ resource_separator=resource_separator,
752
+ prompt_separator=prompt_separator,
753
+ )
754
+ self._mounted_servers[prefix] = mounted_server
755
+ self._cache.clear()
637
756
 
638
- See `get_prompt` for a more ergonomic way to get prompts.
639
- """
640
- try:
641
- messages = await self.get_prompt(name, arguments)
757
+ def unmount(self, prefix: str) -> None:
758
+ self._mounted_servers.pop(prefix)
759
+ self._cache.clear()
642
760
 
643
- return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
644
- except Exception as e:
645
- logger.error(f"Error getting prompt {name}: {e}")
646
- raise ValueError(str(e))
647
-
648
- def mount(
761
+ async def import_server(
649
762
  self,
650
763
  prefix: str,
651
- app: "FastMCP",
764
+ server: "FastMCP",
652
765
  tool_separator: str | None = None,
653
766
  resource_separator: str | None = None,
654
767
  prompt_separator: str | None = None,
655
768
  ) -> None:
656
- """Mount another FastMCP application with a given prefix.
657
-
658
- When an application is mounted:
659
- - The tools are imported with prefixed names using the tool_separator
660
- Example: If app has a tool named "get_weather", it will be available as "weatherget_weather"
661
- - The resources are imported with prefixed URIs using the resource_separator
662
- Example: If app has a resource with URI "weather://forecast", it will be available as "weather+weather://forecast"
663
- - The templates are imported with prefixed URI templates using the resource_separator
664
- Example: If app has a template with URI "weather://location/{id}", it will be available as "weather+weather://location/{id}"
665
- - The prompts are imported with prefixed names using the prompt_separator
666
- Example: If app has a prompt named "weather_prompt", it will be available as "weather_weather_prompt"
667
- - The mounted app's lifespan will be executed when the parent app's lifespan runs,
668
- ensuring that any setup needed by the mounted app is performed
769
+ """
770
+ Import the MCP objects from another FastMCP server into this one,
771
+ optionally with a given prefix.
772
+
773
+ Note that when a server is *imported*, its objects are immediately
774
+ registered to the importing server. This is a one-time operation and
775
+ future changes to the imported server will not be reflected in the
776
+ importing server. Server-level configurations and lifespans are not imported.
777
+
778
+ When an server is mounted: - The tools are imported with prefixed names
779
+ using the tool_separator
780
+ Example: If server has a tool named "get_weather", it will be
781
+ available as "weatherget_weather"
782
+ - The resources are imported with prefixed URIs using the
783
+ resource_separator Example: If server has a resource with URI
784
+ "weather://forecast", it will be available as
785
+ "weather+weather://forecast"
786
+ - The templates are imported with prefixed URI templates using the
787
+ resource_separator Example: If server has a template with URI
788
+ "weather://location/{id}", it will be available as
789
+ "weather+weather://location/{id}"
790
+ - The prompts are imported with prefixed names using the
791
+ prompt_separator Example: If server has a prompt named
792
+ "weather_prompt", it will be available as "weather_weather_prompt"
793
+ - The mounted server's lifespan will be executed when the parent
794
+ server's lifespan runs, ensuring that any setup needed by the mounted
795
+ server is performed
669
796
 
670
797
  Args:
671
- prefix: The prefix to use for the mounted application
672
- app: The FastMCP application to mount
673
- tool_separator: Separator for tool names (defaults to "_")
674
- resource_separator: Separator for resource URIs (defaults to "+")
675
- prompt_separator: Separator for prompt names (defaults to "_")
798
+ prefix: The prefix to use for the mounted server server: The FastMCP
799
+ server to mount tool_separator: Separator for tool names (defaults
800
+ to "_") resource_separator: Separator for resource URIs (defaults to
801
+ "+") prompt_separator: Separator for prompt names (defaults to "_")
676
802
  """
677
803
  if tool_separator is None:
678
804
  tool_separator = "_"
@@ -681,58 +807,30 @@ class FastMCP(Generic[LifespanResultT]):
681
807
  if prompt_separator is None:
682
808
  prompt_separator = "_"
683
809
 
684
- # Mount the app in the list of mounted apps
685
- self._mounted_apps[prefix] = app
686
-
687
- # Import tools from the mounted app
810
+ # Import tools from the mounted server
688
811
  tool_prefix = f"{prefix}{tool_separator}"
689
- self._tool_manager.import_tools(app._tool_manager, tool_prefix)
812
+ for key, tool in (await server.get_tools()).items():
813
+ self._tool_manager.add_tool(tool, key=f"{tool_prefix}{key}")
690
814
 
691
- # Import resources and templates from the mounted app
815
+ # Import resources and templates from the mounted server
692
816
  resource_prefix = f"{prefix}{resource_separator}"
693
- self._resource_manager.import_resources(app._resource_manager, resource_prefix)
694
- self._resource_manager.import_templates(app._resource_manager, resource_prefix)
817
+ for key, resource in (await server.get_resources()).items():
818
+ self._resource_manager.add_resource(resource, key=f"{resource_prefix}{key}")
819
+ for key, template in (await server.get_resource_templates()).items():
820
+ self._resource_manager.add_template(template, key=f"{resource_prefix}{key}")
695
821
 
696
- # Import prompts from the mounted app
822
+ # Import prompts from the mounted server
697
823
  prompt_prefix = f"{prefix}{prompt_separator}"
698
- self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix)
824
+ for key, prompt in (await server.get_prompts()).items():
825
+ self._prompt_manager.add_prompt(prompt, key=f"{prompt_prefix}{key}")
699
826
 
700
- logger.info(f"Mounted app with prefix '{prefix}'")
827
+ logger.info(f"Imported server {server.name} with prefix '{prefix}'")
701
828
  logger.debug(f"Imported tools with prefix '{tool_prefix}'")
702
829
  logger.debug(f"Imported resources with prefix '{resource_prefix}'")
703
830
  logger.debug(f"Imported templates with prefix '{resource_prefix}'")
704
831
  logger.debug(f"Imported prompts with prefix '{prompt_prefix}'")
705
832
 
706
- @classmethod
707
- async def as_proxy(
708
- cls, client: "Client | FastMCP", **settings: Any
709
- ) -> "FastMCPProxy":
710
- """
711
- Create a FastMCP proxy server from a client.
712
-
713
- This method creates a new FastMCP server instance that proxies requests to the provided client.
714
- It discovers the client's tools, resources, prompts, and templates, and creates corresponding
715
- components in the server that forward requests to the client.
716
-
717
- Args:
718
- client: The client to proxy requests to
719
- **settings: Additional settings for the FastMCP server
720
-
721
- Returns:
722
- A FastMCP server that proxies requests to the client
723
- """
724
- from fastmcp.client import Client
725
-
726
- from .proxy import FastMCPProxy
727
-
728
- if isinstance(client, Client):
729
- return await FastMCPProxy.from_client(client=client, **settings)
730
-
731
- elif isinstance(client, FastMCP):
732
- return await FastMCPProxy.from_server(server=client, **settings)
733
-
734
- else:
735
- raise ValueError(f"Unknown client type: {type(client)}")
833
+ self._cache.clear()
736
834
 
737
835
  @classmethod
738
836
  def from_openapi(
@@ -747,11 +845,12 @@ class FastMCP(Generic[LifespanResultT]):
747
845
 
748
846
  @classmethod
749
847
  def from_fastapi(
750
- cls, app: FastAPI, name: str | None = None, **settings: Any
848
+ cls, app: "Any", name: str | None = None, **settings: Any
751
849
  ) -> "FastMCPOpenAPI":
752
850
  """
753
851
  Create a FastMCP server from a FastAPI application.
754
852
  """
853
+
755
854
  from .openapi import FastMCPOpenAPI
756
855
 
757
856
  client = httpx.AsyncClient(
@@ -764,47 +863,11 @@ class FastMCP(Generic[LifespanResultT]):
764
863
  openapi_spec=app.openapi(), client=client, name=name, **settings
765
864
  )
766
865
 
866
+ @classmethod
867
+ def from_client(cls, client: "Client", **settings: Any) -> "FastMCPProxy":
868
+ """
869
+ Create a FastMCP proxy server from a FastMCP client.
870
+ """
871
+ from fastmcp.server.proxy import FastMCPProxy
767
872
 
768
- def _convert_to_content(
769
- result: Any,
770
- _process_as_single_item: bool = False,
771
- ) -> list[TextContent | ImageContent | EmbeddedResource]:
772
- """Convert a result to a sequence of content objects."""
773
- if result is None:
774
- return []
775
-
776
- if isinstance(result, TextContent | ImageContent | EmbeddedResource):
777
- return [result]
778
-
779
- if isinstance(result, Image):
780
- return [result.to_image_content()]
781
-
782
- if isinstance(result, list | tuple) and not _process_as_single_item:
783
- # if the result is a list, then it could either be a list of MCP types,
784
- # or a "regular" list that the tool is returning, or a mix of both.
785
- #
786
- # so we extract all the MCP types / images and convert them as individual content elements,
787
- # and aggregate the rest as a single content element
788
-
789
- mcp_types = []
790
- other_content = []
791
-
792
- for item in result:
793
- if isinstance(item, TextContent | ImageContent | EmbeddedResource | Image):
794
- mcp_types.append(_convert_to_content(item)[0])
795
- else:
796
- other_content.append(item)
797
- if other_content:
798
- other_content = _convert_to_content(
799
- other_content, _process_as_single_item=True
800
- )
801
-
802
- return other_content + mcp_types
803
-
804
- if not isinstance(result, str):
805
- try:
806
- result = json.dumps(pydantic_core.to_jsonable_python(result))
807
- except Exception:
808
- result = str(result)
809
-
810
- return [TextContent(type="text", text=result)]
873
+ return FastMCPProxy(client=client, **settings)