hanzo-mcp 0.6.10__py3-none-any.whl → 0.6.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +11 -2
- hanzo_mcp/cli.py +69 -19
- hanzo_mcp/cli_enhanced.py +15 -12
- hanzo_mcp/cli_plugin.py +91 -0
- hanzo_mcp/config/__init__.py +1 -1
- hanzo_mcp/config/settings.py +75 -8
- hanzo_mcp/config/tool_config.py +2 -2
- hanzo_mcp/dev_server.py +20 -15
- hanzo_mcp/prompts/project_system.py +1 -1
- hanzo_mcp/server.py +18 -4
- hanzo_mcp/server_enhanced.py +69 -0
- hanzo_mcp/tools/__init__.py +78 -30
- hanzo_mcp/tools/agent/__init__.py +1 -1
- hanzo_mcp/tools/agent/agent_tool.py +2 -2
- hanzo_mcp/tools/common/__init__.py +15 -1
- hanzo_mcp/tools/common/base.py +4 -4
- hanzo_mcp/tools/common/batch_tool.py +1 -1
- hanzo_mcp/tools/common/config_tool.py +2 -2
- hanzo_mcp/tools/common/context.py +2 -2
- hanzo_mcp/tools/common/context_fix.py +26 -0
- hanzo_mcp/tools/common/critic_tool.py +196 -0
- hanzo_mcp/tools/common/decorators.py +208 -0
- hanzo_mcp/tools/common/enhanced_base.py +106 -0
- hanzo_mcp/tools/common/mode.py +116 -0
- hanzo_mcp/tools/common/mode_loader.py +105 -0
- hanzo_mcp/tools/common/permissions.py +1 -1
- hanzo_mcp/tools/common/personality.py +936 -0
- hanzo_mcp/tools/common/plugin_loader.py +287 -0
- hanzo_mcp/tools/common/stats.py +4 -4
- hanzo_mcp/tools/common/tool_list.py +1 -1
- hanzo_mcp/tools/common/validation.py +1 -1
- hanzo_mcp/tools/config/__init__.py +3 -1
- hanzo_mcp/tools/config/config_tool.py +1 -1
- hanzo_mcp/tools/config/mode_tool.py +209 -0
- hanzo_mcp/tools/database/__init__.py +1 -1
- hanzo_mcp/tools/editor/__init__.py +1 -1
- hanzo_mcp/tools/filesystem/__init__.py +19 -14
- hanzo_mcp/tools/filesystem/batch_search.py +3 -3
- hanzo_mcp/tools/filesystem/diff.py +2 -2
- hanzo_mcp/tools/filesystem/rules_tool.py +235 -0
- hanzo_mcp/tools/filesystem/{unified_search.py → search_tool.py} +12 -12
- hanzo_mcp/tools/filesystem/{symbols_unified.py → symbols_tool.py} +104 -5
- hanzo_mcp/tools/filesystem/watch.py +3 -2
- hanzo_mcp/tools/jupyter/__init__.py +2 -2
- hanzo_mcp/tools/jupyter/jupyter.py +1 -1
- hanzo_mcp/tools/llm/__init__.py +3 -3
- hanzo_mcp/tools/llm/llm_tool.py +648 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -2
- hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
- hanzo_mcp/tools/shell/__init__.py +6 -6
- hanzo_mcp/tools/shell/base_process.py +4 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +8 -5
- hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +1 -1
- hanzo_mcp/tools/shell/command_executor.py +8 -6
- hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +1 -1
- hanzo_mcp/tools/shell/open.py +2 -2
- hanzo_mcp/tools/shell/{process_unified.py → process_tool.py} +1 -1
- hanzo_mcp/tools/shell/run_command_windows.py +1 -1
- hanzo_mcp/tools/shell/uvx.py +47 -2
- hanzo_mcp/tools/shell/uvx_background.py +47 -2
- hanzo_mcp/tools/shell/{uvx_unified.py → uvx_tool.py} +1 -1
- hanzo_mcp/tools/todo/__init__.py +14 -19
- hanzo_mcp/tools/todo/todo.py +22 -1
- hanzo_mcp/tools/vector/__init__.py +7 -3
- hanzo_mcp/tools/vector/ast_analyzer.py +12 -4
- hanzo_mcp/tools/vector/infinity_store.py +11 -5
- hanzo_mcp/tools/vector/project_manager.py +4 -2
- hanzo_mcp-0.6.13.dist-info/METADATA +359 -0
- {hanzo_mcp-0.6.10.dist-info → hanzo_mcp-0.6.13.dist-info}/RECORD +73 -65
- {hanzo_mcp-0.6.10.dist-info → hanzo_mcp-0.6.13.dist-info}/entry_points.txt +1 -0
- hanzo_mcp/tools/common/palette.py +0 -344
- hanzo_mcp/tools/common/palette_loader.py +0 -108
- hanzo_mcp/tools/config/palette_tool.py +0 -179
- hanzo_mcp/tools/llm/llm_unified.py +0 -851
- hanzo_mcp-0.6.10.dist-info/METADATA +0 -339
- {hanzo_mcp-0.6.10.dist-info → hanzo_mcp-0.6.13.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.6.10.dist-info → hanzo_mcp-0.6.13.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.6.10.dist-info → hanzo_mcp-0.6.13.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Decorators for MCP tools.
|
|
2
|
+
|
|
3
|
+
This module provides decorators that handle common cross-cutting concerns
|
|
4
|
+
for MCP tools, such as context normalization and error handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
import inspect
|
|
9
|
+
from typing import Any, Callable, TypeVar, cast
|
|
10
|
+
from collections.abc import Awaitable, Coroutine
|
|
11
|
+
|
|
12
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
13
|
+
|
|
14
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MockContext:
|
|
18
|
+
"""Mock context for when no real context is available.
|
|
19
|
+
|
|
20
|
+
This is used when tools are called externally through the MCP protocol
|
|
21
|
+
and the Context parameter is not properly serialized.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.request_id = "external-request"
|
|
26
|
+
self.client_id = "external-client"
|
|
27
|
+
|
|
28
|
+
async def info(self, message: str) -> None:
|
|
29
|
+
"""Mock info logging - no-op for external calls."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
async def debug(self, message: str) -> None:
|
|
33
|
+
"""Mock debug logging - no-op for external calls."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
async def warning(self, message: str) -> None:
|
|
37
|
+
"""Mock warning logging - no-op for external calls."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
async def error(self, message: str) -> None:
|
|
41
|
+
"""Mock error logging - no-op for external calls."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
async def report_progress(self, current: int, total: int) -> None:
|
|
45
|
+
"""Mock progress reporting - no-op for external calls."""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
async def read_resource(self, uri: str) -> Any:
|
|
49
|
+
"""Mock resource reading - returns empty result."""
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def with_context_normalization(func: F) -> F:
|
|
54
|
+
"""Decorator that normalizes the context parameter for MCP tools.
|
|
55
|
+
|
|
56
|
+
This decorator intercepts the ctx parameter and ensures it's a valid
|
|
57
|
+
MCPContext object, even when called externally where it might be
|
|
58
|
+
passed as a string, dict, or None.
|
|
59
|
+
|
|
60
|
+
Usage:
|
|
61
|
+
@server.tool()
|
|
62
|
+
@with_context_normalization
|
|
63
|
+
async def my_tool(ctx: MCPContext, param: str) -> str:
|
|
64
|
+
# ctx is guaranteed to be a valid context object
|
|
65
|
+
await ctx.info("Processing...")
|
|
66
|
+
return "result"
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
func: The async function to decorate
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The decorated function with context normalization
|
|
73
|
+
"""
|
|
74
|
+
@functools.wraps(func)
|
|
75
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
76
|
+
# Get function signature to find ctx parameter
|
|
77
|
+
sig = inspect.signature(func)
|
|
78
|
+
params = list(sig.parameters.keys())
|
|
79
|
+
|
|
80
|
+
# Handle ctx in kwargs
|
|
81
|
+
if 'ctx' in kwargs:
|
|
82
|
+
ctx_value = kwargs['ctx']
|
|
83
|
+
if not _is_valid_context(ctx_value):
|
|
84
|
+
kwargs['ctx'] = MockContext()
|
|
85
|
+
|
|
86
|
+
# Handle ctx in args (positional)
|
|
87
|
+
elif 'ctx' in params:
|
|
88
|
+
ctx_index = params.index('ctx')
|
|
89
|
+
if ctx_index < len(args):
|
|
90
|
+
ctx_value = args[ctx_index]
|
|
91
|
+
if not _is_valid_context(ctx_value):
|
|
92
|
+
args_list = list(args)
|
|
93
|
+
args_list[ctx_index] = MockContext()
|
|
94
|
+
args = tuple(args_list)
|
|
95
|
+
|
|
96
|
+
# Call the original function
|
|
97
|
+
return await func(*args, **kwargs)
|
|
98
|
+
|
|
99
|
+
return cast(F, wrapper)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_valid_context(ctx: Any) -> bool:
|
|
103
|
+
"""Check if an object is a valid MCPContext.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
ctx: The object to check
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if ctx is a valid context object
|
|
110
|
+
"""
|
|
111
|
+
# Check for required context methods
|
|
112
|
+
return (
|
|
113
|
+
hasattr(ctx, 'info') and
|
|
114
|
+
hasattr(ctx, 'debug') and
|
|
115
|
+
hasattr(ctx, 'warning') and
|
|
116
|
+
hasattr(ctx, 'error') and
|
|
117
|
+
hasattr(ctx, 'report_progress') and
|
|
118
|
+
# Ensure they're callable
|
|
119
|
+
callable(getattr(ctx, 'info', None)) and
|
|
120
|
+
callable(getattr(ctx, 'debug', None))
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def mcp_tool(
|
|
125
|
+
server: Any,
|
|
126
|
+
name: str | None = None,
|
|
127
|
+
description: str | None = None
|
|
128
|
+
) -> Callable[[F], F]:
|
|
129
|
+
"""Enhanced MCP tool decorator that includes context normalization.
|
|
130
|
+
|
|
131
|
+
This decorator combines the standard MCP tool registration with
|
|
132
|
+
automatic context normalization, providing a single-point solution
|
|
133
|
+
for all tools.
|
|
134
|
+
|
|
135
|
+
Usage:
|
|
136
|
+
@mcp_tool(server, name="my_tool", description="Does something")
|
|
137
|
+
async def my_tool(ctx: MCPContext, param: str) -> str:
|
|
138
|
+
await ctx.info("Processing...")
|
|
139
|
+
return "result"
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
server: The MCP server instance
|
|
143
|
+
name: Optional tool name (defaults to function name)
|
|
144
|
+
description: Optional tool description
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Decorator function
|
|
148
|
+
"""
|
|
149
|
+
def decorator(func: F) -> F:
|
|
150
|
+
# Apply context normalization first
|
|
151
|
+
normalized_func = with_context_normalization(func)
|
|
152
|
+
|
|
153
|
+
# Then apply the server's tool decorator
|
|
154
|
+
if hasattr(server, 'tool'):
|
|
155
|
+
# Use the server's tool decorator
|
|
156
|
+
server_decorator = server.tool(name=name, description=description)
|
|
157
|
+
return server_decorator(normalized_func)
|
|
158
|
+
else:
|
|
159
|
+
# Fallback if server doesn't have tool method
|
|
160
|
+
return normalized_func
|
|
161
|
+
|
|
162
|
+
return decorator
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def create_tool_handler(server: Any, tool: Any) -> Callable[[], None]:
|
|
166
|
+
"""Create a standardized tool registration handler.
|
|
167
|
+
|
|
168
|
+
This function creates a registration method that automatically applies
|
|
169
|
+
context normalization to any tool handler registered with the server.
|
|
170
|
+
|
|
171
|
+
Usage:
|
|
172
|
+
class MyTool(BaseTool):
|
|
173
|
+
def register(self, mcp_server):
|
|
174
|
+
register = create_tool_handler(mcp_server, self)
|
|
175
|
+
register()
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
server: The MCP server instance
|
|
179
|
+
tool: The tool instance with name, description, and handler
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
A function that registers the tool with context normalization
|
|
183
|
+
"""
|
|
184
|
+
def register_with_normalization():
|
|
185
|
+
# Get the original register method
|
|
186
|
+
original_register = tool.__class__.register
|
|
187
|
+
|
|
188
|
+
# Temporarily replace server.tool to wrap with normalization
|
|
189
|
+
original_tool_decorator = server.tool
|
|
190
|
+
|
|
191
|
+
def normalized_tool_decorator(name=None, description=None):
|
|
192
|
+
def decorator(func):
|
|
193
|
+
# Apply context normalization
|
|
194
|
+
normalized = with_context_normalization(func)
|
|
195
|
+
# Apply original decorator
|
|
196
|
+
return original_tool_decorator(name=name, description=description)(normalized)
|
|
197
|
+
return decorator
|
|
198
|
+
|
|
199
|
+
# Monkey-patch temporarily
|
|
200
|
+
server.tool = normalized_tool_decorator
|
|
201
|
+
try:
|
|
202
|
+
# Call original register
|
|
203
|
+
original_register(tool, server)
|
|
204
|
+
finally:
|
|
205
|
+
# Restore original
|
|
206
|
+
server.tool = original_tool_decorator
|
|
207
|
+
|
|
208
|
+
return register_with_normalization
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Enhanced base classes for MCP tools with automatic context handling.
|
|
2
|
+
|
|
3
|
+
This module provides enhanced base classes that automatically handle
|
|
4
|
+
context normalization and other cross-cutting concerns, ensuring
|
|
5
|
+
consistent behavior across all tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Any, get_type_hints, get_args, get_origin
|
|
10
|
+
import inspect
|
|
11
|
+
|
|
12
|
+
from mcp.server import FastMCP
|
|
13
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
14
|
+
|
|
15
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
16
|
+
from hanzo_mcp.tools.common.decorators import with_context_normalization
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EnhancedBaseTool(BaseTool, ABC):
|
|
20
|
+
"""Enhanced base class for MCP tools with automatic context normalization.
|
|
21
|
+
|
|
22
|
+
This base class automatically wraps the tool registration to include
|
|
23
|
+
context normalization, ensuring that all tools handle external calls
|
|
24
|
+
properly without requiring manual decoration or copy-pasted code.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
28
|
+
"""Register this tool with automatic context normalization.
|
|
29
|
+
|
|
30
|
+
This method automatically applies context normalization to the tool
|
|
31
|
+
handler, ensuring it works properly when called externally.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
mcp_server: The FastMCP server instance
|
|
35
|
+
"""
|
|
36
|
+
# Get the tool method from the subclass
|
|
37
|
+
tool_method = self._create_tool_handler()
|
|
38
|
+
|
|
39
|
+
# Apply context normalization decorator
|
|
40
|
+
normalized_method = with_context_normalization(tool_method)
|
|
41
|
+
|
|
42
|
+
# Register with the server
|
|
43
|
+
mcp_server.tool(
|
|
44
|
+
name=self.name,
|
|
45
|
+
description=self.description
|
|
46
|
+
)(normalized_method)
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def _create_tool_handler(self) -> Any:
|
|
50
|
+
"""Create the tool handler function.
|
|
51
|
+
|
|
52
|
+
Subclasses must implement this to return an async function
|
|
53
|
+
that will be registered as the tool handler.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
An async function that handles tool calls
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AutoRegisterTool(BaseTool, ABC):
|
|
62
|
+
"""Base class that automatically generates tool handlers from the call method.
|
|
63
|
+
|
|
64
|
+
This base class inspects the call method signature and automatically
|
|
65
|
+
creates a properly typed tool handler with context normalization.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
69
|
+
"""Register this tool with automatic handler generation.
|
|
70
|
+
|
|
71
|
+
This method inspects the call method signature and automatically
|
|
72
|
+
creates a tool handler with the correct parameters and types.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
mcp_server: The FastMCP server instance
|
|
76
|
+
"""
|
|
77
|
+
# Get the call method signature
|
|
78
|
+
call_method = self.call
|
|
79
|
+
sig = inspect.signature(call_method)
|
|
80
|
+
|
|
81
|
+
# Get type hints for proper typing
|
|
82
|
+
hints = get_type_hints(call_method)
|
|
83
|
+
|
|
84
|
+
# Create a dynamic handler function
|
|
85
|
+
tool_self = self
|
|
86
|
+
|
|
87
|
+
# Build the handler dynamically based on the call signature
|
|
88
|
+
params = list(sig.parameters.items())
|
|
89
|
+
|
|
90
|
+
# Skip 'self' and 'ctx' parameters
|
|
91
|
+
tool_params = [(name, param) for name, param in params
|
|
92
|
+
if name not in ('self', 'ctx')]
|
|
93
|
+
|
|
94
|
+
# Create the handler function dynamically
|
|
95
|
+
async def handler(ctx: MCPContext, **kwargs: Any) -> Any:
|
|
96
|
+
# Call the tool's call method with the context and parameters
|
|
97
|
+
return await tool_self.call(ctx, **kwargs)
|
|
98
|
+
|
|
99
|
+
# Apply context normalization
|
|
100
|
+
normalized_handler = with_context_normalization(handler)
|
|
101
|
+
|
|
102
|
+
# Register with the server
|
|
103
|
+
mcp_server.tool(
|
|
104
|
+
name=self.name,
|
|
105
|
+
description=self.description
|
|
106
|
+
)(normalized_handler)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Mode system for organizing development tools based on programmer personalities."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Dict, List, Optional, Set
|
|
6
|
+
|
|
7
|
+
from hanzo_mcp.tools.common.personality import (
|
|
8
|
+
ToolPersonality,
|
|
9
|
+
register_default_personalities,
|
|
10
|
+
ensure_agent_enabled,
|
|
11
|
+
personalities
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Mode(ToolPersonality):
|
|
17
|
+
"""Development mode combining tool preferences and environment settings."""
|
|
18
|
+
# Inherits all fields from ToolPersonality
|
|
19
|
+
# Adds mode-specific functionality
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def is_active(self) -> bool:
|
|
23
|
+
"""Check if this mode is currently active."""
|
|
24
|
+
return ModeRegistry.get_active() == self
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ModeRegistry:
|
|
28
|
+
"""Registry for development modes."""
|
|
29
|
+
|
|
30
|
+
_modes: Dict[str, Mode] = {}
|
|
31
|
+
_active_mode: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def register(cls, mode: Mode) -> None:
|
|
35
|
+
"""Register a development mode."""
|
|
36
|
+
# Ensure agent is enabled if API keys present
|
|
37
|
+
mode = ensure_agent_enabled(mode)
|
|
38
|
+
cls._modes[mode.name] = mode
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def get(cls, name: str) -> Optional[Mode]:
|
|
42
|
+
"""Get a mode by name."""
|
|
43
|
+
return cls._modes.get(name)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def list(cls) -> List[Mode]:
|
|
47
|
+
"""List all registered modes."""
|
|
48
|
+
return list(cls._modes.values())
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def set_active(cls, name: str) -> None:
|
|
52
|
+
"""Set the active mode."""
|
|
53
|
+
if name not in cls._modes:
|
|
54
|
+
raise ValueError(f"Mode '{name}' not found")
|
|
55
|
+
cls._active_mode = name
|
|
56
|
+
|
|
57
|
+
# Apply environment variables from the mode
|
|
58
|
+
mode = cls._modes[name]
|
|
59
|
+
if mode.environment:
|
|
60
|
+
for key, value in mode.environment.items():
|
|
61
|
+
os.environ[key] = value
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def get_active(cls) -> Optional[Mode]:
|
|
65
|
+
"""Get the active mode."""
|
|
66
|
+
if cls._active_mode:
|
|
67
|
+
return cls._modes.get(cls._active_mode)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def get_active_tools(cls) -> Set[str]:
|
|
72
|
+
"""Get the set of tools from the active mode."""
|
|
73
|
+
mode = cls.get_active()
|
|
74
|
+
if mode:
|
|
75
|
+
return set(mode.tools)
|
|
76
|
+
return set()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def register_default_modes():
|
|
80
|
+
"""Register all default development modes."""
|
|
81
|
+
# Convert personalities to modes
|
|
82
|
+
for personality in personalities:
|
|
83
|
+
mode = Mode(
|
|
84
|
+
name=personality.name,
|
|
85
|
+
programmer=personality.programmer,
|
|
86
|
+
description=personality.description,
|
|
87
|
+
tools=personality.tools,
|
|
88
|
+
environment=personality.environment,
|
|
89
|
+
philosophy=personality.philosophy,
|
|
90
|
+
)
|
|
91
|
+
ModeRegistry.register(mode)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_mode_from_env() -> Optional[str]:
|
|
95
|
+
"""Get mode name from environment variables."""
|
|
96
|
+
# Check for HANZO_MODE, PERSONALITY, or MODE env vars
|
|
97
|
+
return (
|
|
98
|
+
os.environ.get("HANZO_MODE") or
|
|
99
|
+
os.environ.get("PERSONALITY") or
|
|
100
|
+
os.environ.get("MODE")
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def activate_mode_from_env():
|
|
105
|
+
"""Activate mode based on environment variables."""
|
|
106
|
+
mode_name = get_mode_from_env()
|
|
107
|
+
if mode_name:
|
|
108
|
+
try:
|
|
109
|
+
ModeRegistry.set_active(mode_name)
|
|
110
|
+
return True
|
|
111
|
+
except ValueError:
|
|
112
|
+
# Mode not found, ignore
|
|
113
|
+
pass
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Tool mode loader for dynamic tool configuration."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Dict, List, Optional, Set
|
|
5
|
+
|
|
6
|
+
from hanzo_mcp.tools.common.mode import ModeRegistry, register_default_modes, activate_mode_from_env
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ModeLoader:
|
|
10
|
+
"""Loads and manages tool modes for dynamic configuration."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def initialize_modes() -> None:
|
|
14
|
+
"""Initialize the mode system with defaults."""
|
|
15
|
+
# Initialize modes
|
|
16
|
+
register_default_modes()
|
|
17
|
+
|
|
18
|
+
# Check for mode from environment
|
|
19
|
+
activate_mode_from_env()
|
|
20
|
+
|
|
21
|
+
# If no mode set, use default
|
|
22
|
+
if not ModeRegistry.get_active():
|
|
23
|
+
default_mode = os.environ.get("HANZO_DEFAULT_MODE", "hanzo")
|
|
24
|
+
if ModeRegistry.get(default_mode):
|
|
25
|
+
ModeRegistry.set_active(default_mode)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def get_enabled_tools_from_mode(
|
|
29
|
+
base_enabled_tools: Optional[Dict[str, bool]] = None,
|
|
30
|
+
force_mode: Optional[str] = None
|
|
31
|
+
) -> Dict[str, bool]:
|
|
32
|
+
"""Get enabled tools configuration from active mode.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
base_enabled_tools: Base configuration to merge with
|
|
36
|
+
force_mode: Force a specific mode (overrides active)
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dictionary of tool enable states
|
|
40
|
+
"""
|
|
41
|
+
# Initialize if needed
|
|
42
|
+
if not ModeRegistry.list():
|
|
43
|
+
ModeLoader.initialize_modes()
|
|
44
|
+
|
|
45
|
+
# Get mode to use
|
|
46
|
+
tools_list = None
|
|
47
|
+
|
|
48
|
+
if force_mode:
|
|
49
|
+
# Set and get mode
|
|
50
|
+
if ModeRegistry.get(force_mode):
|
|
51
|
+
ModeRegistry.set_active(force_mode)
|
|
52
|
+
mode = ModeRegistry.get_active()
|
|
53
|
+
tools_list = mode.tools if mode else None
|
|
54
|
+
else:
|
|
55
|
+
# Check active mode
|
|
56
|
+
mode = ModeRegistry.get_active()
|
|
57
|
+
if mode:
|
|
58
|
+
tools_list = mode.tools
|
|
59
|
+
|
|
60
|
+
if not tools_list:
|
|
61
|
+
# No active mode, return base config
|
|
62
|
+
return base_enabled_tools or {}
|
|
63
|
+
|
|
64
|
+
# Start with base configuration
|
|
65
|
+
result = base_enabled_tools.copy() if base_enabled_tools else {}
|
|
66
|
+
|
|
67
|
+
# Get all possible tools from registry
|
|
68
|
+
from hanzo_mcp.config.tool_config import TOOL_REGISTRY
|
|
69
|
+
all_possible_tools = set(TOOL_REGISTRY.keys())
|
|
70
|
+
|
|
71
|
+
# Disable all tools first (clean slate for mode)
|
|
72
|
+
for tool in all_possible_tools:
|
|
73
|
+
result[tool] = False
|
|
74
|
+
|
|
75
|
+
# Enable tools from mode
|
|
76
|
+
for tool in tools_list:
|
|
77
|
+
result[tool] = True
|
|
78
|
+
|
|
79
|
+
# Always enable mode tool (meta)
|
|
80
|
+
result["mode"] = True
|
|
81
|
+
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def get_environment_from_mode() -> Dict[str, str]:
|
|
86
|
+
"""Get environment variables from active mode.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Dictionary of environment variables
|
|
90
|
+
"""
|
|
91
|
+
# Check mode
|
|
92
|
+
mode = ModeRegistry.get_active()
|
|
93
|
+
if mode and mode.environment:
|
|
94
|
+
return mode.environment.copy()
|
|
95
|
+
|
|
96
|
+
return {}
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def apply_environment_from_mode() -> None:
|
|
100
|
+
"""Apply environment variables from active mode."""
|
|
101
|
+
env_vars = ModeLoader.get_environment_from_mode()
|
|
102
|
+
for key, value in env_vars.items():
|
|
103
|
+
os.environ[key] = value
|
|
104
|
+
|
|
105
|
+
|