fastmcp 1.0__py3-none-any.whl → 2.0.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.
@@ -1,98 +1,92 @@
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, Sequence
7
+ from contextlib import (
8
+ AbstractAsyncContextManager,
9
+ asynccontextmanager,
10
+ )
11
+ from typing import TYPE_CHECKING, Any, Generic, Literal
10
12
 
13
+ import anyio
14
+ import httpx
11
15
  import pydantic_core
12
- from pydantic import Field
13
16
  import uvicorn
14
- from mcp.server import Server as MCPServer
17
+ from fastapi import FastAPI
18
+ from mcp.server.lowlevel.helper_types import ReadResourceContents
19
+ from mcp.server.lowlevel.server import LifespanResultT
20
+ from mcp.server.lowlevel.server import Server as MCPServer
21
+ from mcp.server.lowlevel.server import lifespan as default_lifespan
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
+ def lifespan_wrapper(
60
+ app: "FastMCP",
61
+ lifespan: Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]],
62
+ ) -> Callable[
63
+ [MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
64
+ ]:
65
+ @asynccontextmanager
66
+ async def wrap(s: MCPServer[LifespanResultT]) -> AsyncIterator[LifespanResultT]:
67
+ async with lifespan(app) as context:
68
+ yield context
55
69
 
56
- class Settings(BaseSettings):
57
- """FastMCP server settings.
70
+ return wrap
58
71
 
59
- All settings can be configured via environment variables with the prefix FASTMCP_.
60
- For example, FASTMCP_DEBUG=true will set debug=True.
61
- """
62
-
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
76
-
77
- # resource settings
78
- warn_on_duplicate_resources: bool = True
79
-
80
- # tool settings
81
- warn_on_duplicate_tools: bool = True
82
-
83
- # prompt settings
84
- warn_on_duplicate_prompts: bool = True
85
-
86
- dependencies: list[str] = Field(
87
- default_factory=list,
88
- description="List of dependencies to install in the server environment",
89
- )
90
72
 
73
+ class FastMCP(Generic[LifespanResultT]):
74
+ def __init__(
75
+ self,
76
+ name: str | None = None,
77
+ instructions: str | None = None,
78
+ lifespan: (
79
+ Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]] | None
80
+ ) = None,
81
+ **settings: Any,
82
+ ):
83
+ self.settings = fastmcp.settings.ServerSettings(**settings)
91
84
 
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")
85
+ self._mcp_server = MCPServer[LifespanResultT](
86
+ name=name or "FastMCP",
87
+ instructions=instructions,
88
+ lifespan=lifespan_wrapper(self, lifespan) if lifespan else default_lifespan, # type: ignore
89
+ )
96
90
  self._tool_manager = ToolManager(
97
91
  warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools
98
92
  )
@@ -104,6 +98,9 @@ class FastMCP:
104
98
  )
105
99
  self.dependencies = self.settings.dependencies
106
100
 
101
+ # Setup for mounted apps
102
+ self._mounted_apps: dict[str, FastMCP] = {}
103
+
107
104
  # Set up MCP protocol handlers
108
105
  self._setup_handlers()
109
106
 
@@ -114,20 +111,33 @@ class FastMCP:
114
111
  def name(self) -> str:
115
112
  return self._mcp_server.name
116
113
 
117
- def run(self, transport: Literal["stdio", "sse"] = "stdio") -> None:
118
- """Run the FastMCP server. Note this is a synchronous function.
114
+ @property
115
+ def instructions(self) -> str | None:
116
+ return self._mcp_server.instructions
117
+
118
+ async def run_async(self, transport: Literal["stdio", "sse"] | None = None) -> None:
119
+ """Run the FastMCP server asynchronously.
119
120
 
120
121
  Args:
121
122
  transport: Transport protocol to use ("stdio" or "sse")
