fastmcp 1.0__py3-none-any.whl → 2.1.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 (39) hide show
  1. fastmcp/__init__.py +15 -4
  2. fastmcp/cli/__init__.py +0 -1
  3. fastmcp/cli/claude.py +13 -11
  4. fastmcp/cli/cli.py +59 -39
  5. fastmcp/client/__init__.py +25 -0
  6. fastmcp/client/base.py +1 -0
  7. fastmcp/client/client.py +226 -0
  8. fastmcp/client/roots.py +75 -0
  9. fastmcp/client/sampling.py +50 -0
  10. fastmcp/client/transports.py +411 -0
  11. fastmcp/prompts/__init__.py +2 -2
  12. fastmcp/prompts/{base.py → prompt.py} +47 -26
  13. fastmcp/prompts/prompt_manager.py +69 -15
  14. fastmcp/resources/__init__.py +6 -6
  15. fastmcp/resources/{base.py → resource.py} +21 -2
  16. fastmcp/resources/resource_manager.py +116 -17
  17. fastmcp/resources/{templates.py → template.py} +36 -11
  18. fastmcp/resources/types.py +18 -13
  19. fastmcp/server/__init__.py +5 -0
  20. fastmcp/server/context.py +222 -0
  21. fastmcp/server/openapi.py +637 -0
  22. fastmcp/server/proxy.py +223 -0
  23. fastmcp/{server.py → server/server.py} +323 -267
  24. fastmcp/settings.py +81 -0
  25. fastmcp/tools/__init__.py +1 -1
  26. fastmcp/tools/{base.py → tool.py} +47 -18
  27. fastmcp/tools/tool_manager.py +57 -16
  28. fastmcp/utilities/func_metadata.py +33 -19
  29. fastmcp/utilities/openapi.py +797 -0
  30. fastmcp/utilities/types.py +15 -4
  31. fastmcp-2.1.0.dist-info/METADATA +770 -0
  32. fastmcp-2.1.0.dist-info/RECORD +39 -0
  33. fastmcp-2.1.0.dist-info/licenses/LICENSE +201 -0
  34. fastmcp/prompts/manager.py +0 -50
  35. fastmcp-1.0.dist-info/METADATA +0 -604
  36. fastmcp-1.0.dist-info/RECORD +0 -28
  37. fastmcp-1.0.dist-info/licenses/LICENSE +0 -21
  38. {fastmcp-1.0.dist-info → fastmcp-2.1.0.dist-info}/WHEEL +0 -0
  39. {fastmcp-1.0.dist-info → fastmcp-2.1.0.dist-info}/entry_points.txt +0 -0
@@ -1,106 +1,132 @@
1
1
  """FastMCP - A more ergonomic interface for MCP servers."""
2
2
 
3
- import asyncio
4
- import functools
5
3
  import inspect
6
4
  import json
7
5
  import re
8
- from itertools import chain
9
- from typing import Any, Callable, Dict, Literal, Sequence, TypeVar, ParamSpec
6
+ from collections.abc import AsyncIterator, Callable
7
+ from contextlib import (
8
+ AbstractAsyncContextManager,
9
+ AsyncExitStack,
10
+ asynccontextmanager,
11
+ )
12
+ from typing import TYPE_CHECKING, Any, Generic, Literal
10
13
 
14
+ import anyio
15
+ import httpx
11
16
  import pydantic_core
12
- from pydantic import Field
13
17
  import uvicorn
14
- from mcp.server import Server as MCPServer
18
+ from fastapi import FastAPI
19
+ from mcp.server.lowlevel.helper_types import ReadResourceContents
20
+ from mcp.server.lowlevel.server import LifespanResultT
21
+ from mcp.server.lowlevel.server import Server as MCPServer
22
+ from mcp.server.session import ServerSession
15
23
  from mcp.server.sse import SseServerTransport
16
24
  from mcp.server.stdio import stdio_server
17
- from mcp.shared.context import RequestContext
18
25
  from mcp.types import (
26
+ AnyFunction,
19
27
  EmbeddedResource,
20
28
  GetPromptResult,
21
29
  ImageContent,
22
30
  TextContent,
23
31
  )
