Skip to content

Commit c3b691e

Browse files
committed
report missing tests
1 parent 64a5ff9 commit c3b691e

File tree

2 files changed

+292
-3
lines changed

2 files changed

+292
-3
lines changed

src/mcpcat/modules/overrides/mcp_server.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ async def wrapped_list_tools_handler(request: ListToolsRequest) -> ServerResult:
9999
tools_list.append(report_missing_tool)
100100

101101
# Add context parameters to existing tools if enabled
102-
if data.options.enable_tool_context:
102+
if data.options.enable_tool_call_context:
103103
for tool in tools_list:
104104
if tool.name != "report_missing": # Don't modify our own tool
105105
if not tool.inputSchema:
@@ -155,9 +155,10 @@ async def wrapped_call_tool_handler(request: CallToolRequest) -> ServerResult:
155155
result = await handle_report_missing(arguments)
156156
event.response = result.model_dump() if result else None
157157
event_queue.publish_event(server, event)
158+
return result
158159

159160
# Extract MCPCat context if enabled
160-
if data.options.enable_tool_context:
161+
if data.options.enable_tool_call_context:
161162
event.user_intent = arguments.pop("context", None)
162163
# Log warning if context is missing and tool is not report_missing
163164
if event.user_intent is None and tool_name != "report_missing":
@@ -170,7 +171,7 @@ async def wrapped_call_tool_handler(request: CallToolRequest) -> ServerResult:
170171
result = await original_call_tool_handler(request)
171172

172173
# Record the trace using existing infrastructure
173-
event.response=result.model_dump() if result else None,
174+
event.response = result.model_dump() if result else None
174175
event_queue.publish_event(server, event)
175176
return result
176177

