mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
544 lines
20 KiB
Python
544 lines
20 KiB
Python
"""
|
|
Unified Development Tools - MCP Integration
|
|
===========================================
|
|
|
|
Exposes the 6 orthogonal development tools as MCP tools:
|
|
- edit: Semantic refactoring via LSP
|
|
- fmt: Code formatting + import normalization
|
|
- test: Run tests narrowly by default
|
|
- build: Compile/build artifacts
|
|
- lint: Static analysis + type checking
|
|
- guard: Repository invariants enforcement
|
|
|
|
Each tool supports:
|
|
- Multi-language detection (go, ts, py, rs, cc, sol, schema)
|
|
- Workspace-aware operation (go.work, package.json, etc.)
|
|
- Consistent input/output schemas
|
|
- Composable workflows
|
|
"""
|
|
|
|
from typing import Any, Dict, List
|
|
|
|
from mcp.server import Server
|
|
from mcp.types import TextContent, Tool
|
|
|
|
try:
|
|
from .build_tool import build_tool_handler
|
|
from .edit_tool import edit_tool_handler
|
|
from .fmt_tool import fmt_tool_handler
|
|
from .guard_tool import guard_tool_handler
|
|
from .lint_tool import lint_tool_handler
|
|
from .test_tool import test_tool_handler
|
|
except ImportError:
|
|
# Fallback for testing
|
|
print("Warning: Could not import all dev tool handlers")
|
|
|
|
# MCP Server instance
|
|
dev_tools_server = Server("hanzo-dev-tools")
|
|
|
|
# Common parameter schemas
|
|
TARGET_PARAM = {
|
|
"type": "string",
|
|
"description": "Target specification: file:<path>, dir:<path>, pkg:<spec>, ws (workspace), or changed (git diff)",
|
|
}
|
|
|
|
LANGUAGE_PARAM = {
|
|
"type": "string",
|
|
"enum": ["auto", "go", "ts", "py", "rs", "cc", "sol", "schema"],
|
|
"default": "auto",
|
|
"description": "Language override (auto-detected by default)",
|
|
}
|
|
|
|
BACKEND_PARAM = {
|
|
"type": "string",
|
|
"enum": [
|
|
"auto",
|
|
"go",
|
|
"pnpm",
|
|
"yarn",
|
|
"npm",
|
|
"bun",
|
|
"pytest",
|
|
"uv",
|
|
"poetry",
|
|
"cargo",
|
|
"cmake",
|
|
"ninja",
|
|
"make",
|
|
"buf",
|
|
"capnp",
|
|
],
|
|
"default": "auto",
|
|
"description": "Backend/tool override (auto-detected by default)",
|
|
}
|
|
|
|
COMMON_PARAMS = {
|
|
"root": {"type": "string", "description": "Workspace root override"},
|
|
"env": {"type": "object", "description": "Additional environment variables"},
|
|
"dry_run": {
|
|
"type": "boolean",
|
|
"default": False,
|
|
"description": "Show planned operations without executing",
|
|
},
|
|
}
|
|
|
|
|
|
@dev_tools_server.list_tools()
|
|
async def list_tools() -> List[Tool]:
|
|
"""List all available development tools"""
|
|
return [
|
|
Tool(
|
|
name="edit",
|
|
description="Semantic refactoring via LSP (rename, code_action, organize_imports)",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"target": TARGET_PARAM,
|
|
"op": {
|
|
"type": "string",
|
|
"enum": [
|
|
"rename",
|
|
"code_action",
|
|
"organize_imports",
|
|
"apply_workspace_edit",
|
|
],
|
|
"description": "Edit operation to perform",
|
|
},
|
|
"file": {
|
|
"type": "string",
|
|
"description": "File path for rename/code_action operations",
|
|
},
|
|
"pos": {
|
|
"type": "object",
|
|
"properties": {
|
|
"line": {"type": "integer"},
|
|
"character": {"type": "integer"},
|
|
},
|
|
"description": "Position for rename/code_action (0-based)",
|
|
},
|
|
"range": {
|
|
"type": "object",
|
|
"properties": {
|
|
"start": {
|
|
"type": "object",
|
|
"properties": {
|
|
"line": {"type": "integer"},
|
|
"character": {"type": "integer"},
|
|
},
|
|
},
|
|
"end": {
|
|
"type": "object",
|
|
"properties": {
|
|
"line": {"type": "integer"},
|
|
"character": {"type": "integer"},
|
|
},
|
|
},
|
|
},
|
|
"description": "Range for code actions",
|
|
},
|
|
"new_name": {
|
|
"type": "string",
|
|
"description": "New name for rename operation",
|
|
},
|
|
"only": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Code action kinds to apply",
|
|
},
|
|
"apply": {
|
|
"type": "boolean",
|
|
"default": True,
|
|
"description": "Whether to apply edits to disk",
|
|
},
|
|
"language": LANGUAGE_PARAM,
|
|
"backend": BACKEND_PARAM,
|
|
**COMMON_PARAMS,
|
|
},
|
|
"required": ["target", "op"],
|
|
},
|
|
),
|
|
Tool(
|
|
name="fmt",
|
|
description="Code formatting + import normalization",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"target": TARGET_PARAM,
|
|
"language": LANGUAGE_PARAM,
|
|
"backend": BACKEND_PARAM,
|
|
"opts": {
|
|
"type": "object",
|
|
"properties": {
|
|
"local_prefix": {
|
|
"type": "string",
|
|
"description": "Local import prefix for Go (e.g. github.com/luxfi)",
|
|
}
|
|
},
|
|
"description": "Formatting options",
|
|
},
|
|
**COMMON_PARAMS,
|
|
},
|
|
"required": ["target"],
|
|
},
|
|
),
|
|
Tool(
|
|
name="test",
|
|
description="Run tests narrowly by default (file→package, dir→subtree, pkg→explicit, ws→all)",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"target": TARGET_PARAM,
|
|
"language": LANGUAGE_PARAM,
|
|
"backend": BACKEND_PARAM,
|
|
"opts": {
|
|
"type": "object",
|
|
"properties": {
|
|
"run": {
|
|
"type": "string",
|
|
"description": "Test name pattern (Go -run)",
|
|
},
|
|
"count": {
|
|
"type": "integer",
|
|
"description": "Test count (Go -count)",
|
|
},
|
|
"race": {
|
|
"type": "boolean",
|
|
"description": "Enable race detection (Go -race)",
|
|
},
|
|
"filter": {
|
|
"type": "string",
|
|
"description": "Test filter (JS --filter, Python -k)",
|
|
},
|
|
"k": {"type": "string", "description": "Pytest -k filter"},
|
|
"m": {"type": "string", "description": "Pytest -m marker"},
|
|
"p": {
|
|
"type": "string",
|
|
"description": "Rust package filter",
|
|
},
|
|
"features": {
|
|
"type": "string",
|
|
"description": "Rust features",
|
|
},
|
|
},
|
|
"description": "Test options",
|
|
},
|
|
**COMMON_PARAMS,
|
|
},
|
|
"required": ["target"],
|
|
},
|
|
),
|
|
Tool(
|
|
name="build",
|
|
description="Compile/build artifacts (same scope as test)",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"target": TARGET_PARAM,
|
|
"language": LANGUAGE_PARAM,
|
|
"backend": BACKEND_PARAM,
|
|
"opts": {
|
|
"type": "object",
|
|
"properties": {
|
|
"race": {
|
|
"type": "boolean",
|
|
"description": "Enable race detection (Go)",
|
|
},
|
|
"release": {
|
|
"type": "boolean",
|
|
"description": "Release build (Rust)",
|
|
},
|
|
"tags": {
|
|
"type": "string",
|
|
"description": "Build tags (Go)",
|
|
},
|
|
"ldflags": {
|
|
"type": "string",
|
|
"description": "Linker flags (Go)",
|
|
},
|
|
"features": {
|
|
"type": "string",
|
|
"description": "Features (Rust)",
|
|
},
|
|
},
|
|
"description": "Build options",
|
|
},
|
|
**COMMON_PARAMS,
|
|
},
|
|
"required": ["target"],
|
|
},
|
|
),
|
|
Tool(
|
|
name="lint",
|
|
description="Static analysis + type checking (language-specific linters + type checkers)",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"target": TARGET_PARAM,
|
|
"language": LANGUAGE_PARAM,
|
|
"backend": BACKEND_PARAM,
|
|
"opts": {
|
|
"type": "object",
|
|
"properties": {
|
|
"fix": {
|
|
"type": "boolean",
|
|
"default": False,
|
|
"description": "Apply fixes where supported",
|
|
}
|
|
},
|
|
"description": "Lint options",
|
|
},
|
|
**COMMON_PARAMS,
|
|
},
|
|
"required": ["target"],
|
|
},
|
|
),
|
|
Tool(
|
|
name="guard",
|
|
description="Repository invariant enforcement (boundaries, forbidden imports, generated files)",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"target": TARGET_PARAM,
|
|
"rules": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "string",
|
|
"description": "Rule identifier",
|
|
},
|
|
"glob": {
|
|
"type": "string",
|
|
"description": "File glob pattern",
|
|
},
|
|
"pattern": {
|
|
"type": "string",
|
|
"description": "Regex pattern to match (for regex rules)",
|
|
},
|
|
"forbid_import_prefix": {
|
|
"type": "string",
|
|
"description": "Forbidden import prefix (for import rules)",
|
|
},
|
|
"forbid_writes": {
|
|
"type": "boolean",
|
|
"description": "Forbid writes to matched files (for generated rules)",
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"description": "Custom violation message",
|
|
},
|
|
"description": {
|
|
"type": "string",
|
|
"description": "Rule description",
|
|
},
|
|
},
|
|
"required": ["id", "glob"],
|
|
},
|
|
"description": "Custom guard rules",
|
|
},
|
|
"use_defaults": {
|
|
"type": "boolean",
|
|
"default": True,
|
|
"description": "Include default Hanzo ecosystem rules",
|
|
},
|
|
"language": LANGUAGE_PARAM,
|
|
"backend": BACKEND_PARAM,
|
|
**COMMON_PARAMS,
|
|
},
|
|
"required": ["target"],
|
|
},
|
|
),
|
|
]
|
|
|
|
|
|
@dev_tools_server.call_tool()
|
|
async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
|
"""Execute development tools"""
|
|
|
|
try:
|
|
if name == "edit":
|
|
result = await edit_tool_handler(**arguments)
|
|
elif name == "fmt":
|
|
result = await fmt_tool_handler(**arguments)
|
|
elif name == "test":
|
|
result = await test_tool_handler(**arguments)
|
|
elif name == "build":
|
|
result = await build_tool_handler(**arguments)
|
|
elif name == "lint":
|
|
result = await lint_tool_handler(**arguments)
|
|
elif name == "guard":
|
|
result = await guard_tool_handler(**arguments)
|
|
else:
|
|
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
|
|
# Format result
|
|
output_lines = [
|
|
f"{'✅' if result['ok'] else '❌'} {name.upper()} - {result['scope_resolved']}",
|
|
f"Language: {result['language_used']} | Backend: {result['backend_used']}",
|
|
f"Root: {result['root']}",
|
|
]
|
|
|
|
if result["touched_files"]:
|
|
output_lines.append(f"Modified {len(result['touched_files'])} files:")
|
|
for file in result["touched_files"][:10]: # Limit to first 10
|
|
output_lines.append(f" • {file}")
|
|
if len(result["touched_files"]) > 10:
|
|
output_lines.append(
|
|
f" ... and {len(result['touched_files']) - 10} more"
|
|
)
|
|
|
|
if result["stdout"]:
|
|
output_lines.append("\n📤 Output:")
|
|
output_lines.append(result["stdout"])
|
|
|
|
if result["stderr"]:
|
|
output_lines.append("\n⚠️ Errors:")
|
|
output_lines.append(result["stderr"])
|
|
|
|
if result["errors"]:
|
|
output_lines.append("\n❌ Issues:")
|
|
for error in result["errors"]:
|
|
output_lines.append(f" • {error}")
|
|
|
|
return [TextContent(type="text", text="\n".join(output_lines))]
|
|
|
|
except Exception as e:
|
|
return [TextContent(type="text", text=f"❌ Error executing {name}: {str(e)}")]
|
|
|
|
|
|
# Composition recipes
|
|
@dev_tools_server.call_tool()
|
|
async def call_tool_compose(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
|
"""
|
|
Composition recipes for common workflows:
|
|
|
|
- rename_and_test: edit(rename) -> fmt(changed) -> test(pkg) -> guard(ws)
|
|
- fix_and_verify: fmt(target) -> lint(target, fix=true) -> test(target)
|
|
- full_check: fmt(ws) -> lint(ws) -> test(ws) -> build(ws) -> guard(ws)
|
|
"""
|
|
|
|
if name == "rename_and_test":
|
|
return await _compose_rename_and_test(arguments)
|
|
elif name == "fix_and_verify":
|
|
return await _compose_fix_and_verify(arguments)
|
|
elif name == "full_check":
|
|
return await _compose_full_check(arguments)
|
|
else:
|
|
return [TextContent(type="text", text=f"Unknown composition: {name}")]
|
|
|
|
|
|
async def _compose_rename_and_test(args: Dict[str, Any]) -> List[TextContent]:
|
|
"""Compose: rename -> format changed files -> test -> guard"""
|
|
results = []
|
|
|
|
# 1. Rename
|
|
rename_result = await edit_tool_handler(
|
|
target=args["target"],
|
|
op="rename",
|
|
**{k: v for k, v in args.items() if k not in ["target"]},
|
|
)
|
|
results.append(("rename", rename_result))
|
|
|
|
if not rename_result["ok"]:
|
|
return [TextContent(type="text", text="❌ Rename failed, stopping workflow")]
|
|
|
|
# 2. Format changed files
|
|
fmt_result = await fmt_tool_handler(target="changed")
|
|
results.append(("format", fmt_result))
|
|
|
|
# 3. Test package
|
|
pkg = args.get("package", ".")
|
|
test_result = await test_tool_handler(target=f"pkg:{pkg}")
|
|
results.append(("test", test_result))
|
|
|
|
# 4. Guard workspace
|
|
guard_result = await guard_tool_handler(target="ws")
|
|
results.append(("guard", guard_result))
|
|
|
|
# Format combined output
|
|
output_lines = ["🔄 RENAME AND TEST WORKFLOW"]
|
|
for step, result in results:
|
|
status = "✅" if result["ok"] else "❌"
|
|
output_lines.append(
|
|
f"{status} {step.upper()}: {result.get('stdout', '')[:100]}"
|
|
)
|
|
|
|
overall_success = all(result["ok"] for _, result in results)
|
|
output_lines.append(
|
|
f"\n{'✅ Workflow completed successfully' if overall_success else '❌ Workflow failed'}"
|
|
)
|
|
|
|
return [TextContent(type="text", text="\n".join(output_lines))]
|
|
|
|
|
|
async def _compose_fix_and_verify(args: Dict[str, Any]) -> List[TextContent]:
|
|
"""Compose: format -> lint with fix -> test"""
|
|
target = args["target"]
|
|
results = []
|
|
|
|
# 1. Format
|
|
fmt_result = await fmt_tool_handler(target=target)
|
|
results.append(("format", fmt_result))
|
|
|
|
# 2. Lint with fix
|
|
lint_result = await lint_tool_handler(target=target, opts={"fix": True})
|
|
results.append(("lint", lint_result))
|
|
|
|
# 3. Test
|
|
test_result = await test_tool_handler(target=target)
|
|
results.append(("test", test_result))
|
|
|
|
output_lines = ["🔧 FIX AND VERIFY WORKFLOW"]
|
|
for step, result in results:
|
|
status = "✅" if result["ok"] else "❌"
|
|
output_lines.append(
|
|
f"{status} {step.upper()}: {result.get('stdout', '')[:100]}"
|
|
)
|
|
|
|
overall_success = all(result["ok"] for _, result in results)
|
|
output_lines.append(
|
|
f"\n{'✅ All checks passed' if overall_success else '❌ Some checks failed'}"
|
|
)
|
|
|
|
return [TextContent(type="text", text="\n".join(output_lines))]
|
|
|
|
|
|
async def _compose_full_check(args: Dict[str, Any]) -> List[TextContent]:
|
|
"""Compose: format -> lint -> test -> build -> guard"""
|
|
results = []
|
|
|
|
# Run all tools on workspace
|
|
tools = [
|
|
("format", fmt_tool_handler, {"target": "ws"}),
|
|
("lint", lint_tool_handler, {"target": "ws"}),
|
|
("test", test_tool_handler, {"target": "ws"}),
|
|
("build", build_tool_handler, {"target": "ws"}),
|
|
("guard", guard_tool_handler, {"target": "ws"}),
|
|
]
|
|
|
|
for step, handler, params in tools:
|
|
result = await handler(**params)
|
|
results.append((step, result))
|
|
|
|
# Stop on first failure for critical steps
|
|
if not result["ok"] and step in ["build", "test"]:
|
|
break
|
|
|
|
output_lines = ["🔍 FULL CHECK WORKFLOW"]
|
|
for step, result in results:
|
|
status = "✅" if result["ok"] else "❌"
|
|
summary = result.get("stdout", "")[:200]
|
|
output_lines.append(f"{status} {step.upper()}: {summary}")
|
|
|
|
overall_success = all(result["ok"] for _, result in results)
|
|
output_lines.append(
|
|
f"\n{'🎉 All checks passed!' if overall_success else '⚠️ Some checks failed'}"
|
|
)
|
|
|
|
return [TextContent(type="text", text="\n".join(output_lines))]
|
|
|
|
|
|
# Export server for use in main MCP server
|
|
__all__ = ["dev_tools_server"]
|