hanzo-mcp 0.5.1__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 (54) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/tools/__init__.py +135 -4
  3. hanzo_mcp/tools/common/base.py +7 -2
  4. hanzo_mcp/tools/common/stats.py +261 -0
  5. hanzo_mcp/tools/common/tool_disable.py +144 -0
  6. hanzo_mcp/tools/common/tool_enable.py +182 -0
  7. hanzo_mcp/tools/common/tool_list.py +263 -0
  8. hanzo_mcp/tools/database/__init__.py +71 -0
  9. hanzo_mcp/tools/database/database_manager.py +246 -0
  10. hanzo_mcp/tools/database/graph_add.py +257 -0
  11. hanzo_mcp/tools/database/graph_query.py +536 -0
  12. hanzo_mcp/tools/database/graph_remove.py +267 -0
  13. hanzo_mcp/tools/database/graph_search.py +348 -0
  14. hanzo_mcp/tools/database/graph_stats.py +345 -0
  15. hanzo_mcp/tools/database/sql_query.py +229 -0
  16. hanzo_mcp/tools/database/sql_search.py +296 -0
  17. hanzo_mcp/tools/database/sql_stats.py +254 -0
  18. hanzo_mcp/tools/editor/__init__.py +11 -0
  19. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  20. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  21. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  22. hanzo_mcp/tools/filesystem/__init__.py +15 -5
  23. hanzo_mcp/tools/filesystem/{unified_search.py → batch_search.py} +254 -131
  24. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  25. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  26. hanzo_mcp/tools/llm/__init__.py +27 -0
  27. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  28. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  29. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  30. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  31. hanzo_mcp/tools/mcp/__init__.py +11 -0
  32. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  33. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  34. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  35. hanzo_mcp/tools/shell/__init__.py +27 -7
  36. hanzo_mcp/tools/shell/logs.py +265 -0
  37. hanzo_mcp/tools/shell/npx.py +194 -0
  38. hanzo_mcp/tools/shell/npx_background.py +254 -0
  39. hanzo_mcp/tools/shell/pkill.py +262 -0
  40. hanzo_mcp/tools/shell/processes.py +279 -0
  41. hanzo_mcp/tools/shell/run_background.py +326 -0
  42. hanzo_mcp/tools/shell/uvx.py +187 -0
  43. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  44. hanzo_mcp/tools/vector/__init__.py +5 -0
  45. hanzo_mcp/tools/vector/git_ingester.py +3 -0
  46. hanzo_mcp/tools/vector/index_tool.py +358 -0
  47. hanzo_mcp/tools/vector/infinity_store.py +98 -0
  48. hanzo_mcp/tools/vector/vector_search.py +11 -6
  49. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/METADATA +1 -1
  50. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/RECORD +54 -16
  51. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/WHEEL +0 -0
  52. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/entry_points.txt +0 -0
  53. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/licenses/LICENSE +0 -0
  54. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,127 @@
