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.

Files changed (93) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +118 -170
  3. hanzo_mcp/cli_enhanced.py +438 -0
  4. hanzo_mcp/config/__init__.py +19 -0
  5. hanzo_mcp/config/settings.py +449 -0
  6. hanzo_mcp/config/tool_config.py +197 -0
  7. hanzo_mcp/prompts/__init__.py +117 -0
  8. hanzo_mcp/prompts/compact_conversation.py +77 -0
  9. hanzo_mcp/prompts/create_release.py +38 -0
  10. hanzo_mcp/prompts/project_system.py +120 -0
  11. hanzo_mcp/prompts/project_todo_reminder.py +111 -0
  12. hanzo_mcp/prompts/utils.py +286 -0
  13. hanzo_mcp/server.py +117 -99
  14. hanzo_mcp/tools/__init__.py +121 -33
  15. hanzo_mcp/tools/agent/__init__.py +8 -11
  16. hanzo_mcp/tools/agent/agent_tool.py +290 -224
  17. hanzo_mcp/tools/agent/prompt.py +16 -13
  18. hanzo_mcp/tools/agent/tool_adapter.py +9 -9
  19. hanzo_mcp/tools/common/__init__.py +17 -16
  20. hanzo_mcp/tools/common/base.py +79 -110
  21. hanzo_mcp/tools/common/batch_tool.py +330 -0
  22. hanzo_mcp/tools/common/config_tool.py +396 -0
  23. hanzo_mcp/tools/common/context.py +26 -292
  24. hanzo_mcp/tools/common/permissions.py +12 -12
  25. hanzo_mcp/tools/common/thinking_tool.py +153 -0
  26. hanzo_mcp/tools/common/validation.py +1 -63
  27. hanzo_mcp/tools/filesystem/__init__.py +97 -57
  28. hanzo_mcp/tools/filesystem/base.py +32 -24
  29. hanzo_mcp/tools/filesystem/content_replace.py +114 -107
  30. hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
  31. hanzo_mcp/tools/filesystem/edit.py +279 -0
  32. hanzo_mcp/tools/filesystem/grep.py +458 -0
  33. hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
  34. hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
  35. hanzo_mcp/tools/filesystem/read.py +255 -0
  36. hanzo_mcp/tools/filesystem/unified_search.py +689 -0
  37. hanzo_mcp/tools/filesystem/write.py +156 -0
  38. hanzo_mcp/tools/jupyter/__init__.py +41 -29
  39. hanzo_mcp/tools/jupyter/base.py +66 -57
  40. hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
  41. hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
  42. hanzo_mcp/tools/shell/__init__.py +29 -20
  43. hanzo_mcp/tools/shell/base.py +87 -45
  44. hanzo_mcp/tools/shell/bash_session.py +731 -0
  45. hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
  46. hanzo_mcp/tools/shell/command_executor.py +435 -384
  47. hanzo_mcp/tools/shell/run_command.py +284 -131
  48. hanzo_mcp/tools/shell/run_command_windows.py +328 -0
  49. hanzo_mcp/tools/shell/session_manager.py +196 -0
  50. hanzo_mcp/tools/shell/session_storage.py +325 -0
  51. hanzo_mcp/tools/todo/__init__.py +66 -0
  52. hanzo_mcp/tools/todo/base.py +319 -0
  53. hanzo_mcp/tools/todo/todo_read.py +148 -0
  54. hanzo_mcp/tools/todo/todo_write.py +378 -0
  55. hanzo_mcp/tools/vector/__init__.py +99 -0
  56. hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
  57. hanzo_mcp/tools/vector/git_ingester.py +482 -0
  58. hanzo_mcp/tools/vector/infinity_store.py +731 -0
  59. hanzo_mcp/tools/vector/mock_infinity.py +162 -0
  60. hanzo_mcp/tools/vector/project_manager.py +361 -0
  61. hanzo_mcp/tools/vector/vector_index.py +116 -0
  62. hanzo_mcp/tools/vector/vector_search.py +225 -0
  63. hanzo_mcp-0.5.1.dist-info/METADATA +276 -0
  64. hanzo_mcp-0.5.1.dist-info/RECORD +68 -0
  65. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/WHEEL +1 -1
  66. hanzo_mcp/tools/agent/base_provider.py +0 -73
  67. hanzo_mcp/tools/agent/litellm_provider.py +0 -45
  68. hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
  69. hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
  70. hanzo_mcp/tools/agent/provider_registry.py +0 -120
  71. hanzo_mcp/tools/common/error_handling.py +0 -86
  72. hanzo_mcp/tools/common/logging_config.py +0 -115
  73. hanzo_mcp/tools/common/session.py +0 -91
  74. hanzo_mcp/tools/common/think_tool.py +0 -123
  75. hanzo_mcp/tools/common/version_tool.py +0 -120
  76. hanzo_mcp/tools/filesystem/edit_file.py +0 -287
  77. hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
  78. hanzo_mcp/tools/filesystem/read_files.py +0 -199
  79. hanzo_mcp/tools/filesystem/search_content.py +0 -275
  80. hanzo_mcp/tools/filesystem/write_file.py +0 -162
  81. hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
  82. hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
  83. hanzo_mcp/tools/project/__init__.py +0 -64
  84. hanzo_mcp/tools/project/analysis.py +0 -886
  85. hanzo_mcp/tools/project/base.py +0 -66
  86. hanzo_mcp/tools/project/project_analyze.py +0 -173
  87. hanzo_mcp/tools/shell/run_script.py +0 -215
  88. hanzo_mcp/tools/shell/script_tool.py +0 -244
  89. hanzo_mcp-0.3.8.dist-info/METADATA +0 -196
  90. hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
  91. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/entry_points.txt +0 -0
  92. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/licenses/LICENSE +0 -0
  93. {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 mcp.server.fastmcp import FastMCP
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
- from hanzo_mcp.tools.common.context import DocumentContext
13
+
12
14
  from hanzo_mcp.tools.common.permissions import PermissionManager
13
- from hanzo_mcp.tools.project.analysis import ProjectAnalyzer, ProjectManager
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 HanzoServer:
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 = "claude-code",
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 = "0.0.0.0",
39
- port: int = 3001,
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
- project_dir: Optional project directory to use as initial working directory
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
- disable_write_tools: Whether to disable write/edit tools (default: False)
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 to bind to for SSE transport (default: '0.0.0.0')
57
- port: Port to use for SSE transport (default: 3001)
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 context, permissions, and command executor
62
- self.document_context = DocumentContext()
69
+ # Initialize permissions and command executor
63
70
  self.permission_manager = PermissionManager()
64
71
 
65
- # Initialize command executor
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
- initial_session_id = name # Use server name as default session ID
74
- self.command_executor.set_working_dir(initial_session_id, project_dir)
75
-
76
- # Initialize project analyzer
77
- self.project_analyzer = ProjectAnalyzer(self.command_executor)
78
-
79
- # Initialize project manager
80
- self.project_manager = ProjectManager(
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
- self.document_context.add_allowed_path(path)
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.disable_write_tools = disable_write_tools
98
- self.disable_search_tools = disable_search_tools
106
+ self.command_timeout = command_timeout
99
107
 
100
- # Store network options
101
- self.host = host
102
- self.port = port
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
- # If using SSE, set the port and host in the environment variables
133
- if transport == "sse":
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()