fastmcp 2.0.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/cli/cli.py +2 -2
- fastmcp/client/client.py +80 -35
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/{base.py → prompt.py} +23 -3
- fastmcp/prompts/prompt_manager.py +27 -11
- fastmcp/resources/__init__.py +2 -2
- fastmcp/resources/{base.py → resource.py} +20 -1
- fastmcp/resources/resource_manager.py +57 -15
- fastmcp/resources/{templates.py → template.py} +23 -3
- fastmcp/resources/types.py +2 -2
- fastmcp/server/openapi.py +16 -4
- fastmcp/server/proxy.py +27 -23
- fastmcp/server/server.py +97 -30
- fastmcp/settings.py +11 -3
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/{base.py → tool.py} +22 -3
- fastmcp/tools/tool_manager.py +22 -16
- fastmcp/utilities/types.py +12 -0
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.0.dist-info}/METADATA +1 -1
- fastmcp-2.1.0.dist-info/RECORD +39 -0
- fastmcp-2.0.0.dist-info/RECORD +0 -39
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/openapi.py
CHANGED
|
@@ -12,7 +12,7 @@ from pydantic.networks import AnyUrl
|
|
|
12
12
|
|
|
13
13
|
from fastmcp.resources import Resource, ResourceTemplate
|
|
14
14
|
from fastmcp.server.server import FastMCP
|
|
15
|
-
from fastmcp.tools.
|
|
15
|
+
from fastmcp.tools.tool import Tool
|
|
16
16
|
from fastmcp.utilities import openapi
|
|
17
17
|
from fastmcp.utilities.func_metadata import func_metadata
|
|
18
18
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -115,6 +115,7 @@ class OpenAPITool(Tool):
|
|
|
115
115
|
parameters: dict[str, Any],
|
|
116
116
|
fn_metadata: Any,
|
|
117
117
|
is_async: bool = True,
|
|
118
|
+
tags: set[str] = set(),
|
|
118
119
|
):
|
|
119
120
|
super().__init__(
|
|
120
121
|
name=name,
|
|
@@ -124,6 +125,7 @@ class OpenAPITool(Tool):
|
|
|
124
125
|
fn_metadata=fn_metadata,
|
|
125
126
|
is_async=is_async,
|
|
126
127
|
context_kwarg="context", # Default context keyword argument
|
|
128
|
+
tags=tags,
|
|
127
129
|
)
|
|
128
130
|
self._client = client
|
|
129
131
|
self._route = route
|
|
@@ -242,12 +244,14 @@ class OpenAPIResource(Resource):
|
|
|
242
244
|
name: str,
|
|
243
245
|
description: str,
|
|
244
246
|
mime_type: str = "application/json",
|
|
247
|
+
tags: set[str] = set(),
|
|
245
248
|
):
|
|
246
249
|
super().__init__(
|
|
247
250
|
uri=AnyUrl(uri), # Convert string to AnyUrl
|
|
248
251
|
name=name,
|
|
249
252
|
description=description,
|
|
250
253
|
mime_type=mime_type,
|
|
254
|
+
tags=tags,
|
|
251
255
|
)
|
|
252
256
|
self._client = client
|
|
253
257
|
self._route = route
|
|
@@ -332,6 +336,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
332
336
|
name: str,
|
|
333
337
|
description: str,
|
|
334
338
|
parameters: dict[str, Any],
|
|
339
|
+
tags: set[str] = set(),
|
|
335
340
|
):
|
|
336
341
|
super().__init__(
|
|
337
342
|
uri_template=uri_template,
|
|
@@ -339,6 +344,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
339
344
|
description=description,
|
|
340
345
|
fn=self._create_resource_fn,
|
|
341
346
|
parameters=parameters,
|
|
347
|
+
tags=tags,
|
|
342
348
|
)
|
|
343
349
|
self._client = client
|
|
344
350
|
self._route = route
|
|
@@ -405,6 +411,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
405
411
|
description=self.description
|
|
406
412
|
or f"Resource for {self._route.path}", # Provide default if None
|
|
407
413
|
mime_type="application/json", # Default, will be updated when read
|
|
414
|
+
tags=set(self._route.tags or []),
|
|
408
415
|
)
|
|
409
416
|
|
|
410
417
|
|
|
@@ -525,10 +532,13 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
525
532
|
parameters=combined_schema,
|
|
526
533
|
fn_metadata=func_metadata(_openapi_passthrough),
|
|
527
534
|
is_async=True,
|
|
535
|
+
tags=set(route.tags or []),
|
|
528
536
|
)
|
|
529
537
|
# Register the tool by directly assigning to the tools dictionary
|
|
530
538
|
self._tool_manager._tools[tool_name] = tool
|
|
531
|
-
logger.debug(
|
|
539
|
+
logger.debug(
|
|
540
|
+
f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
|
|
541
|
+
)
|
|
532
542
|
|
|
533
543
|
def _create_openapi_resource(self, route: openapi.HTTPRoute, operation_id: str):
|
|
534
544
|
"""Creates and registers an OpenAPIResource with enhanced description."""
|
|
@@ -550,11 +560,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
550
560
|
uri=resource_uri,
|
|
551
561
|
name=resource_name,
|
|
552
562
|
description=enhanced_description,
|
|
563
|
+
tags=set(route.tags or []),
|
|
553
564
|
)
|
|
554
565
|
# Register the resource by directly assigning to the resources dictionary
|
|
555
566
|
self._resource_manager._resources[str(resource.uri)] = resource
|
|
556
567
|
logger.debug(
|
|
557
|
-
f"Registered RESOURCE: {resource_uri} ({route.method} {route.path})"
|
|
568
|
+
f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
|
|
558
569
|
)
|
|
559
570
|
|
|
560
571
|
def _create_openapi_template(self, route: openapi.HTTPRoute, operation_id: str):
|
|
@@ -594,11 +605,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
594
605
|
name=template_name,
|
|
595
606
|
description=enhanced_description,
|
|
596
607
|
parameters=template_params_schema,
|
|
608
|
+
tags=set(route.tags or []),
|
|
597
609
|
)
|
|
598
610
|
# Register the template by directly assigning to the templates dictionary
|
|
599
611
|
self._resource_manager._templates[uri_template_str] = template
|
|
600
612
|
logger.debug(
|
|
601
|
-
f"Registered TEMPLATE: {uri_template_str} ({route.method} {route.path})"
|
|
613
|
+
f"Registered TEMPLATE: {uri_template_str} ({route.method} {route.path}) with tags: {route.tags}"
|
|
602
614
|
)
|
|
603
615
|
|
|
604
616
|
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
fastmcp/server/proxy.py
CHANGED
|
@@ -9,7 +9,7 @@ from fastmcp.prompts import Prompt
|
|
|
9
9
|
from fastmcp.resources import Resource, ResourceTemplate
|
|
10
10
|
from fastmcp.server.context import Context
|
|
11
11
|
from fastmcp.server.server import FastMCP
|
|
12
|
-
from fastmcp.tools.
|
|
12
|
+
from fastmcp.tools.tool import Tool
|
|
13
13
|
from fastmcp.utilities.func_metadata import func_metadata
|
|
14
14
|
from fastmcp.utilities.logging import get_logger
|
|
15
15
|
|
|
@@ -40,11 +40,15 @@ class ProxyTool(Tool):
|
|
|
40
40
|
async def run(
|
|
41
41
|
self, arguments: dict[str, Any], context: Context | None = None
|
|
42
42
|
) -> Any:
|
|
43
|
+
# the client context manager will swallow any exceptions inside a TaskGroup
|
|
44
|
+
# so we return the raw result and raise an exception ourselves
|
|
43
45
|
async with self._client:
|
|
44
|
-
result = await self._client.call_tool(
|
|
46
|
+
result = await self._client.call_tool(
|
|
47
|
+
self.name, arguments, _return_raw_result=True
|
|
48
|
+
)
|
|
45
49
|
if result.isError:
|
|
46
50
|
raise ValueError(cast(mcp.types.TextContent, result.content[0]).text)
|
|
47
|
-
return result.content
|
|
51
|
+
return result.content
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
class ProxyResource(Resource):
|
|
@@ -73,12 +77,12 @@ class ProxyResource(Resource):
|
|
|
73
77
|
|
|
74
78
|
async with self._client:
|
|
75
79
|
result = await self._client.read_resource(self.uri)
|
|
76
|
-
if isinstance(result
|
|
77
|
-
return result
|
|
78
|
-
elif isinstance(result
|
|
79
|
-
return result
|
|
80
|
+
if isinstance(result[0], TextResourceContents):
|
|
81
|
+
return result[0].text
|
|
82
|
+
elif isinstance(result[0], BlobResourceContents):
|
|
83
|
+
return result[0].blob
|
|
80
84
|
else:
|
|
81
|
-
raise ValueError(f"Unsupported content type: {type(result
|
|
85
|
+
raise ValueError(f"Unsupported content type: {type(result[0])}")
|
|
82
86
|
|
|
83
87
|
|
|
84
88
|
class ProxyTemplate(ResourceTemplate):
|
|
@@ -103,20 +107,20 @@ class ProxyTemplate(ResourceTemplate):
|
|
|
103
107
|
async with self._client:
|
|
104
108
|
result = await self._client.read_resource(uri)
|
|
105
109
|
|
|
106
|
-
if isinstance(result
|
|
107
|
-
value = result
|
|
108
|
-
elif isinstance(result
|
|
109
|
-
value = result
|
|
110
|
+
if isinstance(result[0], TextResourceContents):
|
|
111
|
+
value = result[0].text
|
|
112
|
+
elif isinstance(result[0], BlobResourceContents):
|
|
113
|
+
value = result[0].blob
|
|
110
114
|
else:
|
|
111
|
-
raise ValueError(f"Unsupported content type: {type(result
|
|
115
|
+
raise ValueError(f"Unsupported content type: {type(result[0])}")
|
|
112
116
|
|
|
113
117
|
return ProxyResource(
|
|
114
118
|
client=self._client,
|
|
115
119
|
uri=uri,
|
|
116
120
|
name=self.name,
|
|
117
121
|
description=self.description,
|
|
118
|
-
mime_type=result
|
|
119
|
-
contents=result
|
|
122
|
+
mime_type=result[0].mimeType,
|
|
123
|
+
contents=result,
|
|
120
124
|
_value=value,
|
|
121
125
|
)
|
|
122
126
|
|
|
@@ -177,15 +181,15 @@ class FastMCPProxy(FastMCP):
|
|
|
177
181
|
|
|
178
182
|
async with client:
|
|
179
183
|
# Register proxies for client tools
|
|
180
|
-
|
|
181
|
-
for tool in
|
|
184
|
+
tools = await client.list_tools()
|
|
185
|
+
for tool in tools:
|
|
182
186
|
tool_proxy = await ProxyTool.from_client(client, tool)
|
|
183
187
|
server._tool_manager._tools[tool_proxy.name] = tool_proxy
|
|
184
188
|
logger.debug(f"Created proxy for tool: {tool_proxy.name}")
|
|
185
189
|
|
|
186
190
|
# Register proxies for client resources
|
|
187
|
-
|
|
188
|
-
for resource in
|
|
191
|
+
resources = await client.list_resources()
|
|
192
|
+
for resource in resources:
|
|
189
193
|
resource_proxy = await ProxyResource.from_client(client, resource)
|
|
190
194
|
server._resource_manager._resources[str(resource_proxy.uri)] = (
|
|
191
195
|
resource_proxy
|
|
@@ -193,8 +197,8 @@ class FastMCPProxy(FastMCP):
|
|
|
193
197
|
logger.debug(f"Created proxy for resource: {resource_proxy.uri}")
|
|
194
198
|
|
|
195
199
|
# Register proxies for client resource templates
|
|
196
|
-
|
|
197
|
-
for template in
|
|
200
|
+
templates = await client.list_resource_templates()
|
|
201
|
+
for template in templates:
|
|
198
202
|
template_proxy = await ProxyTemplate.from_client(client, template)
|
|
199
203
|
server._resource_manager._templates[template_proxy.uri_template] = (
|
|
200
204
|
template_proxy
|
|
@@ -204,8 +208,8 @@ class FastMCPProxy(FastMCP):
|
|
|
204
208
|
)
|
|
205
209
|
|
|
206
210
|
# Register proxies for client prompts
|
|
207
|
-
|
|
208
|
-
for prompt in
|
|
211
|
+
prompts = await client.list_prompts()
|
|
212
|
+
for prompt in prompts:
|
|
209
213
|
prompt_proxy = await ProxyPrompt.from_client(client, prompt)
|
|
210
214
|
server._prompt_manager._prompts[prompt_proxy.name] = prompt_proxy
|
|
211
215
|
logger.debug(f"Created proxy for prompt: {prompt_proxy.name}")
|
fastmcp/server/server.py
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import inspect
|
|
4
4
|
import json
|
|
5
5
|
import re
|
|
6
|
-
from collections.abc import AsyncIterator, Callable
|
|
6
|
+
from collections.abc import AsyncIterator, Callable
|
|
7
7
|
from contextlib import (
|
|
8
8
|
AbstractAsyncContextManager,
|
|
9
|
+
AsyncExitStack,
|
|
9
10
|
asynccontextmanager,
|
|
10
11
|
)
|
|
11
12
|
from typing import TYPE_CHECKING, Any, Generic, Literal
|
|
@@ -18,7 +19,6 @@ from fastapi import FastAPI
|
|
|
18
19
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
19
20
|
from mcp.server.lowlevel.server import LifespanResultT
|
|
20
21
|
from mcp.server.lowlevel.server import Server as MCPServer
|
|
21
|
-
from mcp.server.lowlevel.server import lifespan as default_lifespan
|
|
22
22
|
from mcp.server.session import ServerSession
|
|
23
23
|
from mcp.server.sse import SseServerTransport
|
|
24
24
|
from mcp.server.stdio import stdio_server
|
|
@@ -56,6 +56,19 @@ if TYPE_CHECKING:
|
|
|
56
56
|
logger = get_logger(__name__)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
@asynccontextmanager
|
|
60
|
+
async def default_lifespan(server: "FastMCP") -> AsyncIterator[Any]:
|
|
61
|
+
"""Default lifespan context manager that does nothing.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
server: The server instance this lifespan is managing
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
An empty context object
|
|
68
|
+
"""
|
|
69
|
+
yield {}
|
|
70
|
+
|
|
71
|
+
|
|
59
72
|
def lifespan_wrapper(
|
|
60
73
|
app: "FastMCP",
|
|
61
74
|
lifespan: Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]],
|
|
@@ -64,7 +77,18 @@ def lifespan_wrapper(
|
|
|
64
77
|
]:
|
|
65
78
|
@asynccontextmanager
|
|
66
79
|
async def wrap(s: MCPServer[LifespanResultT]) -> AsyncIterator[LifespanResultT]:
|
|
67
|
-
async with
|
|
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}'")
|
|
91
|
+
|
|
68
92
|
yield context
|
|
69
93
|
|
|
70
94
|
return wrap
|
|
@@ -78,29 +102,34 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
78
102
|
lifespan: (
|
|
79
103
|
Callable[["FastMCP"], AbstractAsyncContextManager[LifespanResultT]] | None
|
|
80
104
|
) = None,
|
|
105
|
+
tags: set[str] | None = None,
|
|
81
106
|
**settings: Any,
|
|
82
107
|
):
|
|
108
|
+
self.tags: set[str] = tags or set()
|
|
83
109
|
self.settings = fastmcp.settings.ServerSettings(**settings)
|
|
84
110
|
|
|
111
|
+
# Setup for mounted apps - must be initialized before _mcp_server
|
|
112
|
+
self._mounted_apps: dict[str, FastMCP] = {}
|
|
113
|
+
|
|
114
|
+
if lifespan is None:
|
|
115
|
+
lifespan = default_lifespan
|
|
116
|
+
|
|
85
117
|
self._mcp_server = MCPServer[LifespanResultT](
|
|
86
118
|
name=name or "FastMCP",
|
|
87
119
|
instructions=instructions,
|
|
88
|
-
lifespan=lifespan_wrapper(self, lifespan)
|
|
120
|
+
lifespan=lifespan_wrapper(self, lifespan),
|
|
89
121
|
)
|
|
90
122
|
self._tool_manager = ToolManager(
|
|
91
|
-
|
|
123
|
+
duplicate_behavior=self.settings.on_duplicate_tools
|
|
92
124
|
)
|
|
93
125
|
self._resource_manager = ResourceManager(
|
|
94
|
-
|
|
126
|
+
duplicate_behavior=self.settings.on_duplicate_resources
|
|
95
127
|
)
|
|
96
128
|
self._prompt_manager = PromptManager(
|
|
97
|
-
|
|
129
|
+
duplicate_behavior=self.settings.on_duplicate_prompts
|
|
98
130
|
)
|
|
99
131
|
self.dependencies = self.settings.dependencies
|
|
100
132
|
|
|
101
|
-
# Setup for mounted apps
|
|
102
|
-
self._mounted_apps: dict[str, FastMCP] = {}
|
|
103
|
-
|
|
104
133
|
# Set up MCP protocol handlers
|
|
105
134
|
self._setup_handlers()
|
|
106
135
|
|
|
@@ -151,6 +180,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
151
180
|
|
|
152
181
|
async def list_tools(self) -> list[MCPTool]:
|
|
153
182
|
"""List all available tools."""
|
|
183
|
+
|
|
154
184
|
tools = self._tool_manager.list_tools()
|
|
155
185
|
return [
|
|
156
186
|
MCPTool(
|
|
@@ -177,7 +207,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
177
207
|
|
|
178
208
|
async def call_tool(
|
|
179
209
|
self, name: str, arguments: dict[str, Any]
|
|
180
|
-
) ->
|
|
210
|
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
181
211
|
"""Call a tool by name with arguments."""
|
|
182
212
|
context = self.get_context()
|
|
183
213
|
result = await self._tool_manager.call_tool(name, arguments, context=context)
|
|
@@ -228,6 +258,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
228
258
|
fn: AnyFunction,
|
|
229
259
|
name: str | None = None,
|
|
230
260
|
description: str | None = None,
|
|
261
|
+
tags: set[str] | None = None,
|
|
231
262
|
) -> None:
|
|
232
263
|
"""Add a tool to the server.
|
|
233
264
|
|
|
@@ -238,11 +269,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
238
269
|
fn: The function to register as a tool
|
|
239
270
|
name: Optional name for the tool (defaults to function name)
|
|
240
271
|
description: Optional description of what the tool does
|
|
272
|
+
tags: Optional set of tags for categorizing the tool
|
|
241
273
|
"""
|
|
242
|
-
self._tool_manager.
|
|
274
|
+
self._tool_manager.add_tool_from_fn(
|
|
275
|
+
fn, name=name, description=description, tags=tags
|
|
276
|
+
)
|
|
243
277
|
|
|
244
278
|
def tool(
|
|
245
|
-
self,
|
|
279
|
+
self,
|
|
280
|
+
name: str | None = None,
|
|
281
|
+
description: str | None = None,
|
|
282
|
+
tags: set[str] | None = None,
|
|
246
283
|
) -> Callable[[AnyFunction], AnyFunction]:
|
|
247
284
|
"""Decorator to register a tool.
|
|
248
285
|
|
|
@@ -253,6 +290,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
253
290
|
Args:
|
|
254
291
|
name: Optional name for the tool (defaults to function name)
|
|
255
292
|
description: Optional description of what the tool does
|
|
293
|
+
tags: Optional set of tags for categorizing the tool
|
|
256
294
|
|
|
257
295
|
Example:
|
|
258
296
|
@server.tool()
|
|
@@ -277,7 +315,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
277
315
|
)
|
|
278
316
|
|
|
279
317
|
def decorator(fn: AnyFunction) -> AnyFunction:
|
|
280
|
-
self.add_tool(fn, name=name, description=description)
|
|
318
|
+
self.add_tool(fn, name=name, description=description, tags=tags)
|
|
281
319
|
return fn
|
|
282
320
|
|
|
283
321
|
return decorator
|
|
@@ -297,6 +335,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
297
335
|
name: str | None = None,
|
|
298
336
|
description: str | None = None,
|
|
299
337
|
mime_type: str | None = None,
|
|
338
|
+
tags: set[str] | None = None,
|
|
300
339
|
) -> Callable[[AnyFunction], AnyFunction]:
|
|
301
340
|
"""Decorator to register a function as a resource.
|
|
302
341
|
|
|
@@ -314,6 +353,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
314
353
|
name: Optional name for the resource
|
|
315
354
|
description: Optional description of the resource
|
|
316
355
|
mime_type: Optional MIME type for the resource
|
|
356
|
+
tags: Optional set of tags for categorizing the resource
|
|
317
357
|
|
|
318
358
|
Example:
|
|
319
359
|
@server.resource("resource://my-resource")
|
|
@@ -358,12 +398,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
358
398
|
)
|
|
359
399
|
|
|
360
400
|
# Register as template
|
|
361
|
-
self._resource_manager.
|
|
401
|
+
self._resource_manager.add_template_from_fn(
|
|
362
402
|
fn=fn,
|
|
363
403
|
uri_template=uri,
|
|
364
404
|
name=name,
|
|
365
405
|
description=description,
|
|
366
406
|
mime_type=mime_type or "text/plain",
|
|
407
|
+
tags=tags,
|
|
367
408
|
)
|
|
368
409
|
else:
|
|
369
410
|
# Register as regular resource
|
|
@@ -373,6 +414,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
373
414
|
description=description,
|
|
374
415
|
mime_type=mime_type or "text/plain",
|
|
375
416
|
fn=fn,
|
|
417
|
+
tags=tags or set(), # Default to empty set if None
|
|
376
418
|
)
|
|
377
419
|
self.add_resource(resource)
|
|
378
420
|
return fn
|
|
@@ -388,13 +430,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
388
430
|
self._prompt_manager.add_prompt(prompt)
|
|
389
431
|
|
|
390
432
|
def prompt(
|
|
391
|
-
self,
|
|
433
|
+
self,
|
|
434
|
+
name: str | None = None,
|
|
435
|
+
description: str | None = None,
|
|
436
|
+
tags: set[str] | None = None,
|
|
392
437
|
) -> Callable[[AnyFunction], AnyFunction]:
|
|
393
438
|
"""Decorator to register a prompt.
|
|
394
439
|
|
|
395
440
|
Args:
|
|
396
441
|
name: Optional name for the prompt (defaults to function name)
|
|
397
442
|
description: Optional description of what the prompt does
|
|
443
|
+
tags: Optional set of tags for categorizing the prompt
|
|
398
444
|
|
|
399
445
|
Example:
|
|
400
446
|
@server.prompt()
|
|
@@ -431,7 +477,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
431
477
|
)
|
|
432
478
|
|
|
433
479
|
def decorator(func: AnyFunction) -> AnyFunction:
|
|
434
|
-
prompt = Prompt.from_function(
|
|
480
|
+
prompt = Prompt.from_function(
|
|
481
|
+
func, name=name, description=description, tags=tags
|
|
482
|
+
)
|
|
435
483
|
self.add_prompt(prompt)
|
|
436
484
|
return func
|
|
437
485
|
|
|
@@ -514,37 +562,56 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
514
562
|
logger.error(f"Error getting prompt {name}: {e}")
|
|
515
563
|
raise ValueError(str(e))
|
|
516
564
|
|
|
517
|
-
def mount(
|
|
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:
|
|
518
573
|
"""Mount another FastMCP application with a given prefix.
|
|
519
574
|
|
|
520
575
|
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
|
|
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
|
|
524
579
|
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
|
|
580
|
+
- The templates are imported with prefixed URI templates using the resource_separator
|
|
526
581
|
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 "
|
|
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
|
|
529
586
|
|
|
530
587
|
Args:
|
|
531
588
|
prefix: The prefix to use for the mounted application
|
|
532
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 "_")
|
|
533
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
|
+
|
|
534
601
|
# Mount the app in the list of mounted apps
|
|
535
602
|
self._mounted_apps[prefix] = app
|
|
536
603
|
|
|
537
|
-
# Import tools from the mounted app
|
|
538
|
-
tool_prefix = f"{prefix}
|
|
604
|
+
# Import tools from the mounted app
|
|
605
|
+
tool_prefix = f"{prefix}{tool_separator}"
|
|
539
606
|
self._tool_manager.import_tools(app._tool_manager, tool_prefix)
|
|
540
607
|
|
|
541
|
-
# Import resources and templates from the mounted app
|
|
542
|
-
resource_prefix = f"{prefix}
|
|
608
|
+
# Import resources and templates from the mounted app
|
|
609
|
+
resource_prefix = f"{prefix}{resource_separator}"
|
|
543
610
|
self._resource_manager.import_resources(app._resource_manager, resource_prefix)
|
|
544
611
|
self._resource_manager.import_templates(app._resource_manager, resource_prefix)
|
|
545
612
|
|
|
546
|
-
# Import prompts
|
|
547
|
-
prompt_prefix = f"{prefix}
|
|
613
|
+
# Import prompts from the mounted app
|
|
614
|
+
prompt_prefix = f"{prefix}{prompt_separator}"
|
|
548
615
|
self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix)
|
|
549
616
|
|
|
550
617
|
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
|
@@ -2,12 +2,14 @@ from __future__ import annotations as _annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
from collections.abc import Callable
|
|
5
|
-
from typing import TYPE_CHECKING, Any
|
|
5
|
+
from typing import TYPE_CHECKING, Annotated, Any
|
|
6
6
|
|
|
7
|
-
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic import BaseModel, BeforeValidator, Field
|
|
8
|
+
from typing_extensions import Self
|
|
8
9
|
|
|
9
10
|
from fastmcp.exceptions import ToolError
|
|
10
11
|
from fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
|
|
12
|
+
from fastmcp.utilities.types import _convert_set_defaults
|
|
11
13
|
|
|
12
14
|
if TYPE_CHECKING:
|
|
13
15
|
from mcp.server.session import ServerSessionT
|
|
@@ -19,7 +21,7 @@ if TYPE_CHECKING:
|
|
|
19
21
|
class Tool(BaseModel):
|
|
20
22
|
"""Internal tool registration info."""
|
|
21
23
|
|
|
22
|
-
fn: Callable[..., Any]
|
|
24
|
+
fn: Callable[..., Any]
|
|
23
25
|
name: str = Field(description="Name of the tool")
|
|
24
26
|
description: str = Field(description="Description of what the tool does")
|
|
25
27
|
parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
|
|
@@ -31,6 +33,9 @@ class Tool(BaseModel):
|
|
|
31
33
|
context_kwarg: str | None = Field(
|
|
32
34
|
None, description="Name of the kwarg that should receive context"
|
|
33
35
|
)
|
|
36
|
+
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
37
|
+
default_factory=set, description="Tags for the tool"
|
|
38
|
+
)
|
|
34
39
|
|
|
35
40
|
@classmethod
|
|
36
41
|
def from_function(
|
|
@@ -39,6 +44,7 @@ class Tool(BaseModel):
|
|
|
39
44
|
name: str | None = None,
|
|
40
45
|
description: str | None = None,
|
|
41
46
|
context_kwarg: str | None = None,
|
|
47
|
+
tags: set[str] | None = None,
|
|
42
48
|
) -> Tool:
|
|
43
49
|
"""Create a Tool from a function."""
|
|
44
50
|
from fastmcp import Context
|
|
@@ -72,6 +78,7 @@ class Tool(BaseModel):
|
|
|
72
78
|
fn_metadata=func_arg_metadata,
|
|
73
79
|
is_async=is_async,
|
|
74
80
|
context_kwarg=context_kwarg,
|
|
81
|
+
tags=tags or set(),
|
|
75
82
|
)
|
|
76
83
|
|
|
77
84
|
async def run(
|
|
@@ -91,3 +98,15 @@ class Tool(BaseModel):
|
|
|
91
98
|
)
|
|
92
99
|
except Exception as e:
|
|
93
100
|
raise ToolError(f"Error executing tool {self.name}: {e}") from e
|
|
101
|
+
|
|
102
|
+
def copy(self, updates: dict[str, Any] | None = None) -> Self:
|
|
103
|
+
"""Copy the tool with optional updates."""
|
|
104
|
+
data = self.model_dump()
|
|
105
|
+
if updates:
|
|
106
|
+
data.update(updates)
|
|
107
|
+
return type(self)(**data)
|
|
108
|
+
|
|
109
|
+
def __eq__(self, other: object) -> bool:
|
|
110
|
+
if not isinstance(other, Tool):
|
|
111
|
+
return False
|
|
112
|
+
return self.model_dump() == other.model_dump()
|