1
+ """Remove MCP servers."""
2
+
3
+ from typing import Annotated, TypedDict, Unpack, final, override
4
+
5
+ from fastmcp import Context as MCPContext
6
+ from pydantic import Field
7
+
8
+ from hanzo_mcp.tools.common.base import BaseTool
9
+ from hanzo_mcp.tools.common.context import create_tool_context
10
+ from hanzo_mcp.tools.mcp.mcp_add import McpAddTool
11
+
12
+
13
+ ServerName = Annotated[
14
+ str,
15
+ Field(
16
+ description="Name of the server to remove",
17
+ min_length=1,
18
+ ),
19
+ ]
20
+
21
+ Force = Annotated[
22
+ bool,
23
+ Field(
24
+ description="Force removal even if server is running",
25
+ default=False,
26
+ ),
27
+ ]
28
+
29
+
30
+ class McpRemoveParams(TypedDict, total=False):
31
+ """Parameters for MCP remove tool."""
32
+
33
+ name: str
34
+ force: bool
35
+
36
+
37
+ @final
38
+ class McpRemoveTool(BaseTool):
39
+ """Tool for removing MCP servers."""
40
+
41
+ def __init__(self):
42
+ """Initialize the MCP remove tool."""
43
+ pass
44
+
45
+ @property
46
+ @override
47
+ def name(self) -> str:
48
+ """Get the tool name."""
49
+ return "mcp_remove"
50
+
51
+ @property
52
+ @override
53
+ def description(self) -> str:
54
+ """Get the tool description."""
55
+ return """Remove previously added MCP servers.
56
+
57
+ This removes MCP servers that were added with mcp_add.
58
+ If the server is running, it will be stopped first.
59
+
60
+ Examples:
61
+ - mcp_remove --name filesystem
62
+ - mcp_remove --name github --force
63
+
64
+ Use 'mcp_stats' to see all servers before removing.
65
+ """
66
+
67
+ @override
68
+ async def call(
69
+ self,
70
+ ctx: MCPContext,
71
+ **params: Unpack[McpRemoveParams],
72
+ ) -> str:
73
+ """Remove an MCP server.
74
+
75
+ Args:
76
+ ctx: MCP context
77
+ **params: Tool parameters
78
+
79
+ Returns:
80
+ Result of removing the server
81
+ """
82
+ tool_ctx = create_tool_context(ctx)
83
+ await tool_ctx.set_tool_info(self.name)
84
+
85
+ # Extract parameters
86
+ name = params.get("name")
87
+ if not name:
88
+ return "Error: name is required"
89
+
90
+ force = params.get("force", False)
91
+
92
+ # Get current servers
93
+ servers = McpAddTool.get_servers()
94
+
95
+ if name not in servers:
96
+ return f"Error: Server '{name}' not found. Use 'mcp_stats' to see available servers."
97
+
98
+ server = servers[name]
99
+
100
+ await tool_ctx.info(f"Removing MCP server '{name}'")
101
+
102
+ # Check if server is running
103
+ if server.get("status") == "running" and server.get("process_id"):
104
+ if not force:
105
+ return f"Error: Server '{name}' is currently running. Use --force to remove anyway."
106
+ else:
107
+ # TODO: Stop the server process
108
+ await tool_ctx.info(f"Stopping running server '{name}'")
109
+
110
+ # Remove from registry
111
+ del McpAddTool._mcp_servers[name]
112
+ McpAddTool._save_servers()
113
+
114
+ output = [
115
+ f"Successfully removed MCP server '{name}'",
116
+ f" Type: {server.get('type', 'unknown')}",
117
+ f" Command: {' '.join(server.get('command', []))}",
118
+ ]
119
+
120
+ if server.get("tools"):
121
+ output.append(f" Tools removed: {len(server['tools'])}")
122
+
123
+ return "\n".join(output)
124
+
125
+ def register(self, mcp_server) -> None:
126
+ """Register this tool with the MCP server."""
127
+ pass
@@ -0,0 +1,165 @@
1
+ """MCP server statistics."""
2
+
3
+ import json
4
+ from typing import TypedDict, Unpack, final, override
5
+ from datetime import datetime
6
+
7
+ from fastmcp import Context as MCPContext
8
+
9
+ from hanzo_mcp.tools.common.base import BaseTool
10
+ from hanzo_mcp.tools.common.context import create_tool_context
11
+ from hanzo_mcp.tools.mcp.mcp_add import McpAddTool
12
+
13
+
14
+ class McpStatsParams(TypedDict, total=False):
15
+ """Parameters for MCP stats tool."""
16
+ pass
17
+
18
+
19
+ @final
20
+ class McpStatsTool(BaseTool):
21
+ """Tool for showing MCP server statistics."""
22
+
23
+ def __init__(self):
24
+ """Initialize the MCP stats tool."""
25
+ pass
26
+
27
+ @property
28
+ @override
29
+ def name(self) -> str:
30
+ """Get the tool name."""
31
+ return "mcp_stats"
32
+
33
+ @property
34
+ @override
35
+ def description(self) -> str:
36
+ """Get the tool description."""
37
+ return """Show statistics about added MCP servers.
38
+
39
+ Displays:
40
+ - Total number of servers
41
+ - Server types (Python, Node.js)
42
+ - Server status (running, stopped, error)
43
+ - Available tools from each server
44
+ - Resource usage per server
45
+
46
+ Example:
47
+ - mcp_stats
48
+ """
49
+
50
+ @override
51
+ async def call(
52
+ self,
53
+ ctx: MCPContext,
54
+ **params: Unpack[McpStatsParams],
55
+ ) -> str:
56
+ """Get MCP server statistics.
57
+
58
+ Args:
59
+ ctx: MCP context
60
+ **params: Tool parameters
61
+
62
+ Returns:
63
+ MCP server statistics
64
+ """
65
+ tool_ctx = create_tool_context(ctx)
66
+ await tool_ctx.set_tool_info(self.name)
67
+
68
+ # Get all servers
69
+ servers = McpAddTool.get_servers()
70
+
71
+ if not servers:
72
+ return "No MCP servers have been added yet.\n\nUse 'mcp_add' to add servers."
73
+
74
+ output = []
75
+ output.append("=== MCP Server Statistics ===")
76
+ output.append(f"Total Servers: {len(servers)}")
77
+ output.append("")
78
+
79
+ # Count by type
80
+ type_counts = {}
81
+ status_counts = {}
82
+ total_tools = 0
83
+ total_resources = 0
84
+
85
+ for server in servers.values():
86
+ # Count types
87
+ server_type = server.get("type", "unknown")
88
+ type_counts[server_type] = type_counts.get(server_type, 0) + 1
89
+
90
+ # Count status
91
+ status = server.get("status", "unknown")
92
+ status_counts[status] = status_counts.get(status, 0) + 1
93
+
94
+ # Count tools and resources
95
+ total_tools += len(server.get("tools", []))
96
+ total_resources += len(server.get("resources", []))
97
+
98
+ # Server types
99
+ output.append("Server Types:")
100
+ for stype, count in sorted(type_counts.items()):
101
+ output.append(f" {stype}: {count}")
102
+ output.append("")
103
+
104
+ # Server status
105
+ output.append("Server Status:")
106
+ for status, count in sorted(status_counts.items()):
107
+ output.append(f" {status}: {count}")
108
+ output.append("")
109
+
110
+ # Tools and resources
111
+ output.append(f"Total Tools Available: {total_tools}")
112
+ output.append(f"Total Resources Available: {total_resources}")
113
+ output.append("")
114
+
115
+ # Individual server details
116
+ output.append("=== Server Details ===")
117
+
118
+ for name, server in sorted(servers.items()):
119
+ output.append(f"\n{name}:")
120
+ output.append(f" Type: {server.get('type', 'unknown')}")
121
+ output.append(f" Status: {server.get('status', 'unknown')}")
122
+ output.append(f" Command: {' '.join(server.get('command', []))}")
123
+
124
+ if server.get("process_id"):
125
+ output.append(f" Process ID: {server['process_id']}")
126
+
127
+ if server.get("error"):
128
+ output.append(f" Error: {server['error']}")
129
+
130
+ tools = server.get("tools", [])
131
+ if tools:
132
+ output.append(f" Tools ({len(tools)}):")
133
+ for tool in tools[:5]: # Show first 5
134
+ output.append(f" - {tool}")
135
+ if len(tools) > 5:
136
+ output.append(f" ... and {len(tools) - 5} more")
137
+
138
+ resources = server.get("resources", [])
139
+ if resources:
140
+ output.append(f" Resources ({len(resources)}):")
141
+ for resource in resources[:5]: # Show first 5
142
+ output.append(f" - {resource}")
143
+ if len(resources) > 5:
144
+ output.append(f" ... and {len(resources) - 5} more")
145
+
146
+ if server.get("env"):
147
+ output.append(f" Environment vars: {list(server['env'].keys())}")
148
+
149
+ # Common MCP servers hint
150
+ output.append("\n=== Available MCP Servers ===")
151
+ output.append("Common servers you can add:")
152
+ output.append(" - @modelcontextprotocol/server-filesystem")
153
+ output.append(" - @modelcontextprotocol/server-github")
154
+ output.append(" - mcp-server-git")
155
+ output.append(" - @modelcontextprotocol/server-postgres")
156
+ output.append(" - @modelcontextprotocol/server-browser-use")
157
+ output.append(" - @modelcontextprotocol/server-iterm2")
158
+ output.append(" - @modelcontextprotocol/server-linear")
159
+ output.append(" - @modelcontextprotocol/server-slack")
160
+
161
+ return "\n".join(output)
162
+
163
+ def register(self, mcp_server) -> None:
164
+ """Register this tool with the MCP server."""
165
+ pass
@@ -11,6 +11,14 @@ from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
11
11
  from hanzo_mcp.tools.common.permissions import PermissionManager