24
- from mcp.types import (
25
- Prompt as MCPPrompt,
26
- PromptArgument as MCPPromptArgument,
27
- )
28
- from mcp.types import (
29
- Resource as MCPResource,
30
- )
31
- from mcp.types import (
32
- ResourceTemplate as MCPResourceTemplate,
33
- )
34
- from mcp.types import (
35
- Tool as MCPTool,
36
- )
37
- from pydantic import BaseModel
32
+ from mcp.types import Prompt as MCPPrompt
33
+ from mcp.types import PromptArgument as MCPPromptArgument
34
+ from mcp.types import Resource as MCPResource
35
+ from mcp.types import ResourceTemplate as MCPResourceTemplate
36
+ from mcp.types import Tool as MCPTool
38
37
  from pydantic.networks import AnyUrl
39
- from pydantic_settings import BaseSettings, SettingsConfigDict
38
+ from starlette.applications import Starlette
39
+ from starlette.requests import Request
40
+ from starlette.routing import Mount, Route
40
41
 
42
+ import fastmcp
43
+ import fastmcp.settings
41
44
  from fastmcp.exceptions import ResourceError
42
45
  from fastmcp.prompts import Prompt, PromptManager
43
- from fastmcp.prompts.base import PromptResult
44
46
  from fastmcp.resources import FunctionResource, Resource, ResourceManager
45
47
  from fastmcp.tools import ToolManager
46
48
  from fastmcp.utilities.logging import configure_logging, get_logger
47
49
  from fastmcp.utilities.types import Image
48
50
 
51
+ if TYPE_CHECKING:
52
+ from fastmcp.client import Client
53
+ from fastmcp.server.context import Context
54
+ from fastmcp.server.openapi import FastMCPOpenAPI
55
+ from fastmcp.server.proxy import FastMCPProxy
49
56
  logger = get_logger(__name__)
50
57
 
51
- P = ParamSpec("P")
52
- R = TypeVar("R")
53
- R_PromptResult = TypeVar("R_PromptResult", bound=PromptResult)
54
58
 
59
+ @asynccontextmanager
60
+ async def default_lifespan(server: "FastMCP") -> AsyncIterator[Any]:
61
+ """Default lifespan context manager that does nothing.
55
62
 
56
- class Settings(BaseSettings):
57
- """FastMCP server settings.
63
+ Args:
64
+ server: The server instance this lifespan is managing
58
65
 
59
- All settings can be configured via environment variables with the prefix FASTMCP_.
60
- For example, FASTMCP_DEBUG=true will set debug=True.
66
+ Returns:
67
+ An empty context object
61
68
  """
69
+ yield {}
70
+
71
+
72
+ def lifespan_wrapper(
73
+ app: "FastMCP",
74
+ lifespan: Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]],
75
+ ) -> Callable[
76
+ [MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
77
+ ]:
78
+ @asynccontextmanager
79
+ async def wrap(s: MCPServer[LifespanResultT]) -> AsyncIterator[LifespanResultT]:
80
+ async with AsyncExitStack() as stack:
81
+ # enter main app's lifespan
82
+ context = await stack.enter_async_context(lifespan(app))
83
+
84
+ # Enter all mounted app lifespans
85
+ for prefix, mounted_app in app._mounted_apps.items():
86
+ mounted_context = mounted_app._mcp_server.lifespan(
87
+ mounted_app._mcp_server
88
+ )
89
+ await stack.enter_async_context(mounted_context)
90
+ logger.debug(f"Prepared lifespan for mounted app '{prefix}'")
62
91
 
63
- model_config: SettingsConfigDict = SettingsConfigDict(
64
- env_prefix="FASTMCP_",
65
- env_file=".env",
66
- extra="ignore",
67
- )
68
-
69
- # Server settings
70
- debug: bool = False
71
- log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
72
-
73
- # HTTP settings
74
- host: str = "0.0.0.0"
75
- port: int = 8000
92
+ yield context
76
93
 
77
- # resource settings
78
- warn_on_duplicate_resources: bool = True
94
+ return wrap
79
95
 
80
- # tool settings
81
- warn_on_duplicate_tools: bool = True
82
96
 
83
- # prompt settings
84
- warn_on_duplicate_prompts: bool = True
97
+ class FastMCP(Generic[LifespanResultT]):
98
+ def __init__(
99
+ self,
100
+ name: str | None = None,
101
+ instructions: str | None = None,
102
+ lifespan: (
103
+ Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]] | None
104
+ ) = None,
105
+ tags: set[str] | None = None,
106
+ **settings: Any,
107
+ ):
108
+ self.tags: set[str] = tags or set()
109
+ self.settings = fastmcp.settings.ServerSettings(**settings)
85
110
 
86
- dependencies: list[str] = Field(
87
- default_factory=list,
88
- description="List of dependencies to install in the server environment",
89
- )
111
+ # Setup for mounted apps - must be initialized before _mcp_server
112
+ self._mounted_apps: dict[str, FastMCP] = {}
90
113
 
114
+ if lifespan is None:
115
+ lifespan = default_lifespan
91
116
 
92
- class FastMCP:
93
- def __init__(self, name: str | None = None, **settings: Any):
94
- self.settings = Settings(**settings)
95
- self._mcp_server = MCPServer(name=name or "FastMCP")
117
+ self._mcp_server = MCPServer[LifespanResultT](
118
+ name=name or "FastMCP",
119
+ instructions=instructions,
120
+ lifespan=lifespan_wrapper(self, lifespan),
121
+ )
96
122
  self._tool_manager = ToolManager(
97
- warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools
123
+ duplicate_behavior=self.settings.on_duplicate_tools
98
124
  )
99
125
  self._resource_manager = ResourceManager(
100
- warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources
126
+ duplicate_behavior=self.settings.on_duplicate_resources
101
127
  )
102
128
  self._prompt_manager = PromptManager(
103
- warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts
129
+ duplicate_behavior=self.settings.on_duplicate_prompts
104
130
  )
105
131
  self.dependencies = self.settings.dependencies
106
132
 
@@ -114,20 +140,33 @@ class FastMCP:
114
140
  def name(self) -> str:
115
141
  return self._mcp_server.name
116
142
 
117
- def run(self, transport: Literal["stdio", "sse"] = "stdio") -> None:
118
- """Run the FastMCP server. Note this is a synchronous function.
143
+ @property
144
+ def instructions(self) -> str | None:
145
+ return self._mcp_server.instructions
146
+
147
+ async def run_async(self, transport: Literal["stdio", "sse"] | None = None) -> None:
148
+ """Run the FastMCP server asynchronously.
119
149
 
120
150
  Args:
121
151
  transport: Transport protocol to use ("stdio" or "sse")
122
152
  """
