hanzo-mcp 0.3.8__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/cli.py +118 -170
- hanzo_mcp/cli_enhanced.py +438 -0
- hanzo_mcp/config/__init__.py +19 -0
- hanzo_mcp/config/settings.py +449 -0
- hanzo_mcp/config/tool_config.py +197 -0
- hanzo_mcp/prompts/__init__.py +117 -0
- hanzo_mcp/prompts/compact_conversation.py +77 -0
- hanzo_mcp/prompts/create_release.py +38 -0
- hanzo_mcp/prompts/project_system.py +120 -0
- hanzo_mcp/prompts/project_todo_reminder.py +111 -0
- hanzo_mcp/prompts/utils.py +286 -0
- hanzo_mcp/server.py +117 -99
- hanzo_mcp/tools/__init__.py +121 -33
- hanzo_mcp/tools/agent/__init__.py +8 -11
- hanzo_mcp/tools/agent/agent_tool.py +290 -224
- hanzo_mcp/tools/agent/prompt.py +16 -13
- hanzo_mcp/tools/agent/tool_adapter.py +9 -9
- hanzo_mcp/tools/common/__init__.py +17 -16
- hanzo_mcp/tools/common/base.py +79 -110
- hanzo_mcp/tools/common/batch_tool.py +330 -0
- hanzo_mcp/tools/common/config_tool.py +396 -0
- hanzo_mcp/tools/common/context.py +26 -292
- hanzo_mcp/tools/common/permissions.py +12 -12
- hanzo_mcp/tools/common/thinking_tool.py +153 -0
- hanzo_mcp/tools/common/validation.py +1 -63
- hanzo_mcp/tools/filesystem/__init__.py +97 -57
- hanzo_mcp/tools/filesystem/base.py +32 -24
- hanzo_mcp/tools/filesystem/content_replace.py +114 -107
- hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
- hanzo_mcp/tools/filesystem/edit.py +279 -0
- hanzo_mcp/tools/filesystem/grep.py +458 -0
- hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
- hanzo_mcp/tools/filesystem/read.py +255 -0
- hanzo_mcp/tools/filesystem/unified_search.py +689 -0
- hanzo_mcp/tools/filesystem/write.py +156 -0
- hanzo_mcp/tools/jupyter/__init__.py +41 -29
- hanzo_mcp/tools/jupyter/base.py +66 -57
- hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
- hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
- hanzo_mcp/tools/shell/__init__.py +29 -20
- hanzo_mcp/tools/shell/base.py +87 -45
- hanzo_mcp/tools/shell/bash_session.py +731 -0
- hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
- hanzo_mcp/tools/shell/command_executor.py +435 -384
- hanzo_mcp/tools/shell/run_command.py +284 -131
- hanzo_mcp/tools/shell/run_command_windows.py +328 -0
- hanzo_mcp/tools/shell/session_manager.py +196 -0
- hanzo_mcp/tools/shell/session_storage.py +325 -0
- hanzo_mcp/tools/todo/__init__.py +66 -0
- hanzo_mcp/tools/todo/base.py +319 -0
- hanzo_mcp/tools/todo/todo_read.py +148 -0
- hanzo_mcp/tools/todo/todo_write.py +378 -0
- hanzo_mcp/tools/vector/__init__.py +99 -0
- 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 +731 -0
- hanzo_mcp/tools/vector/mock_infinity.py +162 -0
- hanzo_mcp/tools/vector/project_manager.py +361 -0
- hanzo_mcp/tools/vector/vector_index.py +116 -0
- hanzo_mcp/tools/vector/vector_search.py +225 -0
- hanzo_mcp-0.5.1.dist-info/METADATA +276 -0
- hanzo_mcp-0.5.1.dist-info/RECORD +68 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/WHEEL +1 -1
- hanzo_mcp/tools/agent/base_provider.py +0 -73
- hanzo_mcp/tools/agent/litellm_provider.py +0 -45
- hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
- hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
- hanzo_mcp/tools/agent/provider_registry.py +0 -120
- hanzo_mcp/tools/common/error_handling.py +0 -86
- hanzo_mcp/tools/common/logging_config.py +0 -115
- hanzo_mcp/tools/common/session.py +0 -91
- hanzo_mcp/tools/common/think_tool.py +0 -123
- hanzo_mcp/tools/common/version_tool.py +0 -120
- hanzo_mcp/tools/filesystem/edit_file.py +0 -287
- hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
- hanzo_mcp/tools/filesystem/read_files.py +0 -199
- hanzo_mcp/tools/filesystem/search_content.py +0 -275
- hanzo_mcp/tools/filesystem/write_file.py +0 -162
- hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
- hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
- hanzo_mcp/tools/project/__init__.py +0 -64
- hanzo_mcp/tools/project/analysis.py +0 -886
- hanzo_mcp/tools/project/base.py +0 -66
- hanzo_mcp/tools/project/project_analyze.py +0 -173
- hanzo_mcp/tools/shell/run_script.py +0 -215
- hanzo_mcp/tools/shell/script_tool.py +0 -244
- hanzo_mcp-0.3.8.dist-info/METADATA +0 -196
- hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from git import Repo
|
|
6
|
+
|
|
7
|
+
GIT_AVAILABLE = True
|
|
8
|
+
except ImportError:
|
|
9
|
+
GIT_AVAILABLE = False
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_os_info() -> tuple[str, str, str]:
|
|
13
|
+
"""Get the operating system information.
|
|
14
|
+
Returns:
|
|
15
|
+
tuple: A tuple containing the system name, release, and version.
|
|
16
|
+
"""
|
|
17
|
+
system = platform.system() # noqa: F821
|
|
18
|
+
release = platform.release()
|
|
19
|
+
version = platform.version()
|
|
20
|
+
|
|
21
|
+
if system == "Darwin":
|
|
22
|
+
system = "MacOS"
|
|
23
|
+
elif system == "Linux":
|
|
24
|
+
try:
|
|
25
|
+
with open("/etc/os-release") as f:
|
|
26
|
+
for line in f:
|
|
27
|
+
if line.startswith("NAME="):
|
|
28
|
+
name = line.split("=")[1].strip().strip('"')
|
|
29
|
+
if "Ubuntu" in name:
|
|
30
|
+
system = "Ubuntu"
|
|
31
|
+
elif "Debian" in name:
|
|
32
|
+
system = "Debian"
|
|
33
|
+
elif "Fedora" in name:
|
|
34
|
+
system = "Fedora"
|
|
35
|
+
elif "CentOS" in name:
|
|
36
|
+
system = "CentOS"
|
|
37
|
+
elif "Arch Linux" in name:
|
|
38
|
+
system = "Arch Linux"
|
|
39
|
+
system = name
|
|
40
|
+
except FileNotFoundError:
|
|
41
|
+
dist = platform.freedesktop_os_release()
|
|
42
|
+
if dist and "NAME" in dist:
|
|
43
|
+
name = dist["NAME"]
|
|
44
|
+
if "Ubuntu" in name:
|
|
45
|
+
system = "Ubuntu"
|
|
46
|
+
system = name
|
|
47
|
+
system = "Linux"
|
|
48
|
+
elif system == "Java":
|
|
49
|
+
system = "Java"
|
|
50
|
+
|
|
51
|
+
return system, release, version
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_directory_structure(
|
|
55
|
+
path: str, max_depth: int = 3, include_filtered: bool = False
|
|
56
|
+
) -> str:
|
|
57
|
+
"""Get a directory structure similar to directory_tree tool.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
path: The directory path to scan
|
|
61
|
+
max_depth: Maximum depth to traverse (0 for unlimited)
|
|
62
|
+
include_filtered: Whether to include normally filtered directories
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Formatted directory structure as a string
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
dir_path = Path(path)
|
|
69
|
+
|
|
70
|
+
if not dir_path.exists() or not dir_path.is_dir():
|
|
71
|
+
return f"Error: {path} is not a valid directory"
|
|
72
|
+
|
|
73
|
+
# Define filtered directories (same as directory_tree.py)
|
|
74
|
+
FILTERED_DIRECTORIES = {
|
|
75
|
+
".git",
|
|
76
|
+
"node_modules",
|
|
77
|
+
".venv",
|
|
78
|
+
"venv",
|
|
79
|
+
"__pycache__",
|
|
80
|
+
".pytest_cache",
|
|
81
|
+
".idea",
|
|
82
|
+
".vs",
|
|
83
|
+
".vscode",
|
|
84
|
+
"dist",
|
|
85
|
+
"build",
|
|
86
|
+
"target",
|
|
87
|
+
".ruff_cache",
|
|
88
|
+
".llm-context",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
def should_filter(current_path: Path) -> bool:
|
|
92
|
+
"""Check if a directory should be filtered."""
|
|
93
|
+
# Don't filter if it's the explicitly requested path
|
|
94
|
+
if str(current_path.absolute()) == str(dir_path.absolute()):
|
|
95
|
+
return False
|
|
96
|
+
# Filter based on directory name if filtering is enabled
|
|
97
|
+
return current_path.name in FILTERED_DIRECTORIES and not include_filtered
|
|
98
|
+
|
|
99
|
+
def build_tree(current_path: Path, current_depth: int = 0) -> list[dict]:
|
|
100
|
+
"""Build directory tree recursively."""
|
|
101
|
+
result = []
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
# Sort entries: directories first, then files alphabetically
|
|
105
|
+
entries = sorted(
|
|
106
|
+
current_path.iterdir(), key=lambda x: (not x.is_dir(), x.name)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
for entry in entries:
|
|
110
|
+
if entry.is_dir():
|
|
111
|
+
entry_data = {"name": entry.name, "type": "directory"}
|
|
112
|
+
|
|
113
|
+
# Check if we should filter this directory
|
|
114
|
+
if should_filter(entry):
|
|
115
|
+
entry_data["skipped"] = "filtered-directory"
|
|
116
|
+
result.append(entry_data)
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Check depth limit (if enabled)
|
|
120
|
+
if max_depth > 0 and current_depth >= max_depth:
|
|
121
|
+
entry_data["skipped"] = "depth-limit"
|
|
122
|
+
result.append(entry_data)
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# Process children recursively
|
|
126
|
+
entry_data["children"] = build_tree(entry, current_depth + 1)
|
|
127
|
+
result.append(entry_data)
|
|
128
|
+
else:
|
|
129
|
+
# Add files only if within depth limit
|
|
130
|
+
if max_depth <= 0 or current_depth < max_depth:
|
|
131
|
+
result.append({"name": entry.name, "type": "file"})
|
|
132
|
+
|
|
133
|
+
except Exception:
|
|
134
|
+
# Skip directories we can't read
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
def format_tree(tree_data: list[dict], level: int = 0) -> list[str]:
|
|
140
|
+
"""Format tree data as indented strings."""
|
|
141
|
+
lines = []
|
|
142
|
+
|
|
143
|
+
for item in tree_data:
|
|
144
|
+
# Indentation based on level
|
|
145
|
+
indent = " " * level
|
|
146
|
+
|
|
147
|
+
# Format based on type
|
|
148
|
+
if item["type"] == "directory":
|
|
149
|
+
if "skipped" in item:
|
|
150
|
+
lines.append(
|
|
151
|
+
f"{indent}{item['name']}/ [skipped - {item['skipped']}]"
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
lines.append(f"{indent}{item['name']}/")
|
|
155
|
+
# Add children with increased indentation if present
|
|
156
|
+
if "children" in item:
|
|
157
|
+
lines.extend(format_tree(item["children"], level + 1))
|
|
158
|
+
else:
|
|
159
|
+
# File
|
|
160
|
+
lines.append(f"{indent}{item['name']}")
|
|
161
|
+
|
|
162
|
+
return lines
|
|
163
|
+
|
|
164
|
+
# Build and format the tree
|
|
165
|
+
tree_data = build_tree(dir_path)
|
|
166
|
+
formatted_lines = format_tree(tree_data)
|
|
167
|
+
|
|
168
|
+
# Add the root directory path as a prefix
|
|
169
|
+
result = f"- {dir_path}/"
|
|
170
|
+
if formatted_lines:
|
|
171
|
+
result += "\n" + "\n".join(f" {line}" for line in formatted_lines)
|
|
172
|
+
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
return f"Error generating directory structure: {str(e)}"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_git_info(path: str) -> dict[str, str | None]:
|
|
180
|
+
"""Get git information for a repository.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
path: Path to the git repository
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Dictionary containing git information
|
|
187
|
+
"""
|
|
188
|
+
if not GIT_AVAILABLE:
|
|
189
|
+
return {
|
|
190
|
+
"current_branch": None,
|
|
191
|
+
"main_branch": None,
|
|
192
|
+
"git_status": "GitPython not available",
|
|
193
|
+
"recent_commits": "GitPython not available",
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
repo = Repo(path)
|
|
198
|
+
|
|
199
|
+
# Get current branch
|
|
200
|
+
try:
|
|
201
|
+
current_branch = repo.active_branch.name
|
|
202
|
+
except Exception:
|
|
203
|
+
current_branch = "HEAD (detached)"
|
|
204
|
+
|
|
205
|
+
# Try to determine main branch
|
|
206
|
+
main_branch = "main" # default
|
|
207
|
+
try:
|
|
208
|
+
# Check if 'main' exists
|
|
209
|
+
if "origin/main" in [ref.name for ref in repo.refs]:
|
|
210
|
+
main_branch = "main"
|
|
211
|
+
elif "origin/master" in [ref.name for ref in repo.refs]:
|
|
212
|
+
main_branch = "master"
|
|
213
|
+
elif "main" in [ref.name for ref in repo.refs]:
|
|
214
|
+
main_branch = "main"
|
|
215
|
+
elif "master" in [ref.name for ref in repo.refs]:
|
|
216
|
+
main_branch = "master"
|
|
217
|
+
except Exception:
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
# Get git status
|
|
221
|
+
try:
|
|
222
|
+
status_lines = []
|
|
223
|
+
|
|
224
|
+
# Check for staged changes
|
|
225
|
+
staged_files = list(repo.index.diff("HEAD"))
|
|
226
|
+
if staged_files:
|
|
227
|
+
for item in staged_files[:25]: # Limit to first 25
|
|
228
|
+
change_type = item.change_type
|
|
229
|
+
status_lines.append(f"{change_type[0].upper()} {item.a_path}")
|
|
230
|
+
if len(staged_files) > 25:
|
|
231
|
+
status_lines.append(
|
|
232
|
+
f"... and {len(staged_files) - 25} more staged files"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Check for unstaged changes
|
|
236
|
+
unstaged_files = list(repo.index.diff(None))
|
|
237
|
+
if unstaged_files:
|
|
238
|
+
for item in unstaged_files[:25]: # Limit to first 25
|
|
239
|
+
status_lines.append(f"M {item.a_path}")
|
|
240
|
+
if len(unstaged_files) > 25:
|
|
241
|
+
status_lines.append(
|
|
242
|
+
f"... and {len(unstaged_files) - 25} more modified files"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Check for untracked files
|
|
246
|
+
untracked_files = repo.untracked_files
|
|
247
|
+
if untracked_files:
|
|
248
|
+
for file in untracked_files[:25]: # Limit to first 25
|
|
249
|
+
status_lines.append(f"?? {file}")
|
|
250
|
+
if len(untracked_files) > 25:
|
|
251
|
+
status_lines.append(
|
|
252
|
+
f"... and {len(untracked_files) - 25} more untracked files"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
git_status = (
|
|
256
|
+
"\n".join(status_lines) if status_lines else "Working tree clean"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
except Exception:
|
|
260
|
+
git_status = "Unable to get git status"
|
|
261
|
+
|
|
262
|
+
# Get recent commits
|
|
263
|
+
try:
|
|
264
|
+
commits = []
|
|
265
|
+
for commit in repo.iter_commits(max_count=5):
|
|
266
|
+
short_hash = commit.hexsha[:7]
|
|
267
|
+
message = commit.message.split("\n")[0] # First line only
|
|
268
|
+
commits.append(f"{short_hash} {message}")
|
|
269
|
+
recent_commits = "\n".join(commits)
|
|
270
|
+
except Exception:
|
|
271
|
+
recent_commits = "Unable to get recent commits"
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
"current_branch": current_branch,
|
|
275
|
+
"main_branch": main_branch,
|
|
276
|
+
"git_status": git_status,
|
|
277
|
+
"recent_commits": recent_commits,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
return {
|
|
282
|
+
"current_branch": None,
|
|
283
|
+
"main_branch": None,
|
|
284
|
+
"git_status": f"Error: {str(e)}",
|
|
285
|
+
"recent_commits": f"Error: {str(e)}",
|
|
286
|
+
}
|
hanzo_mcp/server.py
CHANGED
|
@@ -1,121 +1,189 @@
|
|
|
1
|
-
"""MCP server implementing Hanzo capabilities.
|
|
2
|
-
|
|
3
|
-
Includes improved error handling and debugging for tool execution.
|
|
4
|
-
"""
|
|
1
|
+
"""MCP server implementing Hanzo capabilities."""
|
|
5
2
|
|
|
3
|
+
import atexit
|
|
4
|
+
import signal
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
6
7
|
from typing import Literal, cast, final
|
|
7
8
|
|
|
8
|
-
from
|
|
9
|
+
from fastmcp import FastMCP
|
|
9
10
|
|
|
11
|
+
from hanzo_mcp.prompts import register_all_prompts
|
|
10
12
|
from hanzo_mcp.tools import register_all_tools
|
|
11
|
-
|
|
13
|
+
|
|
12
14
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
13
|
-
from hanzo_mcp.tools.
|
|
14
|
-
from hanzo_mcp.tools.shell.command_executor import CommandExecutor
|
|
15
|
+
from hanzo_mcp.tools.shell.session_storage import SessionStorage
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
@final
|
|
18
|
-
class
|
|
19
|
-
"""MCP server implementing Hanzo capabilities.
|
|
20
|
-
|
|
21
|
-
Includes improved error handling and debugging for tool execution.
|
|
22
|
-
"""
|
|
19
|
+
class HanzoMCPServer:
|
|
20
|
+
"""MCP server implementing Hanzo capabilities."""
|
|
23
21
|
|
|
24
22
|
def __init__(
|
|
25
23
|
self,
|
|
26
|
-
name: str = "
|
|
24
|
+
name: str = "hanzo",
|
|
27
25
|
allowed_paths: list[str] | None = None,
|
|
26
|
+
project_paths: list[str] | None = None,
|
|
28
27
|
project_dir: str | None = None,
|
|
29
28
|
mcp_instance: FastMCP | None = None,
|
|
30
29
|
agent_model: str | None = None,
|
|
31
30
|
agent_max_tokens: int | None = None,
|
|
32
31
|
agent_api_key: str | None = None,
|
|
32
|
+
agent_base_url: str | None = None,
|
|
33
33
|
agent_max_iterations: int = 10,
|
|
34
34
|
agent_max_tool_uses: int = 30,
|
|
35
35
|
enable_agent_tool: bool = False,
|
|
36
|
+
command_timeout: float = 120.0,
|
|
36
37
|
disable_write_tools: bool = False,
|
|
37
38
|
disable_search_tools: bool = False,
|
|
38
|
-
host: str = "
|
|
39
|
-
port: int =
|
|
39
|
+
host: str = "127.0.0.1",
|
|
40
|
+
port: int = 3000,
|
|
41
|
+
enabled_tools: dict[str, bool] | None = None,
|
|
42
|
+
disabled_tools: list[str] | None = None,
|
|
40
43
|
):
|
|
41
|
-
"""Initialize the Hanzo server.
|
|
44
|
+
"""Initialize the Hanzo MCP server.
|
|
42
45
|
|
|
43
46
|
Args:
|
|
44
47
|
name: The name of the server
|
|
45
48
|
allowed_paths: list of paths that the server is allowed to access
|
|
46
|
-
|
|
49
|
+
project_paths: list of project paths to generate prompts for
|
|
50
|
+
project_dir: single project directory (added to allowed_paths and project_paths)
|
|
47
51
|
mcp_instance: Optional FastMCP instance for testing
|
|
48
52
|
agent_model: Optional model name for agent tool in LiteLLM format
|
|
49
53
|
agent_max_tokens: Optional maximum tokens for agent responses
|
|
50
54
|
agent_api_key: Optional API key for the LLM provider
|
|
55
|
+
agent_base_url: Optional base URL for the LLM provider API endpoint
|
|
51
56
|
agent_max_iterations: Maximum number of iterations for agent (default: 10)
|
|
52
57
|
agent_max_tool_uses: Maximum number of total tool uses for agent (default: 30)
|
|
53
58
|
enable_agent_tool: Whether to enable the agent tool (default: False)
|
|
54
|
-
|
|
59
|
+
command_timeout: Default timeout for command execution in seconds (default: 120.0)
|
|
60
|
+
disable_write_tools: Whether to disable write tools (default: False)
|
|
55
61
|
disable_search_tools: Whether to disable search tools (default: False)
|
|
56
|
-
host: Host
|
|
57
|
-
port: Port
|
|
62
|
+
host: Host for SSE server (default: 127.0.0.1)
|
|
63
|
+
port: Port for SSE server (default: 3000)
|
|
64
|
+
enabled_tools: Dictionary of individual tool enable states (default: None)
|
|
65
|
+
disabled_tools: List of tool names to disable (default: None)
|
|
58
66
|
"""
|
|
59
67
|
self.mcp = mcp_instance if mcp_instance is not None else FastMCP(name)
|
|
60
68
|
|
|
61
|
-
# Initialize
|
|
62
|
-
self.document_context = DocumentContext()
|
|
69
|
+
# Initialize permissions and command executor
|
|
63
70
|
self.permission_manager = PermissionManager()
|
|
64
71
|
|
|
65
|
-
#
|
|
66
|
-
self.command_executor = CommandExecutor(
|
|
67
|
-
permission_manager=self.permission_manager,
|
|
68
|
-
verbose=False, # Set to True for debugging
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
# If project_dir is specified, set it as initial working directory for all sessions
|
|
72
|
+
# Handle project_dir parameter
|
|
72
73
|
if project_dir:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
self.document_context, self.permission_manager, self.project_analyzer
|
|
82
|
-
)
|
|
74
|
+
if allowed_paths is None:
|
|
75
|
+
allowed_paths = []
|
|
76
|
+
if project_dir not in allowed_paths:
|
|
77
|
+
allowed_paths.append(project_dir)
|
|
78
|
+
if project_paths is None:
|
|
79
|
+
project_paths = []
|
|
80
|
+
if project_dir not in project_paths:
|
|
81
|
+
project_paths.append(project_dir)
|
|
83
82
|
|
|
84
83
|
# Add allowed paths
|
|
85
84
|
if allowed_paths:
|
|
86
85
|
for path in allowed_paths:
|
|
87
86
|
self.permission_manager.add_allowed_path(path)
|
|
88
|
-
|
|
87
|
+
|
|
88
|
+
# Store paths and options
|
|
89
|
+
self.project_paths = project_paths
|
|
90
|
+
self.project_dir = project_dir
|
|
91
|
+
self.disable_write_tools = disable_write_tools
|
|
92
|
+
self.disable_search_tools = disable_search_tools
|
|
93
|
+
self.host = host
|
|
94
|
+
self.port = port
|
|
95
|
+
self.enabled_tools = enabled_tools or {}
|
|
96
|
+
self.disabled_tools = disabled_tools or []
|
|
89
97
|
|
|
90
98
|
# Store agent options
|
|
91
99
|
self.agent_model = agent_model
|
|
92
100
|
self.agent_max_tokens = agent_max_tokens
|
|
93
101
|
self.agent_api_key = agent_api_key
|
|
102
|
+
self.agent_base_url = agent_base_url
|
|
94
103
|
self.agent_max_iterations = agent_max_iterations
|
|
95
104
|
self.agent_max_tool_uses = agent_max_tool_uses
|
|
96
105
|
self.enable_agent_tool = enable_agent_tool
|
|
97
|
-
self.
|
|
98
|
-
self.disable_search_tools = disable_search_tools
|
|
106
|
+
self.command_timeout = command_timeout
|
|
99
107
|
|
|
100
|
-
#
|
|
101
|
-
self.
|
|
102
|
-
self.
|
|
108
|
+
# Initialize cleanup tracking
|
|
109
|
+
self._cleanup_thread: threading.Thread | None = None
|
|
110
|
+
self._shutdown_event = threading.Event()
|
|
111
|
+
self._cleanup_registered = False
|
|
112
|
+
|
|
113
|
+
# Apply disabled_tools to enabled_tools
|
|
114
|
+
final_enabled_tools = self.enabled_tools.copy()
|
|
115
|
+
for tool_name in self.disabled_tools:
|
|
116
|
+
final_enabled_tools[tool_name] = False
|
|
117
|
+
|
|
118
|
+
# Store the final processed tool configuration
|
|
119
|
+
self.enabled_tools = final_enabled_tools
|
|
103
120
|
|
|
104
121
|
# Register all tools
|
|
105
122
|
register_all_tools(
|
|
106
123
|
mcp_server=self.mcp,
|
|
107
|
-
document_context=self.document_context,
|
|
108
124
|
permission_manager=self.permission_manager,
|
|
109
125
|
agent_model=self.agent_model,
|
|
110
126
|
agent_max_tokens=self.agent_max_tokens,
|
|
111
127
|
agent_api_key=self.agent_api_key,
|
|
128
|
+
agent_base_url=self.agent_base_url,
|
|
112
129
|
agent_max_iterations=self.agent_max_iterations,
|
|
113
130
|
agent_max_tool_uses=self.agent_max_tool_uses,
|
|
114
131
|
enable_agent_tool=self.enable_agent_tool,
|
|
115
132
|
disable_write_tools=self.disable_write_tools,
|
|
116
133
|
disable_search_tools=self.disable_search_tools,
|
|
134
|
+
enabled_tools=final_enabled_tools,
|
|
117
135
|
)
|
|
118
136
|
|
|
137
|
+
register_all_prompts(mcp_server=self.mcp, projects=self.project_paths)
|
|
138
|
+
|
|
139
|
+
def _setup_cleanup_handlers(self) -> None:
|
|
140
|
+
"""Set up signal handlers and background cleanup thread."""
|
|
141
|
+
if self._cleanup_registered:
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
# Register cleanup on normal exit
|
|
145
|
+
atexit.register(self._cleanup_sessions)
|
|
146
|
+
|
|
147
|
+
# Register signal handlers for graceful shutdown
|
|
148
|
+
def signal_handler(signum, frame):
|
|
149
|
+
self._cleanup_sessions()
|
|
150
|
+
self._shutdown_event.set()
|
|
151
|
+
|
|
152
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
153
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
154
|
+
|
|
155
|
+
# Start background cleanup thread for periodic cleanup
|
|
156
|
+
self._cleanup_thread = threading.Thread(
|
|
157
|
+
target=self._background_cleanup, daemon=True
|
|
158
|
+
)
|
|
159
|
+
self._cleanup_thread.start()
|
|
160
|
+
|
|
161
|
+
self._cleanup_registered = True
|
|
162
|
+
|
|
163
|
+
def _background_cleanup(self) -> None:
|
|
164
|
+
"""Background thread for periodic session cleanup."""
|
|
165
|
+
while not self._shutdown_event.is_set():
|
|
166
|
+
try:
|
|
167
|
+
# Clean up expired sessions every 2 minutes
|
|
168
|
+
# Using shorter TTL of 5 minutes (300 seconds)
|
|
169
|
+
SessionStorage.cleanup_expired_sessions(max_age_seconds=300)
|
|
170
|
+
|
|
171
|
+
# Wait for 2 minutes or until shutdown
|
|
172
|
+
self._shutdown_event.wait(timeout=120)
|
|
173
|
+
except Exception:
|
|
174
|
+
# Ignore cleanup errors and continue
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
def _cleanup_sessions(self) -> None:
|
|
178
|
+
"""Clean up all active sessions."""
|
|
179
|
+
try:
|
|
180
|
+
cleared_count = SessionStorage.clear_all_sessions()
|
|
181
|
+
if cleared_count > 0:
|
|
182
|
+
print(f"Cleaned up {cleared_count} tmux sessions on shutdown")
|
|
183
|
+
except Exception:
|
|
184
|
+
# Ignore cleanup errors during shutdown
|
|
185
|
+
pass
|
|
186
|
+
|
|
119
187
|
def run(self, transport: str = "stdio", allowed_paths: list[str] | None = None):
|
|
120
188
|
"""Run the MCP server.
|
|
121
189
|
|
|
@@ -127,60 +195,10 @@ Includes improved error handling and debugging for tool execution.
|
|
|
127
195
|
allowed_paths_list = allowed_paths or []
|
|
128
196
|
for path in allowed_paths_list:
|
|
129
197
|
self.permission_manager.add_allowed_path(path)
|
|
130
|
-
self.document_context.add_allowed_path(path)
|
|
131
198
|
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
import os
|
|
135
|
-
# Set environment variables for FastMCP settings
|
|
136
|
-
os.environ["FASTMCP_PORT"] = str(self.port)
|
|
137
|
-
os.environ["FASTMCP_HOST"] = self.host
|
|
138
|
-
print(f"Starting SSE server on {self.host}:{self.port}")
|
|
199
|
+
# Set up cleanup handlers before running
|
|
200
|
+
self._setup_cleanup_handlers()
|
|
139
201
|
|
|
140
202
|
# Run the server
|
|
141
203
|
transport_type = cast(Literal["stdio", "sse"], transport)
|
|
142
204
|
self.mcp.run(transport=transport_type)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def main():
|
|
146
|
-
"""Run the Hanzo MCP server."""
|
|
147
|
-
import argparse
|
|
148
|
-
|
|
149
|
-
parser = argparse.ArgumentParser(
|
|
150
|
-
description="MCP server implementing Hanzo capabilities"
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
_ = parser.add_argument(
|
|
154
|
-
"--name",
|
|
155
|
-
default="claude-code",
|
|
156
|
-
help="Name of the MCP server (default: claude-code)",
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
_ = parser.add_argument(
|
|
160
|
-
"--transport",
|
|
161
|
-
choices=["stdio", "sse"],
|
|
162
|
-
default="stdio",
|
|
163
|
-
help="Transport protocol to use (default: stdio)",
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
_ = parser.add_argument(
|
|
167
|
-
"--allow-path",
|
|
168
|
-
action="append",
|
|
169
|
-
dest="allowed_paths",
|
|
170
|
-
help="Add an allowed path (can be specified multiple times)",
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
args = parser.parse_args()
|
|
174
|
-
|
|
175
|
-
# Type annotations for args to avoid Any warnings
|
|
176
|
-
name: str = args.name
|
|
177
|
-
transport: str = args.transport
|
|
178
|
-
allowed_paths: list[str] | None = args.allowed_paths
|
|
179
|
-
|
|
180
|
-
# Create and run the server
|
|
181
|
-
server = HanzoServer(name=name, allowed_paths=allowed_paths)
|
|
182
|
-
server.run(transport=transport, allowed_paths=allowed_paths or [])
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if __name__ == "__main__":
|
|
186
|
-
main()
|