12
12
  from hanzo_mcp.tools.shell.bash_session_executor import BashSessionExecutor
13
13
  from hanzo_mcp.tools.shell.command_executor import CommandExecutor
14
+ from hanzo_mcp.tools.shell.run_background import RunBackgroundTool
15
+ from hanzo_mcp.tools.shell.processes import ProcessesTool
16
+ from hanzo_mcp.tools.shell.pkill import PkillTool
17
+ from hanzo_mcp.tools.shell.logs import LogsTool
18
+ from hanzo_mcp.tools.shell.uvx import UvxTool
19
+ from hanzo_mcp.tools.shell.uvx_background import UvxBackgroundTool
20
+ from hanzo_mcp.tools.shell.npx import NpxTool
21
+ from hanzo_mcp.tools.shell.npx_background import NpxBackgroundTool
14
22
 
15
23
  # Export all tool classes
16
24
  __all__ = [
@@ -30,23 +38,35 @@ def get_shell_tools(
30
38
  Returns:
31
39
  List of shell tool instances
32
40
  """
33
- # Detect tmux availability and choose appropriate implementation
41
+ tools = []
42
+
43
+ # Add platform-specific command tool
34
44
  if shutil.which("tmux") is not None:
35
45
  # Use tmux-based implementation for interactive sessions
36
46
  from hanzo_mcp.tools.shell.run_command import RunCommandTool
37
47
 
38
48
  command_executor = BashSessionExecutor(permission_manager)
39
- return [
40
- RunCommandTool(permission_manager, command_executor),
41
- ]
49
+ tools.append(RunCommandTool(permission_manager, command_executor))
42
50
  else:
43
51
  # Use Windows-compatible implementation
44
52
  from hanzo_mcp.tools.shell.run_command_windows import RunCommandTool
45
53
 
46
54
  command_executor = CommandExecutor(permission_manager)
47
- return [
48
- RunCommandTool(permission_manager, command_executor),
49
- ]
55
+ tools.append(RunCommandTool(permission_manager, command_executor))
56
+
57
+ # Add other shell tools
58
+ tools.extend([
59
+ RunBackgroundTool(permission_manager),
60
+ ProcessesTool(),
61
+ PkillTool(),
62
+ LogsTool(),
63
+ UvxTool(permission_manager),
64
+ UvxBackgroundTool(permission_manager),
65
+ NpxTool(permission_manager),
66
+ NpxBackgroundTool(permission_manager),
67
+ ])
68
+
69
+ return tools
50
70
 
51
71
 
52
72
  def register_shell_tools(
@@ -0,0 +1,265 @@
1
+ """Tool for viewing process logs."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Annotated, Optional, TypedDict, Unpack, final, override
6
+
7
+ from fastmcp import Context as MCPContext
8
+ from pydantic import Field
9
+
10
+ from hanzo_mcp.tools.common.base import BaseTool
11
+ from hanzo_mcp.tools.common.context import create_tool_context
12
+ from hanzo_mcp.tools.common.permissions import PermissionManager
13
+ from hanzo_mcp.tools.shell.run_background import RunBackgroundTool
14
+
15
+
16
+ ProcessId = Annotated[
17
+ Optional[str],
18
+ Field(
19
+ description="Process ID from run_background",
20
+ default=None,
21
+ ),
22
+ ]
23
+
24
+ LogFile = Annotated[
25
+ Optional[str],
26
+ Field(
27
+ description="Path to specific log file",
28
+ default=None,
29
+ ),
30
+ ]
31
+
32
+ Lines = Annotated[
33
+ int,
34
+ Field(
35
+ description="Number of lines to show (default: 50, -1 for all)",
36
+ default=50,
37
+ ),
38
+ ]
39
+
40
+ Follow = Annotated[
41
+ bool,
42
+ Field(
43
+ description="Follow log output (tail -f)",
44
+ default=False,
45
+ ),
46
+ ]
47
+
48
+ ListLogs = Annotated[
49
+ bool,
50
+ Field(
51
+ description="List all available log files",
52
+ default=False,
53
+ ),
54
+ ]
55
+
56
+
57
+ class LogsParams(TypedDict, total=False):
58
+ """Parameters for viewing logs."""
59
+
60
+ id: Optional[str]
61
+ file: Optional[str]
62
+ lines: int
63
+ follow: bool
64
+ list: bool
65
+
66
+
67
+ @final
68
+ class LogsTool(BaseTool):
69
+ """Tool for viewing process logs."""
70
+
71
+ def __init__(self, permission_manager: PermissionManager):
72
+ """Initialize the logs tool.
73
+
74
+ Args:
75
+ permission_manager: Permission manager for access control
76
+ """
77
+ self.permission_manager = permission_manager
78
+ self.log_dir = Path.home() / ".hanzo" / "logs"
79
+
80
+ @property
81
+ @override
82
+ def name(self) -> str:
83
+ """Get the tool name."""
84
+ return "logs"
85
+
86
+ @property
87
+ @override
88
+ def description(self) -> str:
89
+ """Get the tool description."""
90
+ return """View logs from background processes.
91
+
92
+ Options:
93
+ - id: Process ID to view logs for
94
+ - file: Specific log file path
95
+ - lines: Number of lines to show (default: 50, -1 for all)
96
+ - list: List all available log files
97
+
98
+ Examples:
99
+ - logs --id abc123 # View logs for specific process
100
+ - logs --id abc123 --lines 100 # View last 100 lines
101
+ - logs --id abc123 --lines -1 # View entire log
102
+ - logs --list # List all log files
103
+ - logs --file /path/to/log # View specific log file
104
+
105
+ Note: Follow mode (--follow) is not supported in MCP context.
106
+ Use run_command with 'tail -f' for continuous monitoring.
107
+ """
108
+
109
+ @override
110
+ async def call(
111
+ self,
112
+ ctx: MCPContext,
113
+ **params: Unpack[LogsParams],
114
+ ) -> str:
115
+ """View process logs.
116
+
117
+ Args:
118
+ ctx: MCP context
119
+ **params: Tool parameters
120
+
121
+ Returns:
122
+ Log content or list of logs
123
+ """
124
+ tool_ctx = create_tool_context(ctx)
125
+ await tool_ctx.set_tool_info(self.name)
126
+
127
+ # Extract parameters
128
+ process_id = params.get("id")
129
+ log_file = params.get("file")
130
+ lines = params.get("lines", 50)
131
+ follow = params.get("follow", False)
132
+ list_logs = params.get("list", False)
133
+
134
+ try:
135
+ # List available logs
136
+ if list_logs:
137
+ return await self._list_logs(tool_ctx)
138
+
139
+ # Determine log file to read
140
+ if process_id:
141
+ # Find log file for process ID
142
+ process = RunBackgroundTool.get_process(process_id)
143
+ if not process:
144
+ return f"Process with ID '{process_id}' not found."
145
+
146
+ if not process.log_file:
147
+ return f"Process '{process_id}' does not have logging enabled."
148
+
149
+ log_path = process.log_file
150
+
151
+ elif log_file:
152
+ # Use specified log file
153
+ log_path = Path(log_file)
154
+
155
+ # Check if it's in the logs directory
156
+ if not log_path.is_absolute():
157
+ log_path = self.log_dir / log_path
158
+
159
+ else:
160
+ return "Error: Must specify --id or --file"
161
+
162
+ # Check permissions
163
+ if not self.permission_manager.has_permission(str(log_path)):
164
+ return f"Permission denied: {log_path}"
165
+
166
+ # Check if file exists
167
+ if not log_path.exists():
168
+ return f"Log file not found: {log_path}"
169
+
170
+ # Note about follow mode
171
+ if follow:
172
+ await tool_ctx.warning("Follow mode not supported in MCP. Showing latest lines instead.")
173
+
174
+ # Read log file
175
+ await tool_ctx.info(f"Reading log file: {log_path}")
176
+
177
+ try:
178
+ with open(log_path, 'r') as f:
179
+ if lines == -1:
180
+ # Read entire file
181
+ content = f.read()
182
+ else:
183
+ # Read last N lines
184
+ all_lines = f.readlines()
185
+ if len(all_lines) <= lines:
186
+ content = ''.join(all_lines)
187
+ else:
188
+ content = ''.join(all_lines[-lines:])
189
+
190
+ if not content:
191
+ return f"Log file is empty: {log_path}"
192
+
193
+ # Add header
194
+ header = f"=== Log: {log_path.name} ===\n"
195
+ if process_id:
196
+ process = RunBackgroundTool.get_process(process_id)
197
+ if process:
198
+ header += f"Process: {process.name} (ID: {process_id})\n"
199
+ header += f"Command: {process.command}\n"
200
+ status = "running" if process.is_running else f"finished (code: {process.return_code})"
201
+ header += f"Status: {status}\n"
202
+ header += f"{'=' * 50}\n"
203
+
204
+ return header + content
205
+
206
+ except Exception as e:
207
+ return f"Error reading log file: {str(e)}"
208
+
209
+ except Exception as e:
210
+ await tool_ctx.error(f"Failed to view logs: {str(e)}")
211
+ return f"Error viewing logs: {str(e)}"
212
+
213
+ async def _list_logs(self, tool_ctx) -> str:
214
+ """List all available log files."""
215
+ await tool_ctx.info("Listing available log files")
216
+
217
+ if not self.log_dir.exists():
218
+ return "No logs directory found."
219
+
220
+ # Get all log files
221
+ log_files = list(self.log_dir.glob("*.log"))
222
+
223
+ if not log_files:
224
+ return "No log files found."
225
+
226
+ # Sort by modification time (newest first)
227
+ log_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
228
+
229
+ # Check which logs belong to active processes
230
+ active_processes = RunBackgroundTool.get_processes()
231
+ active_log_files = {str(p.log_file): (pid, p) for pid, p in active_processes.items() if p.log_file}
232
+
233
+ # Build output
234
+ output = []
235
+ output.append("=== Available Log Files ===\n")
236
+
237
+ for log_file in log_files[:50]: # Limit to 50 most recent
238
+ size = log_file.stat().st_size
239
+ size_str = self._format_size(size)
240
+
241
+ # Check if this belongs to an active process
242
+ if str(log_file) in active_log_files:
243
+ pid, process = active_log_files[str(log_file)]
244
+ status = "active" if process.is_running else "finished"
245
+ output.append(f"{log_file.name:<50} {size_str:>10} [{status}] (ID: {pid})")
246
+ else:
247
+ output.append(f"{log_file.name:<50} {size_str:>10}")
248
+
249
+ output.append(f"\nTotal: {len(log_files)} log file(s)")
250
+ output.append("\nUse 'logs --file <filename>' to view a specific log")
251
+ output.append("Use 'logs --id <process-id>' to view logs for a running process")
252
+
253
+ return "\n".join(output)
254
+
255
+ def _format_size(self, size: int) -> str:
256
+ """Format file size in human-readable format."""
257
+ for unit in ['B', 'KB', 'MB', 'GB']:
258
+ if size < 1024.0:
259
+ return f"{size:.1f} {unit}"
260
+ size /= 1024.0
261
+ return f"{size:.1f} TB"
262
+
263
+ def register(self, mcp_server) -> None:
264
+ """Register this tool with the MCP server."""
265
+ pass