hanzo-mcp 0.5.0__py3-none-any.whl → 0.5.2__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.

Files changed (60) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/config/settings.py +61 -0
  3. hanzo_mcp/tools/__init__.py +158 -12
  4. hanzo_mcp/tools/common/base.py +7 -2
  5. hanzo_mcp/tools/common/config_tool.py +396 -0
  6. hanzo_mcp/tools/common/stats.py +261 -0
  7. hanzo_mcp/tools/common/tool_disable.py +144 -0
  8. hanzo_mcp/tools/common/tool_enable.py +182 -0
  9. hanzo_mcp/tools/common/tool_list.py +263 -0
  10. hanzo_mcp/tools/database/__init__.py +71 -0
  11. hanzo_mcp/tools/database/database_manager.py +246 -0
  12. hanzo_mcp/tools/database/graph_add.py +257 -0
  13. hanzo_mcp/tools/database/graph_query.py +536 -0
  14. hanzo_mcp/tools/database/graph_remove.py +267 -0
  15. hanzo_mcp/tools/database/graph_search.py +348 -0
  16. hanzo_mcp/tools/database/graph_stats.py +345 -0
  17. hanzo_mcp/tools/database/sql_query.py +229 -0
  18. hanzo_mcp/tools/database/sql_search.py +296 -0
  19. hanzo_mcp/tools/database/sql_stats.py +254 -0
  20. hanzo_mcp/tools/editor/__init__.py +11 -0
  21. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  22. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  23. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  24. hanzo_mcp/tools/filesystem/__init__.py +20 -1
  25. hanzo_mcp/tools/filesystem/batch_search.py +812 -0
  26. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  27. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  28. hanzo_mcp/tools/llm/__init__.py +27 -0
  29. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  30. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  31. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  32. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  33. hanzo_mcp/tools/mcp/__init__.py +11 -0
  34. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  35. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  36. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  37. hanzo_mcp/tools/shell/__init__.py +27 -7
  38. hanzo_mcp/tools/shell/logs.py +265 -0
  39. hanzo_mcp/tools/shell/npx.py +194 -0
  40. hanzo_mcp/tools/shell/npx_background.py +254 -0
  41. hanzo_mcp/tools/shell/pkill.py +262 -0
  42. hanzo_mcp/tools/shell/processes.py +279 -0
  43. hanzo_mcp/tools/shell/run_background.py +326 -0
  44. hanzo_mcp/tools/shell/uvx.py +187 -0
  45. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  46. hanzo_mcp/tools/vector/__init__.py +21 -12
  47. hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
  48. hanzo_mcp/tools/vector/git_ingester.py +485 -0
  49. hanzo_mcp/tools/vector/index_tool.py +358 -0
  50. hanzo_mcp/tools/vector/infinity_store.py +465 -1
  51. hanzo_mcp/tools/vector/mock_infinity.py +162 -0
  52. hanzo_mcp/tools/vector/vector_index.py +7 -6
  53. hanzo_mcp/tools/vector/vector_search.py +22 -7
  54. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/METADATA +68 -20
  55. hanzo_mcp-0.5.2.dist-info/RECORD +106 -0
  56. hanzo_mcp-0.5.0.dist-info/RECORD +0 -63
  57. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/WHEEL +0 -0
  58. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/entry_points.txt +0 -0
  59. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/licenses/LICENSE +0 -0
  60. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/top_level.txt +0 -0
