mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
feat(hanzo-mcp): improve tool output formatting for agent display
- Add MCPResourceDocument.to_readable_string() for human-readable output - Update search, find, lsp, refactor tools to use readable format - Cleaner to_json_string() that doesn't double-wrap data - Better formatted results with: - Search stats and timing info - Numbered results with file:line locations - Truncated long matches for readability - Pagination info - Sync improvements to hanzo-tools-core package
This commit is contained in:
@@ -668,9 +668,10 @@ class LSPTool(BaseTool):
|
||||
return MCPResourceDocument(data=result)
|
||||
|
||||
async def call(self, **kwargs) -> str:
|
||||
"""Tool interface for MCP - converts result to JSON string."""
|
||||
"""Tool interface for MCP - converts result to readable format."""
|
||||
result = await self.run(**kwargs)
|
||||
return result.to_json_string()
|
||||
# Use readable format for better Claude Code display
|
||||
return result.to_readable_string()
|
||||
|
||||
def register(self, mcp_server) -> None:
|
||||
"""Register tool with MCP server."""
|
||||
|
||||
@@ -2112,9 +2112,10 @@ Find references: refactor("find_references", file="f.py", line=10, column=5)"""
|
||||
return "\n".join(result)
|
||||
|
||||
async def call(self, **kwargs) -> str:
|
||||
"""Tool interface for MCP - converts result to JSON string."""
|
||||
"""Tool interface for MCP - converts result to readable format."""
|
||||
result = await self.run(**kwargs)
|
||||
return result.to_json_string()
|
||||
# Use readable format for better Claude Code display
|
||||
return result.to_readable_string()
|
||||
|
||||
def register(self, mcp_server) -> None:
|
||||
"""Register tool with MCP server."""
|
||||
|
||||
@@ -388,9 +388,10 @@ class FindTool(BaseTool):
|
||||
|
||||
@auto_timeout("find")
|
||||
async def call(self, ctx=None, **kwargs) -> str:
|
||||
"""Tool interface for MCP - converts result to JSON string."""
|
||||
"""Tool interface for MCP - converts result to readable format."""
|
||||
result = await self.run(**kwargs)
|
||||
return result.to_json_string()
|
||||
# Use readable format for better Claude Code display
|
||||
return result.to_readable_string()
|
||||
|
||||
def register(self, mcp_server) -> None:
|
||||
"""Register tool with MCP server."""
|
||||
|
||||
@@ -195,7 +195,9 @@ class SearchTool(BaseTool):
|
||||
@auto_timeout("search")
|
||||
async def call(self, ctx=None, **kwargs) -> str:
|
||||
result = await self.run(**kwargs)
|
||||
return result.to_json_string()
|
||||
# Use readable format for better Claude Code display
|
||||
# Structured data still available via result.to_json_string() if needed
|
||||
return result.to_readable_string()
|
||||
|
||||
def register(self, mcp_server) -> None:
|
||||
@mcp_server.tool(name=self.name, description=self.description)
|
||||
|
||||
@@ -6,20 +6,103 @@ from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class MCPResourceDocument:
|
||||
"""Resource document returned by MCP tools."""
|
||||
"""Resource document returned by MCP tools.
|
||||
|
||||
Output format options:
|
||||
- to_json_string(): Clean JSON without outer wrapper (default)
|
||||
- to_readable_string(): Human-readable formatted text
|
||||
- to_dict(): Full dict structure with data/metadata
|
||||
"""
|
||||
|
||||
data: Dict[str, Any]
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary format."""
|
||||
"""Convert to dictionary format with data/metadata structure."""
|
||||
result = {"data": self.data}
|
||||
if self.metadata:
|
||||
result["metadata"] = self.metadata
|
||||
return result
|
||||
|
||||
def to_json_string(self) -> str:
|
||||
"""Convert to JSON string."""
|
||||
"""Convert to clean JSON string - just the data, not wrapped."""
|
||||
import json
|
||||
|
||||
return json.dumps(self.to_dict(), indent=2)
|
||||
# Return just the data content, not wrapped in {"data": ...}
|
||||
# This makes output cleaner when MCP adds its own wrapper
|
||||
return json.dumps({"result": self.data}, indent=2)
|
||||
|
||||
def to_readable_string(self) -> str:
|
||||
"""Convert to human-readable formatted string for display."""
|
||||
import json
|
||||
|
||||
lines = []
|
||||
|
||||
# Format the main data
|
||||
if isinstance(self.data, dict):
|
||||
# Handle common result structures
|
||||
if "results" in self.data:
|
||||
results = self.data["results"]
|
||||
stats = self.data.get("stats", {})
|
||||
pagination = self.data.get("pagination", {})
|
||||
|
||||
# Header with stats
|
||||
if stats:
|
||||
query = stats.get("query", "")
|
||||
total = stats.get("total", len(results))
|
||||
time_ms = stats.get("time_ms", {})
|
||||
if time_ms:
|
||||
total_time = sum(time_ms.values()) if isinstance(time_ms, dict) else time_ms
|
||||
lines.append(f"# Search: '{query}' ({total} results, {total_time}ms)")
|
||||
else:
|
||||
lines.append(f"# Found {total} results")
|
||||
lines.append("")
|
||||
|
||||
# Results
|
||||
for i, result in enumerate(results[:50], 1): # Limit display
|
||||
if isinstance(result, dict):
|
||||
file_path = result.get("file", result.get("path", ""))
|
||||
line = result.get("line", "")
|
||||
match = result.get("match", result.get("text", ""))
|
||||
rtype = result.get("type", "")
|
||||
|
||||
if file_path:
|
||||
loc = f"{file_path}:{line}" if line else file_path
|
||||
lines.append(f"{i}. {loc}")
|
||||
if match:
|
||||
# Truncate long matches
|
||||
match_preview = match[:200] + "..." if len(match) > 200 else match
|
||||
lines.append(f" {match_preview}")
|
||||
if rtype:
|
||||
lines.append(f" [{rtype}]")
|
||||
else:
|
||||
lines.append(f"{i}. {json.dumps(result)}")
|
||||
else:
|
||||
lines.append(f"{i}. {result}")
|
||||
|
||||
# Pagination info
|
||||
if pagination:
|
||||
page = pagination.get("page", 1)
|
||||
total = pagination.get("total", 0)
|
||||
has_next = pagination.get("has_next", False)
|
||||
if has_next:
|
||||
lines.append(f"\n... showing page {page} of {(total // 50) + 1}")
|
||||
else:
|
||||
# Generic dict - format as key-value pairs
|
||||
for key, value in self.data.items():
|
||||
if isinstance(value, (dict, list)):
|
||||
lines.append(f"{key}:")
|
||||
lines.append(json.dumps(value, indent=2))
|
||||
else:
|
||||
lines.append(f"{key}: {value}")
|
||||
else:
|
||||
# Non-dict data - just dump as JSON
|
||||
lines.append(json.dumps(self.data, indent=2))
|
||||
|
||||
# Add metadata footer if present
|
||||
if self.metadata:
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
for key, value in self.metadata.items():
|
||||
lines.append(f"{key}: {value}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -1,24 +1,155 @@
|
||||
"""Type definitions for Hanzo tool packages."""
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCPResourceDocument:
|
||||
"""Resource document returned by MCP tools."""
|
||||
"""Resource document returned by MCP tools.
|
||||
|
||||
Output format options:
|
||||
- to_json_string(): Clean JSON format (default for structured data)
|
||||
- to_readable_string(): Human-readable formatted text for display
|
||||
- to_dict(): Full dict structure with data/metadata
|
||||
"""
|
||||
|
||||
data: Dict[str, Any]
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary format."""
|
||||
"""Convert to dictionary format with data/metadata structure."""
|
||||
result = {"data": self.data}
|
||||
if self.metadata:
|
||||
result["metadata"] = self.metadata
|
||||
return result
|
||||
|
||||
def to_json_string(self) -> str:
|
||||
"""Convert to JSON string."""
|
||||
return json.dumps(self.to_dict(), indent=2)
|
||||
"""Convert to clean JSON string."""
|
||||
# Return wrapped in "result" for consistency
|
||||
return json.dumps({"result": self.data}, indent=2)
|
||||
|
||||
def to_readable_string(self) -> str:
|
||||
"""Convert to human-readable formatted string for display.
|
||||
|
||||
Optimized for readability in Claude Code output panels.
|
||||
"""
|
||||
lines: List[str] = []
|
||||
|
||||
if isinstance(self.data, dict):
|
||||
# Handle search/find results with "results" array
|
||||
if "results" in self.data:
|
||||
results = self.data["results"]
|
||||
stats = self.data.get("stats", {})
|
||||
pagination = self.data.get("pagination", {})
|
||||
|
||||
# Header with stats
|
||||
if stats:
|
||||
query = stats.get("query", stats.get("pattern", ""))
|
||||
total = stats.get("total", len(results))
|
||||
time_ms = stats.get("time_ms", {})
|
||||
if time_ms:
|
||||
if isinstance(time_ms, dict):
|
||||
total_time = sum(time_ms.values())
|
||||
else:
|
||||
total_time = time_ms
|
||||
lines.append(f"# Search: '{query}' ({total} results, {total_time}ms)")
|
||||
else:
|
||||
lines.append(f"# Found {total} results for '{query}'")
|
||||
else:
|
||||
lines.append(f"# Found {len(results)} results")
|
||||
lines.append("")
|
||||
|
||||
# Format each result
|
||||
for i, result in enumerate(results[:50], 1):
|
||||
if isinstance(result, dict):
|
||||
# Common patterns for search results
|
||||
file_path = result.get("file", result.get("path", ""))
|
||||
line_num = result.get("line", result.get("line_number", ""))
|
||||
match_text = result.get("match", result.get("text", result.get("content", "")))
|
||||
result_type = result.get("type", "")
|
||||
|
||||
if file_path:
|
||||
loc = f"{file_path}:{line_num}" if line_num else file_path
|
||||
lines.append(f"{i}. {loc}")
|
||||
if match_text:
|
||||
# Truncate long matches for readability
|
||||
preview = match_text[:200] + "..." if len(match_text) > 200 else match_text
|
||||
lines.append(f" {preview}")
|
||||
if result_type:
|
||||
lines.append(f" [{result_type}]")
|
||||
else:
|
||||
lines.append(f"{i}. {json.dumps(result, default=str)}")
|
||||
else:
|
||||
lines.append(f"{i}. {result}")
|
||||
|
||||
# Show pagination info
|
||||
if pagination:
|
||||
page = pagination.get("page", 1)
|
||||
total = pagination.get("total", 0)
|
||||
has_next = pagination.get("has_next", False)
|
||||
if has_next and total > 0:
|
||||
total_pages = (total // 50) + 1
|
||||
lines.append(f"\n... page {page} of {total_pages} ({total} total)")
|
||||
|
||||
# Handle command execution results
|
||||
elif "output" in self.data or "stdout" in self.data or "stderr" in self.data:
|
||||
# Shell command output
|
||||
exit_code = self.data.get("exit_code", self.data.get("returncode", 0))
|
||||
stdout = self.data.get("output", self.data.get("stdout", ""))
|
||||
stderr = self.data.get("stderr", "")
|
||||
elapsed = self.data.get("elapsed", self.data.get("time_ms", ""))
|
||||
|
||||
if exit_code == 0:
|
||||
lines.append(f"✓ Command succeeded")
|
||||
else:
|
||||
lines.append(f"✗ Command failed (exit {exit_code})")
|
||||
|
||||
if elapsed:
|
||||
lines.append(f"Time: {elapsed}ms" if isinstance(elapsed, (int, float)) else f"Time: {elapsed}")
|
||||
lines.append("")
|
||||
|
||||
if stdout:
|
||||
lines.append(stdout.rstrip())
|
||||
if stderr:
|
||||
lines.append("\n--- stderr ---")
|
||||
lines.append(stderr.rstrip())
|
||||
|
||||
# Handle error results
|
||||
elif "error" in self.data:
|
||||
lines.append(f"Error: {self.data['error']}")
|
||||
if "details" in self.data:
|
||||
lines.append(f"Details: {self.data['details']}")
|
||||
|
||||
# Generic dict - format as key-value pairs
|
||||
else:
|
||||
for key, value in self.data.items():
|
||||
if isinstance(value, (dict, list)):
|
||||
lines.append(f"{key}:")
|
||||
lines.append(json.dumps(value, indent=2, default=str))
|
||||
else:
|
||||
lines.append(f"{key}: {value}")
|
||||
|
||||
elif isinstance(self.data, list):
|
||||
# List data - format as numbered items
|
||||
for i, item in enumerate(self.data[:50], 1):
|
||||
if isinstance(item, dict):
|
||||
lines.append(f"{i}. {json.dumps(item, default=str)}")
|
||||
else:
|
||||
lines.append(f"{i}. {item}")
|
||||
if len(self.data) > 50:
|
||||
lines.append(f"\n... {len(self.data) - 50} more items")
|
||||
|
||||
else:
|
||||
# Scalar or other - just convert to string
|
||||
lines.append(str(self.data))
|
||||
|
||||
# Add metadata footer if present
|
||||
if self.metadata:
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
for key, value in self.metadata.items():
|
||||
lines.append(f"{key}: {value}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
Reference in New Issue
Block a user