122
123
  """
123
- TRANSPORTS = Literal["stdio", "sse"]
124
- if transport not in TRANSPORTS.__args__: # type: ignore
124
+ if transport is None:
125
+ transport = "stdio"
126
+ if transport not in ["stdio", "sse"]:
125
127
  raise ValueError(f"Unknown transport: {transport}")
126
128
 
127
129
  if transport == "stdio":
128
- asyncio.run(self.run_stdio_async())
130
+ await self.run_stdio_async()
129
131
  else: # transport == "sse"
130
- asyncio.run(self.run_sse_async())
132
+ await self.run_sse_async()
133
+
134
+ def run(self, transport: Literal["stdio", "sse"] | None = None) -> None:
135
+ """Run the FastMCP server. Note this is a synchronous function.
136
+
137
+ Args:
138
+ transport: Transport protocol to use ("stdio" or "sse")
139
+ """
140
+ anyio.run(self.run_async, transport)
131
141
 
132
142
  def _setup_handlers(self) -> None:
133
143
  """Set up core MCP protocol handlers."""
@@ -137,8 +147,7 @@ class FastMCP:
137
147
  self._mcp_server.read_resource()(self.read_resource)
138
148
  self._mcp_server.list_prompts()(self.list_prompts)
139
149
  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)
150
+ self._mcp_server.list_resource_templates()(self.list_resource_templates)
142
151
 
143
152
  async def list_tools(self) -> list[MCPTool]:
144
153
  """List all available tools."""
@@ -152,19 +161,22 @@ class FastMCP:
152
161
  for info in tools
153
162
  ]
154
163
 
155
- def get_context(self) -> "Context":
164
+ def get_context(self) -> "Context[ServerSession, LifespanResultT]":
156
165
  """
157
166
  Returns a Context object. Note that the context will only be valid
158
167
  during a request; outside a request, most methods will error.
159
168
  """
169
+
160
170
  try:
161
171
  request_context = self._mcp_server.request_context
162
172
  except LookupError:
163
173
  request_context = None
174
+ from fastmcp.server.context import Context
175
+
164
176
  return Context(request_context=request_context, fastmcp=self)
165
177
 
166
178
  async def call_tool(
167
- self, name: str, arguments: dict
179
+ self, name: str, arguments: dict[str, Any]
168
180
  ) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
169
181
  """Call a tool by name with arguments."""
170
182
  context = self.get_context()
@@ -197,21 +209,23 @@ class FastMCP:
197
209
  for template in templates
198
210
  ]
199
211
 
200
- async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
212
+ async def read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
201
213
  """Read a resource by URI."""
214
+
202
215
  resource = await self._resource_manager.get_resource(uri)
203
216
  if not resource:
204
217
  raise ResourceError(f"Unknown resource: {uri}")
205
218
 
206
219
  try:
207
- return await resource.read()
220
+ content = await resource.read()
221
+ return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
208
222
  except Exception as e:
209
223
  logger.error(f"Error reading resource {uri}: {e}")
210
224
  raise ResourceError(str(e))
211
225
 
212
226
  def add_tool(
213
227
  self,
214
- fn: Callable,
228
+ fn: AnyFunction,
215
229
  name: str | None = None,
216
230
  description: str | None = None,
217
231
  ) -> None:
@@ -229,11 +243,12 @@ class FastMCP:
229
243
 
230
244
  def tool(
231
245
  self, name: str | None = None, description: str | None = None
232
- ) -> Callable[[Callable[P, R]], Callable[P, R]]:
246
+ ) -> Callable[[AnyFunction], AnyFunction]:
233
247
  """Decorator to register a tool.
234
248
 
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.
249
+ Tools can optionally request a Context object by adding a parameter with the
250
+ Context type annotation. The context provides access to MCP capabilities like
251
+ logging, progress reporting, and resource access.
237
252
 
238
253
  Args:
239
254
  name: Optional name for the tool (defaults to function name)
@@ -261,7 +276,7 @@ class FastMCP:
261
276
  "Did you forget to call it? Use @tool() instead of @tool"
262
277
  )
263
278
 
264
- def decorator(fn: Callable[P, R]) -> Callable[P, R]:
279
+ def decorator(fn: AnyFunction) -> AnyFunction:
265
280
  self.add_tool(fn, name=name, description=description)
266
281
  return fn
267
282
 
@@ -282,7 +297,7 @@ class FastMCP:
282
297
  name: str | None = None,
283
298
  description: str | None = None,
284
299
  mime_type: str | None = None,
