Spaces:
Running
Running
deepak-stratforge commited on
Commit ·
c5ee465
1
Parent(s): f15abd4
feat(tool): add support for excluding arguments from tool definition
Browse filesThis introduces an `exclude_args` parameter to omit specified arguments
(such as `state`, `memory`, etc.).
It will exclude the mentioned args from the schema sent to the LLM and still allow them
to be passed when the tool is called.
This doesn't come with the MCP protocol but can be useful in many use cases.
src/fastmcp/server/openapi.py
CHANGED
|
@@ -226,6 +226,7 @@ class OpenAPITool(Tool):
|
|
| 226 |
tags: set[str] = set(),
|
| 227 |
timeout: float | None = None,
|
| 228 |
annotations: ToolAnnotations | None = None,
|
|
|
|
| 229 |
serializer: Callable[[Any], str] | None = None,
|
| 230 |
):
|
| 231 |
super().__init__(
|
|
@@ -235,6 +236,7 @@ class OpenAPITool(Tool):
|
|
| 235 |
fn=self._execute_request, # We'll use an instance method instead of a global function
|
| 236 |
tags=tags,
|
| 237 |
annotations=annotations,
|
|
|
|
| 238 |
serializer=serializer,
|
| 239 |
)
|
| 240 |
self._client = client
|
|
|
|
| 226 |
tags: set[str] = set(),
|
| 227 |
timeout: float | None = None,
|
| 228 |
annotations: ToolAnnotations | None = None,
|
| 229 |
+
exclude_args: list[str] | None = None,
|
| 230 |
serializer: Callable[[Any], str] | None = None,
|
| 231 |
):
|
| 232 |
super().__init__(
|
|
|
|
| 236 |
fn=self._execute_request, # We'll use an instance method instead of a global function
|
| 237 |
tags=tags,
|
| 238 |
annotations=annotations,
|
| 239 |
+
exclude_args=exclude_args,
|
| 240 |
serializer=serializer,
|
| 241 |
)
|
| 242 |
self._client = client
|
src/fastmcp/server/server.py
CHANGED
|
@@ -497,6 +497,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 497 |
description: str | None = None,
|
| 498 |
tags: set[str] | None = None,
|
| 499 |
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
|
|
| 500 |
) -> None:
|
| 501 |
"""Add a tool to the server.
|
| 502 |
|
|
@@ -519,6 +520,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 519 |
description=description,
|
| 520 |
tags=tags,
|
| 521 |
annotations=annotations,
|
|
|
|
| 522 |
)
|
| 523 |
self._cache.clear()
|
| 524 |
|
|
@@ -540,6 +542,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 540 |
description: str | None = None,
|
| 541 |
tags: set[str] | None = None,
|
| 542 |
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
|
|
| 543 |
) -> Callable[[AnyFunction], AnyFunction]:
|
| 544 |
"""Decorator to register a tool.
|
| 545 |
|
|
@@ -583,6 +586,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 583 |
description=description,
|
| 584 |
tags=tags,
|
| 585 |
annotations=annotations,
|
|
|
|
| 586 |
)
|
| 587 |
return fn
|
| 588 |
|
|
|
|
| 497 |
description: str | None = None,
|
| 498 |
tags: set[str] | None = None,
|
| 499 |
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
| 500 |
+
exclude_args: list[str] | None = None,
|
| 501 |
) -> None:
|
| 502 |
"""Add a tool to the server.
|
| 503 |
|
|
|
|
| 520 |
description=description,
|
| 521 |
tags=tags,
|
| 522 |
annotations=annotations,
|
| 523 |
+
exclude_args=exclude_args,
|
| 524 |
)
|
| 525 |
self._cache.clear()
|
| 526 |
|
|
|
|
| 542 |
description: str | None = None,
|
| 543 |
tags: set[str] | None = None,
|
| 544 |
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
| 545 |
+
exclude_args: list[str] | None = None,
|
| 546 |
) -> Callable[[AnyFunction], AnyFunction]:
|
| 547 |
"""Decorator to register a tool.
|
| 548 |
|
|
|
|
| 586 |
description=description,
|
| 587 |
tags=tags,
|
| 588 |
annotations=annotations,
|
| 589 |
+
exclude_args=exclude_args,
|
| 590 |
)
|
| 591 |
return fn
|
| 592 |
|
src/fastmcp/tools/tool.py
CHANGED
|
@@ -46,6 +46,10 @@ class Tool(BaseModel):
|
|
| 46 |
annotations: ToolAnnotations | None = Field(
|
| 47 |
None, description="Additional annotations about the tool"
|
| 48 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
serializer: Callable[[Any], str] | None = Field(
|
| 50 |
None, description="Optional custom serializer for tool results"
|
| 51 |
)
|
|
@@ -58,6 +62,7 @@ class Tool(BaseModel):
|
|
| 58 |
description: str | None = None,
|
| 59 |
tags: set[str] | None = None,
|
| 60 |
annotations: ToolAnnotations | None = None,
|
|
|
|
| 61 |
serializer: Callable[[Any], str] | None = None,
|
| 62 |
) -> Tool:
|
| 63 |
"""Create a Tool from a function."""
|
|
@@ -86,10 +91,14 @@ class Tool(BaseModel):
|
|
| 86 |
schema = type_adapter.json_schema()
|
| 87 |
|
| 88 |
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
|
|
| 89 |
if context_kwarg:
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
schema = compress_schema(schema, prune_params=prune_params)
|
| 95 |
|
|
@@ -100,6 +109,7 @@ class Tool(BaseModel):
|
|
| 100 |
parameters=schema,
|
| 101 |
tags=tags or set(),
|
| 102 |
annotations=annotations,
|
|
|
|
| 103 |
serializer=serializer,
|
| 104 |
)
|
| 105 |
|
|
|
|
| 46 |
annotations: ToolAnnotations | None = Field(
|
| 47 |
None, description="Additional annotations about the tool"
|
| 48 |
)
|
| 49 |
+
exclude_args: list[str] | None = Field(
|
| 50 |
+
None,
|
| 51 |
+
description="Arguments to exclude from the tool schema, such as State, Memory, or Credential",
|
| 52 |
+
)
|
| 53 |
serializer: Callable[[Any], str] | None = Field(
|
| 54 |
None, description="Optional custom serializer for tool results"
|
| 55 |
)
|
|
|
|
| 62 |
description: str | None = None,
|
| 63 |
tags: set[str] | None = None,
|
| 64 |
annotations: ToolAnnotations | None = None,
|
| 65 |
+
exclude_args: list[str] | None = None,
|
| 66 |
serializer: Callable[[Any], str] | None = None,
|
| 67 |
) -> Tool:
|
| 68 |
"""Create a Tool from a function."""
|
|
|
|
| 91 |
schema = type_adapter.json_schema()
|
| 92 |
|
| 93 |
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
| 94 |
+
temp_prune_params: list[str] = []
|
| 95 |
if context_kwarg:
|
| 96 |
+
temp_prune_params.append(context_kwarg)
|
| 97 |
+
if exclude_args:
|
| 98 |
+
temp_prune_params.extend(exclude_args)
|
| 99 |
+
prune_params: list[str] | None = (
|
| 100 |
+
None if not temp_prune_params else temp_prune_params
|
| 101 |
+
)
|
| 102 |
|
| 103 |
schema = compress_schema(schema, prune_params=prune_params)
|
| 104 |
|
|
|
|
| 109 |
parameters=schema,
|
| 110 |
tags=tags or set(),
|
| 111 |
annotations=annotations,
|
| 112 |
+
exclude_args=exclude_args,
|
| 113 |
serializer=serializer,
|
| 114 |
)
|
| 115 |
|
src/fastmcp/tools/tool_manager.py
CHANGED
|
@@ -66,6 +66,7 @@ class ToolManager:
|
|
| 66 |
description: str | None = None,
|
| 67 |
tags: set[str] | None = None,
|
| 68 |
annotations: ToolAnnotations | None = None,
|
|
|
|
| 69 |
) -> Tool:
|
| 70 |
"""Add a tool to the server."""
|
| 71 |
tool = Tool.from_function(
|
|
@@ -75,6 +76,7 @@ class ToolManager:
|
|
| 75 |
tags=tags,
|
| 76 |
annotations=annotations,
|
| 77 |
serializer=self._serializer,
|
|
|
|
| 78 |
)
|
| 79 |
return self.add_tool(tool)
|
| 80 |
|
|
|
|
| 66 |
description: str | None = None,
|
| 67 |
tags: set[str] | None = None,
|
| 68 |
annotations: ToolAnnotations | None = None,
|
| 69 |
+
exclude_args: list[str] | None = None,
|
| 70 |
) -> Tool:
|
| 71 |
"""Add a tool to the server."""
|
| 72 |
tool = Tool.from_function(
|
|
|
|
| 76 |
tags=tags,
|
| 77 |
annotations=annotations,
|
| 78 |
serializer=self._serializer,
|
| 79 |
+
exclude_args=exclude_args,
|
| 80 |
)
|
| 81 |
return self.add_tool(tool)
|
| 82 |
|
tests/server/test_tool_exclude_args.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any
|
| 2 |
+
|
| 3 |
+
from mcp.types import TextContent
|
| 4 |
+
|
| 5 |
+
from fastmcp import Client, FastMCP
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
async def test_tool_exclude_args_in_tool_manager():
|
| 9 |
+
"""Test that tool args are excluded in the tool manager."""
|
| 10 |
+
mcp = FastMCP("Test Server")
|
| 11 |
+
|
| 12 |
+
@mcp.tool(exclude_args=["state"])
|
| 13 |
+
def echo(message: str, state: dict[str, Any] | None = None) -> str:
|
| 14 |
+
"""Echo back the message provided."""
|
| 15 |
+
if state:
|
| 16 |
+
# State was read
|
| 17 |
+
pass
|
| 18 |
+
return message
|
| 19 |
+
|
| 20 |
+
tools = mcp._tool_manager.list_tools()
|
| 21 |
+
assert len(tools) == 1
|
| 22 |
+
assert tools[0].exclude_args is not None
|
| 23 |
+
for args in tools[0].exclude_args:
|
| 24 |
+
assert args not in tools[0].parameters
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
async def test_add_tool_method_exclude_args():
|
| 28 |
+
"""Test that tool exclude_args work with the add_tool method."""
|
| 29 |
+
mcp = FastMCP("Test Server")
|
| 30 |
+
|
| 31 |
+
def create_item(
|
| 32 |
+
name: str, value: int, state: dict[str, Any] | None = None
|
| 33 |
+
) -> dict[str, Any]:
|
| 34 |
+
"""Create a new item."""
|
| 35 |
+
if state:
|
| 36 |
+
# State was read
|
| 37 |
+
pass
|
| 38 |
+
return {"name": name, "value": value}
|
| 39 |
+
|
| 40 |
+
mcp.add_tool(create_item, name="create_item", exclude_args=["state"])
|
| 41 |
+
|
| 42 |
+
# Check internal tool objects directly
|
| 43 |
+
tools = mcp._tool_manager.list_tools()
|
| 44 |
+
assert len(tools) == 1
|
| 45 |
+
assert tools[0].exclude_args is not None
|
| 46 |
+
assert tools[0].exclude_args == ["state"]
|
| 47 |
+
for args in tools[0].exclude_args:
|
| 48 |
+
assert args not in tools[0].parameters
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
async def test_tool_functionality_with_exclude_args():
|
| 52 |
+
"""Test that tool functionality is preserved when using exclude_args."""
|
| 53 |
+
mcp = FastMCP("Test Server")
|
| 54 |
+
|
| 55 |
+
def create_item(
|
| 56 |
+
name: str, value: int, state: dict[str, Any] | None = None
|
| 57 |
+
) -> dict[str, Any]:
|
| 58 |
+
"""Create a new item."""
|
| 59 |
+
if state:
|
| 60 |
+
# state was read
|
| 61 |
+
pass
|
| 62 |
+
return {"name": name, "value": value}
|
| 63 |
+
|
| 64 |
+
mcp.add_tool(create_item, name="create_item", exclude_args=["state"])
|
| 65 |
+
|
| 66 |
+
# Use the tool to verify functionality is preserved
|
| 67 |
+
async with Client(mcp) as client:
|
| 68 |
+
result = await client.call_tool(
|
| 69 |
+
"create_item", {"name": "test_item", "value": 42}
|
| 70 |
+
)
|
| 71 |
+
assert len(result) == 1
|
| 72 |
+
assert isinstance(result[0], TextContent)
|
| 73 |
+
|
| 74 |
+
# The result should contain the expected JSON
|
| 75 |
+
assert '"name": "test_item"' in result[0].text
|
| 76 |
+
assert '"value": 42' in result[0].text
|