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