@@ -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
@@ -0,0 +1,261 @@
1
+ """Comprehensive system and MCP statistics."""
2
+
3
+ import os
4
+ import psutil
5
+ import shutil
6
+ from typing import TypedDict, Unpack, final, override
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ from fastmcp import Context as MCPContext
11
+
12
+ from hanzo_mcp.tools.common.base import BaseTool
13
+ from hanzo_mcp.tools.common.context import create_tool_context
14
+ from hanzo_mcp.tools.shell.run_background import RunBackgroundTool
15
+ from hanzo_mcp.tools.mcp.mcp_add import McpAddTool
16
+ from hanzo_mcp.tools.database.database_manager import DatabaseManager
17
+
18
+
19
+ class StatsParams(TypedDict, total=False):
20
+ """Parameters for stats tool."""
21
+ pass
22
+
23
+
24
+ @final
25
+ class StatsTool(BaseTool):
26
+ """Tool for showing comprehensive system and MCP statistics."""
27
+
28
+ def __init__(self, db_manager: DatabaseManager = None):
29
+ """Initialize the stats tool.
30
+
31
+ Args:
32
+ db_manager: Optional database manager for DB stats
33
+ """
34
+ self.db_manager = db_manager
35
+
36
+ @property
37
+ @override
38
+ def name(self) -> str:
39
+ """Get the tool name."""
40
+ return "stats"
41
+
42
+ @property
43
+ @override
44
+ def description(self) -> str:
45
+ """Get the tool description."""
46
+ return """Show comprehensive system and Hanzo MCP statistics.
47
+
48
+ Displays:
49
+ - System resources (CPU, memory, disk)
50
+ - Running processes
51
+ - Database usage
52
+ - MCP server status
53
+ - Tool usage statistics
54
+ - Warnings for high resource usage
55
+
56
+ Example:
57
+ - stats
58
+ """
59
+
60
+ @override
61
+ async def call(
62
+ self,
63
+ ctx: MCPContext,
64
+ **params: Unpack[StatsParams],
65
+ ) -> str:
66
+ """Get comprehensive statistics.
67
+
68
+ Args:
69
+ ctx: MCP context
70
+ **params: Tool parameters
71
+
72
+ Returns:
73
+ Comprehensive statistics
74
+ """
75
+ tool_ctx = create_tool_context(ctx)
76
+ await tool_ctx.set_tool_info(self.name)
77
+
78
+ output = []
79
+ warnings = []
80
+
81
+ # Header
82
+ output.append("=== Hanzo MCP System Statistics ===")
83
+ output.append(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
84
+ output.append("")
85
+
86
+ # System Resources
87
+ output.append("=== System Resources ===")
88
+
89
+ # CPU
90
+ cpu_percent = psutil.cpu_percent(interval=1)
91
+ cpu_count = psutil.cpu_count()
92
+ output.append(f"CPU Usage: {cpu_percent}% ({cpu_count} cores)")
93
+ if cpu_percent > 90:
94
+ warnings.append(f"⚠️ HIGH CPU USAGE: {cpu_percent}%")
95
+
96
+ # Memory
97
+ memory = psutil.virtual_memory()
98
+ memory_used_gb = memory.used / (1024**3)
99
+ memory_total_gb = memory.total / (1024**3)
100
+ memory_percent = memory.percent
101
+ output.append(f"Memory: {memory_used_gb:.1f}/{memory_total_gb:.1f} GB ({memory_percent}%)")
102
+ if memory_percent > 90:
103
+ warnings.append(f"⚠️ HIGH MEMORY USAGE: {memory_percent}%")
104
+
105
+ # Disk
106
+ disk = psutil.disk_usage('/')
107
+ disk_used_gb = disk.used / (1024**3)
108
+ disk_total_gb = disk.total / (1024**3)
109
+ disk_percent = disk.percent
110
+ disk_free_gb = disk.free / (1024**3)
111
+ output.append(f"Disk: {disk_used_gb:.1f}/{disk_total_gb:.1f} GB ({disk_percent}%)")
112
+ output.append(f"Free Space: {disk_free_gb:.1f} GB")
113
+ if disk_percent > 90:
114
+ warnings.append(f"⚠️ LOW DISK SPACE: Only {disk_free_gb:.1f} GB free ({100-disk_percent:.1f}% remaining)")
115
+
116
+ output.append("")
117
+
118
+ # Background Processes
119
+ output.append("=== Background Processes ===")
120
+ processes = RunBackgroundTool.get_processes()
121
+ running_count = 0
122
+ total_memory_mb = 0
123
+
124
+ if processes:
125
+ for proc in processes.values():
126
+ if proc.is_running():
127
+ running_count += 1
128
+ try:
129
+ ps_proc = psutil.Process(proc.process.pid)
130
+ memory_mb = ps_proc.memory_info().rss / (1024**2)
131
+ total_memory_mb += memory_mb
132
+ except:
133
+ pass
134
+
135
+ output.append(f"Running Processes: {running_count}")
136
+ output.append(f"Total Memory Usage: {total_memory_mb:.1f} MB")
137
+
138
+ # List top processes by memory
139
+ if running_count > 0:
140
+ output.append("\nTop Processes:")
141
+ proc_list = []
142
+ for proc_id, proc in processes.items():
143
+ if proc.is_running():
144
+ try:
145
+ ps_proc = psutil.Process(proc.process.pid)
146
+ memory_mb = ps_proc.memory_info().rss / (1024**2)
147
+ cpu = ps_proc.cpu_percent(interval=0.1)
148
+ proc_list.append((proc.name, memory_mb, cpu, proc_id))
149
+ except:
150
+ proc_list.append((proc.name, 0, 0, proc_id))
151
+
152
+ proc_list.sort(key=lambda x: x[1], reverse=True)
153
+ for name, mem, cpu, pid in proc_list[:5]:
154
+ output.append(f" - {name} ({pid}): {mem:.1f} MB, {cpu:.1f}% CPU")
155
+ else:
156
+ output.append("No background processes running")
157
+
158
+ output.append("")
159
+
160
+ # Database Usage
161
+ if self.db_manager:
162
+ output.append("=== Database Usage ===")
163
+ db_dir = Path.home() / ".hanzo" / "db"
164
+ total_db_size = 0
165
+
166
+ if db_dir.exists():
167
+ for db_file in db_dir.rglob("*.db"):
168
+ size = db_file.stat().st_size
169
+ total_db_size += size
170
+
171
+ output.append(f"Total Database Size: {total_db_size / (1024**2):.1f} MB")
172
+ output.append(f"Active Projects: {len(self.db_manager.projects)}")
173
+
174
+ # List largest databases
175
+ db_sizes = []
176
+ for db_file in db_dir.rglob("*.db"):
177
+ size = db_file.stat().st_size / (1024**2)
178
+ if size > 0.1: # Only show DBs > 100KB
179
+ project = db_file.parent.parent.name
180
+ db_type = db_file.stem
181
+ db_sizes.append((project, db_type, size))
182
+
183
+ if db_sizes:
184
+ db_sizes.sort(key=lambda x: x[2], reverse=True)
185
+ output.append("\nLargest Databases:")
186
+ for project, db_type, size in db_sizes[:5]:
187
+ output.append(f" - {project}/{db_type}: {size:.1f} MB")
188
+ else:
189
+ output.append("No databases found")
190
+
191
+ output.append("")
192
+
193
+ # MCP Servers
194
+ output.append("=== MCP Servers ===")
195
+ mcp_servers = McpAddTool.get_servers()
196
+ if mcp_servers:
197
+ running_mcp = sum(1 for s in mcp_servers.values() if s.get("status") == "running")
198
+ total_mcp_tools = sum(len(s.get("tools", [])) for s in mcp_servers.values())
199
+
200
+ output.append(f"Total Servers: {len(mcp_servers)}")
201
+ output.append(f"Running: {running_mcp}")
202
+ output.append(f"Total Tools Available: {total_mcp_tools}")
203
+ else:
204
+ output.append("No MCP servers configured")
205
+
206
+ output.append("")
207
+
208
+ # Hanzo MCP Specifics
209
+ output.append("=== Hanzo MCP ===")
210
+
211
+ # Log directory size
212
+ log_dir = Path.home() / ".hanzo" / "logs"
213
+ if log_dir.exists():
214
+ log_size = sum(f.stat().st_size for f in log_dir.rglob("*") if f.is_file())
215
+ log_count = len(list(log_dir.rglob("*.log")))
216
+ output.append(f"Log Files: {log_count} ({log_size / (1024**2):.1f} MB)")
217
+
218
+ if log_size > 100 * 1024**2: # > 100MB
219
+ warnings.append(f"⚠️ Large log directory: {log_size / (1024**2):.1f} MB")
220
+
221
+ # Config directory
222
+ config_dir = Path.home() / ".hanzo" / "mcp"
223
+ if config_dir.exists():
224
+ config_count = len(list(config_dir.rglob("*.json")))
225
+ output.append(f"Config Files: {config_count}")
226
+
227
+ # Tool status (if available)
228
+ # TODO: Track tool usage statistics
229
+ output.append("\nTool Categories:")
230
+ output.append(" - File Operations: grep, find_files, read, write, edit")
231
+ output.append(" - Shell: bash, run_background, processes, pkill")
232
+ output.append(" - Database: sql_query, graph_query, vector_search")
233
+ output.append(" - Package Runners: uvx, npx, uvx_background, npx_background")
234
+ output.append(" - MCP Management: mcp_add, mcp_remove, mcp_stats")
235
+
236
+ # Warnings Section
237
+ if warnings:
238
+ output.append("\n=== ⚠️ WARNINGS ===")
239
+ for warning in warnings:
240
+ output.append(warning)
241
+ output.append("")
242
+
243
+ # Recommendations
244
+ output.append("=== Recommendations ===")
245
+ if disk_free_gb < 5:
246
+ output.append("- Free up disk space (< 5GB remaining)")
247
+ if memory_percent > 80:
248
+ output.append("- Close unused applications to free memory")
249
+ if running_count > 10:
250
+ output.append("- Consider stopping unused background processes")
251
+ if log_size > 50 * 1024**2:
252
+ output.append("- Clean up old log files in ~/.hanzo/logs")
253
+
254
+ if not any([disk_free_gb < 5, memory_percent > 80, running_count > 10, log_size > 50 * 1024**2]):
255
+ output.append("✅ System resources are healthy")
256
+
257
+ return "\n".join(output)
258
+
259
+ def register(self, mcp_server) -> None:
260
+ """Register this tool with the MCP server."""
261
+ pass