tests/test_report_missing.py

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
"""Test report_missing functionality."""
2+
3+
import pytest
4+
5+
from mcpcat import MCPCatOptions, track
6+
7+
from .test_utils.client import create_test_client
8+
from .test_utils.todo_server import create_todo_server
9+
10+
11+
class TestReportMissing:
12+
"""Test report_missing functionality."""
13+
14+
@pytest.mark.asyncio
15+
async def test_report_missing_tool_injection(self):
16+
"""Test that report_missing tool is properly injected when enabled."""
17+
# Create a new server instance
18+
server = create_todo_server()
19+
20+
# Track the server with report_missing enabled
21+
options = MCPCatOptions(enable_report_missing=True)
22+
track(server, "test_project", options)
23+
24+
# Use client to list all tools and verify report_missing is injected
25+
async with create_test_client(server) as client:
26+
# List all tools on the server
27+
tools_result = await client.list_tools()
28+
29+
# Get tool names
30+
tool_names = [tool.name for tool in tools_result.tools]
31+
32+
# Verify original tools are present
33+
assert "add_todo" in tool_names
34+
assert "list_todos" in tool_names
35+
assert "complete_todo" in tool_names
36+
37+
# Verify report_missing tool was injected
38+
assert "report_missing" in tool_names
39+
40+
@pytest.mark.asyncio
41+
async def test_report_missing_disabled_by_default(self):
42+
"""Verify tool is NOT injected when enable_report_missing=False."""
43+
server = create_todo_server()
44+
45+
# Track with report_missing disabled
46+
options = MCPCatOptions(enable_report_missing=False)
47+
track(server, "test_project", options)
48+
49+
async with create_test_client(server) as client:
50+
tools_result = await client.list_tools()
51+
tool_names = [tool.name for tool in tools_result.tools]
52+
53+
# Verify report_missing is NOT present
54+
assert "report_missing" not in tool_names
55+
# But original tools should still be there
56+
assert "add_todo" in tool_names
57+
58+
@pytest.mark.asyncio
59+
async def test_report_missing_tool_call_success(self):
60+
"""Call report_missing tool and verify it executes successfully."""
61+
server = create_todo_server()
62+
options = MCPCatOptions(enable_report_missing=True)
63+
track(server, "test_project", options)
64+
65+
async with create_test_client(server) as client:
66+
result = await client.call_tool(
67+
"report_missing",
68+
{
69+
"missing_tool": "translate_text",
70+
"description": "Need a tool to translate text between languages"
71+
}
72+
)
73+
74+
# Verify successful response
75+
assert result.content
76+
assert len(result.content) == 1
77+
assert result.content[0].type == "text"
78+
assert "translate_text" in result.content[0].text
79+
assert "Thank you for reporting" in result.content[0].text
80+
81+
@pytest.mark.asyncio
82+
async def test_report_missing_with_valid_params(self):
83+
"""Test with both required parameters (missing_tool, description)."""
84+
server = create_todo_server()
85+
options = MCPCatOptions(enable_report_missing=True)
86+
track(server, "test_project", options)
87+
88+
async with create_test_client(server) as client:
89+
# Test with different valid parameters
90+
test_cases = [
91+
{
92+
"missing_tool": "database_query",
93+
"description": "Execute SQL queries on a database"
94+
},
95+
{
96+
"missing_tool": "send_email",
97+
"description": "Send emails with attachments"
98+
},
99+
{
100+
"missing_tool": "generate_chart",
101+
"description": "Create charts from data"
102+
}
103+
]
104+
105+
for params in test_cases:
106+
result = await client.call_tool("report_missing", params)
107+
assert result.content[0].text
108+
assert params["missing_tool"] in result.content[0].text
109+
110+
@pytest.mark.asyncio
111+
async def test_report_missing_with_missing_params(self):
112+
"""Test error handling when required parameters are missing."""
113+
server = create_todo_server()
114+
options = MCPCatOptions(enable_report_missing=True)
115+
track(server, "test_project", options)
116+
117+
async with create_test_client(server) as client:
118+
# Test with missing parameters - should still work but with empty strings
119+
result = await client.call_tool("report_missing", {})
120+
assert result.content[0].text
121+
assert "Thank you for reporting" in result.content[0].text
122+
123+
# Test with only one parameter
124+
result = await client.call_tool(
125+
"report_missing",
126+
{"missing_tool": "test_tool"}
127+
)
128+
assert result.content[0].text
129+
assert "test_tool" in result.content[0].text
130+
131+
@pytest.mark.asyncio
132+
async def test_report_missing_with_extra_params(self):
133+
"""Test that extra parameters are ignored/handled properly."""
134+
server = create_todo_server()
135+
options = MCPCatOptions(enable_report_missing=True)
136+
track(server, "test_project", options)
137+
138+
async with create_test_client(server) as client:
139+
result = await client.call_tool(
140+
"report_missing",
141+
{
142+
"missing_tool": "api_caller",
143+
"description": "Call external APIs",
144+
"extra_param": "should be ignored",
145+
"another_extra": 12345
146+
}
147+
)
148+
149+
# Should still work normally
150+
assert result.content[0].text
151+
assert "api_caller" in result.content[0].text
152+
153+
@pytest.mark.asyncio
154+
async def test_report_missing_with_other_tools(self):
155+
"""Verify report_missing doesn't interfere with existing server tools."""
156+
server = create_todo_server()
157+
options = MCPCatOptions(enable_report_missing=True)
158+
track(server, "test_project", options)
159+
160+
async with create_test_client(server) as client:
161+
# First use a regular tool
162+
add_result = await client.call_tool(
163+
"add_todo",
164+
{"text": "Test todo item"}
165+
)
166+
assert "Added todo" in add_result.content[0].text
167+
168+
# Then use report_missing
169+
report_result = await client.call_tool(
170+
"report_missing",
171+
{
172+
"missing_tool": "delete_todo",
173+
"description": "Delete a todo item"
174+
}
175+
)
176+
assert "delete_todo" in report_result.content[0].text
177+
178+
# Verify the original tool still works
179+
list_result = await client.call_tool("list_todos")
180+
assert "Test todo item" in list_result.content[0].text
181+
182+
@pytest.mark.asyncio
183+
async def test_multiple_report_missing_calls(self):
184+
"""Test calling report_missing multiple times in succession."""
185+
server = create_todo_server()
186+
options = MCPCatOptions(enable_report_missing=True)
187+
track(server, "test_project", options)
188+
189+
async with create_test_client(server) as client:
190+
# Call report_missing multiple times
191+
tools_to_report = [
192+
("tool1", "Description 1"),
193+
("tool2", "Description 2"),
194+
("tool3", "Description 3")
195+
]
196+
197+
for tool_name, description in tools_to_report:
198+
result = await client.call_tool(
199+
"report_missing",
200+
{
201+
"missing_tool": tool_name,
202+
"description": description
203+
}
204+
)
205+
# Each call should work identically
206+
assert result.content[0].text
207+
assert tool_name in result.content[0].text
208+
assert "Thank you for reporting" in result.content[0].text
209+
210+
@pytest.mark.asyncio
211+
async def test_report_missing_with_context_enabled(self):
212+
"""Test interaction when both report_missing and tool_context are enabled."""
213+
server = create_todo_server()
214+
options = MCPCatOptions(
215+
enable_report_missing=True,
216+
enable_tool_call_context=True
217+
)
218+
track(server, "test_project", options)
219+
220+
async with create_test_client(server) as client:
221+
tools_result = await client.list_tools()
222+
223+
# Find the report_missing tool
224+
report_missing_tool = None
225+
other_tool = None
226+
for tool in tools_result.tools:
227+
if tool.name == "report_missing":
228+
report_missing_tool = tool
229+
elif tool.name == "add_todo":
230+
other_tool = tool
231+
232+
assert report_missing_tool is not None
233+
assert other_tool is not None
234+
235+
# Verify context is NOT added to report_missing tool
236+
assert "context" not in report_missing_tool.inputSchema.get("properties", {})
237+
238+
# But context should be added to other tools
239+
assert "context" in other_tool.inputSchema.get("properties", {})
240+
241+
@pytest.mark.skip(reason="Creating empty low-level server is complex and already tested via FastMCP")
242+
@pytest.mark.asyncio
243+
async def test_report_missing_on_server_without_tools(self):
244+
"""Test on a server that has no tools initially."""
245+
# This is a complex edge case that would require creating a low-level
246+
# server with proper handler setup. The functionality is already tested
247+
# through other tests using FastMCP servers.
248+
pass
249+
250+
@pytest.mark.asyncio
251+
async def test_report_missing_with_null_values(self):
252+
"""Test with null/None values for parameters."""
253+
server = create_todo_server()
254+
options = MCPCatOptions(enable_report_missing=True)
255+
track(server, "test_project", options)
256+
257+
async with create_test_client(server) as client:
258+
# Test with None values - they should be treated as empty strings
259+
result = await client.call_tool(
260+
"report_missing",
261+
{
262+
"missing_tool": None,
263+
"description": None
264+
}
265+
)
266+
# Should still return a valid response
267+
assert result.content[0].text
268+
assert "Thank you for reporting" in result.content[0].text
269+
270+
@pytest.mark.asyncio
271+
async def test_report_missing_with_invalid_types(self):
272+
"""Test with wrong parameter types (e.g., number instead of string)."""
273+
server = create_todo_server()
274+
options = MCPCatOptions(enable_report_missing=True)
275+
track(server, "test_project", options)
276+
277+
async with create_test_client(server) as client:
278+
# Test with numbers instead of strings
279+
result = await client.call_tool(
280+
"report_missing",
281+
{
282+
"missing_tool": 12345,
283+
"description": 67890
284+
}
285+
)
286+
# Should convert to string and work
287+
assert result.content[0].text
288+
assert "12345" in result.content[0].text

0 commit comments

Comments
 (0)