123
- TRANSPORTS = Literal["stdio", "sse"]
124
- if transport not in TRANSPORTS.__args__: # type: ignore
153
+ if transport is None:
154
+ transport = "stdio"
155
+ if transport not in ["stdio", "sse"]:
125
156
  raise ValueError(f"Unknown transport: {transport}")
126
157
 
127
158
  if transport == "stdio":
128
- asyncio.run(self.run_stdio_async())
159
+ await self.run_stdio_async()
129
160
  else: # transport == "sse"
130
- asyncio.run(self.run_sse_async())
161
+ await self.run_sse_async()
162
+
163
+ def run(self, transport: Literal["stdio", "sse"] | None = None) -> None:
164
+ """Run the FastMCP server. Note this is a synchronous function.
165
+
166
+ Args:
167
+ transport: Transport protocol to use ("stdio" or "sse")
168
+ """
169
+ anyio.run(self.run_async, transport)
131
170
 
132
171
  def _setup_handlers(self) -> None:
133
172
  """Set up core MCP protocol handlers."""
@@ -137,11 +176,11 @@ class FastMCP:
137
176
  self._mcp_server.read_resource()(self.read_resource)
138
177
  self._mcp_server.list_prompts()(self.list_prompts)
139
178
  self._mcp_server.get_prompt()(self.get_prompt)
140
- # TODO: This has not been added to MCP yet, see https://github.com/jlowin/fastmcp/issues/10
141
- # self._mcp_server.list_resource_templates()(self.list_resource_templates)
179
+ self._mcp_server.list_resource_templates()(self.list_resource_templates)
142
180
 
143
181
  async def list_tools(self) -> list[MCPTool]:
144
182
  """List all available tools."""
183
+
145
184
  tools = self._tool_manager.list_tools()
146
185
  return [
147
186
  MCPTool(
@@ -152,20 +191,23 @@ class FastMCP:
152
191
  for info in tools
153
192
  ]
154
193
 
155
- def get_context(self) -> "Context":
194
+ def get_context(self) -> "Context[ServerSession, LifespanResultT]":
156
195
  """
157
196
  Returns a Context object. Note that the context will only be valid
158
197
  during a request; outside a request, most methods will error.
159
198
  """
199
+
160
200
  try:
161
201
  request_context = self._mcp_server.request_context
162
202
  except LookupError:
163
203
  request_context = None
204
+ from fastmcp.server.context import Context
205
+
164
206
  return Context(request_context=request_context, fastmcp=self)
165
207
 
166
208
  async def call_tool(
167
- self, name: str, arguments: dict
168
- ) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
209
+ self, name: str, arguments: dict[str, Any]
210
+ ) -> list[TextContent | ImageContent | EmbeddedResource]:
169
211
  """Call a tool by name with arguments."""