285
- ) -> Callable[[Callable[P, R]], Callable[P, R]]:
300
+ ) -> Callable[[AnyFunction], AnyFunction]:
286
301
  """Decorator to register a function as a resource.
287
302
 
288
303
  The function will be called when the resource is read to generate its content.
@@ -305,9 +320,19 @@ class FastMCP:
305
320
  def get_data() -> str:
306
321
  return "Hello, world!"
307
322
 
323
+ @server.resource("resource://my-resource")
324
+ async get_data() -> str:
325
+ data = await fetch_data()
326
+ return f"Hello, world! {data}"
327
+
308
328
  @server.resource("resource://{city}/weather")
309
329
  def get_weather(city: str) -> str:
310
330
  return f"Weather for {city}"
331
+
332
+ @server.resource("resource://{city}/weather")
333
+ async def get_weather(city: str) -> str:
334
+ data = await fetch_weather(city)
335
+ return f"Weather for {city}: {data}"
311
336
  """
312
337
  # Check if user passed function directly instead of calling decorator
313
338
  if callable(uri):
@@ -316,11 +341,7 @@ class FastMCP:
316
341
  "Did you forget to call it? Use @resource('uri') instead of @resource"
317
342
  )
318
343
 
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
-
344
+ def decorator(fn: AnyFunction) -> AnyFunction:
324
345
  # Check if this should be a template
325
346
  has_uri_params = "{" in uri and "}" in uri
326
347
  has_func_params = bool(inspect.signature(fn).parameters)
@@ -338,7 +359,7 @@ class FastMCP:
338
359
 
339
360
  # Register as template
340
361
  self._resource_manager.add_template(
341
- wrapper,
362
+ fn=fn,
342
363
  uri_template=uri,
343
364
  name=name,
344
365
  description=description,
@@ -351,10 +372,10 @@ class FastMCP:
351
372
  name=name,
352
373
  description=description,
353
374
  mime_type=mime_type or "text/plain",
354
- fn=wrapper,
375
+ fn=fn,
355
376
  )
356
377
  self.add_resource(resource)
357
- return wrapper
378
+ return fn
358
379
 
359
380
  return decorator
360
381
 
@@ -368,7 +389,7 @@ class FastMCP:
368
389
 
