hanzo-mcp 0.5.0__py3-none-any.whl → 0.5.1__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/config/settings.py +61 -0
- hanzo_mcp/tools/__init__.py +26 -11
- hanzo_mcp/tools/common/config_tool.py +396 -0
- hanzo_mcp/tools/filesystem/__init__.py +10 -1
- hanzo_mcp/tools/filesystem/unified_search.py +689 -0
- hanzo_mcp/tools/vector/__init__.py +16 -12
- hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
- hanzo_mcp/tools/vector/git_ingester.py +482 -0
- hanzo_mcp/tools/vector/infinity_store.py +367 -1
- hanzo_mcp/tools/vector/mock_infinity.py +162 -0
- hanzo_mcp/tools/vector/vector_index.py +7 -6
- hanzo_mcp/tools/vector/vector_search.py +11 -1
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.1.dist-info}/METADATA +68 -20
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.1.dist-info}/RECORD +19 -14
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.1.dist-info}/top_level.txt +0 -0
hanzo_mcp/__init__.py
CHANGED
hanzo_mcp/config/settings.py
CHANGED
|
@@ -280,6 +280,67 @@ def get_project_config_path(project_dir: Optional[str] = None) -> Optional[Path]
|
|
|
280
280
|
return None
|
|
281
281
|
|
|
282
282
|
|
|
283
|
+
def ensure_project_hanzo_dir(project_dir: str) -> Path:
|
|
284
|
+
"""Ensure .hanzo directory exists in project and return its path."""
|
|
285
|
+
project_path = Path(project_dir)
|
|
286
|
+
hanzo_dir = project_path / ".hanzo"
|
|
287
|
+
hanzo_dir.mkdir(exist_ok=True)
|
|
288
|
+
|
|
289
|
+
# Create default structure
|
|
290
|
+
(hanzo_dir / "db").mkdir(exist_ok=True) # Vector database
|
|
291
|
+
|
|
292
|
+
# Create default project config if it doesn't exist
|
|
293
|
+
config_path = hanzo_dir / "mcp-settings.json"
|
|
294
|
+
if not config_path.exists():
|
|
295
|
+
default_project_config = {
|
|
296
|
+
"name": project_path.name,
|
|
297
|
+
"root_path": str(project_path),
|
|
298
|
+
"rules": [
|
|
299
|
+
"Follow project-specific coding standards",
|
|
300
|
+
"Test all changes before committing",
|
|
301
|
+
"Update documentation for new features"
|
|
302
|
+
],
|
|
303
|
+
"workflows": {
|
|
304
|
+
"development": {
|
|
305
|
+
"steps": ["edit", "test", "commit"],
|
|
306
|
+
"tools": ["read", "write", "edit", "run_command"]
|
|
307
|
+
},
|
|
308
|
+
"documentation": {
|
|
309
|
+
"steps": ["read", "analyze", "write"],
|
|
310
|
+
"tools": ["read", "write", "vector_search"]
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
"tasks": [],
|
|
314
|
+
"enabled_tools": {},
|
|
315
|
+
"disabled_tools": [],
|
|
316
|
+
"mcp_servers": []
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
with open(config_path, 'w') as f:
|
|
320
|
+
json.dump(default_project_config, f, indent=2)
|
|
321
|
+
|
|
322
|
+
return hanzo_dir
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def detect_project_from_path(file_path: str) -> Optional[Dict[str, str]]:
|
|
326
|
+
"""Detect project information from a file path by looking for LLM.md."""
|
|
327
|
+
path = Path(file_path).resolve()
|
|
328
|
+
current_path = path.parent if path.is_file() else path
|
|
329
|
+
|
|
330
|
+
while current_path != current_path.parent: # Stop at filesystem root
|
|
331
|
+
llm_md_path = current_path / "LLM.md"
|
|
332
|
+
if llm_md_path.exists():
|
|
333
|
+
return {
|
|
334
|
+
"name": current_path.name,
|
|
335
|
+
"root_path": str(current_path),
|
|
336
|
+
"llm_md_path": str(llm_md_path),
|
|
337
|
+
"hanzo_dir": str(ensure_project_hanzo_dir(str(current_path)))
|
|
338
|
+
}
|
|
339
|
+
current_path = current_path.parent
|
|
340
|
+
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
|
|
283
344
|
def load_settings(
|
|
284
345
|
project_dir: Optional[str] = None,
|
|
285
346
|
config_overrides: Optional[Dict[str, Any]] = None
|
hanzo_mcp/tools/__init__.py
CHANGED
|
@@ -77,12 +77,35 @@ def register_all_tools(
|
|
|
77
77
|
"grep": is_tool_enabled("grep", not disable_search_tools),
|
|
78
78
|
"grep_ast": is_tool_enabled("grep_ast", not disable_search_tools),
|
|
79
79
|
"content_replace": is_tool_enabled("content_replace", not disable_write_tools),
|
|
80
|
+
"unified_search": is_tool_enabled("unified_search", not disable_search_tools),
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
# Vector tools setup (needed for unified search)
|
|
84
|
+
project_manager = None
|
|
85
|
+
vector_enabled = {
|
|
86
|
+
"vector_index": is_tool_enabled("vector_index", False),
|
|
87
|
+
"vector_search": is_tool_enabled("vector_search", False),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Create project manager if vector tools or unified search are enabled
|
|
91
|
+
if any(vector_enabled.values()) or filesystem_enabled.get("unified_search", False):
|
|
92
|
+
if vector_config:
|
|
93
|
+
from hanzo_mcp.tools.vector.project_manager import ProjectVectorManager
|
|
94
|
+
search_paths = [str(path) for path in permission_manager.allowed_paths]
|
|
95
|
+
project_manager = ProjectVectorManager(
|
|
96
|
+
global_db_path=vector_config.get("data_path"),
|
|
97
|
+
embedding_model=vector_config.get("embedding_model", "text-embedding-3-small"),
|
|
98
|
+
dimension=vector_config.get("dimension", 1536),
|
|
99
|
+
)
|
|
100
|
+
# Auto-detect projects from search paths
|
|
101
|
+
detected_projects = project_manager.detect_projects(search_paths)
|
|
102
|
+
print(f"Detected {len(detected_projects)} projects with LLM.md files")
|
|
103
|
+
|
|
82
104
|
filesystem_tools = register_filesystem_tools(
|
|
83
105
|
mcp_server,
|
|
84
106
|
permission_manager,
|
|
85
107
|
enabled_tools=filesystem_enabled,
|
|
108
|
+
project_manager=project_manager,
|
|
86
109
|
)
|
|
87
110
|
for tool in filesystem_tools:
|
|
88
111
|
all_tools[tool.name] = tool
|
|
@@ -137,22 +160,14 @@ def register_all_tools(
|
|
|
137
160
|
for tool in thinking_tool:
|
|
138
161
|
all_tools[tool.name] = tool
|
|
139
162
|
|
|
140
|
-
# Register vector tools if enabled
|
|
141
|
-
vector_enabled
|
|
142
|
-
"vector_index": is_tool_enabled("vector_index", False),
|
|
143
|
-
"vector_search": is_tool_enabled("vector_search", False),
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if any(vector_enabled.values()) and vector_config:
|
|
147
|
-
# Get allowed paths for project detection
|
|
148
|
-
search_paths = [str(path) for path in permission_manager.allowed_paths]
|
|
149
|
-
|
|
163
|
+
# Register vector tools if enabled (reuse project_manager if available)
|
|
164
|
+
if any(vector_enabled.values()) and project_manager:
|
|
150
165
|
vector_tools = register_vector_tools(
|
|
151
166
|
mcp_server,
|
|
152
167
|
permission_manager,
|
|
153
168
|
vector_config=vector_config,
|
|
154
169
|
enabled_tools=vector_enabled,
|
|
155
|
-
|
|
170
|
+
project_manager=project_manager,
|
|
156
171
|
)
|
|
157
172
|
for tool in vector_tools:
|
|
158
173
|
all_tools[tool.name] = tool
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Configuration management tool for dynamic settings updates."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional, TypedDict, Unpack, Any, final
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastmcp import Context as MCPContext
|
|
8
|
+
|
|
9
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
10
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
11
|
+
from hanzo_mcp.config.settings import (
|
|
12
|
+
HanzoMCPSettings,
|
|
13
|
+
MCPServerConfig,
|
|
14
|
+
ProjectConfig,
|
|
15
|
+
load_settings,
|
|
16
|
+
save_settings,
|
|
17
|
+
detect_project_from_path,
|
|
18
|
+
ensure_project_hanzo_dir
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigToolParams(TypedDict, total=False):
|
|
23
|
+
"""Parameters for configuration management operations."""
|
|
24
|
+
|
|
25
|
+
action: str # get, set, add_server, remove_server, add_project, etc.
|
|
26
|
+
scope: Optional[str] # global, project, current
|
|
27
|
+
setting_path: Optional[str] # dot-notation path like "agent.enabled"
|
|
28
|
+
value: Optional[Any] # new value to set
|
|
29
|
+
server_name: Optional[str] # for MCP server operations
|
|
30
|
+
server_config: Optional[Dict[str, Any]] # for adding servers
|
|
31
|
+
project_name: Optional[str] # for project operations
|
|
32
|
+
project_path: Optional[str] # for project path detection
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@final
|
|
36
|
+
class ConfigTool(BaseTool):
|
|
37
|
+
"""Tool for managing Hanzo MCP configuration dynamically."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, permission_manager: PermissionManager):
|
|
40
|
+
"""Initialize the configuration tool.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
permission_manager: Permission manager for access control
|
|
44
|
+
"""
|
|
45
|
+
self.permission_manager = permission_manager
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def name(self) -> str:
|
|
49
|
+
"""Get the tool name."""
|
|
50
|
+
return "config"
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def description(self) -> str:
|
|
54
|
+
"""Get the tool description."""
|
|
55
|
+
return """Dynamically manage Hanzo MCP configuration settings through conversation.
|
|
56
|
+
|
|
57
|
+
Can get/set global settings, project-specific settings, manage MCP servers, configure tools,
|
|
58
|
+
and handle project workflows. Supports dot-notation for nested settings like 'agent.enabled'.
|
|
59
|
+
|
|
60
|
+
Perfect for AI-driven configuration where users can say things like:
|
|
61
|
+
- "Enable the agent tool for this project"
|
|
62
|
+
- "Add a new MCP server for file operations"
|
|
63
|
+
- "Disable write tools globally but enable them for the current project"
|
|
64
|
+
- "Show me the current project configuration"
|
|
65
|
+
|
|
66
|
+
Automatically detects projects based on LLM.md files and manages .hanzo/ directories."""
|
|
67
|
+
|
|
68
|
+
async def call(
|
|
69
|
+
self,
|
|
70
|
+
ctx: MCPContext,
|
|
71
|
+
**params: Unpack[ConfigToolParams],
|
|
72
|
+
) -> str:
|
|
73
|
+
"""Manage configuration settings.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
ctx: MCP context
|
|
77
|
+
**params: Tool parameters
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Configuration operation result
|
|
81
|
+
"""
|
|
82
|
+
action = params.get("action", "get")
|
|
83
|
+
scope = params.get("scope", "global")
|
|
84
|
+
setting_path = params.get("setting_path")
|
|
85
|
+
value = params.get("value")
|
|
86
|
+
server_name = params.get("server_name")
|
|
87
|
+
server_config = params.get("server_config")
|
|
88
|
+
project_name = params.get("project_name")
|
|
89
|
+
project_path = params.get("project_path")
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
if action == "get":
|
|
93
|
+
return await self._get_config(scope, setting_path, project_name, project_path)
|
|
94
|
+
elif action == "set":
|
|
95
|
+
return await self._set_config(scope, setting_path, value, project_name, project_path)
|
|
96
|
+
elif action == "add_server":
|
|
97
|
+
return await self._add_mcp_server(server_name, server_config, scope, project_name)
|
|
98
|
+
elif action == "remove_server":
|
|
99
|
+
return await self._remove_mcp_server(server_name, scope, project_name)
|
|
100
|
+
elif action == "enable_server":
|
|
101
|
+
return await self._enable_mcp_server(server_name, scope, project_name)
|
|
102
|
+
elif action == "disable_server":
|
|
103
|
+
return await self._disable_mcp_server(server_name, scope, project_name)
|
|
104
|
+
elif action == "trust_server":
|
|
105
|
+
return await self._trust_mcp_server(server_name)
|
|
106
|
+
elif action == "add_project":
|
|
107
|
+
return await self._add_project(project_name, project_path)
|
|
108
|
+
elif action == "set_current_project":
|
|
109
|
+
return await self._set_current_project(project_name, project_path)
|
|
110
|
+
elif action == "list_servers":
|
|
111
|
+
return await self._list_mcp_servers(scope, project_name)
|
|
112
|
+
elif action == "list_projects":
|
|
113
|
+
return await self._list_projects()
|
|
114
|
+
elif action == "detect_project":
|
|
115
|
+
return await self._detect_project(project_path)
|
|
116
|
+
else:
|
|
117
|
+
return f"Error: Unknown action '{action}'. Available actions: get, set, add_server, remove_server, enable_server, disable_server, trust_server, add_project, set_current_project, list_servers, list_projects, detect_project"
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return f"Error managing configuration: {str(e)}"
|
|
121
|
+
|
|
122
|
+
async def _get_config(self, scope: str, setting_path: Optional[str], project_name: Optional[str], project_path: Optional[str]) -> str:
|
|
123
|
+
"""Get configuration value(s)."""
|
|
124
|
+
# Load appropriate settings
|
|
125
|
+
if scope == "project" or project_name or project_path:
|
|
126
|
+
if project_path:
|
|
127
|
+
project_info = detect_project_from_path(project_path)
|
|
128
|
+
if project_info:
|
|
129
|
+
settings = load_settings(project_info["root_path"])
|
|
130
|
+
else:
|
|
131
|
+
return f"No project detected at path: {project_path}"
|
|
132
|
+
else:
|
|
133
|
+
settings = load_settings()
|
|
134
|
+
else:
|
|
135
|
+
settings = load_settings()
|
|
136
|
+
|
|
137
|
+
if not setting_path:
|
|
138
|
+
# Return full config summary
|
|
139
|
+
if scope == "project" and settings.current_project:
|
|
140
|
+
project = settings.get_current_project()
|
|
141
|
+
if project:
|
|
142
|
+
return f"Current Project Configuration ({project.name}):\\n{json.dumps(project.__dict__, indent=2)}"
|
|
143
|
+
|
|
144
|
+
# Return global config summary
|
|
145
|
+
summary = {
|
|
146
|
+
"server": settings.server.__dict__,
|
|
147
|
+
"enabled_tools": settings.get_enabled_tools(),
|
|
148
|
+
"disabled_tools": settings.get_disabled_tools(),
|
|
149
|
+
"agent": settings.agent.__dict__,
|
|
150
|
+
"vector_store": settings.vector_store.__dict__,
|
|
151
|
+
"hub_enabled": settings.hub_enabled,
|
|
152
|
+
"mcp_servers": {name: server.__dict__ for name, server in settings.mcp_servers.items()},
|
|
153
|
+
"current_project": settings.current_project,
|
|
154
|
+
"projects": list(settings.projects.keys()),
|
|
155
|
+
}
|
|
156
|
+
return f"Configuration Summary:\\n{json.dumps(summary, indent=2)}"
|
|
157
|
+
|
|
158
|
+
# Get specific setting
|
|
159
|
+
value = self._get_nested_value(settings.__dict__, setting_path)
|
|
160
|
+
if value is not None:
|
|
161
|
+
return f"{setting_path}: {json.dumps(value, indent=2)}"
|
|
162
|
+
else:
|
|
163
|
+
return f"Setting '{setting_path}' not found"
|
|
164
|
+
|
|
165
|
+
async def _set_config(self, scope: str, setting_path: Optional[str], value: Any, project_name: Optional[str], project_path: Optional[str]) -> str:
|
|
166
|
+
"""Set configuration value."""
|
|
167
|
+
if not setting_path:
|
|
168
|
+
return "Error: setting_path is required for set action"
|
|
169
|
+
|
|
170
|
+
# Load settings
|
|
171
|
+
project_dir = None
|
|
172
|
+
if scope == "project" or project_name or project_path:
|
|
173
|
+
if project_path:
|
|
174
|
+
project_info = detect_project_from_path(project_path)
|
|
175
|
+
if project_info:
|
|
176
|
+
project_dir = project_info["root_path"]
|
|
177
|
+
else:
|
|
178
|
+
return f"No project detected at path: {project_path}"
|
|
179
|
+
|
|
180
|
+
settings = load_settings(project_dir)
|
|
181
|
+
|
|
182
|
+
# Set the value
|
|
183
|
+
if self._set_nested_value(settings.__dict__, setting_path, value):
|
|
184
|
+
# Save settings
|
|
185
|
+
if scope == "project" or project_dir:
|
|
186
|
+
save_settings(settings, global_config=False)
|
|
187
|
+
else:
|
|
188
|
+
save_settings(settings, global_config=True)
|
|
189
|
+
return f"Successfully set {setting_path} = {json.dumps(value)}"
|
|
190
|
+
else:
|
|
191
|
+
return f"Error: Could not set '{setting_path}'"
|
|
192
|
+
|
|
193
|
+
async def _add_mcp_server(self, server_name: Optional[str], server_config: Optional[Dict[str, Any]], scope: str, project_name: Optional[str]) -> str:
|
|
194
|
+
"""Add a new MCP server."""
|
|
195
|
+
if not server_name or not server_config:
|
|
196
|
+
return "Error: server_name and server_config are required"
|
|
197
|
+
|
|
198
|
+
settings = load_settings()
|
|
199
|
+
|
|
200
|
+
# Create server config
|
|
201
|
+
mcp_server = MCPServerConfig(
|
|
202
|
+
name=server_name,
|
|
203
|
+
command=server_config.get("command", ""),
|
|
204
|
+
args=server_config.get("args", []),
|
|
205
|
+
enabled=server_config.get("enabled", True),
|
|
206
|
+
trusted=server_config.get("trusted", False),
|
|
207
|
+
description=server_config.get("description", ""),
|
|
208
|
+
capabilities=server_config.get("capabilities", []),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if settings.add_mcp_server(mcp_server):
|
|
212
|
+
save_settings(settings)
|
|
213
|
+
return f"Successfully added MCP server '{server_name}'"
|
|
214
|
+
else:
|
|
215
|
+
return f"Error: MCP server '{server_name}' already exists"
|
|
216
|
+
|
|
217
|
+
async def _remove_mcp_server(self, server_name: Optional[str], scope: str, project_name: Optional[str]) -> str:
|
|
218
|
+
"""Remove an MCP server."""
|
|
219
|
+
if not server_name:
|
|
220
|
+
return "Error: server_name is required"
|
|
221
|
+
|
|
222
|
+
settings = load_settings()
|
|
223
|
+
|
|
224
|
+
if settings.remove_mcp_server(server_name):
|
|
225
|
+
save_settings(settings)
|
|
226
|
+
return f"Successfully removed MCP server '{server_name}'"
|
|
227
|
+
else:
|
|
228
|
+
return f"Error: MCP server '{server_name}' not found"
|
|
229
|
+
|
|
230
|
+
async def _enable_mcp_server(self, server_name: Optional[str], scope: str, project_name: Optional[str]) -> str:
|
|
231
|
+
"""Enable an MCP server."""
|
|
232
|
+
if not server_name:
|
|
233
|
+
return "Error: server_name is required"
|
|
234
|
+
|
|
235
|
+
settings = load_settings()
|
|
236
|
+
|
|
237
|
+
if settings.enable_mcp_server(server_name):
|
|
238
|
+
save_settings(settings)
|
|
239
|
+
return f"Successfully enabled MCP server '{server_name}'"
|
|
240
|
+
else:
|
|
241
|
+
return f"Error: MCP server '{server_name}' not found"
|
|
242
|
+
|
|
243
|
+
async def _disable_mcp_server(self, server_name: Optional[str], scope: str, project_name: Optional[str]) -> str:
|
|
244
|
+
"""Disable an MCP server."""
|
|
245
|
+
if not server_name:
|
|
246
|
+
return "Error: server_name is required"
|
|
247
|
+
|
|
248
|
+
settings = load_settings()
|
|
249
|
+
|
|
250
|
+
if settings.disable_mcp_server(server_name):
|
|
251
|
+
save_settings(settings)
|
|
252
|
+
return f"Successfully disabled MCP server '{server_name}'"
|
|
253
|
+
else:
|
|
254
|
+
return f"Error: MCP server '{server_name}' not found"
|
|
255
|
+
|
|
256
|
+
async def _trust_mcp_server(self, server_name: Optional[str]) -> str:
|
|
257
|
+
"""Trust an MCP server."""
|
|
258
|
+
if not server_name:
|
|
259
|
+
return "Error: server_name is required"
|
|
260
|
+
|
|
261
|
+
settings = load_settings()
|
|
262
|
+
|
|
263
|
+
if settings.trust_mcp_server(server_name):
|
|
264
|
+
save_settings(settings)
|
|
265
|
+
return f"Successfully trusted MCP server '{server_name}'"
|
|
266
|
+
else:
|
|
267
|
+
return f"Error: MCP server '{server_name}' not found"
|
|
268
|
+
|
|
269
|
+
async def _add_project(self, project_name: Optional[str], project_path: Optional[str]) -> str:
|
|
270
|
+
"""Add a project configuration."""
|
|
271
|
+
if not project_path:
|
|
272
|
+
return "Error: project_path is required"
|
|
273
|
+
|
|
274
|
+
# Detect or create project
|
|
275
|
+
project_info = detect_project_from_path(project_path)
|
|
276
|
+
if not project_info:
|
|
277
|
+
return f"No LLM.md found in project path: {project_path}"
|
|
278
|
+
|
|
279
|
+
if not project_name:
|
|
280
|
+
project_name = project_info["name"]
|
|
281
|
+
|
|
282
|
+
settings = load_settings()
|
|
283
|
+
|
|
284
|
+
project_config = ProjectConfig(
|
|
285
|
+
name=project_name,
|
|
286
|
+
root_path=project_info["root_path"],
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if settings.add_project(project_config):
|
|
290
|
+
save_settings(settings)
|
|
291
|
+
return f"Successfully added project '{project_name}' at {project_info['root_path']}"
|
|
292
|
+
else:
|
|
293
|
+
return f"Error: Project '{project_name}' already exists"
|
|
294
|
+
|
|
295
|
+
async def _set_current_project(self, project_name: Optional[str], project_path: Optional[str]) -> str:
|
|
296
|
+
"""Set the current active project."""
|
|
297
|
+
settings = load_settings()
|
|
298
|
+
|
|
299
|
+
if project_path:
|
|
300
|
+
project_info = detect_project_from_path(project_path)
|
|
301
|
+
if project_info:
|
|
302
|
+
project_name = project_info["name"]
|
|
303
|
+
# Auto-add project if not exists
|
|
304
|
+
if project_name not in settings.projects:
|
|
305
|
+
await self._add_project(project_name, project_path)
|
|
306
|
+
settings = load_settings() # Reload after adding
|
|
307
|
+
|
|
308
|
+
if not project_name:
|
|
309
|
+
return "Error: project_name or project_path is required"
|
|
310
|
+
|
|
311
|
+
if settings.set_current_project(project_name):
|
|
312
|
+
save_settings(settings)
|
|
313
|
+
return f"Successfully set current project to '{project_name}'"
|
|
314
|
+
else:
|
|
315
|
+
return f"Error: Project '{project_name}' not found"
|
|
316
|
+
|
|
317
|
+
async def _list_mcp_servers(self, scope: str, project_name: Optional[str]) -> str:
|
|
318
|
+
"""List MCP servers."""
|
|
319
|
+
settings = load_settings()
|
|
320
|
+
|
|
321
|
+
if not settings.mcp_servers:
|
|
322
|
+
return "No MCP servers configured"
|
|
323
|
+
|
|
324
|
+
servers_info = []
|
|
325
|
+
for name, server in settings.mcp_servers.items():
|
|
326
|
+
status = "enabled" if server.enabled else "disabled"
|
|
327
|
+
trust = "trusted" if server.trusted else "untrusted"
|
|
328
|
+
servers_info.append(f"- {name}: {server.command} ({status}, {trust})")
|
|
329
|
+
|
|
330
|
+
return f"MCP Servers:\\n" + "\\n".join(servers_info)
|
|
331
|
+
|
|
332
|
+
async def _list_projects(self) -> str:
|
|
333
|
+
"""List projects."""
|
|
334
|
+
settings = load_settings()
|
|
335
|
+
|
|
336
|
+
if not settings.projects:
|
|
337
|
+
return "No projects configured"
|
|
338
|
+
|
|
339
|
+
projects_info = []
|
|
340
|
+
for name, project in settings.projects.items():
|
|
341
|
+
current = " (current)" if name == settings.current_project else ""
|
|
342
|
+
projects_info.append(f"- {name}: {project.root_path}{current}")
|
|
343
|
+
|
|
344
|
+
return f"Projects:\\n" + "\\n".join(projects_info)
|
|
345
|
+
|
|
346
|
+
async def _detect_project(self, project_path: Optional[str]) -> str:
|
|
347
|
+
"""Detect project from path."""
|
|
348
|
+
if not project_path:
|
|
349
|
+
import os
|
|
350
|
+
project_path = os.getcwd()
|
|
351
|
+
|
|
352
|
+
project_info = detect_project_from_path(project_path)
|
|
353
|
+
if project_info:
|
|
354
|
+
return f"Project detected:\\n{json.dumps(project_info, indent=2)}"
|
|
355
|
+
else:
|
|
356
|
+
return f"No project detected at path: {project_path}"
|
|
357
|
+
|
|
358
|
+
def _get_nested_value(self, obj: Dict[str, Any], path: str) -> Any:
|
|
359
|
+
"""Get nested value using dot notation."""
|
|
360
|
+
keys = path.split(".")
|
|
361
|
+
current = obj
|
|
362
|
+
|
|
363
|
+
for key in keys:
|
|
364
|
+
if isinstance(current, dict) and key in current:
|
|
365
|
+
current = current[key]
|
|
366
|
+
elif hasattr(current, key):
|
|
367
|
+
current = getattr(current, key)
|
|
368
|
+
else:
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
return current
|
|
372
|
+
|
|
373
|
+
def _set_nested_value(self, obj: Dict[str, Any], path: str, value: Any) -> bool:
|
|
374
|
+
"""Set nested value using dot notation."""
|
|
375
|
+
keys = path.split(".")
|
|
376
|
+
current = obj
|
|
377
|
+
|
|
378
|
+
# Navigate to parent
|
|
379
|
+
for key in keys[:-1]:
|
|
380
|
+
if isinstance(current, dict) and key in current:
|
|
381
|
+
current = current[key]
|
|
382
|
+
elif hasattr(current, key):
|
|
383
|
+
current = getattr(current, key)
|
|
384
|
+
else:
|
|
385
|
+
return False
|
|
386
|
+
|
|
387
|
+
# Set final value
|
|
388
|
+
final_key = keys[-1]
|
|
389
|
+
if isinstance(current, dict):
|
|
390
|
+
current[final_key] = value
|
|
391
|
+
return True
|
|
392
|
+
elif hasattr(current, final_key):
|
|
393
|
+
setattr(current, final_key, value)
|
|
394
|
+
return True
|
|
395
|
+
|
|
396
|
+
return False
|
|
@@ -17,6 +17,7 @@ from hanzo_mcp.tools.filesystem.grep_ast_tool import GrepAstTool
|
|
|
17
17
|
from hanzo_mcp.tools.filesystem.multi_edit import MultiEdit
|
|
18
18
|
from hanzo_mcp.tools.filesystem.read import ReadTool
|
|
19
19
|
from hanzo_mcp.tools.filesystem.write import Write
|
|
20
|
+
from hanzo_mcp.tools.filesystem.unified_search import UnifiedSearchTool
|
|
20
21
|
|
|
21
22
|
# Export all tool classes
|
|
22
23
|
__all__ = [
|
|
@@ -28,6 +29,7 @@ __all__ = [
|
|
|
28
29
|
"Grep",
|
|
29
30
|
"ContentReplaceTool",
|
|
30
31
|
"GrepAstTool",
|
|
32
|
+
"UnifiedSearchTool",
|
|
31
33
|
"get_filesystem_tools",
|
|
32
34
|
"register_filesystem_tools",
|
|
33
35
|
]
|
|
@@ -79,6 +81,7 @@ def register_filesystem_tools(
|
|
|
79
81
|
disable_write_tools: bool = False,
|
|
80
82
|
disable_search_tools: bool = False,
|
|
81
83
|
enabled_tools: dict[str, bool] | None = None,
|
|
84
|
+
project_manager=None,
|
|
82
85
|
) -> list[BaseTool]:
|
|
83
86
|
"""Register filesystem tools with the MCP server.
|
|
84
87
|
|
|
@@ -88,6 +91,7 @@ def register_filesystem_tools(
|
|
|
88
91
|
disable_write_tools: Whether to disable write tools (default: False)
|
|
89
92
|
disable_search_tools: Whether to disable search tools (default: False)
|
|
90
93
|
enabled_tools: Dictionary of individual tool enable states (default: None)
|
|
94
|
+
project_manager: Optional project manager for unified search (default: None)
|
|
91
95
|
|
|
92
96
|
Returns:
|
|
93
97
|
List of registered tools
|
|
@@ -102,6 +106,7 @@ def register_filesystem_tools(
|
|
|
102
106
|
"grep": Grep,
|
|
103
107
|
"grep_ast": GrepAstTool,
|
|
104
108
|
"content_replace": ContentReplaceTool,
|
|
109
|
+
"unified_search": UnifiedSearchTool,
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
tools = []
|
|
@@ -111,7 +116,11 @@ def register_filesystem_tools(
|
|
|
111
116
|
for tool_name, enabled in enabled_tools.items():
|
|
112
117
|
if enabled and tool_name in tool_classes:
|
|
113
118
|
tool_class = tool_classes[tool_name]
|
|
114
|
-
|
|
119
|
+
if tool_name == "unified_search":
|
|
120
|
+
# Unified search requires project_manager
|
|
121
|
+
tools.append(tool_class(permission_manager, project_manager))
|
|
122
|
+
else:
|
|
123
|
+
tools.append(tool_class(permission_manager))
|
|
115
124
|
else:
|
|
116
125
|
# Use category-level configuration (backward compatibility)
|
|
117
126
|
if disable_write_tools and disable_search_tools:
|