170
212
  context = self.get_context()
171
213
  result = await self._tool_manager.call_tool(name, arguments, context=context)
@@ -197,23 +239,26 @@ class FastMCP:
197
239
  for template in templates
198
240
  ]
199
241
 
200
- async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
242
+ async def read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
201
243
  """Read a resource by URI."""
244
+
202
245
  resource = await self._resource_manager.get_resource(uri)
203
246
  if not resource:
204
247
  raise ResourceError(f"Unknown resource: {uri}")
205
248
 
206
249
  try:
207
- return await resource.read()
250
+ content = await resource.read()
251
+ return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
208
252
  except Exception as e:
209
253
  logger.error(f"Error reading resource {uri}: {e}")
210
254
  raise ResourceError(str(e))
211
255
 
212
256
  def add_tool(
213
257
  self,
214
- fn: Callable,
258
+ fn: AnyFunction,
215
259
  name: str | None = None,
216
260
  description: str | None = None,
261
+ tags: set[str] | None = None,
217
262
  ) -> None:
218
263
  """Add a tool to the server.
219
264
 
@@ -224,20 +269,28 @@ class FastMCP:
224
269
  fn: The function to register as a tool
225
270
  name: Optional name for the tool (defaults to function name)
226
271
  description: Optional description of what the tool does
272
+ tags: Optional set of tags for categorizing the tool
227
273
  """
228
- self._tool_manager.add_tool(fn, name=name, description=description)
274
+ self._tool_manager.add_tool_from_fn(
275
+ fn, name=name, description=description, tags=tags
276
+ )
229
277
 
230
278
  def tool(
231
- self, name: str | None = None, description: str | None = None
232
- ) -> Callable[[Callable[P, R]], Callable[P, R]]:
279
+ self,
280
+ name: str | None = None,
281
+ description: str | None = None,
282
+ tags: set[str] | None = None,
283
+ ) -> Callable[[AnyFunction], AnyFunction]:
233
284
  """Decorator to register a tool.
234
285
 
235
- Tools can optionally request a Context object by adding a parameter with the Context type annotation.
236
- The context provides access to MCP capabilities like logging, progress reporting, and resource access.
286
+ Tools can optionally request a Context object by adding a parameter with the
287
+ Context type annotation. The context provides access to MCP capabilities like
288
+ logging, progress reporting, and resource access.
237
289
 
238
290
  Args:
239
291
  name: Optional name for the tool (defaults to function name)
240
292
  description: Optional description of what the tool does
293
+ tags: Optional set of tags for categorizing the tool
241
294
 
242
295
  Example:
243
296
  @server.tool()
@@ -261,8 +314,8 @@ class FastMCP:
261
314
  "Did you forget to call it? Use @tool() instead of @tool"
262
315
  )
263
316
 
264
- def decorator(fn: Callable[P, R]) -> Callable[P, R]:
265
- self.add_tool(fn, name=name, description=description)
317
+ def decorator(fn: AnyFunction) -> AnyFunction:
318
+ self.add_tool(fn, name=name, description=description, tags=tags)
266
319
  return fn
267
320
 
268
321
  return decorator
@@ -282,7 +335,8 @@ class FastMCP:
282
335
  name: str | None = None,
283
336
  description: str | None = None,
284
337
  mime_type: str | None = None,
285
- ) -> Callable[[Callable[P, R]], Callable[P, R]]:
338
+ tags: set[str] | None = None,
339
+ ) -> Callable[[AnyFunction], AnyFunction]:
286
340
  """Decorator to register a function as a resource.
287
341
 
288
342
  The function will be called when the resource is read to generate its content.
@@ -299,15 +353,26 @@ class FastMCP:
299
353
  name: Optional name for the resource
300
354
  description: Optional description of the resource
301
355
  mime_type: Optional MIME type for the resource
356
+ tags: Optional set of tags for categorizing the resource
302
357
 
303
358
  Example:
304
359
  @server.resource("resource://my-resource")
305
360
  def get_data() -> str:
306
361
  return "Hello, world!"
307
362
 
363
+ @server.resource("resource://my-resource")
364
+ async get_data() -> str:
365
+ data = await fetch_data()
366
+ return f"Hello, world! {data}"
367
+
308
368
  @server.resource("resource://{city}/weather")
309
369
  def get_weather(city: str) -> str:
310
370
  return f"Weather for {city}"
