mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
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:
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
Generated
+1
-1
@@ -1003,7 +1003,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "hanzo-mcp"
|
||||
version = "0.8.14"
|
||||
version = "0.9.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "bashlex" },
|
||||
|
||||
Reference in New Issue
Block a user