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.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/config/settings.py +61 -0
- hanzo_mcp/tools/__init__.py +158 -12
- hanzo_mcp/tools/common/base.py +7 -2
- hanzo_mcp/tools/common/config_tool.py +396 -0
- hanzo_mcp/tools/common/stats.py +261 -0
- hanzo_mcp/tools/common/tool_disable.py +144 -0
- hanzo_mcp/tools/common/tool_enable.py +182 -0
- hanzo_mcp/tools/common/tool_list.py +263 -0
- hanzo_mcp/tools/database/__init__.py +71 -0
- hanzo_mcp/tools/database/database_manager.py +246 -0
- hanzo_mcp/tools/database/graph_add.py +257 -0
- hanzo_mcp/tools/database/graph_query.py +536 -0
- hanzo_mcp/tools/database/graph_remove.py +267 -0
- hanzo_mcp/tools/database/graph_search.py +348 -0
- hanzo_mcp/tools/database/graph_stats.py +345 -0
- hanzo_mcp/tools/database/sql_query.py +229 -0
- hanzo_mcp/tools/database/sql_search.py +296 -0
- hanzo_mcp/tools/database/sql_stats.py +254 -0
- hanzo_mcp/tools/editor/__init__.py +11 -0
- hanzo_mcp/tools/editor/neovim_command.py +272 -0
- hanzo_mcp/tools/editor/neovim_edit.py +290 -0
- hanzo_mcp/tools/editor/neovim_session.py +356 -0
- hanzo_mcp/tools/filesystem/__init__.py +20 -1
- hanzo_mcp/tools/filesystem/batch_search.py +812 -0
- hanzo_mcp/tools/filesystem/find_files.py +348 -0
- hanzo_mcp/tools/filesystem/git_search.py +505 -0
- hanzo_mcp/tools/llm/__init__.py +27 -0
- hanzo_mcp/tools/llm/consensus_tool.py +351 -0
- hanzo_mcp/tools/llm/llm_manage.py +413 -0
- hanzo_mcp/tools/llm/llm_tool.py +346 -0
- hanzo_mcp/tools/llm/provider_tools.py +412 -0
- hanzo_mcp/tools/mcp/__init__.py +11 -0
- hanzo_mcp/tools/mcp/mcp_add.py +263 -0
- hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
- hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
- hanzo_mcp/tools/shell/__init__.py +27 -7
- hanzo_mcp/tools/shell/logs.py +265 -0
- hanzo_mcp/tools/shell/npx.py +194 -0
- hanzo_mcp/tools/shell/npx_background.py +254 -0
- hanzo_mcp/tools/shell/pkill.py +262 -0
- hanzo_mcp/tools/shell/processes.py +279 -0
- hanzo_mcp/tools/shell/run_background.py +326 -0
- hanzo_mcp/tools/shell/uvx.py +187 -0
- hanzo_mcp/tools/shell/uvx_background.py +249 -0
- hanzo_mcp/tools/vector/__init__.py +21 -12
- hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
- hanzo_mcp/tools/vector/git_ingester.py +485 -0
- hanzo_mcp/tools/vector/index_tool.py +358 -0
- hanzo_mcp/tools/vector/infinity_store.py +465 -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 +22 -7
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/METADATA +68 -20
- hanzo_mcp-0.5.2.dist-info/RECORD +106 -0
- hanzo_mcp-0.5.0.dist-info/RECORD +0 -63
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.5.0.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|