Files
zeekay ffe73447a3 security: fix 3 HIGH + 3 MEDIUM findings from red team review
- H-1: guard negative abs_off in obj_bytes() (buffer read bypass)
- H-2: remove CERT_NONE from ZapClient TLS (MITM protection)
- H-3: add MAX_MESSAGE_SIZE check in ZapServer (OOM DoS)
- M-5: log exceptions instead of bare pass in server
- L-1: validate ZAP version in Message.parse()
- L-2: wrap req_id at u32 boundary in CloudClient
2026-03-31 20:57:16 -07:00

465 lines
16 KiB
Python

"""Enhanced command-line interface for the Hanzo AI server with full tool configuration."""
import argparse
import logging
import os
from typing import Any, Dict
from hanzo_mcp.config import (
TOOL_REGISTRY,
HanzoMCPSettings,
load_settings,
save_settings,
)
from hanzo_mcp.server import HanzoMCPServer
def create_parser() -> argparse.ArgumentParser:
"""Create the argument parser with all tool configuration options."""
parser = argparse.ArgumentParser(
description="Hanzo AI server with comprehensive tool configuration",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Tool Configuration:
Each tool can be individually enabled/disabled using CLI flags.
Use --list-tools to see all available tools and their current status.
Configuration Files:
User: ~/.hanzo/settings.json
Project: .hanzo/settings.json
Local: .hanzo/settings.local.json
Examples:
# Start server with only read tools
hanzo-mcp --disable-write --disable-edit --disable-multi-edit
# Enable agent tool with custom model
hanzo-mcp --enable-dispatch-agent --agent-model anthropic/claude-3-sonnet
# Save current configuration
hanzo-mcp --save-config
""",
)
# Basic server options
server_group = parser.add_argument_group("Server Configuration")
server_group.add_argument(
"--name",
default="hanzo-mcp",
help="Name of the MCP server (default: hanzo-mcp)",
)
server_group.add_argument(
"--transport",
choices=["stdio", "sse"],
default="stdio",
help="Transport protocol to use (default: stdio)",
)
server_group.add_argument(
"--host",
default="127.0.0.1",
help="Host for SSE server (default: 127.0.0.1)",
)
server_group.add_argument(
"--port",
type=int,
default=8888,
help="Port for SSE server (default: 8888)",
)
server_group.add_argument(
"--log-level",
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
default="INFO",
help="Logging level (default: INFO)",
)
server_group.add_argument(
"--command-timeout",
type=float,
default=120.0,
help="Default timeout for command execution in seconds (default: 120.0)",
)
# Path configuration
path_group = parser.add_argument_group("Path Configuration")
path_group.add_argument(
"--allow-path",
action="append",
dest="allowed_paths",
help="Add an allowed path (can be specified multiple times)",
)
path_group.add_argument(
"--project",
action="append",
dest="project_paths",
help="Add a project path (can be specified multiple times)",
)
path_group.add_argument(
"--project-dir",
help="Single project directory (added to both allowed and project paths)",
)
# Individual tool configuration
tool_group = parser.add_argument_group("Individual Tool Configuration")
# Add CLI flags for each tool
for tool_name, tool_config in TOOL_REGISTRY.items():
tool_config.cli_flag.lstrip("-")
help_text = f"{tool_config.description}"
if tool_config.enabled:
# Tool is enabled by default, add disable flag
tool_group.add_argument(
f"--disable-{tool_name.replace('_', '-')}",
action="store_true",
help=f"Disable {tool_name} tool: {help_text}",
)
else:
# Tool is disabled by default, add enable flag
tool_group.add_argument(
f"--enable-{tool_name.replace('_', '-')}",
action="store_true",
help=f"Enable {tool_name} tool: {help_text}",
)
# Category-level tool configuration (for backward compatibility)
category_group = parser.add_argument_group("Category-level Tool Configuration")
category_group.add_argument(
"--disable-write-tools",
action="store_true",
help="Disable all write tools (write, edit, multi_edit, content_replace)",
)
category_group.add_argument(
"--disable-search-tools",
action="store_true",
help="Disable all search tools (grep, grep_ast)",
)
category_group.add_argument(
"--disable-filesystem-tools",
action="store_true",
help="Disable all filesystem tools",
)
category_group.add_argument(
"--disable-jupyter-tools",
action="store_true",
help="Disable all Jupyter notebook tools",
)
category_group.add_argument(
"--disable-shell-tools",
action="store_true",
help="Disable shell command execution tools",
)
category_group.add_argument(
"--disable-todo-tools",
action="store_true",
help="Disable todo management tools",
)
# Agent configuration
agent_group = parser.add_argument_group("Agent Tool Configuration")
agent_group.add_argument(
"--agent-model",
help="Model name in LLM format (e.g., 'openai/gpt-4o', 'anthropic/claude-3-sonnet')",
)
agent_group.add_argument(
"--agent-max-tokens",
type=int,
help="Maximum tokens for agent responses",
)
agent_group.add_argument(
"--agent-api-key",
help="API key for the LLM provider",
)
agent_group.add_argument(
"--agent-base-url",
help="Base URL for the LLM provider API endpoint",
)
agent_group.add_argument(
"--agent-max-iterations",
type=int,
default=10,
help="Maximum iterations for agent (default: 10)",
)
agent_group.add_argument(
"--agent-max-tool-uses",
type=int,
default=30,
help="Maximum tool uses for agent (default: 30)",
)
# Vector store configuration
vector_group = parser.add_argument_group("Vector Store Configuration")
vector_group.add_argument(
"--enable-vector-store",
action="store_true",
help="Enable local vector store (Infinity database)",
)
vector_group.add_argument(
"--vector-store-path",
help="Path for vector store data (default: ~/.config/hanzo/vector-store)",
)
vector_group.add_argument(
"--embedding-model",
default="text-embedding-3-small",
help="Embedding model for vector store (default: text-embedding-3-small)",
)
# Configuration management
config_group = parser.add_argument_group("Configuration Management")
config_group.add_argument(
"--config-file",
help="Load configuration from specific file",
)
config_group.add_argument(
"--save-config",
action="store_true",
help="Save current configuration to global config file",
)
config_group.add_argument(
"--save-project-config",
action="store_true",
help="Save current configuration to project config file",
)
config_group.add_argument(
"--list-tools",
action="store_true",
help="List all available tools and their status",
)
# Installation
install_group = parser.add_argument_group("Installation")
install_group.add_argument(
"--install",
action="store_true",
help="Install server configuration in Claude Desktop",
)
return parser
def apply_cli_overrides(args: argparse.Namespace) -> Dict[str, Any]:
"""Convert CLI arguments to configuration overrides."""
overrides = {}
# Server configuration
server_config = {}
if hasattr(args, "name") and args.name != "hanzo-mcp":
server_config["name"] = args.name
if hasattr(args, "host") and args.host != "127.0.0.1":
server_config["host"] = args.host
if hasattr(args, "port") and args.port != 8888:
server_config["port"] = args.port
if hasattr(args, "transport") and args.transport != "stdio":
server_config["transport"] = args.transport
if hasattr(args, "log_level") and args.log_level != "INFO":
server_config["log_level"] = args.log_level
if hasattr(args, "command_timeout") and args.command_timeout != 120.0:
server_config["command_timeout"] = args.command_timeout
if server_config:
overrides["server"] = server_config
# Path configuration
if hasattr(args, "allowed_paths") and args.allowed_paths:
overrides["allowed_paths"] = args.allowed_paths
if hasattr(args, "project_paths") and args.project_paths:
overrides["project_paths"] = args.project_paths
if hasattr(args, "project_dir") and args.project_dir:
overrides["project_dir"] = args.project_dir
# Tool configuration
enabled_tools = {}
# Handle individual tool flags
for tool_name, tool_config in TOOL_REGISTRY.items():
tool_name.replace("_", "-")
if tool_config.enabled:
# Check for disable flag
disable_flag = f"disable_{tool_name}"
if hasattr(args, disable_flag) and getattr(args, disable_flag):
enabled_tools[tool_name] = False
else:
# Check for enable flag
enable_flag = f"enable_{tool_name}"
if hasattr(args, enable_flag) and getattr(args, enable_flag):
enabled_tools[tool_name] = True
# Handle category-level disables
if hasattr(args, "disable_write_tools") and args.disable_write_tools:
for tool_name in ["write", "edit", "multi_edit", "content_replace"]:
enabled_tools[tool_name] = False
if hasattr(args, "disable_search_tools") and args.disable_search_tools:
for tool_name in ["grep", "grep_ast"]:
enabled_tools[tool_name] = False
if hasattr(args, "disable_filesystem_tools") and args.disable_filesystem_tools:
filesystem_tools = [
"read",
"write",
"edit",
"multi_edit",
"tree",
"grep",
"grep_ast",
"content_replace",
]
for tool_name in filesystem_tools:
enabled_tools[tool_name] = False
if hasattr(args, "disable_jupyter_tools") and args.disable_jupyter_tools:
for tool_name in ["notebook_read", "notebook_edit"]:
enabled_tools[tool_name] = False
if hasattr(args, "disable_shell_tools") and args.disable_shell_tools:
enabled_tools["run_command"] = False
if hasattr(args, "disable_todo_tools") and args.disable_todo_tools:
for tool_name in ["todo_read", "todo_write"]:
enabled_tools[tool_name] = False
if enabled_tools:
overrides["enabled_tools"] = enabled_tools
# Agent configuration
agent_config = {}
if hasattr(args, "agent_model") and args.agent_model:
agent_config["model"] = args.agent_model
agent_config["enabled"] = True
if hasattr(args, "agent_api_key") and args.agent_api_key:
agent_config["api_key"] = args.agent_api_key
if hasattr(args, "agent_base_url") and args.agent_base_url:
agent_config["base_url"] = args.agent_base_url
if hasattr(args, "agent_max_tokens") and args.agent_max_tokens:
agent_config["max_tokens"] = args.agent_max_tokens
if hasattr(args, "agent_max_iterations") and args.agent_max_iterations != 10:
agent_config["max_iterations"] = args.agent_max_iterations
if hasattr(args, "agent_max_tool_uses") and args.agent_max_tool_uses != 30:
agent_config["max_tool_uses"] = args.agent_max_tool_uses
if agent_config:
overrides["agent"] = agent_config
# Vector store configuration
vector_config = {}
if hasattr(args, "enable_vector_store") and args.enable_vector_store:
vector_config["enabled"] = True
if hasattr(args, "vector_store_path") and args.vector_store_path:
vector_config["data_path"] = args.vector_store_path
if (
hasattr(args, "embedding_model")
and args.embedding_model != "text-embedding-3-small"
):
vector_config["embedding_model"] = args.embedding_model
if vector_config:
overrides["vector_store"] = vector_config
return overrides
def list_tools(settings: HanzoMCPSettings) -> None:
"""List all tools and their current status."""
logger = logging.getLogger(__name__)
logger.info("Hanzo AI Tools Status:")
logger.info("=" * 50)
categories = {}
for tool_name, tool_config in TOOL_REGISTRY.items():
category = tool_config.category.value
if category not in categories:
categories[category] = []
enabled = settings.is_tool_enabled(tool_name)
status = "✅ ENABLED " if enabled else "❌ DISABLED"
categories[category].append((tool_name, status, tool_config.description))
for category, tools in categories.items():
logger.info(f"\n{category.upper()} TOOLS:")
logger.info("-" * 30)
for tool_name, status, description in tools:
logger.info(f" {status} {tool_name:<15} - {description}")
logger.info(f"\nTotal: {len(TOOL_REGISTRY)} tools")
enabled_count = len(settings.get_enabled_tools())
logger.info(
f"Enabled: {enabled_count}, Disabled: {len(TOOL_REGISTRY) - enabled_count}"
)
def main() -> None:
"""Run the enhanced CLI for the Hanzo AI server."""
parser = create_parser()
args = parser.parse_args()
# Handle list tools command
if hasattr(args, "list_tools") and args.list_tools:
settings = load_settings()
list_tools(settings)
return
# Load configuration with CLI overrides
config_overrides = apply_cli_overrides(args)
project_dir = getattr(args, "project_dir", None)
settings = load_settings(project_dir=project_dir, config_overrides=config_overrides)
# Handle configuration saving
logger = logging.getLogger(__name__)
if hasattr(args, "save_config") and args.save_config:
saved_path = save_settings(settings, global_config=True)
logger.info(f"Configuration saved to: {saved_path}")
return
if hasattr(args, "save_project_config") and args.save_project_config:
saved_path = save_settings(settings, global_config=False)
logger.info(f"Project configuration saved to: {saved_path}")
return
# Handle installation
if hasattr(args, "install") and args.install:
from hanzo_mcp.cli import install_claude_desktop_config
install_claude_desktop_config(
settings.server.name,
settings.allowed_paths,
)
return
# Set up allowed paths
allowed_paths = settings.allowed_paths[:]
if settings.project_dir and settings.project_dir not in allowed_paths:
allowed_paths.append(settings.project_dir)
if not allowed_paths:
allowed_paths = [os.getcwd()]
# Create and run server
server = HanzoMCPServer(
name=settings.server.name,
allowed_paths=allowed_paths,
project_dir=settings.project_dir,
agent_model=settings.agent.model,
agent_max_tokens=settings.agent.max_tokens,
agent_api_key=settings.agent.api_key,
agent_base_url=settings.agent.base_url,
agent_max_iterations=settings.agent.max_iterations,
agent_max_tool_uses=settings.agent.max_tool_uses,
enable_agent_tool=settings.agent.enabled
or settings.is_tool_enabled("dispatch_agent"),
disable_write_tools=not any(
settings.is_tool_enabled(t)
for t in ["write", "edit", "multi_edit", "content_replace"]
),
disable_search_tools=not any(
settings.is_tool_enabled(t) for t in ["grep", "grep_ast"]
),
host=settings.server.host,
port=settings.server.port,
enabled_tools=settings.enabled_tools, # Pass individual tool configuration
)
server.run(transport=settings.server.transport)
if __name__ == "__main__":
main()