371
+
372
+ @server.resource("resource://{city}/weather")
373
+ async def get_weather(city: str) -> str:
374
+ data = await fetch_weather(city)
375
+ return f"Weather for {city}: {data}"
311
376
  """
312
377
  # Check if user passed function directly instead of calling decorator
313
378
  if callable(uri):
@@ -316,11 +381,7 @@ class FastMCP:
316
381
  "Did you forget to call it? Use @resource('uri') instead of @resource"
317
382
  )
318
383
 
319
- def decorator(fn: Callable[P, R]) -> Callable[P, R]:
320
- @functools.wraps(fn)
321
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
322
- return fn(*args, **kwargs)
323
-
384
+ def decorator(fn: AnyFunction) -> AnyFunction:
324
385
  # Check if this should be a template
325
386
  has_uri_params = "{" in uri and "}" in uri
326
387
  has_func_params = bool(inspect.signature(fn).parameters)
@@ -337,12 +398,13 @@ class FastMCP:
337
398
  )
338
399
 
339
400
  # Register as template
340
- self._resource_manager.add_template(
341
- wrapper,
401
+ self._resource_manager.add_template_from_fn(
402
+ fn=fn,
342
403
  uri_template=uri,
343
404
  name=name,
344
405
  description=description,
345
406
  mime_type=mime_type or "text/plain",
407
+ tags=tags,
346
408
  )
347
409
  else:
348
410
  # Register as regular resource
@@ -351,10 +413,11 @@ class FastMCP:
351
413
  name=name,
352
414
  description=description,
353
415
  mime_type=mime_type or "text/plain",
354
- fn=wrapper,
416
+ fn=fn,
417
+ tags=tags or set(), # Default to empty set if None
355
418
  )
356
419
  self.add_resource(resource)
357
- return wrapper
420
+ return fn
358
421
 
359
422
  return decorator
360
423
 
@@ -367,13 +430,17 @@ class FastMCP:
367
430
  self._prompt_manager.add_prompt(prompt)
368
431
 
369
432
  def prompt(
370
- self, name: str | None = None, description: str | None = None
371
- ) -> Callable[[Callable[P, R_PromptResult]], Callable[P, R_PromptResult]]:
433
+ self,
434
+ name: str | None = None,
435
+ description: str | None = None,
436
+ tags: set[str] | None = None,
437
+ ) -> Callable[[AnyFunction], AnyFunction]:
372
438
  """Decorator to register a prompt.
373
439
 
374
440
  Args:
375
441
  name: Optional name for the prompt (defaults to function name)
376
442
  description: Optional description of what the prompt does
443
+ tags: Optional set of tags for categorizing the prompt
377
444
 
378
445
  Example:
379
446
  @server.prompt()
@@ -409,8 +476,10 @@ class FastMCP:
409
476
  "Did you forget to call it? Use @prompt() instead of @prompt"
410
477
  )
411
478
 
412
- def decorator(func: Callable[P, R_PromptResult]) -> Callable[P, R_PromptResult]:
413
- prompt = Prompt.from_function(func, name=name, description=description)
479
+ def decorator(func: AnyFunction) -> AnyFunction:
480
+ prompt = Prompt.from_function(
481
+ func, name=name, description=description, tags=tags
482
+ )
414
483
  self.add_prompt(prompt)
415
484
  return func
416
485
 
@@ -427,14 +496,26 @@ class FastMCP:
427
496
 
428
497
  async def run_sse_async(self) -> None:
429
498
  """Run the server using SSE transport."""
430
- from starlette.applications import Starlette
431
- from starlette.routing import Route, Mount
499
+ starlette_app = self.sse_app()
500
+
501
+ config = uvicorn.Config(
502
+ starlette_app,
503
+ host=self.settings.host,
504
+ port=self.settings.port,
505
+ log_level=self.settings.log_level.lower(),
506
+ )
507
+ server = uvicorn.Server(config)
508
+ await server.serve()
432
509
 
433
- sse = SseServerTransport("/messages/")
510
+ def sse_app(self) -> Starlette:
511
+ """Return an instance of the SSE server app."""
512
+ sse = SseServerTransport(self.settings.message_path)
434
513
 
435
- async def handle_sse(request):
514
+ async def handle_sse(request: Request) -> None:
436
515
  async with sse.connect_sse(
437
- request.scope, request.receive, request._send
516
+ request.scope,
517
+ request.receive,
518
+ request._send, # type: ignore[reportPrivateUsage]
438
519
  ) as streams:
439
520
  await self._mcp_server.run(
440
521
  streams[0],
@@ -442,23 +523,14 @@ class FastMCP:
442
523
  self._mcp_server.create_initialization_options(),
443
524
  )
