hanzo-mcp 0.3.8__py3-none-any.whl → 0.5.0__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 +1 -1
- hanzo_mcp/cli.py +118 -170
- hanzo_mcp/cli_enhanced.py +438 -0
- hanzo_mcp/config/__init__.py +19 -0
- hanzo_mcp/config/settings.py +388 -0
- hanzo_mcp/config/tool_config.py +197 -0
- hanzo_mcp/prompts/__init__.py +117 -0
- hanzo_mcp/prompts/compact_conversation.py +77 -0
- hanzo_mcp/prompts/create_release.py +38 -0
- hanzo_mcp/prompts/project_system.py +120 -0
- hanzo_mcp/prompts/project_todo_reminder.py +111 -0
- hanzo_mcp/prompts/utils.py +286 -0
- hanzo_mcp/server.py +117 -99
- hanzo_mcp/tools/__init__.py +105 -32
- hanzo_mcp/tools/agent/__init__.py +8 -11
- hanzo_mcp/tools/agent/agent_tool.py +290 -224
- hanzo_mcp/tools/agent/prompt.py +16 -13
- hanzo_mcp/tools/agent/tool_adapter.py +9 -9
- hanzo_mcp/tools/common/__init__.py +17 -16
- hanzo_mcp/tools/common/base.py +79 -110
- hanzo_mcp/tools/common/batch_tool.py +330 -0
- hanzo_mcp/tools/common/context.py +26 -292
- hanzo_mcp/tools/common/permissions.py +12 -12
- hanzo_mcp/tools/common/thinking_tool.py +153 -0
- hanzo_mcp/tools/common/validation.py +1 -63
- hanzo_mcp/tools/filesystem/__init__.py +88 -57
- hanzo_mcp/tools/filesystem/base.py +32 -24
- hanzo_mcp/tools/filesystem/content_replace.py +114 -107
- hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
- hanzo_mcp/tools/filesystem/edit.py +279 -0
- hanzo_mcp/tools/filesystem/grep.py +458 -0
- hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
- hanzo_mcp/tools/filesystem/read.py +255 -0
- hanzo_mcp/tools/filesystem/write.py +156 -0
- hanzo_mcp/tools/jupyter/__init__.py +41 -29
- hanzo_mcp/tools/jupyter/base.py +66 -57
- hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
- hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
- hanzo_mcp/tools/shell/__init__.py +29 -20
- hanzo_mcp/tools/shell/base.py +87 -45
- hanzo_mcp/tools/shell/bash_session.py +731 -0
- hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
- hanzo_mcp/tools/shell/command_executor.py +435 -384
- hanzo_mcp/tools/shell/run_command.py +284 -131
- hanzo_mcp/tools/shell/run_command_windows.py +328 -0
- hanzo_mcp/tools/shell/session_manager.py +196 -0
- hanzo_mcp/tools/shell/session_storage.py +325 -0
- hanzo_mcp/tools/todo/__init__.py +66 -0
- hanzo_mcp/tools/todo/base.py +319 -0
- hanzo_mcp/tools/todo/todo_read.py +148 -0
- hanzo_mcp/tools/todo/todo_write.py +378 -0
- hanzo_mcp/tools/vector/__init__.py +95 -0
- hanzo_mcp/tools/vector/infinity_store.py +365 -0
- hanzo_mcp/tools/vector/project_manager.py +361 -0
- hanzo_mcp/tools/vector/vector_index.py +115 -0
- hanzo_mcp/tools/vector/vector_search.py +215 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/METADATA +33 -1
- hanzo_mcp-0.5.0.dist-info/RECORD +63 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/WHEEL +1 -1
- hanzo_mcp/tools/agent/base_provider.py +0 -73
- hanzo_mcp/tools/agent/litellm_provider.py +0 -45
- hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
- hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
- hanzo_mcp/tools/agent/provider_registry.py +0 -120
- hanzo_mcp/tools/common/error_handling.py +0 -86
- hanzo_mcp/tools/common/logging_config.py +0 -115
- hanzo_mcp/tools/common/session.py +0 -91
- hanzo_mcp/tools/common/think_tool.py +0 -123
- hanzo_mcp/tools/common/version_tool.py +0 -120
- hanzo_mcp/tools/filesystem/edit_file.py +0 -287
- hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
- hanzo_mcp/tools/filesystem/read_files.py +0 -199
- hanzo_mcp/tools/filesystem/search_content.py +0 -275
- hanzo_mcp/tools/filesystem/write_file.py +0 -162
- hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
- hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
- hanzo_mcp/tools/project/__init__.py +0 -64
- hanzo_mcp/tools/project/analysis.py +0 -886
- hanzo_mcp/tools/project/base.py +0 -66
- hanzo_mcp/tools/project/project_analyze.py +0 -173
- hanzo_mcp/tools/shell/run_script.py +0 -215
- hanzo_mcp/tools/shell/script_tool.py +0 -244
- hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""Settings management for Hanzo MCP.
|
|
2
|
+
|
|
3
|
+
Handles loading and saving configuration from multiple sources:
|
|
4
|
+
1. Default settings
|
|
5
|
+
2. Global config file (~/.config/hanzo/mcp-settings.json)
|
|
6
|
+
3. Project-specific config file
|
|
7
|
+
4. Environment variables
|
|
8
|
+
5. CLI arguments (highest priority)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Union
|
|
15
|
+
from dataclasses import dataclass, asdict, field
|
|
16
|
+
|
|
17
|
+
from .tool_config import TOOL_REGISTRY, ToolCategory
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class MCPServerConfig:
|
|
22
|
+
"""Configuration for an external MCP server."""
|
|
23
|
+
name: str
|
|
24
|
+
command: str
|
|
25
|
+
args: List[str] = field(default_factory=list)
|
|
26
|
+
enabled: bool = True
|
|
27
|
+
trusted: bool = False # Whether this server is trusted
|
|
28
|
+
description: str = ""
|
|
29
|
+
capabilities: List[str] = field(default_factory=list) # tools, prompts, resources
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class VectorStoreConfig:
|
|
34
|
+
"""Configuration for the vector store."""
|
|
35
|
+
enabled: bool = False
|
|
36
|
+
provider: str = "infinity" # infinity, chroma, etc.
|
|
37
|
+
data_path: Optional[str] = None # Will default to ~/.config/hanzo/vector-store
|
|
38
|
+
embedding_model: str = "text-embedding-3-small"
|
|
39
|
+
chunk_size: int = 1000
|
|
40
|
+
chunk_overlap: int = 200
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ProjectConfig:
|
|
45
|
+
"""Configuration specific to a project."""
|
|
46
|
+
name: str
|
|
47
|
+
root_path: str
|
|
48
|
+
rules: List[str] = field(default_factory=list)
|
|
49
|
+
workflows: Dict[str, Any] = field(default_factory=dict)
|
|
50
|
+
tasks: List[Dict[str, Any]] = field(default_factory=list)
|
|
51
|
+
enabled_tools: Dict[str, bool] = field(default_factory=dict)
|
|
52
|
+
disabled_tools: List[str] = field(default_factory=list)
|
|
53
|
+
mcp_servers: List[str] = field(default_factory=list) # Names of enabled MCP servers for this project
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class AgentConfig:
|
|
58
|
+
"""Configuration for agent tools."""
|
|
59
|
+
enabled: bool = False
|
|
60
|
+
model: Optional[str] = None
|
|
61
|
+
api_key: Optional[str] = None
|
|
62
|
+
base_url: Optional[str] = None
|
|
63
|
+
max_tokens: Optional[int] = None
|
|
64
|
+
max_iterations: int = 10
|
|
65
|
+
max_tool_uses: int = 30
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class ServerConfig:
|
|
70
|
+
"""Configuration for the MCP server."""
|
|
71
|
+
name: str = "hanzo-mcp"
|
|
72
|
+
host: str = "127.0.0.1"
|
|
73
|
+
port: int = 3000
|
|
74
|
+
transport: str = "stdio" # stdio or sse
|
|
75
|
+
log_level: str = "INFO"
|
|
76
|
+
command_timeout: float = 120.0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class HanzoMCPSettings:
|
|
81
|
+
"""Complete configuration for Hanzo MCP."""
|
|
82
|
+
# Server settings
|
|
83
|
+
server: ServerConfig = field(default_factory=ServerConfig)
|
|
84
|
+
|
|
85
|
+
# Paths and permissions
|
|
86
|
+
allowed_paths: List[str] = field(default_factory=list)
|
|
87
|
+
project_paths: List[str] = field(default_factory=list)
|
|
88
|
+
project_dir: Optional[str] = None
|
|
89
|
+
|
|
90
|
+
# Tool configuration
|
|
91
|
+
enabled_tools: Dict[str, bool] = field(default_factory=dict)
|
|
92
|
+
disabled_tools: List[str] = field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
# Agent configuration
|
|
95
|
+
agent: AgentConfig = field(default_factory=AgentConfig)
|
|
96
|
+
|
|
97
|
+
# Vector store configuration
|
|
98
|
+
vector_store: VectorStoreConfig = field(default_factory=VectorStoreConfig)
|
|
99
|
+
|
|
100
|
+
# MCP Hub configuration
|
|
101
|
+
mcp_servers: Dict[str, MCPServerConfig] = field(default_factory=dict)
|
|
102
|
+
hub_enabled: bool = False
|
|
103
|
+
trusted_servers: List[str] = field(default_factory=list) # Whitelist of trusted server names
|
|
104
|
+
|
|
105
|
+
# Project-specific configurations
|
|
106
|
+
projects: Dict[str, ProjectConfig] = field(default_factory=dict)
|
|
107
|
+
current_project: Optional[str] = None
|
|
108
|
+
|
|
109
|
+
def __post_init__(self):
|
|
110
|
+
"""Initialize default tool states if not specified."""
|
|
111
|
+
if not self.enabled_tools:
|
|
112
|
+
self.enabled_tools = {name: config.enabled for name, config in TOOL_REGISTRY.items()}
|
|
113
|
+
|
|
114
|
+
# Apply disabled_tools list to enabled_tools dict
|
|
115
|
+
for tool_name in self.disabled_tools:
|
|
116
|
+
if tool_name in TOOL_REGISTRY:
|
|
117
|
+
self.enabled_tools[tool_name] = False
|
|
118
|
+
|
|
119
|
+
def is_tool_enabled(self, tool_name: str) -> bool:
|
|
120
|
+
"""Check if a specific tool is enabled."""
|
|
121
|
+
# Check disabled_tools list first (highest priority)
|
|
122
|
+
if tool_name in self.disabled_tools:
|
|
123
|
+
return False
|
|
124
|
+
return self.enabled_tools.get(tool_name, TOOL_REGISTRY.get(tool_name, type('obj', (object,), {'enabled': False})).enabled)
|
|
125
|
+
|
|
126
|
+
def enable_tool(self, tool_name: str) -> bool:
|
|
127
|
+
"""Enable a specific tool."""
|
|
128
|
+
if tool_name in TOOL_REGISTRY:
|
|
129
|
+
self.enabled_tools[tool_name] = True
|
|
130
|
+
# Remove from disabled list if present
|
|
131
|
+
if tool_name in self.disabled_tools:
|
|
132
|
+
self.disabled_tools.remove(tool_name)
|
|
133
|
+
return True
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def disable_tool(self, tool_name: str) -> bool:
|
|
137
|
+
"""Disable a specific tool."""
|
|
138
|
+
if tool_name in TOOL_REGISTRY:
|
|
139
|
+
self.enabled_tools[tool_name] = False
|
|
140
|
+
# Add to disabled list if not present
|
|
141
|
+
if tool_name not in self.disabled_tools:
|
|
142
|
+
self.disabled_tools.append(tool_name)
|
|
143
|
+
return True
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def get_enabled_tools(self) -> List[str]:
|
|
147
|
+
"""Get list of enabled tool names."""
|
|
148
|
+
return [name for name in TOOL_REGISTRY.keys() if self.is_tool_enabled(name)]
|
|
149
|
+
|
|
150
|
+
def get_disabled_tools(self) -> List[str]:
|
|
151
|
+
"""Get list of disabled tool names."""
|
|
152
|
+
return [name for name in TOOL_REGISTRY.keys() if not self.is_tool_enabled(name)]
|
|
153
|
+
|
|
154
|
+
# MCP Server Management
|
|
155
|
+
def add_mcp_server(self, server_config: MCPServerConfig) -> bool:
|
|
156
|
+
"""Add a new MCP server configuration."""
|
|
157
|
+
if server_config.name in self.mcp_servers:
|
|
158
|
+
return False # Server already exists
|
|
159
|
+
self.mcp_servers[server_config.name] = server_config
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
def remove_mcp_server(self, server_name: str) -> bool:
|
|
163
|
+
"""Remove an MCP server configuration."""
|
|
164
|
+
if server_name in self.mcp_servers:
|
|
165
|
+
del self.mcp_servers[server_name]
|
|
166
|
+
# Remove from trusted list if present
|
|
167
|
+
if server_name in self.trusted_servers:
|
|
168
|
+
self.trusted_servers.remove(server_name)
|
|
169
|
+
return True
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
def enable_mcp_server(self, server_name: str) -> bool:
|
|
173
|
+
"""Enable an MCP server."""
|
|
174
|
+
if server_name in self.mcp_servers:
|
|
175
|
+
self.mcp_servers[server_name].enabled = True
|
|
176
|
+
return True
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
def disable_mcp_server(self, server_name: str) -> bool:
|
|
180
|
+
"""Disable an MCP server."""
|
|
181
|
+
if server_name in self.mcp_servers:
|
|
182
|
+
self.mcp_servers[server_name].enabled = False
|
|
183
|
+
return True
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def trust_mcp_server(self, server_name: str) -> bool:
|
|
187
|
+
"""Add server to trusted list."""
|
|
188
|
+
if server_name in self.mcp_servers:
|
|
189
|
+
if server_name not in self.trusted_servers:
|
|
190
|
+
self.trusted_servers.append(server_name)
|
|
191
|
+
self.mcp_servers[server_name].trusted = True
|
|
192
|
+
return True
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
def get_enabled_mcp_servers(self) -> List[MCPServerConfig]:
|
|
196
|
+
"""Get list of enabled MCP servers."""
|
|
197
|
+
return [server for server in self.mcp_servers.values() if server.enabled]
|
|
198
|
+
|
|
199
|
+
def get_trusted_mcp_servers(self) -> List[MCPServerConfig]:
|
|
200
|
+
"""Get list of trusted MCP servers."""
|
|
201
|
+
return [server for server in self.mcp_servers.values() if server.trusted]
|
|
202
|
+
|
|
203
|
+
# Project Management
|
|
204
|
+
def add_project(self, project_config: ProjectConfig) -> bool:
|
|
205
|
+
"""Add a project configuration."""
|
|
206
|
+
if project_config.name in self.projects:
|
|
207
|
+
return False # Project already exists
|
|
208
|
+
self.projects[project_config.name] = project_config
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
def remove_project(self, project_name: str) -> bool:
|
|
212
|
+
"""Remove a project configuration."""
|
|
213
|
+
if project_name in self.projects:
|
|
214
|
+
del self.projects[project_name]
|
|
215
|
+
if self.current_project == project_name:
|
|
216
|
+
self.current_project = None
|
|
217
|
+
return True
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
def set_current_project(self, project_name: str) -> bool:
|
|
221
|
+
"""Set the current active project."""
|
|
222
|
+
if project_name in self.projects:
|
|
223
|
+
self.current_project = project_name
|
|
224
|
+
return True
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
def get_current_project(self) -> Optional[ProjectConfig]:
|
|
228
|
+
"""Get the current project configuration."""
|
|
229
|
+
if self.current_project:
|
|
230
|
+
return self.projects.get(self.current_project)
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
def get_project_tools(self, project_name: Optional[str] = None) -> Dict[str, bool]:
|
|
234
|
+
"""Get tool configuration for a specific project."""
|
|
235
|
+
if not project_name:
|
|
236
|
+
project_name = self.current_project
|
|
237
|
+
|
|
238
|
+
if project_name and project_name in self.projects:
|
|
239
|
+
project = self.projects[project_name]
|
|
240
|
+
# Start with global tool settings
|
|
241
|
+
tools = self.enabled_tools.copy()
|
|
242
|
+
# Apply project-specific settings
|
|
243
|
+
tools.update(project.enabled_tools)
|
|
244
|
+
# Apply project-specific disabled tools
|
|
245
|
+
for tool_name in project.disabled_tools:
|
|
246
|
+
tools[tool_name] = False
|
|
247
|
+
return tools
|
|
248
|
+
|
|
249
|
+
return self.enabled_tools
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def get_config_dir() -> Path:
|
|
253
|
+
"""Get the configuration directory for Hanzo MCP."""
|
|
254
|
+
if os.name == "nt": # Windows
|
|
255
|
+
config_dir = Path(os.environ.get("APPDATA", "")) / "hanzo"
|
|
256
|
+
else: # Unix/macOS
|
|
257
|
+
config_dir = Path.home() / ".config" / "hanzo"
|
|
258
|
+
|
|
259
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
260
|
+
return config_dir
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def get_global_config_path() -> Path:
|
|
264
|
+
"""Get the path to the global configuration file."""
|
|
265
|
+
return get_config_dir() / "mcp-settings.json"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def get_project_config_path(project_dir: Optional[str] = None) -> Optional[Path]:
|
|
269
|
+
"""Get the path to the project-specific configuration file."""
|
|
270
|
+
if project_dir:
|
|
271
|
+
project_path = Path(project_dir)
|
|
272
|
+
config_candidates = [
|
|
273
|
+
project_path / ".hanzo" / "mcp-settings.json",
|
|
274
|
+
project_path / "hanzo-mcp.json",
|
|
275
|
+
project_path / ".hanzo-mcp.json",
|
|
276
|
+
]
|
|
277
|
+
for config_path in config_candidates:
|
|
278
|
+
if config_path.exists():
|
|
279
|
+
return config_path
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def load_settings(
|
|
284
|
+
project_dir: Optional[str] = None,
|
|
285
|
+
config_overrides: Optional[Dict[str, Any]] = None
|
|
286
|
+
) -> HanzoMCPSettings:
|
|
287
|
+
"""Load settings from all sources in priority order.
|
|
288
|
+
|
|
289
|
+
Priority (highest to lowest):
|
|
290
|
+
1. config_overrides (usually from CLI)
|
|
291
|
+
2. Project-specific config file
|
|
292
|
+
3. Global config file
|
|
293
|
+
4. Defaults
|
|
294
|
+
"""
|
|
295
|
+
# Start with defaults
|
|
296
|
+
settings = HanzoMCPSettings()
|
|
297
|
+
|
|
298
|
+
# Load global config
|
|
299
|
+
global_config_path = get_global_config_path()
|
|
300
|
+
if global_config_path.exists():
|
|
301
|
+
try:
|
|
302
|
+
with open(global_config_path) as f:
|
|
303
|
+
global_config = json.load(f)
|
|
304
|
+
settings = _merge_config(settings, global_config)
|
|
305
|
+
except Exception as e:
|
|
306
|
+
print(f"Warning: Failed to load global config: {e}")
|
|
307
|
+
|
|
308
|
+
# Load project config
|
|
309
|
+
project_config_path = get_project_config_path(project_dir)
|
|
310
|
+
if project_config_path:
|
|
311
|
+
try:
|
|
312
|
+
with open(project_config_path) as f:
|
|
313
|
+
project_config = json.load(f)
|
|
314
|
+
settings = _merge_config(settings, project_config)
|
|
315
|
+
except Exception as e:
|
|
316
|
+
print(f"Warning: Failed to load project config: {e}")
|
|
317
|
+
|
|
318
|
+
# Apply CLI overrides
|
|
319
|
+
if config_overrides:
|
|
320
|
+
settings = _merge_config(settings, config_overrides)
|
|
321
|
+
|
|
322
|
+
return settings
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def save_settings(settings: HanzoMCPSettings, global_config: bool = True) -> Path:
|
|
326
|
+
"""Save settings to configuration file.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
settings: Settings to save
|
|
330
|
+
global_config: If True, save to global config, otherwise save to project config
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Path where settings were saved
|
|
334
|
+
"""
|
|
335
|
+
if global_config:
|
|
336
|
+
config_path = get_global_config_path()
|
|
337
|
+
else:
|
|
338
|
+
# Save to current directory project config
|
|
339
|
+
config_path = Path.cwd() / ".hanzo-mcp.json"
|
|
340
|
+
|
|
341
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
342
|
+
|
|
343
|
+
with open(config_path, 'w') as f:
|
|
344
|
+
json.dump(asdict(settings), f, indent=2)
|
|
345
|
+
|
|
346
|
+
return config_path
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _merge_config(base_settings: HanzoMCPSettings, config_dict: Dict[str, Any]) -> HanzoMCPSettings:
|
|
350
|
+
"""Merge configuration dictionary into settings object."""
|
|
351
|
+
# Convert to dict, merge, then convert back
|
|
352
|
+
base_dict = asdict(base_settings)
|
|
353
|
+
|
|
354
|
+
def deep_merge(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
|
|
355
|
+
"""Deep merge two dictionaries."""
|
|
356
|
+
for key, value in update.items():
|
|
357
|
+
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
|
|
358
|
+
base[key] = deep_merge(base[key], value)
|
|
359
|
+
else:
|
|
360
|
+
base[key] = value
|
|
361
|
+
return base
|
|
362
|
+
|
|
363
|
+
merged = deep_merge(base_dict, config_dict)
|
|
364
|
+
|
|
365
|
+
# Reconstruct the settings object
|
|
366
|
+
mcp_servers = {}
|
|
367
|
+
for name, server_data in merged.get("mcp_servers", {}).items():
|
|
368
|
+
mcp_servers[name] = MCPServerConfig(**server_data)
|
|
369
|
+
|
|
370
|
+
projects = {}
|
|
371
|
+
for name, project_data in merged.get("projects", {}).items():
|
|
372
|
+
projects[name] = ProjectConfig(**project_data)
|
|
373
|
+
|
|
374
|
+
return HanzoMCPSettings(
|
|
375
|
+
server=ServerConfig(**merged.get("server", {})),
|
|
376
|
+
allowed_paths=merged.get("allowed_paths", []),
|
|
377
|
+
project_paths=merged.get("project_paths", []),
|
|
378
|
+
project_dir=merged.get("project_dir"),
|
|
379
|
+
enabled_tools=merged.get("enabled_tools", {}),
|
|
380
|
+
disabled_tools=merged.get("disabled_tools", []),
|
|
381
|
+
agent=AgentConfig(**merged.get("agent", {})),
|
|
382
|
+
vector_store=VectorStoreConfig(**merged.get("vector_store", {})),
|
|
383
|
+
mcp_servers=mcp_servers,
|
|
384
|
+
hub_enabled=merged.get("hub_enabled", False),
|
|
385
|
+
trusted_servers=merged.get("trusted_servers", []),
|
|
386
|
+
projects=projects,
|
|
387
|
+
current_project=merged.get("current_project"),
|
|
388
|
+
)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Tool configuration definitions for Hanzo MCP."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ToolCategory(str, Enum):
|
|
9
|
+
"""Categories of tools available in Hanzo MCP."""
|
|
10
|
+
FILESYSTEM = "filesystem"
|
|
11
|
+
SHELL = "shell"
|
|
12
|
+
JUPYTER = "jupyter"
|
|
13
|
+
TODO = "todo"
|
|
14
|
+
AGENT = "agent"
|
|
15
|
+
COMMON = "common"
|
|
16
|
+
VECTOR = "vector"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ToolConfig:
|
|
21
|
+
"""Configuration for an individual tool."""
|
|
22
|
+
name: str
|
|
23
|
+
category: ToolCategory
|
|
24
|
+
enabled: bool = True
|
|
25
|
+
description: str = ""
|
|
26
|
+
requires_permissions: bool = True
|
|
27
|
+
cli_flag: str = ""
|
|
28
|
+
|
|
29
|
+
def __post_init__(self):
|
|
30
|
+
"""Generate CLI flag if not provided."""
|
|
31
|
+
if not self.cli_flag:
|
|
32
|
+
self.cli_flag = f"--{'enable' if self.enabled else 'disable'}-{self.name.replace('_', '-')}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Complete tool registry with all 17 tools
|
|
36
|
+
TOOL_REGISTRY: Dict[str, ToolConfig] = {
|
|
37
|
+
# Filesystem Tools (8)
|
|
38
|
+
"read": ToolConfig(
|
|
39
|
+
name="read",
|
|
40
|
+
category=ToolCategory.FILESYSTEM,
|
|
41
|
+
description="Read file contents with line numbers and truncation support",
|
|
42
|
+
cli_flag="--disable-read"
|
|
43
|
+
),
|
|
44
|
+
"write": ToolConfig(
|
|
45
|
+
name="write",
|
|
46
|
+
category=ToolCategory.FILESYSTEM,
|
|
47
|
+
description="Write content to files, create new files or overwrite existing ones",
|
|
48
|
+
cli_flag="--disable-write"
|
|
49
|
+
),
|
|
50
|
+
"edit": ToolConfig(
|
|
51
|
+
name="edit",
|
|
52
|
+
category=ToolCategory.FILESYSTEM,
|
|
53
|
+
description="Make precise string replacements in files with validation",
|
|
54
|
+
cli_flag="--disable-edit"
|
|
55
|
+
),
|
|
56
|
+
"multi_edit": ToolConfig(
|
|
57
|
+
name="multi_edit",
|
|
58
|
+
category=ToolCategory.FILESYSTEM,
|
|
59
|
+
description="Perform multiple edits to a single file in one operation",
|
|
60
|
+
cli_flag="--disable-multi-edit"
|
|
61
|
+
),
|
|
62
|
+
"directory_tree": ToolConfig(
|
|
63
|
+
name="directory_tree",
|
|
64
|
+
category=ToolCategory.FILESYSTEM,
|
|
65
|
+
description="Display directory structure as a tree",
|
|
66
|
+
cli_flag="--disable-directory-tree"
|
|
67
|
+
),
|
|
68
|
+
"grep": ToolConfig(
|
|
69
|
+
name="grep",
|
|
70
|
+
category=ToolCategory.FILESYSTEM,
|
|
71
|
+
description="Fast content search using ripgrep or fallback Python implementation",
|
|
72
|
+
cli_flag="--disable-grep"
|
|
73
|
+
),
|
|
74
|
+
"grep_ast": ToolConfig(
|
|
75
|
+
name="grep_ast",
|
|
76
|
+
category=ToolCategory.FILESYSTEM,
|
|
77
|
+
description="Search source code with AST context using tree-sitter",
|
|
78
|
+
cli_flag="--disable-grep-ast"
|
|
79
|
+
),
|
|
80
|
+
"content_replace": ToolConfig(
|
|
81
|
+
name="content_replace",
|
|
82
|
+
category=ToolCategory.FILESYSTEM,
|
|
83
|
+
description="Bulk text replacement across multiple files",
|
|
84
|
+
cli_flag="--disable-content-replace"
|
|
85
|
+
),
|
|
86
|
+
|
|
87
|
+
# Shell Tools (1)
|
|
88
|
+
"run_command": ToolConfig(
|
|
89
|
+
name="run_command",
|
|
90
|
+
category=ToolCategory.SHELL,
|
|
91
|
+
description="Execute shell commands with session support",
|
|
92
|
+
cli_flag="--disable-run-command"
|
|
93
|
+
),
|
|
94
|
+
|
|
95
|
+
# Jupyter Tools (2)
|
|
96
|
+
"notebook_read": ToolConfig(
|
|
97
|
+
name="notebook_read",
|
|
98
|
+
category=ToolCategory.JUPYTER,
|
|
99
|
+
description="Read Jupyter notebook files (.ipynb)",
|
|
100
|
+
cli_flag="--disable-notebook-read"
|
|
101
|
+
),
|
|
102
|
+
"notebook_edit": ToolConfig(
|
|
103
|
+
name="notebook_edit",
|
|
104
|
+
category=ToolCategory.JUPYTER,
|
|
105
|
+
description="Edit Jupyter notebook cells (replace, insert, delete)",
|
|
106
|
+
cli_flag="--disable-notebook-edit"
|
|
107
|
+
),
|
|
108
|
+
|
|
109
|
+
# Todo Tools (2)
|
|
110
|
+
"todo_read": ToolConfig(
|
|
111
|
+
name="todo_read",
|
|
112
|
+
category=ToolCategory.TODO,
|
|
113
|
+
description="Read the current todo list for a session",
|
|
114
|
+
cli_flag="--disable-todo-read"
|
|
115
|
+
),
|
|
116
|
+
"todo_write": ToolConfig(
|
|
117
|
+
name="todo_write",
|
|
118
|
+
category=ToolCategory.TODO,
|
|
119
|
+
description="Create and manage structured task lists",
|
|
120
|
+
cli_flag="--disable-todo-write"
|
|
121
|
+
),
|
|
122
|
+
|
|
123
|
+
# Agent Tools (1)
|
|
124
|
+
"dispatch_agent": ToolConfig(
|
|
125
|
+
name="dispatch_agent",
|
|
126
|
+
category=ToolCategory.AGENT,
|
|
127
|
+
enabled=False, # Disabled by default
|
|
128
|
+
description="Delegate tasks to sub-agents for concurrent/specialized processing",
|
|
129
|
+
cli_flag="--enable-dispatch-agent"
|
|
130
|
+
),
|
|
131
|
+
|
|
132
|
+
# Common Tools (3)
|
|
133
|
+
"think": ToolConfig(
|
|
134
|
+
name="think",
|
|
135
|
+
category=ToolCategory.COMMON,
|
|
136
|
+
description="Provide structured thinking space for complex reasoning",
|
|
137
|
+
cli_flag="--disable-think"
|
|
138
|
+
),
|
|
139
|
+
"batch": ToolConfig(
|
|
140
|
+
name="batch",
|
|
141
|
+
category=ToolCategory.COMMON,
|
|
142
|
+
description="Execute multiple tools in parallel or serial",
|
|
143
|
+
cli_flag="--disable-batch"
|
|
144
|
+
),
|
|
145
|
+
|
|
146
|
+
# Vector Tools (2)
|
|
147
|
+
"vector_index": ToolConfig(
|
|
148
|
+
name="vector_index",
|
|
149
|
+
category=ToolCategory.VECTOR,
|
|
150
|
+
enabled=False, # Disabled by default
|
|
151
|
+
description="Index documents in local vector database for semantic search",
|
|
152
|
+
cli_flag="--enable-vector-index"
|
|
153
|
+
),
|
|
154
|
+
"vector_search": ToolConfig(
|
|
155
|
+
name="vector_search",
|
|
156
|
+
category=ToolCategory.VECTOR,
|
|
157
|
+
enabled=False, # Disabled by default
|
|
158
|
+
description="Search documents using semantic similarity in vector database",
|
|
159
|
+
cli_flag="--enable-vector-search"
|
|
160
|
+
),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_tools_by_category(category: ToolCategory) -> List[ToolConfig]:
|
|
165
|
+
"""Get all tools in a specific category."""
|
|
166
|
+
return [tool for tool in TOOL_REGISTRY.values() if tool.category == category]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_enabled_tools() -> List[ToolConfig]:
|
|
170
|
+
"""Get all enabled tools."""
|
|
171
|
+
return [tool for tool in TOOL_REGISTRY.values() if tool.enabled]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_disabled_tools() -> List[ToolConfig]:
|
|
175
|
+
"""Get all disabled tools."""
|
|
176
|
+
return [tool for tool in TOOL_REGISTRY.values() if not tool.enabled]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def enable_tool(tool_name: str) -> bool:
|
|
180
|
+
"""Enable a specific tool. Returns True if successful."""
|
|
181
|
+
if tool_name in TOOL_REGISTRY:
|
|
182
|
+
TOOL_REGISTRY[tool_name].enabled = True
|
|
183
|
+
return True
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def disable_tool(tool_name: str) -> bool:
|
|
188
|
+
"""Disable a specific tool. Returns True if successful."""
|
|
189
|
+
if tool_name in TOOL_REGISTRY:
|
|
190
|
+
TOOL_REGISTRY[tool_name].enabled = False
|
|
191
|
+
return True
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def is_tool_enabled(tool_name: str) -> bool:
|
|
196
|
+
"""Check if a tool is enabled."""
|
|
197
|
+
return TOOL_REGISTRY.get(tool_name, ToolConfig("", ToolCategory.COMMON, False)).enabled
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from fastmcp import FastMCP
|
|
4
|
+
|
|
5
|
+
from hanzo_mcp.prompts.compact_conversation import COMPACT_CONVERSATION_PROMPT
|
|
6
|
+
from hanzo_mcp.prompts.create_release import CREATE_RELEASE_PROMPT
|
|
7
|
+
from hanzo_mcp.prompts.project_system import PROJECT_SYSTEM_PROMPT
|
|
8
|
+
from hanzo_mcp.prompts.project_todo_reminder import (
|
|
9
|
+
PROJECT_TODO_EMPTY_REMINDER,
|
|
10
|
+
get_project_todo_reminder,
|
|
11
|
+
)
|
|
12
|
+
from hanzo_mcp.prompts.utils import (
|
|
13
|
+
get_directory_structure,
|
|
14
|
+
get_git_info,
|
|
15
|
+
get_os_info,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
CONTINUE_FROM_LAST_SESSION_PROMPT = """<system-reminder>
|
|
19
|
+
This is a reminder that your todo list is currently empty. DO NOT mention this to the user explicitly because they are already aware. If you are working on tasks that would benefit from a todo list please use the TodoWrite tool to create one. If not, please feel free to ignore. Again do not mention this message to the user.
|
|
20
|
+
</system-reminder>
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_project_system_prompt(project_path: str):
|
|
25
|
+
"""Factory function to create a project system prompt function."""
|
|
26
|
+
|
|
27
|
+
def project_system_prompt() -> str:
|
|
28
|
+
"""
|
|
29
|
+
Summarize the conversation so far for a specific project.
|
|
30
|
+
"""
|
|
31
|
+
working_directory = project_path
|
|
32
|
+
is_git_repo = os.path.isdir(os.path.join(working_directory, ".git"))
|
|
33
|
+
platform, _, os_version = get_os_info()
|
|
34
|
+
|
|
35
|
+
# Get directory structure
|
|
36
|
+
directory_structure = get_directory_structure(
|
|
37
|
+
working_directory, max_depth=3, include_filtered=False
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Get git information
|
|
41
|
+
git_info = get_git_info(working_directory)
|
|
42
|
+
current_branch = git_info.get("current_branch", "")
|
|
43
|
+
main_branch = git_info.get("main_branch", "")
|
|
44
|
+
git_status = git_info.get("git_status", "")
|
|
45
|
+
recent_commits = git_info.get("recent_commits", "")
|
|
46
|
+
|
|
47
|
+
return PROJECT_SYSTEM_PROMPT.format(
|
|
48
|
+
working_directory=working_directory,
|
|
49
|
+
is_git_repo=is_git_repo,
|
|
50
|
+
platform=platform,
|
|
51
|
+
os_version=os_version,
|
|
52
|
+
directory_structure=directory_structure,
|
|
53
|
+
current_branch=current_branch,
|
|
54
|
+
main_branch=main_branch,
|
|
55
|
+
git_status=git_status,
|
|
56
|
+
recent_commits=recent_commits,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return project_system_prompt
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def register_all_prompts(
|
|
63
|
+
mcp_server: FastMCP, projects: list[str] | None = None
|
|
64
|
+
) -> None:
|
|
65
|
+
@mcp_server.prompt(name="Compact current conversation")
|
|
66
|
+
def compact() -> str:
|
|
67
|
+
"""
|
|
68
|
+
Summarize the conversation so far.
|
|
69
|
+
"""
|
|
70
|
+
return COMPACT_CONVERSATION_PROMPT
|
|
71
|
+
|
|
72
|
+
@mcp_server.prompt(name="Create a new release")
|
|
73
|
+
def create_release() -> str:
|
|
74
|
+
"""
|
|
75
|
+
Create a new release for my project.
|
|
76
|
+
"""
|
|
77
|
+
return CREATE_RELEASE_PROMPT
|
|
78
|
+
|
|
79
|
+
@mcp_server.prompt(name="Continue todo by session id")
|
|
80
|
+
def continue_todo_by_session_id(session_id: str) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Continue from the last todo list for the current session.
|
|
83
|
+
"""
|
|
84
|
+
return get_project_todo_reminder(session_id)
|
|
85
|
+
|
|
86
|
+
@mcp_server.prompt(name="Continue latest todo")
|
|
87
|
+
def continue_latest_todo() -> str:
|
|
88
|
+
"""
|
|
89
|
+
Continue from the last todo list for the current session.
|
|
90
|
+
"""
|
|
91
|
+
return get_project_todo_reminder()
|
|
92
|
+
|
|
93
|
+
@mcp_server.prompt(name="System prompt")
|
|
94
|
+
def manual_project_system_prompt(project_path: str) -> str:
|
|
95
|
+
"""
|
|
96
|
+
Detailed system prompt include env,git etc information about the specified project.
|
|
97
|
+
"""
|
|
98
|
+
return create_project_system_prompt(project_path)()
|
|
99
|
+
|
|
100
|
+
if projects is None:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
for project in projects:
|
|
104
|
+
# Register the prompt with the factory function
|
|
105
|
+
mcp_server.prompt(
|
|
106
|
+
name=f"System prompt for {os.path.basename(project)}",
|
|
107
|
+
description=f"Detailed system prompt include env,git etc information about {project}",
|
|
108
|
+
)(create_project_system_prompt(project))
|
|
109
|
+
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
__all__ = [
|
|
114
|
+
"register_all_prompts",
|
|
115
|
+
"get_project_todo_reminder",
|
|
116
|
+
"PROJECT_TODO_EMPTY_REMINDER",
|
|
117
|
+
]
|