369
390
  def prompt(
370
391
  self, name: str | None = None, description: str | None = None
371
- ) -> Callable[[Callable[P, R_PromptResult]], Callable[P, R_PromptResult]]:
392
+ ) -> Callable[[AnyFunction], AnyFunction]:
372
393
  """Decorator to register a prompt.
373
394
 
374
395
  Args:
@@ -409,7 +430,7 @@ class FastMCP:
409
430
  "Did you forget to call it? Use @prompt() instead of @prompt"
410
431
  )
411
432
 
412
- def decorator(func: Callable[P, R_PromptResult]) -> Callable[P, R_PromptResult]:
433
+ def decorator(func: AnyFunction) -> AnyFunction:
413
434
  prompt = Prompt.from_function(func, name=name, description=description)
414
435
  self.add_prompt(prompt)
415
436
  return func
@@ -427,14 +448,26 @@ class FastMCP:
427
448
 
428
449
  async def run_sse_async(self) -> None:
429
450
  """Run the server using SSE transport."""
430
- from starlette.applications import Starlette
431
- from starlette.routing import Route, Mount
451
+ starlette_app = self.sse_app()
452
+
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()
432
461
 
433
- sse = SseServerTransport("/messages/")
462
+ def sse_app(self) -> Starlette:
463
+ """Return an instance of the SSE server app."""
464
+ sse = SseServerTransport(self.settings.message_path)
434
465
 
435
- async def handle_sse(request):
466
+ async def handle_sse(request: Request) -> None:
436
467
  async with sse.connect_sse(
437
- request.scope, request.receive, request._send
468
+ request.scope,
469
+ request.receive,
470
+ request._send, # type: ignore[reportPrivateUsage]
438
471
  ) as streams:
439
472
  await self._mcp_server.run(
440
473
  streams[0],
@@ -442,23 +475,14 @@ class FastMCP:
442
475
  self._mcp_server.create_initialization_options(),
443
476
  )
444
477
 
445
- starlette_app = Starlette(
478
+ return Starlette(
446
479
  debug=self.settings.debug,
447
480
  routes=[
448
- Route("/sse", endpoint=handle_sse),
449
- Mount("/messages/", app=sse.handle_post_message),
481
+ Route(self.settings.sse_path, endpoint=handle_sse),
482
+ Mount(self.settings.message_path, app=sse.handle_post_message),
450
483
  ],
451
484
  )
452
485
 
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
486
  async def list_prompts(self) -> list[MCPPrompt]:
463
487
  """List all available prompts."""
464
488
  prompts = self._prompt_manager.list_prompts()
@@ -479,7 +503,7 @@ class FastMCP:
479
503
  ]
480
504
 
481
505
  async def get_prompt(
482
- self, name: str, arguments: Dict[str, Any] | None = None
506
+ self, name: str, arguments: dict[str, Any] | None = None
483
507
  ) -> GetPromptResult:
484
508
  """Get a prompt by name with arguments."""
485
509
  try:
@@ -490,182 +514,147 @@ class FastMCP:
490
514
  logger.error(f"Error getting prompt {name}: {e}")
491
515
  raise ValueError(str(e))
492
516
 
517
+ def mount(self, prefix: str, app: "FastMCP") -> None:
518
+ """Mount another FastMCP application with a given prefix.
493
519
 
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]
520
+ When an application is mounted:
521
+ - The tools are imported with prefixed names
522
+ Example: If app has a tool named "get_weather", it will be available as "weather/get_weather"
523
+ - The resources are imported with prefixed URIs
524
+ Example: If app has a resource with URI "weather://forecast", it will be available as "weather+weather://forecast"
525
+ - The templates are imported with prefixed URI templates
526
+ Example: If app has a template with URI "weather://location/{id}", it will be available as "weather+weather://location/{id}"
527
+ - The prompts are imported with prefixed names
528
+ Example: If app has a prompt named "weather_prompt", it will be available as "weather/weather_prompt"
503
529
 
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")
530
+ Args:
531
+ prefix: The prefix to use for the mounted application
532
+ app: The FastMCP application to mount
533
+ """
534
+ # Mount the app in the list of mounted apps
535
+ self._mounted_apps[prefix] = app
536
+
537
+ # Import tools from the mounted app with / delimiter
538
+ tool_prefix = f"{prefix}/"
539
+ self._tool_manager.import_tools(app._tool_manager, tool_prefix)
540
+
541
+ # Import resources and templates from the mounted app with + delimiter
542
+ resource_prefix = f"{prefix}+"
543
+ self._resource_manager.import_resources(app._resource_manager, resource_prefix)
544
+ self._resource_manager.import_templates(app._resource_manager, resource_prefix)
545
+
546
+ # Import prompts with / delimiter
547
+ prompt_prefix = f"{prefix}/"
548
+ self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix)
549
+
550
+ logger.info(f"Mounted app with prefix '{prefix}'")
551
+ logger.debug(f"Imported tools with prefix '{tool_prefix}'")
552
+ logger.debug(f"Imported resources with prefix '{resource_prefix}'")
553
+ logger.debug(f"Imported templates with prefix '{resource_prefix}'")
554
+ logger.debug(f"Imported prompts with prefix '{prompt_prefix}'")
555
+
556
+ @classmethod
557
+ async def as_proxy(
558
+ cls, client: "Client | FastMCP", **settings: Any
559
+ ) -> "FastMCPProxy":
560
+ """
561
+ Create a FastMCP proxy server from a client.
535
562
 
536
- # Report progress
537
- ctx.report_progress(50, 100)
563
+ This method creates a new FastMCP server instance that proxies requests to the provided client.
564
+ It discovers the client's tools, resources, prompts, and templates, and creates corresponding
565
+ components in the server that forward requests to the client.
538
566
 
539
- # Access resources
540
- data = ctx.read_resource("resource://data")
567
+ Args:
568
+ client: The client to proxy requests to
569
+ **settings: Additional settings for the FastMCP server
541
570
 
