Skip to content

Commit e5a7d40

Browse files
strawgateclaude
andcommitted
Add tests for functools.partial support
Covers tools, prompts, and resources: - Sync and async partials as tools - Name preservation with update_wrapper - Partial without update_wrapper (uses underlying fn name) - Registration via add_tool and @mcp.tool() decorator - Registration via @mcp.prompt() and @mcp.resource() decorators 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e744de5 commit e5a7d40

1 file changed

Lines changed: 129 additions & 0 deletions

File tree

tests/tools/tool/test_partial.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""Tests for functools.partial support as tools, prompts, and resources.
2+
3+
See https://github.com/PrefectHQ/fastmcp/issues/3266
4+
"""
5+
6+
import functools
7+
8+
from mcp.types import TextContent
9+
10+
from fastmcp import Client, FastMCP
11+
from fastmcp.tools.function_tool import FunctionTool as Tool
12+
13+
14+
class TestPartialTool:
15+
"""Test tools created from functools.partial objects."""
16+
17+
async def test_partial_sync(self):
18+
def add(x: int, y: int) -> int:
19+
return x + y
20+
21+
partial_add = functools.partial(add, y=10)
22+
functools.update_wrapper(partial_add, add)
23+
24+
tool = Tool.from_function(partial_add)
25+
result = await tool.run({"x": 5})
26+
assert result.content == [TextContent(type="text", text="15")]
27+
28+
async def test_partial_async(self):
29+
async def multiply(x: int, factor: int) -> int:
30+
return x * factor
31+
32+
partial_mul = functools.partial(multiply, factor=3)
33+
functools.update_wrapper(partial_mul, multiply)
34+
35+
tool = Tool.from_function(partial_mul)
36+
result = await tool.run({"x": 7})
37+
assert result.content == [TextContent(type="text", text="21")]
38+
39+
async def test_partial_preserves_name(self):
40+
def greet(name: str, greeting: str = "Hello") -> str:
41+
"""Greet someone."""
42+
return f"{greeting}, {name}!"
43+
44+
partial_greet = functools.partial(greet, greeting="Hi")
45+
functools.update_wrapper(partial_greet, greet)
46+
47+
tool = Tool.from_function(partial_greet)
48+
assert tool.name == "greet"
49+
assert tool.description == "Greet someone."
50+
51+
async def test_partial_without_update_wrapper(self):
52+
def add(x: int, y: int) -> int:
53+
return x + y
54+
55+
partial_add = functools.partial(add, y=10)
56+
57+
tool = Tool.from_function(partial_add, name="add_ten")
58+
result = await tool.run({"x": 5})
59+
assert result.content == [TextContent(type="text", text="15")]
60+
61+
async def test_partial_with_add_tool(self):
62+
mcp = FastMCP("test")
63+
64+
def greet(name: str, greeting: str = "Hello") -> str:
65+
return f"{greeting}, {name}!"
66+
67+
partial_greet = functools.partial(greet, greeting="Hey")
68+
functools.update_wrapper(partial_greet, greet)
69+
70+
mcp.add_tool(partial_greet)
71+
72+
result = await mcp.call_tool("greet", {"name": "World"})
73+
assert result.content == [TextContent(type="text", text="Hey, World!")]
74+
75+
async def test_partial_with_server_tool_decorator(self):
76+
mcp = FastMCP("test")
77+
78+
def add(x: int, y: int) -> int:
79+
return x + y
80+
81+
partial_add = functools.partial(add, y=100)
82+
functools.update_wrapper(partial_add, add)
83+
84+
mcp.tool(partial_add)
85+
86+
result = await mcp.call_tool("add", {"x": 5})
87+
assert result.content == [TextContent(type="text", text="105")]
88+
89+
90+
class TestPartialPrompt:
91+
"""Test prompts created from functools.partial objects."""
92+
93+
async def test_partial_prompt_with_decorator(self):
94+
"""Partial can be registered via @mcp.prompt() decorator."""
95+
mcp = FastMCP("test")
96+
97+
def greet_prompt(name: str, lang: str) -> str:
98+
return f"Say hello to {name} in {lang}."
99+
100+
partial_greet = functools.partial(greet_prompt, lang="French")
101+
functools.update_wrapper(partial_greet, greet_prompt)
102+
103+
mcp.prompt(partial_greet)
104+
105+
async with Client(mcp) as client:
106+
result = await client.get_prompt("greet_prompt", {"name": "Alice"})
107+
assert "Alice" in str(result.messages[0])
108+
assert "French" in str(result.messages[0])
109+
110+
111+
class TestPartialResource:
112+
"""Test resources created from functools.partial objects."""
113+
114+
async def test_partial_resource_with_decorator(self):
115+
"""Partial can be registered via @mcp.resource() decorator."""
116+
mcp = FastMCP("test")
117+
118+
def get_data(key: str, fmt: str = "text") -> str:
119+
return f"{key} in {fmt} format"
120+
121+
partial_data = functools.partial(get_data, fmt="json")
122+
functools.update_wrapper(partial_data, get_data)
123+
124+
mcp.resource("data://{key}")(partial_data)
125+
126+
async with Client(mcp) as client:
127+
content = await client.read_resource("data://users")
128+
assert "users" in str(content)
129+
assert "json" in str(content)

0 commit comments

Comments
 (0)