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