"""Built-in tool to test a single MCP tool call in isolation."""
import asyncio
from ._internal.base import Tool, ToolContext, ToolResult
class TestMcpToolTool(Tool):
name = "test_mcp_tool"
description = (
"Execute a single MCP tool call for testing. "
"Pass server_name, tool_name, and arguments. "
"Returns the raw output and whether the tool reported an error. "
"Always use this after writing or editing an MCP server to verify "
"each tool works before reporting success to the user. "
"Do NOT use mcp_status for testing individual tools — mcp_status is for discovery only."
)
parameters = {
"type": "object",
"properties": {
"server_name": {
"type": "string",
"description": "MCP server name as listed in mcp_servers.d/<name>.json.",
},
"tool_name": {
"type": "string",
"description": "Tool name exposed by the server (without mcp: prefix).",
},
"arguments": {
"type": "object",
"description": "Arguments dict to pass to the tool. Omit or pass {} for tools with no required params.",
},
},
"required": ["server_name", "tool_name"],
}
def __init__(self, mcp_manager=None) -> None:
self._mcp_manager = mcp_manager
async def execute(self, params: dict, ctx: ToolContext | None = None) -> ToolResult:
server_name = (params.get("server_name") or "").strip()
tool_name = (params.get("tool_name") or "").strip()
arguments: dict = params.get("arguments") or {}
if not server_name:
return ToolResult(success=False, output="server_name is required.", error="missing server_name")
if not tool_name:
return ToolResult(success=False, output="tool_name is required.", error="missing tool_name")
manager = self._mcp_manager
if manager is None:
# Fallback to module-level global in case startup wiring was
# skipped because of an earlier exception in the same block.
from navi.api.deps import _mcp_manager as _global_mcp_manager
manager = _global_mcp_manager
if manager is None:
return ToolResult(
success=False,
output="MCP manager not available. Is the server running and MCP configured?",
error="no manager",
)
# Quick check: is the server even connected?
client = manager.clients.get(server_name)
if client is None:
return ToolResult(
success=False,
output=(
f"MCP server '{server_name}' is not connected.\n\n"
"Possible causes:\n"
"1. The server config file is missing from mcp_servers.d/ — run create_mcp_server again.\n"
"2. The server exited immediately on startup (check that main() is called at the bottom of mcp_server.py).\n"
"3. The server threw a traceback on startup — run `timeout 5 .venv/bin/python -m app.mcp_server` to see it.\n\n"
"Next steps:\n"
"1. Call `mcp_status` to see which servers are listed.\n"
"2. Inspect `mcp_servers.d/<name>.json` for correct absolute paths.\n"
"3. Call `reload_tools` to reconnect.\n"
"4. If still failing, read mcp_server.py and verify main() is called."
),
error="not_connected",
)
try:
output, is_error = await asyncio.wait_for(
manager.call_tool(server_name, tool_name, arguments),
timeout=30.0,
)
except asyncio.TimeoutError:
return ToolResult(
success=False,
output="Tool call timed out after 30 seconds.",
error="timeout",
)
except Exception as exc:
import traceback as _tb
hint = ""
if "not connected" in str(exc).lower():
hint = "\n\nHint: The server disconnected during the call. This usually means the server process crashed. Check the server code for runtime errors."
return ToolResult(
success=False,
output=f"Tool call failed: {exc}{hint}\n\nTraceback:\n{_tb.format_exc()}",
error="call failed",
)
if is_error:
return ToolResult(
success=False,
output=f"Tool reported an error.\n\nOutput:\n{output}",
error="tool error",
)
if not output or output.strip() == "":
return ToolResult(
success=True,
output=(
"Tool returned successfully but the response was EMPTY.\n\n"
"This is suspicious — the tool may have an early return or silent failure. "
"Check the tool implementation for missing return values or empty branches."
),
)
return ToolResult(
success=True,
output=f"OK — tool returned successfully.\n\nOutput:\n{output}",
)