444
525
 
445
- starlette_app = Starlette(
526
+ return Starlette(
446
527
  debug=self.settings.debug,
447
528
  routes=[
448
- Route("/sse", endpoint=handle_sse),
449
- Mount("/messages/", app=sse.handle_post_message),
529
+ Route(self.settings.sse_path, endpoint=handle_sse),
530
+ Mount(self.settings.message_path, app=sse.handle_post_message),
450
531
  ],
451
532
  )
452
533
 
453
- config = uvicorn.Config(
454
- starlette_app,
455
- host=self.settings.host,
456
- port=self.settings.port,
457
- log_level=self.settings.log_level.lower(),
458
- )
459
- server = uvicorn.Server(config)
460
- await server.serve()
461
-
462
534
  async def list_prompts(self) -> list[MCPPrompt]:
463
535
  """List all available prompts."""
464
536
  prompts = self._prompt_manager.list_prompts()
@@ -479,7 +551,7 @@ class FastMCP:
479
551
  ]
480
552
 
481
553
  async def get_prompt(
482
- self, name: str, arguments: Dict[str, Any] | None = None
554
+ self, name: str, arguments: dict[str, Any] | None = None
483
555
  ) -> GetPromptResult:
484
556
  """Get a prompt by name with arguments."""
485
557
  try:
@@ -490,182 +562,166 @@ class FastMCP:
490
562
  logger.error(f"Error getting prompt {name}: {e}")
491
563
  raise ValueError(str(e))
492
564
 
565
+ def mount(
566
+ self,
567
+ prefix: str,
568
+ app: "FastMCP",
569
+ tool_separator: str | None = None,
570
+ resource_separator: str | None = None,
571
+ prompt_separator: str | None = None,
572
+ ) -> None:
573
+ """Mount another FastMCP application with a given prefix.
574
+
575
+ When an application is mounted:
576
+ - The tools are imported with prefixed names using the tool_separator
577
+ Example: If app has a tool named "get_weather", it will be available as "weatherget_weather"
578
+ - The resources are imported with prefixed URIs using the resource_separator
579
+ Example: If app has a resource with URI "weather://forecast", it will be available as "weather+weather://forecast"
580
+ - The templates are imported with prefixed URI templates using the resource_separator
581
+ Example: If app has a template with URI "weather://location/{id}", it will be available as "weather+weather://location/{id}"
582
+ - The prompts are imported with prefixed names using the prompt_separator
583
+ Example: If app has a prompt named "weather_prompt", it will be available as "weather_weather_prompt"
584
+ - The mounted app's lifespan will be executed when the parent app's lifespan runs,
585
+ ensuring that any setup needed by the mounted app is performed
493
586
 
494
- def _convert_to_content(
495
- result: Any,
496
- ) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
497
- """Convert a result to a sequence of content objects."""
498
- if result is None:
499
- return []
500
-
501
- if isinstance(result, (TextContent, ImageContent, EmbeddedResource)):
502
- return [result]
503
-
504
- if isinstance(result, Image):
505
- return [result.to_image_content()]
506
-
507
- if isinstance(result, (list, tuple)):
508
- return list(chain.from_iterable(_convert_to_content(item) for item in result))
509
-
510
- if not isinstance(result, str):
511
- try:
512
- result = json.dumps(pydantic_core.to_jsonable_python(result))
513
- except Exception:
514
- result = str(result)
515
-
516
- return [TextContent(type="text", text=result)]
517
-
518
-
519
- class Context(BaseModel):
520
- """Context object providing access to MCP capabilities.
521
-
522
- This provides a cleaner interface to MCP's RequestContext functionality.
523
- It gets injected into tool and resource functions that request it via type hints.
524
-
525
- To use context in a tool function, add a parameter with the Context type annotation:
526
-
527
- ```python
528
- @server.tool()
529
- def my_tool(x: int, ctx: Context) -> str:
530
- # Log messages to the client
531
- ctx.info(f"Processing {x}")
532
- ctx.debug("Debug info")
533
- ctx.warning("Warning message")
534
- ctx.error("Error message")
587
+ Args:
588
+ prefix: The prefix to use for the mounted application
589
+ app: The FastMCP application to mount
590
+ tool_separator: Separator for tool names (defaults to "_")
591
+ resource_separator: Separator for resource URIs (defaults to "+")
592
+ prompt_separator: Separator for prompt names (defaults to "_")
593
+ """
594
+ if tool_separator is None:
595
+ tool_separator = "_"
596
+ if resource_separator is None:
597
+ resource_separator = "+"
598
+ if prompt_separator is None:
599
+ prompt_separator = "_"
600
+
601
+ # Mount the app in the list of mounted apps
602
+ self._mounted_apps[prefix] = app
603
+
604
+ # Import tools from the mounted app
605
+ tool_prefix = f"{prefix}{tool_separator}"
606
+ self._tool_manager.import_tools(app._tool_manager, tool_prefix)
607
+
608
+ # Import resources and templates from the mounted app
609
+ resource_prefix = f"{prefix}{resource_separator}"
610
+ self._resource_manager.import_resources(app._resource_manager, resource_prefix)
611
+ self._resource_manager.import_templates(app._resource_manager, resource_prefix)
612
+
613
+ # Import prompts from the mounted app
614
+ prompt_prefix = f"{prefix}{prompt_separator}"
615
+ self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix)
616
+
617
+ logger.info(f"Mounted app with prefix '{prefix}'")
618
+ logger.debug(f"Imported tools with prefix '{tool_prefix}'")
619
+ logger.debug(f"Imported resources with prefix '{resource_prefix}'")
620
+ logger.debug(f"Imported templates with prefix '{resource_prefix}'")
621
+ logger.debug(f"Imported prompts with prefix '{prompt_prefix}'")
622
+
623
+ @classmethod
624
+ async def as_proxy(
625
+ cls, client: "Client | FastMCP", **settings: Any
626
+ ) -> "FastMCPProxy":
627
+ """
628
+ Create a FastMCP proxy server from a client.
535
629
 
536
- # Report progress
537
- ctx.report_progress(50, 100)
630
+ This method creates a new FastMCP server instance that proxies requests to the provided client.
631
+ It discovers the client's tools, resources, prompts, and templates, and creates corresponding
632
+ components in the server that forward requests to the client.
538
633
 
539
- # Access resources
540
- data = ctx.read_resource("resource://data")
634
+ Args:
635
+ client: The client to proxy requests to
636
+ **settings: Additional settings for the FastMCP server
541
637
 
542
- # Get request info
543
- request_id = ctx.request_id
544
- client_id = ctx.client_id
638
+ Returns:
639
+ A FastMCP server that proxies requests to the client
640
+ """
641
+ from fastmcp.client import Client
545
642
 
546
- return str(x)
547
- ```
643
+ from .proxy import FastMCPProxy
548
644
 
