fastmcp 2.9.0__py3-none-any.whl → 2.9.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/client/client.py +2 -2
- fastmcp/client/logging.py +1 -2
- fastmcp/client/messages.py +126 -0
- fastmcp/prompts/prompt.py +18 -2
- fastmcp/prompts/prompt_manager.py +2 -2
- fastmcp/resources/resource.py +16 -0
- fastmcp/resources/resource_manager.py +4 -4
- fastmcp/resources/template.py +17 -1
- fastmcp/server/auth/providers/bearer.py +38 -11
- fastmcp/server/context.py +72 -8
- fastmcp/server/low_level.py +35 -0
- fastmcp/server/server.py +134 -46
- fastmcp/tools/tool.py +16 -0
- fastmcp/tools/tool_manager.py +2 -2
- fastmcp/tools/tool_transform.py +42 -16
- fastmcp/utilities/openapi.py +70 -2
- {fastmcp-2.9.0.dist-info → fastmcp-2.9.1.dist-info}/METADATA +1 -1
- {fastmcp-2.9.0.dist-info → fastmcp-2.9.1.dist-info}/RECORD +21 -19
- {fastmcp-2.9.0.dist-info → fastmcp-2.9.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.9.0.dist-info → fastmcp-2.9.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.9.0.dist-info → fastmcp-2.9.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -23,7 +23,6 @@ import mcp.types
|
|
|
23
23
|
import uvicorn
|
|
24
24
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
25
25
|
from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
|
|
26
|
-
from mcp.server.lowlevel.server import Server as MCPServer
|
|
27
26
|
from mcp.server.stdio import stdio_server
|
|
28
27
|
from mcp.types import (
|
|
29
28
|
AnyFunction,
|
|
@@ -54,6 +53,7 @@ from fastmcp.server.http import (
|
|
|
54
53
|
create_sse_app,
|
|
55
54
|
create_streamable_http_app,
|
|
56
55
|
)
|
|
56
|
+
from fastmcp.server.low_level import LowLevelServer
|
|
57
57
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
58
58
|
from fastmcp.settings import Settings
|
|
59
59
|
from fastmcp.tools import ToolManager
|
|
@@ -99,10 +99,12 @@ def _lifespan_wrapper(
|
|
|
99
99
|
[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
100
100
|
],
|
|
101
101
|
) -> Callable[
|
|
102
|
-
[
|
|
102
|
+
[LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
103
103
|
]:
|
|
104
104
|
@asynccontextmanager
|
|
105
|
-
async def wrap(
|
|
105
|
+
async def wrap(
|
|
106
|
+
s: LowLevelServer[LifespanResultT],
|
|
107
|
+
) -> AsyncIterator[LifespanResultT]:
|
|
106
108
|
async with AsyncExitStack() as stack:
|
|
107
109
|
context = await stack.enter_async_context(lifespan(app))
|
|
108
110
|
yield context
|
|
@@ -179,7 +181,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
179
181
|
lifespan = default_lifespan
|
|
180
182
|
else:
|
|
181
183
|
self._has_lifespan = True
|
|
182
|
-
self._mcp_server =
|
|
184
|
+
self._mcp_server = LowLevelServer[LifespanResultT](
|
|
183
185
|
name=name or "FastMCP",
|
|
184
186
|
version=version,
|
|
185
187
|
instructions=instructions,
|
|
@@ -363,6 +365,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
363
365
|
return await self._resource_manager.get_resource_templates()
|
|
364
366
|
|
|
365
367
|
async def get_resource_template(self, key: str) -> ResourceTemplate:
|
|
368
|
+
"""Get a registered resource template by key."""
|
|
366
369
|
templates = await self.get_resource_templates()
|
|
367
370
|
if key not in templates:
|
|
368
371
|
raise NotFoundError(f"Unknown resource template: {key}")
|
|
@@ -403,9 +406,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
403
406
|
include_in_schema: Whether to include in OpenAPI schema, defaults to True
|
|
404
407
|
|
|
405
408
|
Example:
|
|
409
|
+
Register a custom HTTP route for a health check endpoint:
|
|
410
|
+
```python
|
|
406
411
|
@server.custom_route("/health", methods=["GET"])
|
|
407
412
|
async def health_check(request: Request) -> Response:
|
|
408
413
|
return JSONResponse({"status": "ok"})
|
|
414
|
+
```
|
|
409
415
|
"""
|
|
410
416
|
|
|
411
417
|
def decorator(
|
|
@@ -427,7 +433,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
427
433
|
async def _mcp_list_tools(self) -> list[MCPTool]:
|
|
428
434
|
logger.debug("Handler called: list_tools")
|
|
429
435
|
|
|
430
|
-
with fastmcp.server.context.Context(fastmcp=self):
|
|
436
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
431
437
|
tools = await self._list_tools()
|
|
432
438
|
return [tool.to_mcp_tool(name=tool.key) for tool in tools]
|
|
433
439
|
|
|
@@ -450,7 +456,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
450
456
|
|
|
451
457
|
return mcp_tools
|
|
452
458
|
|
|
453
|
-
with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
459
|
+
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
454
460
|
# Create the middleware context.
|
|
455
461
|
mw_context = MiddlewareContext(
|
|
456
462
|
message=mcp.types.ListToolsRequest(method="tools/list"),
|
|
@@ -466,7 +472,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
466
472
|
async def _mcp_list_resources(self) -> list[MCPResource]:
|
|
467
473
|
logger.debug("Handler called: list_resources")
|
|
468
474
|
|
|
469
|
-
with fastmcp.server.context.Context(fastmcp=self):
|
|
475
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
470
476
|
resources = await self._list_resources()
|
|
471
477
|
return [
|
|
472
478
|
resource.to_mcp_resource(uri=resource.key) for resource in resources
|
|
@@ -491,7 +497,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
491
497
|
|
|
492
498
|
return mcp_resources
|
|
493
499
|
|
|
494
|
-
with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
500
|
+
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
495
501
|
# Create the middleware context.
|
|
496
502
|
mw_context = MiddlewareContext(
|
|
497
503
|
message={}, # List resources doesn't have parameters
|
|
@@ -507,7 +513,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
507
513
|
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
|
|
508
514
|
logger.debug("Handler called: list_resource_templates")
|
|
509
515
|
|
|
510
|
-
with fastmcp.server.context.Context(fastmcp=self):
|
|
516
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
511
517
|
templates = await self._list_resource_templates()
|
|
512
518
|
return [
|
|
513
519
|
template.to_mcp_template(uriTemplate=template.key)
|
|
@@ -533,7 +539,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
533
539
|
|
|
534
540
|
return mcp_templates
|
|
535
541
|
|
|
536
|
-
with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
542
|
+
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
537
543
|
# Create the middleware context.
|
|
538
544
|
mw_context = MiddlewareContext(
|
|
539
545
|
message={}, # List resource templates doesn't have parameters
|
|
@@ -549,7 +555,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
549
555
|
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
|
|
550
556
|
logger.debug("Handler called: list_prompts")
|
|
551
557
|
|
|
552
|
-
with fastmcp.server.context.Context(fastmcp=self):
|
|
558
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
553
559
|
prompts = await self._list_prompts()
|
|
554
560
|
return [prompt.to_mcp_prompt(name=prompt.key) for prompt in prompts]
|
|
555
561
|
|
|
@@ -572,7 +578,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
572
578
|
|
|
573
579
|
return mcp_prompts
|
|
574
580
|
|
|
575
|
-
with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
581
|
+
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
|
576
582
|
# Create the middleware context.
|
|
577
583
|
mw_context = MiddlewareContext(
|
|
578
584
|
message=mcp.types.ListPromptsRequest(method="prompts/list"),
|
|
@@ -602,7 +608,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
602
608
|
"""
|
|
603
609
|
logger.debug("Handler called: call_tool %s with %s", key, arguments)
|
|
604
610
|
|
|
605
|
-
with fastmcp.server.context.Context(fastmcp=self):
|
|
611
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
606
612
|
try:
|
|
607
613
|
return await self._call_tool(key, arguments)
|
|
608
614
|
except DisabledError:
|
|
@@ -643,7 +649,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
643
649
|
"""
|
|
644
650
|
logger.debug("Handler called: read_resource %s", uri)
|
|
645
651
|
|
|
646
|
-
with fastmcp.server.context.Context(fastmcp=self):
|
|
652
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
647
653
|
try:
|
|
648
654
|
return await self._read_resource(uri)
|
|
649
655
|
except DisabledError:
|
|
@@ -698,7 +704,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
698
704
|
"""
|
|
699
705
|
logger.debug("Handler called: get_prompt %s with %s", name, arguments)
|
|
700
706
|
|
|
701
|
-
with fastmcp.server.context.Context(fastmcp=self):
|
|
707
|
+
async with fastmcp.server.context.Context(fastmcp=self):
|
|
702
708
|
try:
|
|
703
709
|
return await self._get_prompt(name, arguments)
|
|
704
710
|
except DisabledError:
|
|
@@ -747,6 +753,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
747
753
|
self._tool_manager.add_tool(tool)
|
|
748
754
|
self._cache.clear()
|
|
749
755
|
|
|
756
|
+
# Send notification if we're in a request context
|
|
757
|
+
try:
|
|
758
|
+
from fastmcp.server.dependencies import get_context
|
|
759
|
+
|
|
760
|
+
context = get_context()
|
|
761
|
+
context._queue_tool_list_changed() # type: ignore[private-use]
|
|
762
|
+
except RuntimeError:
|
|
763
|
+
pass # No context available
|
|
764
|
+
|
|
750
765
|
def remove_tool(self, name: str) -> None:
|
|
751
766
|
"""Remove a tool from the server.
|
|
752
767
|
|
|
@@ -759,6 +774,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
759
774
|
self._tool_manager.remove_tool(name)
|
|
760
775
|
self._cache.clear()
|
|
761
776
|
|
|
777
|
+
# Send notification if we're in a request context
|
|
778
|
+
try:
|
|
779
|
+
from fastmcp.server.dependencies import get_context
|
|
780
|
+
|
|
781
|
+
context = get_context()
|
|
782
|
+
context._queue_tool_list_changed() # type: ignore[private-use]
|
|
783
|
+
except RuntimeError:
|
|
784
|
+
pass # No context available
|
|
785
|
+
|
|
762
786
|
@overload
|
|
763
787
|
def tool(
|
|
764
788
|
self,
|
|
@@ -814,15 +838,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
814
838
|
name: Optional name for the tool (keyword-only, alternative to name_or_fn)
|
|
815
839
|
description: Optional description of what the tool does
|
|
816
840
|
tags: Optional set of tags for categorizing the tool
|
|
817
|
-
annotations: Optional annotations about the tool's behavior
|
|
841
|
+
annotations: Optional annotations about the tool's behavior
|
|
818
842
|
exclude_args: Optional list of argument names to exclude from the tool schema
|
|
819
843
|
enabled: Optional boolean to enable or disable the tool
|
|
820
844
|
|
|
821
|
-
|
|
845
|
+
Examples:
|
|
846
|
+
Register a tool with a custom name:
|
|
847
|
+
```python
|
|
822
848
|
@server.tool
|
|
823
849
|
def my_tool(x: int) -> str:
|
|
824
850
|
return str(x)
|
|
825
851
|
|
|
852
|
+
# Register a tool with a custom name
|
|
826
853
|
@server.tool
|
|
827
854
|
def my_tool(x: int) -> str:
|
|
828
855
|
return str(x)
|
|
@@ -837,6 +864,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
837
864
|
|
|
838
865
|
# Direct function call
|
|
839
866
|
server.tool(my_function, name="custom_name")
|
|
867
|
+
```
|
|
840
868
|
"""
|
|
841
869
|
if isinstance(annotations, dict):
|
|
842
870
|
annotations = ToolAnnotations(**annotations)
|
|
@@ -911,6 +939,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
911
939
|
self._resource_manager.add_resource(resource)
|
|
912
940
|
self._cache.clear()
|
|
913
941
|
|
|
942
|
+
# Send notification if we're in a request context
|
|
943
|
+
try:
|
|
944
|
+
from fastmcp.server.dependencies import get_context
|
|
945
|
+
|
|
946
|
+
context = get_context()
|
|
947
|
+
context._queue_resource_list_changed() # type: ignore[private-use]
|
|
948
|
+
except RuntimeError:
|
|
949
|
+
pass # No context available
|
|
950
|
+
|
|
914
951
|
def add_template(self, template: ResourceTemplate) -> None:
|
|
915
952
|
"""Add a resource template to the server.
|
|
916
953
|
|
|
@@ -919,6 +956,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
919
956
|
"""
|
|
920
957
|
self._resource_manager.add_template(template)
|
|
921
958
|
|
|
959
|
+
# Send notification if we're in a request context
|
|
960
|
+
try:
|
|
961
|
+
from fastmcp.server.dependencies import get_context
|
|
962
|
+
|
|
963
|
+
context = get_context()
|
|
964
|
+
context._queue_resource_list_changed() # type: ignore[private-use]
|
|
965
|
+
except RuntimeError:
|
|
966
|
+
pass # No context available
|
|
967
|
+
|
|
922
968
|
def add_resource_fn(
|
|
923
969
|
self,
|
|
924
970
|
fn: AnyFunction,
|
|
@@ -991,7 +1037,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
991
1037
|
tags: Optional set of tags for categorizing the resource
|
|
992
1038
|
enabled: Optional boolean to enable or disable the resource
|
|
993
1039
|
|
|
994
|
-
|
|
1040
|
+
Examples:
|
|
1041
|
+
Register a resource with a custom name:
|
|
1042
|
+
```python
|
|
995
1043
|
@server.resource("resource://my-resource")
|
|
996
1044
|
def get_data() -> str:
|
|
997
1045
|
return "Hello, world!"
|
|
@@ -1014,6 +1062,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1014
1062
|
async def get_weather(city: str) -> str:
|
|
1015
1063
|
data = await fetch_weather(city)
|
|
1016
1064
|
return f"Weather for {city}: {data}"
|
|
1065
|
+
```
|
|
1017
1066
|
"""
|
|
1018
1067
|
# Check if user passed function directly instead of calling decorator
|
|
1019
1068
|
if inspect.isroutine(uri):
|
|
@@ -1087,6 +1136,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1087
1136
|
self._prompt_manager.add_prompt(prompt)
|
|
1088
1137
|
self._cache.clear()
|
|
1089
1138
|
|
|
1139
|
+
# Send notification if we're in a request context
|
|
1140
|
+
try:
|
|
1141
|
+
from fastmcp.server.dependencies import get_context
|
|
1142
|
+
|
|
1143
|
+
context = get_context()
|
|
1144
|
+
context._queue_prompt_list_changed() # type: ignore[private-use]
|
|
1145
|
+
except RuntimeError:
|
|
1146
|
+
pass # No context available
|
|
1147
|
+
|
|
1090
1148
|
@overload
|
|
1091
1149
|
def prompt(
|
|
1092
1150
|
self,
|
|
@@ -1138,7 +1196,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1138
1196
|
tags: Optional set of tags for categorizing the prompt
|
|
1139
1197
|
enabled: Optional boolean to enable or disable the prompt
|
|
1140
1198
|
|
|
1141
|
-
|
|
1199
|
+
Examples:
|
|
1200
|
+
|
|
1201
|
+
```python
|
|
1142
1202
|
@server.prompt
|
|
1143
1203
|
def analyze_table(table_name: str) -> list[Message]:
|
|
1144
1204
|
schema = read_table_schema(table_name)
|
|
@@ -1182,6 +1242,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1182
1242
|
|
|
1183
1243
|
# Direct function call
|
|
1184
1244
|
server.prompt(my_function, name="custom_name")
|
|
1245
|
+
```
|
|
1185
1246
|
"""
|
|
1186
1247
|
|
|
1187
1248
|
if isinstance(name_or_fn, classmethod):
|
|
@@ -1787,10 +1848,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1787
1848
|
) -> FastMCPProxy:
|
|
1788
1849
|
"""Create a FastMCP proxy server for the given backend.
|
|
1789
1850
|
|
|
1790
|
-
The
|
|
1791
|
-
instance or any value accepted as the
|
|
1792
|
-
|
|
1793
|
-
|
|
1851
|
+
The `backend` argument can be either an existing `fastmcp.client.Client`
|
|
1852
|
+
instance or any value accepted as the `transport` argument of
|
|
1853
|
+
`fastmcp.client.Client`. This mirrors the convenience of the
|
|
1854
|
+
`fastmcp.client.Client` constructor.
|
|
1794
1855
|
"""
|
|
1795
1856
|
from fastmcp.client.client import Client
|
|
1796
1857
|
from fastmcp.server.proxy import FastMCPProxy
|
|
@@ -1827,14 +1888,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1827
1888
|
Given a component, determine if it should be enabled. Returns True if it should be enabled; False if it should not.
|
|
1828
1889
|
|
|
1829
1890
|
Rules:
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1891
|
+
- If the component's enabled property is False, always return False.
|
|
1892
|
+
- If both include_tags and exclude_tags are None, return True.
|
|
1893
|
+
- If exclude_tags is provided, check each exclude tag:
|
|
1833
1894
|
- If the exclude tag is a string, it must be present in the input tags to exclude.
|
|
1834
|
-
|
|
1895
|
+
- If include_tags is provided, check each include tag:
|
|
1835
1896
|
- If the include tag is a string, it must be present in the input tags to include.
|
|
1836
|
-
|
|
1837
|
-
|
|
1897
|
+
- If include_tags is provided and none of the include tags match, return False.
|
|
1898
|
+
- If include_tags is not provided, return True.
|
|
1838
1899
|
"""
|
|
1839
1900
|
if not component.enabled:
|
|
1840
1901
|
return False
|
|
@@ -1875,12 +1936,21 @@ def add_resource_prefix(
|
|
|
1875
1936
|
The resource URI with the prefix added
|
|
1876
1937
|
|
|
1877
1938
|
Examples:
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
"
|
|
1882
|
-
|
|
1883
|
-
|
|
1939
|
+
With new style:
|
|
1940
|
+
```python
|
|
1941
|
+
add_resource_prefix("resource://path/to/resource", "prefix")
|
|
1942
|
+
"resource://prefix/path/to/resource"
|
|
1943
|
+
```
|
|
1944
|
+
With legacy style:
|
|
1945
|
+
```python
|
|
1946
|
+
add_resource_prefix("resource://path/to/resource", "prefix")
|
|
1947
|
+
"prefix+resource://path/to/resource"
|
|
1948
|
+
```
|
|
1949
|
+
With absolute path:
|
|
1950
|
+
```python
|
|
1951
|
+
add_resource_prefix("resource:///absolute/path", "prefix")
|
|
1952
|
+
"resource://prefix//absolute/path"
|
|
1953
|
+
```
|
|
1884
1954
|
|
|
1885
1955
|
Raises:
|
|
1886
1956
|
ValueError: If the URI doesn't match the expected protocol://path format
|
|
@@ -1926,12 +1996,21 @@ def remove_resource_prefix(
|
|
|
1926
1996
|
The resource URI with the prefix removed
|
|
1927
1997
|
|
|
1928
1998
|
Examples:
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
"resource://path/to/resource"
|
|
1933
|
-
|
|
1934
|
-
|
|
1999
|
+
With new style:
|
|
2000
|
+
```python
|
|
2001
|
+
remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
2002
|
+
"resource://path/to/resource"
|
|
2003
|
+
```
|
|
2004
|
+
With legacy style:
|
|
2005
|
+
```python
|
|
2006
|
+
remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
2007
|
+
"resource://path/to/resource"
|
|
2008
|
+
```
|
|
2009
|
+
With absolute path:
|
|
2010
|
+
```python
|
|
2011
|
+
remove_resource_prefix("resource://prefix//absolute/path", "prefix")
|
|
2012
|
+
"resource:///absolute/path"
|
|
2013
|
+
```
|
|
1935
2014
|
|
|
1936
2015
|
Raises:
|
|
1937
2016
|
ValueError: If the URI doesn't match the expected protocol://path format
|
|
@@ -1984,12 +2063,21 @@ def has_resource_prefix(
|
|
|
1984
2063
|
True if the URI has the specified prefix, False otherwise
|
|
1985
2064
|
|
|
1986
2065
|
Examples:
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
True
|
|
1991
|
-
|
|
2066
|
+
With new style:
|
|
2067
|
+
```python
|
|
2068
|
+
has_resource_prefix("resource://prefix/path/to/resource", "prefix")
|
|
2069
|
+
True
|
|
2070
|
+
```
|
|
2071
|
+
With legacy style:
|
|
2072
|
+
```python
|
|
2073
|
+
has_resource_prefix("prefix+resource://path/to/resource", "prefix")
|
|
2074
|
+
True
|
|
2075
|
+
```
|
|
2076
|
+
With other path:
|
|
2077
|
+
```python
|
|
2078
|
+
has_resource_prefix("resource://other/path/to/resource", "prefix")
|
|
1992
2079
|
False
|
|
2080
|
+
```
|
|
1993
2081
|
|
|
1994
2082
|
Raises:
|
|
1995
2083
|
ValueError: If the URI doesn't match the expected protocol://path format
|
fastmcp/tools/tool.py
CHANGED
|
@@ -46,6 +46,22 @@ class Tool(FastMCPComponent):
|
|
|
46
46
|
default=None, description="Optional custom serializer for tool results"
|
|
47
47
|
)
|
|
48
48
|
|
|
49
|
+
def enable(self) -> None:
|
|
50
|
+
super().enable()
|
|
51
|
+
try:
|
|
52
|
+
context = get_context()
|
|
53
|
+
context._queue_tool_list_changed() # type: ignore[private-use]
|
|
54
|
+
except RuntimeError:
|
|
55
|
+
pass # No context available
|
|
56
|
+
|
|
57
|
+
def disable(self) -> None:
|
|
58
|
+
super().disable()
|
|
59
|
+
try:
|
|
60
|
+
context = get_context()
|
|
61
|
+
context._queue_tool_list_changed() # type: ignore[private-use]
|
|
62
|
+
except RuntimeError:
|
|
63
|
+
pass # No context available
|
|
64
|
+
|
|
49
65
|
def to_mcp_tool(self, **overrides: Any) -> MCPTool:
|
|
50
66
|
kwargs = {
|
|
51
67
|
"name": self.name,
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -186,12 +186,12 @@ class ToolManager:
|
|
|
186
186
|
|
|
187
187
|
# raise ToolErrors as-is
|
|
188
188
|
except ToolError as e:
|
|
189
|
-
logger.exception(f"Error calling tool {key!r}
|
|
189
|
+
logger.exception(f"Error calling tool {key!r}")
|
|
190
190
|
raise e
|
|
191
191
|
|
|
192
192
|
# Handle other exceptions
|
|
193
193
|
except Exception as e:
|
|
194
|
-
logger.exception(f"Error calling tool {key!r}
|
|
194
|
+
logger.exception(f"Error calling tool {key!r}")
|
|
195
195
|
if self.mask_error_details:
|
|
196
196
|
# Mask internal details
|
|
197
197
|
raise ToolError(f"Error calling tool {key!r}") from e
|
fastmcp/tools/tool_transform.py
CHANGED
|
@@ -100,35 +100,55 @@ class ArgTransform:
|
|
|
100
100
|
examples: Examples for the argument. Use ... for no change.
|
|
101
101
|
|
|
102
102
|
Examples:
|
|
103
|
-
|
|
103
|
+
Rename argument 'old_name' to 'new_name'
|
|
104
|
+
```python
|
|
104
105
|
ArgTransform(name="new_name")
|
|
106
|
+
```
|
|
105
107
|
|
|
106
|
-
|
|
108
|
+
Change description only
|
|
109
|
+
```python
|
|
107
110
|
ArgTransform(description="Updated description")
|
|
111
|
+
```
|
|
108
112
|
|
|
109
|
-
|
|
113
|
+
Add a default value (makes argument optional)
|
|
114
|
+
```python
|
|
110
115
|
ArgTransform(default=42)
|
|
116
|
+
```
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
Add a default factory (makes argument optional)
|
|
119
|
+
```python
|
|
113
120
|
ArgTransform(default_factory=lambda: time.time())
|
|
121
|
+
```
|
|
114
122
|
|
|
115
|
-
|
|
123
|
+
Change the type
|
|
124
|
+
```python
|
|
116
125
|
ArgTransform(type=str)
|
|
126
|
+
```
|
|
117
127
|
|
|
118
|
-
|
|
128
|
+
Hide the argument entirely from clients
|
|
129
|
+
```python
|
|
119
130
|
ArgTransform(hide=True)
|
|
131
|
+
```
|
|
120
132
|
|
|
121
|
-
|
|
133
|
+
Hide argument but pass a constant value to parent
|
|
134
|
+
```python
|
|
122
135
|
ArgTransform(hide=True, default="constant_value")
|
|
136
|
+
```
|
|
123
137
|
|
|
124
|
-
|
|
138
|
+
Hide argument but pass a factory-generated value to parent
|
|
139
|
+
```python
|
|
125
140
|
ArgTransform(hide=True, default_factory=lambda: uuid.uuid4().hex)
|
|
141
|
+
```
|
|
126
142
|
|
|
127
|
-
|
|
143
|
+
Make an optional parameter required (removes any default)
|
|
144
|
+
```python
|
|
128
145
|
ArgTransform(required=True)
|
|
146
|
+
```
|
|
129
147
|
|
|
130
|
-
|
|
148
|
+
Combine multiple transformations
|
|
149
|
+
```python
|
|
131
150
|
ArgTransform(name="new_name", description="New desc", default=None, type=int)
|
|
151
|
+
```
|
|
132
152
|
"""
|
|
133
153
|
|
|
134
154
|
name: str | EllipsisType = NotSet
|
|
@@ -279,9 +299,9 @@ class TransformedTool(Tool):
|
|
|
279
299
|
name: New name for the tool. Defaults to parent tool's name.
|
|
280
300
|
transform_args: Optional transformations for parent tool arguments.
|
|
281
301
|
Only specified arguments are transformed, others pass through unchanged:
|
|
282
|
-
-
|
|
283
|
-
-
|
|
284
|
-
-
|
|
302
|
+
- Simple rename (str)
|
|
303
|
+
- Complex transformation (rename/description/default/drop) (ArgTransform)
|
|
304
|
+
- Drop the argument (None)
|
|
285
305
|
description: New description. Defaults to parent's description.
|
|
286
306
|
tags: New tags. Defaults to parent's tags.
|
|
287
307
|
annotations: New annotations. Defaults to parent's annotations.
|
|
@@ -290,23 +310,29 @@ class TransformedTool(Tool):
|
|
|
290
310
|
Returns:
|
|
291
311
|
TransformedTool with the specified transformations.
|
|
292
312
|
|
|
293
|
-
|
|
313
|
+
Examples:
|
|
294
314
|
# Transform specific arguments only
|
|
315
|
+
```python
|
|
295
316
|
Tool.from_tool(parent, transform_args={"old": "new"}) # Others unchanged
|
|
317
|
+
```
|
|
296
318
|
|
|
297
319
|
# Custom function with partial transforms
|
|
320
|
+
```python
|
|
298
321
|
async def custom(x: int, y: int) -> str:
|
|
299
322
|
result = await forward(x=x, y=y)
|
|
300
323
|
return f"Custom: {result}"
|
|
301
324
|
|
|
302
325
|
Tool.from_tool(parent, transform_fn=custom, transform_args={"a": "x", "b": "y"})
|
|
326
|
+
```
|
|
303
327
|
|
|
304
328
|
# Using **kwargs (gets all args, transformed and untransformed)
|
|
329
|
+
```python
|
|
305
330
|
async def flexible(**kwargs) -> str:
|
|
306
331
|
result = await forward(**kwargs)
|
|
307
332
|
return f"Got: {kwargs}"
|
|
308
333
|
|
|
309
334
|
Tool.from_tool(parent, transform_fn=flexible, transform_args={"a": "x"})
|
|
335
|
+
```
|
|
310
336
|
"""
|
|
311
337
|
transform_args = transform_args or {}
|
|
312
338
|
|
|
@@ -423,8 +449,8 @@ class TransformedTool(Tool):
|
|
|
423
449
|
|
|
424
450
|
Returns:
|
|
425
451
|
A tuple containing:
|
|
426
|
-
-
|
|
427
|
-
-
|
|
452
|
+
- The new JSON schema for the transformed tool as a dictionary
|
|
453
|
+
- Async function that validates and forwards calls to the parent tool
|
|
428
454
|
"""
|
|
429
455
|
|
|
430
456
|
# Build transformed schema and mapping
|