fastmcp 2.0.0__py3-none-any.whl → 2.1.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 +4 -2
- fastmcp/client/client.py +80 -35
- fastmcp/client/transports.py +22 -0
- fastmcp/exceptions.py +4 -0
- fastmcp/prompts/__init__.py +2 -2
- fastmcp/prompts/{base.py → prompt.py} +29 -19
- fastmcp/prompts/prompt_manager.py +29 -12
- fastmcp/resources/__init__.py +3 -3
- fastmcp/resources/{base.py → resource.py} +20 -1
- fastmcp/resources/resource_manager.py +145 -19
- fastmcp/resources/{templates.py → template.py} +23 -3
- fastmcp/resources/types.py +2 -2
- fastmcp/server/context.py +1 -1
- fastmcp/server/openapi.py +16 -4
- fastmcp/server/proxy.py +31 -27
- fastmcp/server/server.py +247 -97
- fastmcp/settings.py +11 -3
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/{base.py → tool.py} +26 -4
- fastmcp/tools/tool_manager.py +22 -16
- fastmcp/utilities/decorators.py +101 -0
- fastmcp/utilities/func_metadata.py +4 -1
- fastmcp/utilities/openapi.py +671 -292
- fastmcp/utilities/types.py +12 -0
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.1.dist-info}/METADATA +72 -52
- fastmcp-2.1.1.dist-info/RECORD +40 -0
- fastmcp-2.0.0.dist-info/RECORD +0 -39
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"""FastMCP - A more ergonomic interface for MCP servers."""
|
|
2
2
|
|
|
3
|
-
import inspect
|
|
4
3
|
import json
|
|
5
|
-
import
|
|
6
|
-
from collections.abc import AsyncIterator, Callable, Sequence
|
|
4
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
7
5
|
from contextlib import (
|
|
8
6
|
AbstractAsyncContextManager,
|
|
7
|
+
AsyncExitStack,
|
|
9
8
|
asynccontextmanager,
|
|
10
9
|
)
|
|
11
10
|
from typing import TYPE_CHECKING, Any, Generic, Literal
|
|
@@ -18,7 +17,6 @@ from fastapi import FastAPI
|
|
|
18
17
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
19
18
|
from mcp.server.lowlevel.server import LifespanResultT
|
|
20
19
|
from mcp.server.lowlevel.server import Server as MCPServer
|
|
21
|
-
from mcp.server.lowlevel.server import lifespan as default_lifespan
|
|
22
20
|
from mcp.server.session import ServerSession
|
|
23
21
|
from mcp.server.sse import SseServerTransport
|
|
24
22
|
from mcp.server.stdio import stdio_server
|
|
@@ -43,8 +41,12 @@ import fastmcp
|
|
|
43
41
|
import fastmcp.settings
|
|
44
42
|
from fastmcp.exceptions import ResourceError
|
|
45
43
|
from fastmcp.prompts import Prompt, PromptManager
|
|
46
|
-
from fastmcp.
|
|
44
|
+
from fastmcp.prompts.prompt import Message, PromptResult
|
|
45
|
+
from fastmcp.resources import Resource, ResourceManager
|
|
46
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
47
47
|
from fastmcp.tools import ToolManager
|
|
48
|
+
from fastmcp.tools.tool import Tool
|
|
49
|
+
from fastmcp.utilities.decorators import DecoratedFunction
|
|
48
50
|
from fastmcp.utilities.logging import configure_logging, get_logger
|
|
49
51
|
from fastmcp.utilities.types import Image
|
|
50
52
|
|
|
@@ -56,6 +58,19 @@ if TYPE_CHECKING:
|
|
|
56
58
|
logger = get_logger(__name__)
|
|
57
59
|
|
|
58
60
|
|
|
61
|
+
@asynccontextmanager
|
|
62
|
+
async def default_lifespan(server: "FastMCP") -> AsyncIterator[Any]:
|
|
63
|
+
"""Default lifespan context manager that does nothing.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
server: The server instance this lifespan is managing
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
An empty context object
|
|
70
|
+
"""
|
|
71
|
+
yield {}
|
|
72
|
+
|
|
73
|
+
|
|
59
74
|
def lifespan_wrapper(
|
|
60
75
|
app: "FastMCP",
|
|
61
76
|
lifespan: Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]],
|
|
@@ -64,7 +79,18 @@ def lifespan_wrapper(
|
|
|
64
79
|
]:
|
|
65
80
|
@asynccontextmanager
|
|
66
81
|
async def wrap(s: MCPServer[LifespanResultT]) -> AsyncIterator[LifespanResultT]:
|
|
67
|
-
async with
|
|
82
|
+
async with AsyncExitStack() as stack:
|
|
83
|
+
# enter main app's lifespan
|
|
84
|
+
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
|
+
|
|
68
94
|
yield context
|
|
69
95
|
|
|
70
96
|
return wrap
|
|
@@ -78,29 +104,34 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
78
104
|
lifespan: (
|
|
79
105
|
Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]] | None
|
|
80
106
|
) = None,
|
|
107
|
+
tags: set[str] | None = None,
|
|
81
108
|
**settings: Any,
|
|
82
109
|
):
|
|
110
|
+
self.tags: set[str] = tags or set()
|
|
83
111
|
self.settings = fastmcp.settings.ServerSettings(**settings)
|
|
84
112
|
|
|
113
|
+
# Setup for mounted apps - must be initialized before _mcp_server
|
|
114
|
+
self._mounted_apps: dict[str, FastMCP] = {}
|
|
115
|
+
|
|
116
|
+
if lifespan is None:
|
|
117
|
+
lifespan = default_lifespan
|
|
118
|
+
|
|
85
119
|
self._mcp_server = MCPServer[LifespanResultT](
|
|
86
120
|
name=name or "FastMCP",
|
|
87
121
|
instructions=instructions,
|
|
88
|
-
lifespan=lifespan_wrapper(self, lifespan)
|
|
122
|
+
lifespan=lifespan_wrapper(self, lifespan),
|
|
89
123
|
)
|
|
90
124
|
self._tool_manager = ToolManager(
|
|
91
|
-
|
|
125
|
+
duplicate_behavior=self.settings.on_duplicate_tools
|
|
92
126
|
)
|
|
93
127
|
self._resource_manager = ResourceManager(
|
|
94
|
-
|
|
128
|
+
duplicate_behavior=self.settings.on_duplicate_resources
|
|
95
129
|
)
|
|
96
130
|
self._prompt_manager = PromptManager(
|
|
97
|
-
|
|
131
|
+
duplicate_behavior=self.settings.on_duplicate_prompts
|
|
98
132
|
)
|
|
99
133
|
self.dependencies = self.settings.dependencies
|
|
100
134
|
|
|
101
|
-
# Setup for mounted apps
|
|
102
|
-
self._mounted_apps: dict[str, FastMCP] = {}
|
|
103
|
-
|
|
104
135
|
# Set up MCP protocol handlers
|
|
105
136
|
self._setup_handlers()
|
|
106
137
|
|
|
@@ -137,21 +168,32 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
137
168
|
Args:
|
|
138
169
|
transport: Transport protocol to use ("stdio" or "sse")
|
|
139
170
|
"""
|
|
171
|
+
logger.info(f'Starting server "{self.name}"...')
|
|
140
172
|
anyio.run(self.run_async, transport)
|
|
141
173
|
|
|
142
174
|
def _setup_handlers(self) -> None:
|
|
143
175
|
"""Set up core MCP protocol handlers."""
|
|
144
|
-
self._mcp_server.list_tools()(self.
|
|
176
|
+
self._mcp_server.list_tools()(self._mcp_list_tools)
|
|
145
177
|
self._mcp_server.call_tool()(self.call_tool)
|
|
146
|
-
self._mcp_server.list_resources()(self.
|
|
147
|
-
self._mcp_server.read_resource()(self.
|
|
148
|
-
self._mcp_server.list_prompts()(self.
|
|
149
|
-
self._mcp_server.get_prompt()(self.
|
|
150
|
-
self._mcp_server.list_resource_templates()(self.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
178
|
+
self._mcp_server.list_resources()(self._mcp_list_resources)
|
|
179
|
+
self._mcp_server.read_resource()(self._mcp_read_resource)
|
|
180
|
+
self._mcp_server.list_prompts()(self._mcp_list_prompts)
|
|
181
|
+
self._mcp_server.get_prompt()(self._mcp_get_prompt)
|
|
182
|
+
self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
|
|
183
|
+
|
|
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
|
+
|
|
155
197
|
return [
|
|
156
198
|
MCPTool(
|
|
157
199
|
name=info.name,
|
|
@@ -177,17 +219,25 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
177
219
|
|
|
178
220
|
async def call_tool(
|
|
179
221
|
self, name: str, arguments: dict[str, Any]
|
|
180
|
-
) ->
|
|
222
|
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
181
223
|
"""Call a tool by name with arguments."""
|
|
182
224
|
context = self.get_context()
|
|
183
225
|
result = await self._tool_manager.call_tool(name, arguments, context=context)
|
|
184
226
|
converted_result = _convert_to_content(result)
|
|
185
227
|
return converted_result
|
|
186
228
|
|
|
187
|
-
|
|
188
|
-
|
|
229
|
+
def list_resources(self) -> list[Resource]:
|
|
230
|
+
return self._resource_manager.list_resources()
|
|
189
231
|
|
|
190
|
-
|
|
232
|
+
async def _mcp_list_resources(self) -> list[MCPResource]:
|
|
233
|
+
"""
|
|
234
|
+
List all available resources, in the format expected by the low-level MCP
|
|
235
|
+
server.
|
|
236
|
+
|
|
237
|
+
See `list_resources` for a more ergonomic way to list resources.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
resources = self.list_resources()
|
|
191
241
|
return [
|
|
192
242
|
MCPResource(
|
|
193
243
|
uri=resource.uri,
|
|
@@ -198,8 +248,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
198
248
|
for resource in resources
|
|
199
249
|
]
|
|
200
250
|
|
|
201
|
-
|
|
202
|
-
|
|
251
|
+
def list_resource_templates(self) -> list[ResourceTemplate]:
|
|
252
|
+
return self._resource_manager.list_templates()
|
|
253
|
+
|
|
254
|
+
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
|
|
255
|
+
"""
|
|
256
|
+
List all available resource templates, in the format expected by the low-level
|
|
257
|
+
MCP server.
|
|
258
|
+
|
|
259
|
+
See `list_resource_templates` for a more ergonomic way to list resource
|
|
260
|
+
templates.
|
|
261
|
+
"""
|
|
262
|
+
templates = self.list_resource_templates()
|
|
203
263
|
return [
|
|
204
264
|
MCPResourceTemplate(
|
|
205
265
|
uriTemplate=template.uri_template,
|
|
@@ -209,15 +269,27 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
209
269
|
for template in templates
|
|
210
270
|
]
|
|
211
271
|
|
|
212
|
-
async def read_resource(self, uri: AnyUrl | str) ->
|
|
272
|
+
async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
|
|
213
273
|
"""Read a resource by URI."""
|
|
274
|
+
resource = await self._resource_manager.get_resource(uri)
|
|
275
|
+
if not resource:
|
|
276
|
+
raise ResourceError(f"Unknown resource: {uri}")
|
|
277
|
+
return await resource.read()
|
|
278
|
+
|
|
279
|
+
async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
280
|
+
"""
|
|
281
|
+
Read a resource by URI, in the format expected by the low-level MCP
|
|
282
|
+
server.
|
|
283
|
+
|
|
284
|
+
See `read_resource` for a more ergonomic way to read resources.
|
|
285
|
+
"""
|
|
214
286
|
|
|
215
287
|
resource = await self._resource_manager.get_resource(uri)
|
|
216
288
|
if not resource:
|
|
217
289
|
raise ResourceError(f"Unknown resource: {uri}")
|
|
218
290
|
|
|
219
291
|
try:
|
|
220
|
-
content = await
|
|
292
|
+
content = await self.read_resource(uri)
|
|
221
293
|
return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
|
|
222
294
|
except Exception as e:
|
|
223
295
|
logger.error(f"Error reading resource {uri}: {e}")
|
|
@@ -228,6 +300,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
228
300
|
fn: AnyFunction,
|
|
229
301
|
name: str | None = None,
|
|
230
302
|
description: str | None = None,
|
|
303
|
+
tags: set[str] | None = None,
|
|
231
304
|
) -> None:
|
|
232
305
|
"""Add a tool to the server.
|
|
233
306
|
|
|
@@ -238,11 +311,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
238
311
|
fn: The function to register as a tool
|
|
239
312
|
name: Optional name for the tool (defaults to function name)
|
|
240
313
|
description: Optional description of what the tool does
|
|
314
|
+
tags: Optional set of tags for categorizing the tool
|
|
241
315
|
"""
|
|
242
|
-
self._tool_manager.
|
|
316
|
+
self._tool_manager.add_tool_from_fn(
|
|
317
|
+
fn, name=name, description=description, tags=tags
|
|
318
|
+
)
|
|
243
319
|
|
|
244
320
|
def tool(
|
|
245
|
-
self,
|
|
321
|
+
self,
|
|
322
|
+
name: str | None = None,
|
|
323
|
+
description: str | None = None,
|
|
324
|
+
tags: set[str] | None = None,
|
|
246
325
|
) -> Callable[[AnyFunction], AnyFunction]:
|
|
247
326
|
"""Decorator to register a tool.
|
|
248
327
|
|
|
@@ -253,6 +332,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
253
332
|
Args:
|
|
254
333
|
name: Optional name for the tool (defaults to function name)
|
|
255
334
|
description: Optional description of what the tool does
|
|
335
|
+
tags: Optional set of tags for categorizing the tool
|
|
256
336
|
|
|
257
337
|
Example:
|
|
258
338
|
@server.tool()
|
|
@@ -269,6 +349,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
269
349
|
await context.report_progress(50, 100)
|
|
270
350
|
return str(x)
|
|
271
351
|
"""
|
|
352
|
+
|
|
272
353
|
# Check if user passed function directly instead of calling decorator
|
|
273
354
|
if callable(name):
|
|
274
355
|
raise TypeError(
|
|
@@ -277,8 +358,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
277
358
|
)
|
|
278
359
|
|
|
279
360
|
def decorator(fn: AnyFunction) -> AnyFunction:
|
|
280
|
-
self.add_tool(fn, name=name, description=description)
|
|
281
|
-
return fn
|
|
361
|
+
self.add_tool(fn, name=name, description=description, tags=tags)
|
|
362
|
+
return DecoratedFunction(fn)
|
|
282
363
|
|
|
283
364
|
return decorator
|
|
284
365
|
|
|
@@ -288,8 +369,40 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
288
369
|
Args:
|
|
289
370
|
resource: A Resource instance to add
|
|
290
371
|
"""
|
|
372
|
+
|
|
291
373
|
self._resource_manager.add_resource(resource)
|
|
292
374
|
|
|
375
|
+
def add_resource_fn(
|
|
376
|
+
self,
|
|
377
|
+
fn: AnyFunction,
|
|
378
|
+
uri: str,
|
|
379
|
+
name: str | None = None,
|
|
380
|
+
description: str | None = None,
|
|
381
|
+
mime_type: str | None = None,
|
|
382
|
+
tags: set[str] | None = None,
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Add a resource or template to the server from a function.
|
|
385
|
+
|
|
386
|
+
If the URI contains parameters (e.g. "resource://{param}") or the function
|
|
387
|
+
has parameters, it will be registered as a template resource.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
fn: The function to register as a resource
|
|
391
|
+
uri: The URI for the resource
|
|
392
|
+
name: Optional name for the resource
|
|
393
|
+
description: Optional description of the resource
|
|
394
|
+
mime_type: Optional MIME type for the resource
|
|
395
|
+
tags: Optional set of tags for categorizing the resource
|
|
396
|
+
"""
|
|
397
|
+
self._resource_manager.add_resource_or_template_from_fn(
|
|
398
|
+
fn=fn,
|
|
399
|
+
uri=uri,
|
|
400
|
+
name=name,
|
|
401
|
+
description=description,
|
|
402
|
+
mime_type=mime_type,
|
|
403
|
+
tags=tags,
|
|
404
|
+
)
|
|
405
|
+
|
|
293
406
|
def resource(
|
|
294
407
|
self,
|
|
295
408
|
uri: str,
|
|
@@ -297,6 +410,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
297
410
|
name: str | None = None,
|
|
298
411
|
description: str | None = None,
|
|
299
412
|
mime_type: str | None = None,
|
|
413
|
+
tags: set[str] | None = None,
|
|
300
414
|
) -> Callable[[AnyFunction], AnyFunction]:
|
|
301
415
|
"""Decorator to register a function as a resource.
|
|
302
416
|
|
|
@@ -314,6 +428,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
314
428
|
name: Optional name for the resource
|
|
315
429
|
description: Optional description of the resource
|
|
316
430
|
mime_type: Optional MIME type for the resource
|
|
431
|
+
tags: Optional set of tags for categorizing the resource
|
|
317
432
|
|
|
318
433
|
Example:
|
|
319
434
|
@server.resource("resource://my-resource")
|
|
@@ -342,59 +457,49 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
342
457
|
)
|
|
343
458
|
|
|
344
459
|
def decorator(fn: AnyFunction) -> AnyFunction:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if uri_params != func_params:
|
|
355
|
-
raise ValueError(
|
|
356
|
-
f"Mismatch between URI parameters {uri_params} "
|
|
357
|
-
f"and function parameters {func_params}"
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
# Register as template
|
|
361
|
-
self._resource_manager.add_template(
|
|
362
|
-
fn=fn,
|
|
363
|
-
uri_template=uri,
|
|
364
|
-
name=name,
|
|
365
|
-
description=description,
|
|
366
|
-
mime_type=mime_type or "text/plain",
|
|
367
|
-
)
|
|
368
|
-
else:
|
|
369
|
-
# Register as regular resource
|
|
370
|
-
resource = FunctionResource(
|
|
371
|
-
uri=AnyUrl(uri),
|
|
372
|
-
name=name,
|
|
373
|
-
description=description,
|
|
374
|
-
mime_type=mime_type or "text/plain",
|
|
375
|
-
fn=fn,
|
|
376
|
-
)
|
|
377
|
-
self.add_resource(resource)
|
|
378
|
-
return fn
|
|
460
|
+
self._resource_manager.add_resource_or_template_from_fn(
|
|
461
|
+
fn=fn,
|
|
462
|
+
uri=uri,
|
|
463
|
+
name=name,
|
|
464
|
+
description=description,
|
|
465
|
+
mime_type=mime_type,
|
|
466
|
+
tags=tags,
|
|
467
|
+
)
|
|
468
|
+
return DecoratedFunction(fn)
|
|
379
469
|
|
|
380
470
|
return decorator
|
|
381
471
|
|
|
382
|
-
def add_prompt(
|
|
472
|
+
def add_prompt(
|
|
473
|
+
self,
|
|
474
|
+
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
|
475
|
+
name: str | None = None,
|
|
476
|
+
description: str | None = None,
|
|
477
|
+
tags: set[str] | None = None,
|
|
478
|
+
) -> None:
|
|
383
479
|
"""Add a prompt to the server.
|
|
384
480
|
|
|
385
481
|
Args:
|
|
386
482
|
prompt: A Prompt instance to add
|
|
387
483
|
"""
|
|
388
|
-
self._prompt_manager.
|
|
484
|
+
self._prompt_manager.add_prompt_from_fn(
|
|
485
|
+
fn=fn,
|
|
486
|
+
name=name,
|
|
487
|
+
description=description,
|
|
488
|
+
tags=tags,
|
|
489
|
+
)
|
|
389
490
|
|
|
390
491
|
def prompt(
|
|
391
|
-
self,
|
|
492
|
+
self,
|
|
493
|
+
name: str | None = None,
|
|
494
|
+
description: str | None = None,
|
|
495
|
+
tags: set[str] | None = None,
|
|
392
496
|
) -> Callable[[AnyFunction], AnyFunction]:
|
|
393
497
|
"""Decorator to register a prompt.
|
|
394
498
|
|
|
395
499
|
Args:
|
|
396
500
|
name: Optional name for the prompt (defaults to function name)
|
|
397
501
|
description: Optional description of what the prompt does
|
|
502
|
+
tags: Optional set of tags for categorizing the prompt
|
|
398
503
|
|
|
399
504
|
Example:
|
|
400
505
|
@server.prompt()
|
|
@@ -431,9 +536,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
431
536
|
)
|
|
432
537
|
|
|
433
538
|
def decorator(func: AnyFunction) -> AnyFunction:
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return func
|
|
539
|
+
self.add_prompt(func, name=name, description=description, tags=tags)
|
|
540
|
+
return DecoratedFunction(func)
|
|
437
541
|
|
|
438
542
|
return decorator
|
|
439
543
|
|
|
@@ -446,15 +550,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
446
550
|
self._mcp_server.create_initialization_options(),
|
|
447
551
|
)
|
|
448
552
|
|
|
449
|
-
async def run_sse_async(
|
|
553
|
+
async def run_sse_async(
|
|
554
|
+
self,
|
|
555
|
+
host: str | None = None,
|
|
556
|
+
port: int | None = None,
|
|
557
|
+
log_level: str | None = None,
|
|
558
|
+
) -> None:
|
|
450
559
|
"""Run the server using SSE transport."""
|
|
451
560
|
starlette_app = self.sse_app()
|
|
452
561
|
|
|
453
562
|
config = uvicorn.Config(
|
|
454
563
|
starlette_app,
|
|
455
|
-
host=self.settings.host,
|
|
456
|
-
port=self.settings.port,
|
|
457
|
-
log_level=self.settings.log_level.lower(),
|
|
564
|
+
host=host or self.settings.host,
|
|
565
|
+
port=port or self.settings.port,
|
|
566
|
+
log_level=log_level or self.settings.log_level.lower(),
|
|
458
567
|
)
|
|
459
568
|
server = uvicorn.Server(config)
|
|
460
569
|
await server.serve()
|
|
@@ -483,9 +592,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
483
592
|
],
|
|
484
593
|
)
|
|
485
594
|
|
|
486
|
-
|
|
487
|
-
"""
|
|
488
|
-
|
|
595
|
+
def list_prompts(self) -> list[Prompt]:
|
|
596
|
+
"""
|
|
597
|
+
List all available prompts.
|
|
598
|
+
"""
|
|
599
|
+
return self._prompt_manager.list_prompts()
|
|
600
|
+
|
|
601
|
+
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
|
|
602
|
+
"""
|
|
603
|
+
List all available prompts, in the format expected by the low-level MCP
|
|
604
|
+
server.
|
|
605
|
+
|
|
606
|
+
See `list_prompts` for a more ergonomic way to list prompts.
|
|
607
|
+
"""
|
|
608
|
+
prompts = self.list_prompts()
|
|
489
609
|
return [
|
|
490
610
|
MCPPrompt(
|
|
491
611
|
name=prompt.name,
|
|
@@ -504,47 +624,77 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
504
624
|
|
|
505
625
|
async def get_prompt(
|
|
506
626
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
507
|
-
) ->
|
|
627
|
+
) -> list[Message]:
|
|
508
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:
|
|
634
|
+
"""
|
|
635
|
+
Get a prompt by name with arguments, in the format expected by the low-level
|
|
636
|
+
MCP server.
|
|
637
|
+
|
|
638
|
+
See `get_prompt` for a more ergonomic way to get prompts.
|
|
639
|
+
"""
|
|
509
640
|
try:
|
|
510
|
-
messages = await self.
|
|
641
|
+
messages = await self.get_prompt(name, arguments)
|
|
511
642
|
|
|
512
643
|
return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
|
|
513
644
|
except Exception as e:
|
|
514
645
|
logger.error(f"Error getting prompt {name}: {e}")
|
|
515
646
|
raise ValueError(str(e))
|
|
516
647
|
|
|
517
|
-
def mount(
|
|
648
|
+
def mount(
|
|
649
|
+
self,
|
|
650
|
+
prefix: str,
|
|
651
|
+
app: "FastMCP",
|
|
652
|
+
tool_separator: str | None = None,
|
|
653
|
+
resource_separator: str | None = None,
|
|
654
|
+
prompt_separator: str | None = None,
|
|
655
|
+
) -> None:
|
|
518
656
|
"""Mount another FastMCP application with a given prefix.
|
|
519
657
|
|
|
520
658
|
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 "
|
|
523
|
-
- The resources are imported with prefixed URIs
|
|
659
|
+
- The tools are imported with prefixed names using the tool_separator
|
|
660
|
+
Example: If app has a tool named "get_weather", it will be available as "weatherget_weather"
|
|
661
|
+
- The resources are imported with prefixed URIs using the resource_separator
|
|
524
662
|
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
|
|
663
|
+
- The templates are imported with prefixed URI templates using the resource_separator
|
|
526
664
|
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 "
|
|
665
|
+
- The prompts are imported with prefixed names using the prompt_separator
|
|
666
|
+
Example: If app has a prompt named "weather_prompt", it will be available as "weather_weather_prompt"
|
|
667
|
+
- The mounted app's lifespan will be executed when the parent app's lifespan runs,
|
|
668
|
+
ensuring that any setup needed by the mounted app is performed
|
|
529
669
|
|
|
530
670
|
Args:
|
|
531
671
|
prefix: The prefix to use for the mounted application
|
|
532
672
|
app: The FastMCP application to mount
|
|
673
|
+
tool_separator: Separator for tool names (defaults to "_")
|
|
674
|
+
resource_separator: Separator for resource URIs (defaults to "+")
|
|
675
|
+
prompt_separator: Separator for prompt names (defaults to "_")
|
|
533
676
|
"""
|
|
677
|
+
if tool_separator is None:
|
|
678
|
+
tool_separator = "_"
|
|
679
|
+
if resource_separator is None:
|
|
680
|
+
resource_separator = "+"
|
|
681
|
+
if prompt_separator is None:
|
|
682
|
+
prompt_separator = "_"
|
|
683
|
+
|
|
534
684
|
# Mount the app in the list of mounted apps
|
|
535
685
|
self._mounted_apps[prefix] = app
|
|
536
686
|
|
|
537
|
-
# Import tools from the mounted app
|
|
538
|
-
tool_prefix = f"{prefix}
|
|
687
|
+
# Import tools from the mounted app
|
|
688
|
+
tool_prefix = f"{prefix}{tool_separator}"
|
|
539
689
|
self._tool_manager.import_tools(app._tool_manager, tool_prefix)
|
|
540
690
|
|
|
541
|
-
# Import resources and templates from the mounted app
|
|
542
|
-
resource_prefix = f"{prefix}
|
|
691
|
+
# Import resources and templates from the mounted app
|
|
692
|
+
resource_prefix = f"{prefix}{resource_separator}"
|
|
543
693
|
self._resource_manager.import_resources(app._resource_manager, resource_prefix)
|
|
544
694
|
self._resource_manager.import_templates(app._resource_manager, resource_prefix)
|
|
545
695
|
|
|
546
|
-
# Import prompts
|
|
547
|
-
prompt_prefix = f"{prefix}
|
|
696
|
+
# Import prompts from the mounted app
|
|
697
|
+
prompt_prefix = f"{prefix}{prompt_separator}"
|
|
548
698
|
self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix)
|
|
549
699
|
|
|
550
700
|
logger.info(f"Mounted app with prefix '{prefix}'")
|
fastmcp/settings.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
|
+
from enum import Enum
|
|
3
4
|
from typing import TYPE_CHECKING, Literal
|
|
4
5
|
|
|
5
6
|
from pydantic import Field
|
|
@@ -11,6 +12,13 @@ if TYPE_CHECKING:
|
|
|
11
12
|
LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
class DuplicateBehavior(Enum):
|
|
16
|
+
WARN = "warn"
|
|
17
|
+
ERROR = "error"
|
|
18
|
+
REPLACE = "replace"
|
|
19
|
+
IGNORE = "ignore"
|
|
20
|
+
|
|
21
|
+
|
|
14
22
|
class Settings(BaseSettings):
|
|
15
23
|
"""FastMCP settings."""
|
|
16
24
|
|
|
@@ -47,13 +55,13 @@ class ServerSettings(BaseSettings):
|
|
|
47
55
|
debug: bool = False
|
|
48
56
|
|
|
49
57
|
# resource settings
|
|
50
|
-
|
|
58
|
+
on_duplicate_resources: DuplicateBehavior = DuplicateBehavior.WARN
|
|
51
59
|
|
|
52
60
|
# tool settings
|
|
53
|
-
|
|
61
|
+
on_duplicate_tools: DuplicateBehavior = DuplicateBehavior.WARN
|
|
54
62
|
|
|
55
63
|
# prompt settings
|
|
56
|
-
|
|
64
|
+
on_duplicate_prompts: DuplicateBehavior = DuplicateBehavior.WARN
|
|
57
65
|
|
|
58
66
|
dependencies: list[str] = Field(
|
|
59
67
|
default_factory=list,
|
fastmcp/tools/__init__.py
CHANGED