549
- The context parameter name can be anything as long as it's annotated with Context.
550
- The context is optional - tools that don't need it can omit the parameter.
551
- """
645
+ if isinstance(client, Client):
646
+ return await FastMCPProxy.from_client(client=client, **settings)
552
647
 
553
- _request_context: RequestContext | None
554
- _fastmcp: FastMCP | None
648
+ elif isinstance(client, FastMCP):
649
+ return await FastMCPProxy.from_server(server=client, **settings)
555
650
 
556
- def __init__(
557
- self,
558
- *,
559
- request_context: RequestContext | None = None,
560
- fastmcp: FastMCP | None = None,
561
- **kwargs: Any,
562
- ):
563
- super().__init__(**kwargs)
564
- self._request_context = request_context
565
- self._fastmcp = fastmcp
651
+ else:
652
+ raise ValueError(f"Unknown client type: {type(client)}")
566
653
 
567
- @property
568
- def fastmcp(self) -> FastMCP:
569
- """Access to the FastMCP server."""
570
- if self._fastmcp is None:
571
- raise ValueError("Context is not available outside of a request")
572
- return self._fastmcp
654
+ @classmethod
655
+ def from_openapi(
656
+ cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, **settings: Any
657
+ ) -> "FastMCPOpenAPI":
658
+ """
659
+ Create a FastMCP server from an OpenAPI specification.
660
+ """
661
+ from .openapi import FastMCPOpenAPI
573
662
 
574
- @property
575
- def request_context(self) -> RequestContext:
576
- """Access to the underlying request context."""
577
- if self._request_context is None:
578
- raise ValueError("Context is not available outside of a request")
579
- return self._request_context
580
-
581
- async def report_progress(
582
- self, progress: float, total: float | None = None
583
- ) -> None:
584
- """Report progress for the current operation.
663
+ return FastMCPOpenAPI(openapi_spec=openapi_spec, client=client, **settings)
585
664
 
586
- Args:
587
- progress: Current progress value e.g. 24
588
- total: Optional total value e.g. 100
665
+ @classmethod
666
+ def from_fastapi(
667
+ cls, app: FastAPI, name: str | None = None, **settings: Any
668
+ ) -> "FastMCPOpenAPI":
669
+ """
670
+ Create a FastMCP server from a FastAPI application.
589
671
  """
