deepak-stratforge commited on
Commit
c5ee465
·
1 Parent(s): f15abd4

feat(tool): add support for excluding arguments from tool definition

Browse files

This 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
- prune_params = [context_kwarg]
91
- else:
92
- prune_params = None
 
 
 
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