fix(mcp): fix read tool to work with 2 arguments for agent compatibility

Fixed the read tool and all filesystem tools to properly work with agents
that call them with 2 positional arguments (ctx, file_path).

Changes:
- Updated auto_timeout decorator to handle instance methods properly
  - Changed wrapper signature to accept *args to handle both methods and functions
  - Properly extracts ctx from correct position (second arg for methods, first for functions)

- Made set_tool_context_info async and updated all callers
  - Changed from sync to async def
  - Added await for all calls to set_tool_context_info()
  - Fixed in: read, write, edit, multi_edit, ast_tool, rules_tool, symbols_tool

- Tests now pass without warnings
- Both run() and call() methods work correctly
- Agents can now properly invoke read tool with expected signature

Version: 0.9.1 -> 0.9.2
This commit is contained in:
2025-09-29 16:51:43 -07:00
parent e27d82cd68
commit c8eb744dd3
11 changed files with 46 additions and 28 deletions
@@ -128,47 +128,67 @@ class MCPToolTimeoutManager:
def with_auto_timeout(tool_name: str, timeout_manager: Optional[MCPToolTimeoutManager] = None):
"""Decorator to add automatic timeout and backgrounding to MCP tools.
Args:
tool_name: Name of the tool (for logging and process tracking)
timeout_manager: Optional timeout manager instance
Returns:
Decorator function
"""
if timeout_manager is None:
timeout_manager = MCPToolTimeoutManager()
def decorator(func: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
@functools.wraps(func)
async def wrapper(ctx: MCPContext, **params: Any) -> Any:
async def wrapper(*args: Any, **params: Any) -> Any:
# Handle both method calls (with self) and function calls
# For methods: args = (self, ctx), For functions: args = (ctx,)
if len(args) >= 2:
# Method call: self, ctx, **params
self_or_ctx = args[0]
ctx = args[1]
call_func = lambda: func(self_or_ctx, ctx, **params)
elif len(args) == 1:
# Function call: ctx, **params
ctx = args[0]
call_func = lambda: func(ctx, **params)
else:
raise TypeError(f"Expected at least 1 argument (ctx), got {len(args)}")
# Fast path for tests - skip timeout logic
if os.getenv("HANZO_MCP_FAST_TESTS") == "1":
return await func(ctx, **params)
return await call_func()
# Get tool-specific timeout
tool_timeout = timeout_manager._get_timeout_for_tool(tool_name)
# Create task for the tool execution
tool_task = asyncio.create_task(func(ctx, **params))
tool_task = asyncio.create_task(call_func())
try:
# Wait for completion with timeout
result = await asyncio.wait_for(tool_task, timeout=tool_timeout)
return result
except asyncio.TimeoutError:
# Tool timed out - background it
process_id = f"{tool_name}_{uuid.uuid4().hex[:8]}"
log_file = timeout_manager.process_manager.create_log_file(process_id)
# Start background execution
# Start background execution (need to reconstruct the call)
async def background_call():
if len(args) >= 2:
return await func(args[0], ctx, **params)
else:
return await func(ctx, **params)
asyncio.create_task(
timeout_manager._background_tool_execution(
func, tool_name, ctx, process_id, log_file, **params
background_call, tool_name, ctx, process_id, log_file, **params
)
)
# Return backgrounding message
timeout_formatted = format_timeout(tool_timeout)
return (
@@ -179,7 +199,7 @@ def with_auto_timeout(tool_name: str, timeout_manager: Optional[MCPToolTimeoutMa
f"Use 'process --action kill --id {process_id}' to cancel\\n\\n"
f"The {tool_name} operation is continuing in the background..."
)
return wrapper
return decorator
@@ -114,7 +114,7 @@ Searches code structure intelligently, understanding syntax and providing semant
Tool result
"""
tool_ctx = self.create_tool_context(ctx)
self.set_tool_context_info(tool_ctx)
await self.set_tool_context_info(tool_ctx)
# Extract parameters
pattern: Pattern = params["pattern"]
@@ -104,10 +104,10 @@ class FilesystemBaseTool(FileSystemTool, ABC):
tool_ctx = create_tool_context(ctx)
return tool_ctx
def set_tool_context_info(self, tool_ctx: ToolContext) -> None:
async def set_tool_context_info(self, tool_ctx: ToolContext) -> None:
"""Set the tool info on the context.
Args:
tool_ctx: Tool context
"""
tool_ctx.set_tool_info(self.name)
await tool_ctx.set_tool_info(self.name)
@@ -108,7 +108,7 @@ Usage:
Tool result
"""
tool_ctx = self.create_tool_context(ctx)
self.set_tool_context_info(tool_ctx)
await self.set_tool_context_info(tool_ctx)
# Extract parameters
file_path: FilePath = params["file_path"]
@@ -152,7 +152,7 @@ If you want to create a new file, use:
Tool result
"""
tool_ctx = self.create_tool_context(ctx)
self.set_tool_context_info(tool_ctx)
await self.set_tool_context_info(tool_ctx)
# Extract parameters
file_path: FilePath = params["file_path"]
@@ -94,8 +94,6 @@ Usage:
@override
@auto_timeout("read")
async def call(
self,
ctx: MCPContext,
@@ -111,7 +109,7 @@ Usage:
Tool result
"""
tool_ctx = self.create_tool_context(ctx)
self.set_tool_context_info(tool_ctx)
await self.set_tool_context_info(tool_ctx)
# Extract parameters
file_path = params.get("file_path")
@@ -94,7 +94,7 @@ understand project-specific requirements and preferences."""
Tool result
"""
tool_ctx = self.create_tool_context(ctx)
self.set_tool_context_info(tool_ctx)
await self.set_tool_context_info(tool_ctx)
# Extract parameters
search_path = params.get("path", ".")
@@ -138,7 +138,7 @@ Finds code structures (functions, classes, methods) with full context."""
) -> str:
"""Execute symbols operation."""
tool_ctx = self.create_tool_context(ctx)
self.set_tool_context_info(tool_ctx)
await self.set_tool_context_info(tool_ctx)
# Extract action
action = params.get("action", "search")
@@ -92,7 +92,7 @@ Usage:
Tool result
"""
tool_ctx = self.create_tool_context(ctx)
self.set_tool_context_info(tool_ctx)
await self.set_tool_context_info(tool_ctx)
# Extract parameters
file_path: FilePath = params["file_path"]
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "hanzo-mcp"
version = "0.9.1"
version = "0.9.2"
description = "The Zen of Hanzo MCP: One server to rule them all. The ultimate MCP that orchestrates all others."
readme = "README.md"
requires-python = ">=3.12"
+1 -1
View File
@@ -1003,7 +1003,7 @@ wheels = [
[[package]]
name = "hanzo-mcp"
version = "0.8.14"
version = "0.9.1"
source = { editable = "." }
dependencies = [
{ name = "bashlex" },