672
+ from .openapi import FastMCPOpenAPI
590
673
 
591
- progress_token = (
592
- self.request_context.meta.progressToken
593
- if self.request_context.meta
594
- else None
674
+ client = httpx.AsyncClient(
675
+ transport=httpx.ASGITransport(app=app), base_url="http://fastapi"
595
676
  )
596
677
 
597
- if not progress_token:
598
- return
678
+ name = name or app.title
599
679
 
600
- await self.request_context.session.send_progress_notification(
601
- progress_token=progress_token, progress=progress, total=total
680
+ return FastMCPOpenAPI(
681
+ openapi_spec=app.openapi(), client=client, name=name, **settings
602
682
  )
603
683
 
604
- async def read_resource(self, uri: str | AnyUrl) -> str | bytes:
605
- """Read a resource by URI.
606
684
 
607
- Args:
608
- uri: Resource URI to read
685
+ def _convert_to_content(
686
+ result: Any,
687
+ _process_as_single_item: bool = False,
688
+ ) -> list[TextContent | ImageContent | EmbeddedResource]:
689
+ """Convert a result to a sequence of content objects."""
690
+ if result is None:
691
+ return []
609
692
 
610
- Returns:
611
- The resource content as either text or bytes
612
- """
613
- assert (
614
- self._fastmcp is not None
615
- ), "Context is not available outside of a request"
616
- return await self._fastmcp.read_resource(uri)
693
+ if isinstance(result, TextContent | ImageContent | EmbeddedResource):
694
+ return [result]
617
695
 
618
- def log(
619
- self,
620
- level: Literal["debug", "info", "warning", "error"],
621
- message: str,
622
- *,
623
- logger_name: str | None = None,
624
- ) -> None:
625
- """Send a log message to the client.
696
+ if isinstance(result, Image):
697
+ return [result.to_image_content()]
626
698
 
627
- Args:
628
- level: Log level (debug, info, warning, error)
629
- message: Log message
630
- logger_name: Optional logger name
631
- **extra: Additional structured data to include
632
- """
633
- self.request_context.session.send_log_message(
634
- level=level, data=message, logger=logger_name
635
- )
699
+ if isinstance(result, list | tuple) and not _process_as_single_item:
700
+ # if the result is a list, then it could either be a list of MCP types,
701
+ # or a "regular" list that the tool is returning, or a mix of both.
702
+ #
703
+ # so we extract all the MCP types / images and convert them as individual content elements,
704
+ # and aggregate the rest as a single content element
636
705
 
637
- @property
638
- def client_id(self) -> str | None:
639
- """Get the client ID if available."""
640
- return (
641
- getattr(self.request_context.meta, "client_id", None)
642
- if self.request_context.meta
643
- else None
644
- )
706
+ mcp_types = []
707
+ other_content = []
645
708
 
646
- @property
647
- def request_id(self) -> str:
648
- """Get the unique ID for this request."""
649
- return str(self.request_context.request_id)
709
+ for item in result:
710
+ if isinstance(item, TextContent | ImageContent | EmbeddedResource | Image):
711
+ mcp_types.append(_convert_to_content(item)[0])
712
+ else:
713
+ other_content.append(item)
714
+ if other_content:
715
+ other_content = _convert_to_content(
716
+ other_content, _process_as_single_item=True
717
+ )
650
718
 
651
- @property
652
- def session(self):
653
- """Access to the underlying session for advanced usage."""
654
- return self.request_context.session
655
-
656
- # Convenience methods for common log levels
657
- def debug(self, message: str, **extra: Any) -> None:
658
- """Send a debug log message."""
659
- self.log("debug", message, **extra)
660
-
661
- def info(self, message: str, **extra: Any) -> None:
662
- """Send an info log message."""
663
- self.log("info", message, **extra)
664
-
665
- def warning(self, message: str, **extra: Any) -> None:
666
- """Send a warning log message."""
667
- self.log("warning", message, **extra)
668
-
669
- def error(self, message: str, **extra: Any) -> None:
670
- """Send an error log message."""
671
- self.log("error", message, **extra)
719
+ return other_content + mcp_types
720
+
721
+ if not isinstance(result, str):
722
+ try:
723
+ result = json.dumps(pydantic_core.to_jsonable_python(result))
724
+ except Exception:
725
+ result = str(result)
726
+
727
+ return [TextContent(type="text", text=result)]