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.
- fastmcp/__init__.py +15 -4
- fastmcp/cli/__init__.py +0 -1
- fastmcp/cli/claude.py +13 -11
- fastmcp/cli/cli.py +59 -39
- fastmcp/client/__init__.py +25 -0
- fastmcp/client/base.py +1 -0
- fastmcp/client/client.py +226 -0
- fastmcp/client/roots.py +75 -0
- fastmcp/client/sampling.py +50 -0
- fastmcp/client/transports.py +411 -0
- fastmcp/prompts/__init__.py +2 -2
- fastmcp/prompts/{base.py → prompt.py} +47 -26
- fastmcp/prompts/prompt_manager.py +69 -15
- fastmcp/resources/__init__.py +6 -6
- fastmcp/resources/{base.py → resource.py} +21 -2
- fastmcp/resources/resource_manager.py +116 -17
- fastmcp/resources/{templates.py → template.py} +36 -11
- fastmcp/resources/types.py +18 -13
- fastmcp/server/__init__.py +5 -0
- fastmcp/server/context.py +222 -0
- fastmcp/server/openapi.py +637 -0
- fastmcp/server/proxy.py +223 -0
- fastmcp/{server.py → server/server.py} +323 -267
- fastmcp/settings.py +81 -0
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/{base.py → tool.py} +47 -18
- fastmcp/tools/tool_manager.py +57 -16
- fastmcp/utilities/func_metadata.py +33 -19
- fastmcp/utilities/openapi.py +797 -0
- fastmcp/utilities/types.py +15 -4
- fastmcp-2.1.0.dist-info/METADATA +770 -0
- fastmcp-2.1.0.dist-info/RECORD +39 -0
- fastmcp-2.1.0.dist-info/licenses/LICENSE +201 -0
- fastmcp/prompts/manager.py +0 -50
- fastmcp-1.0.dist-info/METADATA +0 -604
- fastmcp-1.0.dist-info/RECORD +0 -28
- fastmcp-1.0.dist-info/licenses/LICENSE +0 -21
- {fastmcp-1.0.dist-info → fastmcp-2.1.0.dist-info}/WHEEL +0 -0
- {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
|
|
9
|
-
from
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
63
|
+
Args:
|
|
64
|
+
server: The server instance this lifespan is managing
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
123
|
+
duplicate_behavior=self.settings.on_duplicate_tools
|
|
98
124
|
)
|
|
99
125
|
self._resource_manager = ResourceManager(
|
|
100
|
-
|
|
126
|
+
duplicate_behavior=self.settings.on_duplicate_resources
|
|
101
127
|
)
|
|
102
128
|
self._prompt_manager = PromptManager(
|
|
103
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
159
|
+
await self.run_stdio_async()
|
|
129
160
|
else: # transport == "sse"
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
) ->
|
|
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) ->
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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,
|
|
232
|
-
|
|
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
|
|
236
|
-
The context provides access to MCP capabilities like
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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.
|
|
341
|
-
|
|
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=
|
|
416
|
+
fn=fn,
|
|
417
|
+
tags=tags or set(), # Default to empty set if None
|
|
355
418
|
)
|
|
356
419
|
self.add_resource(resource)
|
|
357
|
-
return
|
|
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,
|
|
371
|
-
|
|
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:
|
|
413
|
-
prompt = Prompt.from_function(
|
|
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
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
526
|
+
return Starlette(
|
|
446
527
|
debug=self.settings.debug,
|
|
447
528
|
routes=[
|
|
448
|
-
Route(
|
|
449
|
-
Mount(
|
|
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:
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
537
|
-
|
|
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
|
-
|
|
540
|
-
|
|
634
|
+
Args:
|
|
635
|
+
client: The client to proxy requests to
|
|
636
|
+
**settings: Additional settings for the FastMCP server
|
|
541
637
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
638
|
+
Returns:
|
|
639
|
+
A FastMCP server that proxies requests to the client
|
|
640
|
+
"""
|
|
641
|
+
from fastmcp.client import Client
|
|
545
642
|
|
|
546
|
-
|
|
547
|
-
```
|
|
643
|
+
from .proxy import FastMCPProxy
|
|
548
644
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
"""
|
|
645
|
+
if isinstance(client, Client):
|
|
646
|
+
return await FastMCPProxy.from_client(client=client, **settings)
|
|
552
647
|
|
|
553
|
-
|
|
554
|
-
|
|
648
|
+
elif isinstance(client, FastMCP):
|
|
649
|
+
return await FastMCPProxy.from_server(server=client, **settings)
|
|
555
650
|
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
@
|
|
568
|
-
def
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
592
|
-
|
|
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
|
-
|
|
598
|
-
return
|
|
678
|
+
name = name or app.title
|
|
599
679
|
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
608
|
-
|
|
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
|
-
|
|
611
|
-
|
|
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
|
-
|
|
619
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
638
|
-
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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)]
|