542
- # Get request info
543
- request_id = ctx.request_id
544
- client_id = ctx.client_id
571
+ Returns:
572
+ A FastMCP server that proxies requests to the client
573
+ """
574
+ from fastmcp.client import Client
545
575
 
546
- return str(x)
547
- ```
576
+ from .proxy import FastMCPProxy
548
577
 
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
- """
578
+ if isinstance(client, Client):
579
+ return await FastMCPProxy.from_client(client=client, **settings)
552
580
 
553
- _request_context: RequestContext | None
554
- _fastmcp: FastMCP | None
581
+ elif isinstance(client, FastMCP):
582
+ return await FastMCPProxy.from_server(server=client, **settings)
555
583
 
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
584
+ else:
585
+ raise ValueError(f"Unknown client type: {type(client)}")
566
586
 
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
587
+ @classmethod
588
+ def from_openapi(
589
+ cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, **settings: Any
590
+ ) -> "FastMCPOpenAPI":
591
+ """
592
+ Create a FastMCP server from an OpenAPI specification.
593
+ """
594
+ from .openapi import FastMCPOpenAPI
573
595
 
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.
596
+ return FastMCPOpenAPI(openapi_spec=openapi_spec, client=client, **settings)
585
597
 
586
- Args:
587
- progress: Current progress value e.g. 24
588
- total: Optional total value e.g. 100
598
+ @classmethod
599
+ def from_fastapi(
600
+ cls, app: FastAPI, name: str | None = None, **settings: Any
601
+ ) -> "FastMCPOpenAPI":
589
602
  """
603
+ Create a FastMCP server from a FastAPI application.
604
+ """
605
+ from .openapi import FastMCPOpenAPI
590
606
 
591
- progress_token = (
592
- self.request_context.meta.progressToken
593
- if self.request_context.meta
594
- else None
607
+ client = httpx.AsyncClient(
608
+ transport=httpx.ASGITransport(app=app), base_url="http://fastapi"
595
609
  )
596
610
 
597
- if not progress_token:
598
- return
611
+ name = name or app.title
599
612
 
600
- await self.request_context.session.send_progress_notification(
601
- progress_token=progress_token, progress=progress, total=total
613
+ return FastMCPOpenAPI(
614
+ openapi_spec=app.openapi(), client=client, name=name, **settings
602
615
  )
603
616
 
604
- async def read_resource(self, uri: str | AnyUrl) -> str | bytes:
605
- """Read a resource by URI.
606
617
 
607
- Args:
608
- uri: Resource URI to read
618
+ def _convert_to_content(
619
+ result: Any,
620
+ _process_as_single_item: bool = False,
621
+ ) -> list[TextContent | ImageContent | EmbeddedResource]:
622
+ """Convert a result to a sequence of content objects."""
623
+ if result is None:
624
+ return []
609
625
 
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)
626
+ if isinstance(result, TextContent | ImageContent | EmbeddedResource):
627
+ return [result]
617
628
 
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.
629
+ if isinstance(result, Image):
630
+ return [result.to_image_content()]
626
631
 
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
- )
632
+ if isinstance(result, list | tuple) and not _process_as_single_item:
633
+ # if the result is a list, then it could either be a list of MCP types,
634
+ # or a "regular" list that the tool is returning, or a mix of both.
635
+ #
636
+ # so we extract all the MCP types / images and convert them as individual content elements,
637
+ # and aggregate the rest as a single content element
636
638
 
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
- )
639
+ mcp_types = []
640
+ other_content = []
645
641
 
646
- @property
647
- def request_id(self) -> str:
648
- """Get the unique ID for this request."""
649
- return str(self.request_context.request_id)
642
+ for item in result:
643
+ if isinstance(item, TextContent | ImageContent | EmbeddedResource | Image):
644
+ mcp_types.append(_convert_to_content(item)[0])
645
+ else:
646
+ other_content.append(item)
647
+ if other_content:
648
+ other_content = _convert_to_content(
649
+ other_content, _process_as_single_item=True
650
+ )
650
651
 
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)
652
+ return other_content + mcp_types
653
+
654
+ if not isinstance(result, str):
655
+ try:
656
+ result = json.dumps(pydantic_core.to_jsonable_python(result))
657
+ except Exception:
658
+ result = str(result)
659
+
660
+ return [TextContent(type="text", text=result)]