hanzo-mcp 0.8.8__py3-none-any.whl → 0.9.0__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 (167) hide show
  1. hanzo_mcp/__init__.py +1 -3
  2. hanzo_mcp/analytics/posthog_analytics.py +4 -17
  3. hanzo_mcp/bridge.py +9 -25
  4. hanzo_mcp/cli.py +8 -17
  5. hanzo_mcp/cli_enhanced.py +5 -14
  6. hanzo_mcp/cli_plugin.py +3 -9
  7. hanzo_mcp/config/settings.py +6 -20
  8. hanzo_mcp/config/tool_config.py +2 -4
  9. hanzo_mcp/core/base_agent.py +88 -88
  10. hanzo_mcp/core/model_registry.py +238 -210
  11. hanzo_mcp/dev_server.py +5 -15
  12. hanzo_mcp/prompts/__init__.py +2 -6
  13. hanzo_mcp/prompts/project_todo_reminder.py +3 -9
  14. hanzo_mcp/prompts/tool_explorer.py +1 -3
  15. hanzo_mcp/prompts/utils.py +7 -21
  16. hanzo_mcp/server.py +6 -7
  17. hanzo_mcp/tools/__init__.py +29 -32
  18. hanzo_mcp/tools/agent/__init__.py +2 -1
  19. hanzo_mcp/tools/agent/agent.py +10 -30
  20. hanzo_mcp/tools/agent/agent_tool.py +23 -17
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
  22. hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
  23. hanzo_mcp/tools/agent/cli_tools.py +76 -75
  24. hanzo_mcp/tools/agent/code_auth.py +1 -3
  25. hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
  26. hanzo_mcp/tools/agent/critic_tool.py +8 -24
  27. hanzo_mcp/tools/agent/iching_tool.py +12 -36
  28. hanzo_mcp/tools/agent/network_tool.py +7 -18
  29. hanzo_mcp/tools/agent/prompt.py +1 -5
  30. hanzo_mcp/tools/agent/review_tool.py +10 -25
  31. hanzo_mcp/tools/agent/swarm_alias.py +1 -3
  32. hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
  33. hanzo_mcp/tools/common/batch_tool.py +15 -45
  34. hanzo_mcp/tools/common/config_tool.py +9 -28
  35. hanzo_mcp/tools/common/context.py +1 -3
  36. hanzo_mcp/tools/common/critic_tool.py +1 -3
  37. hanzo_mcp/tools/common/decorators.py +2 -6
  38. hanzo_mcp/tools/common/enhanced_base.py +2 -6
  39. hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
  40. hanzo_mcp/tools/common/forgiving_edit.py +9 -28
  41. hanzo_mcp/tools/common/mode.py +1 -5
  42. hanzo_mcp/tools/common/paginated_base.py +3 -11
  43. hanzo_mcp/tools/common/paginated_response.py +10 -30
  44. hanzo_mcp/tools/common/pagination.py +3 -9
  45. hanzo_mcp/tools/common/path_utils.py +34 -0
  46. hanzo_mcp/tools/common/permissions.py +14 -13
  47. hanzo_mcp/tools/common/personality.py +983 -701
  48. hanzo_mcp/tools/common/plugin_loader.py +3 -15
  49. hanzo_mcp/tools/common/stats.py +7 -19
  50. hanzo_mcp/tools/common/thinking_tool.py +1 -3
  51. hanzo_mcp/tools/common/tool_disable.py +2 -6
  52. hanzo_mcp/tools/common/tool_list.py +2 -6
  53. hanzo_mcp/tools/common/validation.py +1 -3
  54. hanzo_mcp/tools/compiler/__init__.py +8 -0
  55. hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
  56. hanzo_mcp/tools/config/config_tool.py +7 -13
  57. hanzo_mcp/tools/config/index_config.py +1 -3
  58. hanzo_mcp/tools/config/mode_tool.py +5 -15
  59. hanzo_mcp/tools/database/database_manager.py +3 -9
  60. hanzo_mcp/tools/database/graph.py +1 -3
  61. hanzo_mcp/tools/database/graph_add.py +3 -9
  62. hanzo_mcp/tools/database/graph_query.py +11 -34
  63. hanzo_mcp/tools/database/graph_remove.py +3 -9
  64. hanzo_mcp/tools/database/graph_search.py +6 -20
  65. hanzo_mcp/tools/database/graph_stats.py +11 -33
  66. hanzo_mcp/tools/database/sql.py +4 -12
  67. hanzo_mcp/tools/database/sql_query.py +6 -10
  68. hanzo_mcp/tools/database/sql_search.py +2 -6
  69. hanzo_mcp/tools/database/sql_stats.py +5 -15
  70. hanzo_mcp/tools/editor/neovim_command.py +1 -3
  71. hanzo_mcp/tools/editor/neovim_session.py +7 -13
  72. hanzo_mcp/tools/environment/__init__.py +8 -0
  73. hanzo_mcp/tools/environment/environment_detector.py +594 -0
  74. hanzo_mcp/tools/filesystem/__init__.py +28 -26
  75. hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
  76. hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
  77. hanzo_mcp/tools/filesystem/base.py +20 -12
  78. hanzo_mcp/tools/filesystem/content_replace.py +7 -12
  79. hanzo_mcp/tools/filesystem/diff.py +2 -10
  80. hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
  81. hanzo_mcp/tools/filesystem/edit.py +10 -18
  82. hanzo_mcp/tools/filesystem/find.py +312 -179
  83. hanzo_mcp/tools/filesystem/git_search.py +12 -24
  84. hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
  85. hanzo_mcp/tools/filesystem/read.py +14 -30
  86. hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
  87. hanzo_mcp/tools/filesystem/search.py +1160 -0
  88. hanzo_mcp/tools/filesystem/watch.py +2 -4
  89. hanzo_mcp/tools/filesystem/write.py +7 -10
  90. hanzo_mcp/tools/framework/__init__.py +8 -0
  91. hanzo_mcp/tools/framework/framework_modes.py +714 -0
  92. hanzo_mcp/tools/jupyter/base.py +6 -20
  93. hanzo_mcp/tools/jupyter/jupyter.py +4 -12
  94. hanzo_mcp/tools/llm/consensus_tool.py +8 -24
  95. hanzo_mcp/tools/llm/llm_manage.py +2 -6
  96. hanzo_mcp/tools/llm/llm_tool.py +17 -58
  97. hanzo_mcp/tools/llm/llm_unified.py +18 -59
  98. hanzo_mcp/tools/llm/provider_tools.py +1 -3
  99. hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
  100. hanzo_mcp/tools/mcp/mcp_add.py +3 -5
  101. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  102. hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
  103. hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
  104. hanzo_mcp/tools/memory/__init__.py +33 -40
  105. hanzo_mcp/tools/memory/conversation_memory.py +636 -0
  106. hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
  107. hanzo_mcp/tools/memory/memory_tools.py +7 -19
  108. hanzo_mcp/tools/search/find_tool.py +12 -34
  109. hanzo_mcp/tools/search/unified_search.py +27 -81
  110. hanzo_mcp/tools/shell/__init__.py +16 -4
  111. hanzo_mcp/tools/shell/auto_background.py +2 -6
  112. hanzo_mcp/tools/shell/base.py +1 -5
  113. hanzo_mcp/tools/shell/base_process.py +5 -7
  114. hanzo_mcp/tools/shell/bash_session.py +7 -24
  115. hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
  116. hanzo_mcp/tools/shell/bash_tool.py +3 -7
  117. hanzo_mcp/tools/shell/command_executor.py +26 -79
  118. hanzo_mcp/tools/shell/logs.py +4 -16
  119. hanzo_mcp/tools/shell/npx.py +2 -8
  120. hanzo_mcp/tools/shell/npx_tool.py +1 -3
  121. hanzo_mcp/tools/shell/pkill.py +4 -12
  122. hanzo_mcp/tools/shell/process_tool.py +2 -8
  123. hanzo_mcp/tools/shell/processes.py +5 -17
  124. hanzo_mcp/tools/shell/run_background.py +1 -3
  125. hanzo_mcp/tools/shell/run_command.py +1 -3
  126. hanzo_mcp/tools/shell/run_command_windows.py +1 -3
  127. hanzo_mcp/tools/shell/run_tool.py +56 -0
  128. hanzo_mcp/tools/shell/session_manager.py +2 -6
  129. hanzo_mcp/tools/shell/session_storage.py +2 -6
  130. hanzo_mcp/tools/shell/streaming_command.py +7 -23
  131. hanzo_mcp/tools/shell/uvx.py +4 -14
  132. hanzo_mcp/tools/shell/uvx_background.py +2 -6
  133. hanzo_mcp/tools/shell/uvx_tool.py +1 -3
  134. hanzo_mcp/tools/shell/zsh_tool.py +12 -20
  135. hanzo_mcp/tools/todo/todo.py +1 -3
  136. hanzo_mcp/tools/vector/__init__.py +97 -50
  137. hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
  138. hanzo_mcp/tools/vector/git_ingester.py +10 -30
  139. hanzo_mcp/tools/vector/index_tool.py +3 -9
  140. hanzo_mcp/tools/vector/infinity_store.py +11 -30
  141. hanzo_mcp/tools/vector/mock_infinity.py +159 -0
  142. hanzo_mcp/tools/vector/node_tool.py +538 -0
  143. hanzo_mcp/tools/vector/project_manager.py +4 -12
  144. hanzo_mcp/tools/vector/unified_vector.py +384 -0
  145. hanzo_mcp/tools/vector/vector.py +2 -6
  146. hanzo_mcp/tools/vector/vector_index.py +8 -8
  147. hanzo_mcp/tools/vector/vector_search.py +7 -21
  148. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
  149. hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
  150. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
  151. hanzo_mcp/tools/agent/swarm_tool.py +0 -723
  152. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
  153. hanzo_mcp/tools/filesystem/batch_search.py +0 -900
  154. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
  155. hanzo_mcp/tools/filesystem/find_files.py +0 -369
  156. hanzo_mcp/tools/filesystem/grep.py +0 -467
  157. hanzo_mcp/tools/filesystem/search_tool.py +0 -767
  158. hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
  159. hanzo_mcp/tools/filesystem/tree.py +0 -270
  160. hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
  161. hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
  162. hanzo_mcp/tools/todo/todo_read.py +0 -143
  163. hanzo_mcp/tools/todo/todo_write.py +0 -374
  164. hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
  165. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
  166. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
  167. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
@@ -140,15 +140,9 @@ recall_facts(queries=["company policies"], scope="global", limit=5)
140
140
  if fact.metadata and fact.metadata.get("kb_name"):
141
141
  kb_info = f" (KB: {fact.metadata['kb_name']})"
142
142
  formatted.append(f"{i}. {fact.content}{kb_info}")
143
- if (
144
- fact.metadata and len(fact.metadata) > 2
145
- ): # More than just type and kb_name
143
+ if fact.metadata and len(fact.metadata) > 2: # More than just type and kb_name
146
144
  # Show other metadata
147
- other_meta = {
148
- k: v
149
- for k, v in fact.metadata.items()
150
- if k not in ["type", "kb_name"]
151
- }
145
+ other_meta = {k: v for k, v in fact.metadata.items() if k not in ["type", "kb_name"]}
152
146
  if other_meta:
153
147
  formatted.append(f" Metadata: {other_meta}")
154
148
 
@@ -167,9 +161,7 @@ recall_facts(queries=["company policies"], scope="global", limit=5)
167
161
  scope: str = "project",
168
162
  limit: int = 10,
169
163
  ) -> str:
170
- return await tool_self.call(
171
- ctx, queries=queries, kb_name=kb_name, scope=scope, limit=limit
172
- )
164
+ return await tool_self.call(ctx, queries=queries, kb_name=kb_name, scope=scope, limit=limit)
173
165
 
174
166
 
175
167
  @final
@@ -265,9 +257,7 @@ store_facts(facts=["Company founded in 2020"], scope="global", kb_name="company_
265
257
  scope: str = "project",
266
258
  metadata: Optional[Dict[str, Any]] = None,
267
259
  ) -> str:
268
- return await tool_self.call(
269
- ctx, facts=facts, kb_name=kb_name, scope=scope, metadata=metadata
270
- )
260
+ return await tool_self.call(ctx, facts=facts, kb_name=kb_name, scope=scope, metadata=metadata)
271
261
 
272
262
 
273
263
  @final
@@ -323,11 +313,7 @@ summarize_to_memory(content="Company guidelines...", topic="Guidelines", scope="
323
313
 
324
314
  # Use the memory service to create a summary
325
315
  # This would typically use an LLM to summarize, but for now we'll store as-is
326
- summary = (
327
- f"Summary of {topic}:\n{content[:500]}..."
328
- if len(content) > 500
329
- else content
330
- )
316
+ summary = f"Summary of {topic}:\n{content[:500]}..." if len(content) > 500 else content
331
317
 
332
318
  # Store the summary as a memory
333
319
  from hanzo_memory.services.memory import get_memory_service
@@ -358,9 +344,7 @@ summarize_to_memory(content="Company guidelines...", topic="Guidelines", scope="
358
344
  if auto_facts:
359
345
  # In a real implementation, this would use LLM to extract key facts
360
346
  # For now, we'll just note it
361
- result += (
362
- "\n(Auto-fact extraction would extract key facts from the summary)"
363
- )
347
+ result += "\n(Auto-fact extraction would extract key facts from the summary)"
364
348
 
365
349
  return result
366
350
 
@@ -377,9 +361,7 @@ summarize_to_memory(content="Company guidelines...", topic="Guidelines", scope="
377
361
  scope: str = "project",
378
362
  auto_facts: bool = True,
379
363
  ) -> str:
380
- return await tool_self.call(
381
- ctx, content=content, topic=topic, scope=scope, auto_facts=auto_facts
382
- )
364
+ return await tool_self.call(ctx, content=content, topic=topic, scope=scope, auto_facts=auto_facts)
383
365
 
384
366
 
385
367
  @final
@@ -21,7 +21,7 @@ try:
21
21
  except ImportError:
22
22
  MEMORY_AVAILABLE = False
23
23
  raise ImportError(
24
- "hanzo-memory package is required for memory tools. Install it from ~/work/hanzo/ide/pkg/memory"
24
+ "hanzo-memory package is required for memory tools. Install it from ~/work/hanzo/python-sdk/pkg/hanzo-memory"
25
25
  )
26
26
 
27
27
 
@@ -127,9 +127,7 @@ recall_memories(queries=["coding standards"], scope="global")
127
127
  tool_self = self
128
128
 
129
129
  @mcp_server.tool(name=self.name, description=self.description)
130
- async def recall_memories(
131
- ctx: MCPContext, queries: List[str], limit: int = 10, scope: str = "project"
132
- ) -> str:
130
+ async def recall_memories(ctx: MCPContext, queries: List[str], limit: int = 10, scope: str = "project") -> str:
133
131
  return await tool_self.call(ctx, queries=queries, limit=limit, scope=scope)
134
132
 
135
133
 
@@ -247,9 +245,7 @@ update_memories(updates=[
247
245
  # The hanzo-memory service doesn't have update implemented yet
248
246
  # When it's implemented, we would call:
249
247
  # success = self.service.update_memory(self.user_id, memory_id, content=statement)
250
- await tool_ctx.warning(
251
- f"Memory update not fully implemented in hanzo-memory yet: {memory_id}"
252
- )
248
+ await tool_ctx.warning(f"Memory update not fully implemented in hanzo-memory yet: {memory_id}")
253
249
  success_count += 1
254
250
 
255
251
  return f"Would update {success_count} of {len(updates)} memories (update not fully implemented in hanzo-memory yet)."
@@ -260,9 +256,7 @@ update_memories(updates=[
260
256
  tool_self = self
261
257
 
262
258
  @mcp_server.tool(name=self.name, description=self.description)
263
- async def update_memories(
264
- ctx: MCPContext, updates: List[Dict[str, str]]
265
- ) -> str:
259
+ async def update_memories(ctx: MCPContext, updates: List[Dict[str, str]]) -> str:
266
260
  return await tool_self.call(ctx, updates=updates)
267
261
 
268
262
 
@@ -398,13 +392,9 @@ manage_memories(
398
392
 
399
393
  if memory_id and statement:
400
394
  # Update not fully implemented in hanzo-memory yet
401
- await tool_ctx.warning(
402
- f"Memory update not fully implemented: {memory_id}"
403
- )
395
+ await tool_ctx.warning(f"Memory update not fully implemented: {memory_id}")
404
396
  success_count += 1
405
- results.append(
406
- f"Would update {success_count} memories (update pending implementation)"
407
- )
397
+ results.append(f"Would update {success_count} memories (update pending implementation)")
408
398
 
409
399
  # Delete memories
410
400
  if deletions:
@@ -433,6 +423,4 @@ manage_memories(
433
423
  updates: Optional[List[Dict[str, str]]] = None,
434
424
  deletions: Optional[List[str]] = None,
435
425
  ) -> str:
436
- return await tool_self.call(
437
- ctx, creations=creations, updates=updates, deletions=deletions
438
- )
426
+ return await tool_self.call(ctx, creations=creations, updates=updates, deletions=deletions)
@@ -75,8 +75,8 @@ class FindTool(BaseTool):
75
75
  reading or searching within files.
76
76
  """
77
77
 
78
- def __init__(self):
79
- super().__init__()
78
+ def __init__(self, permission_manager=None):
79
+ super().__init__(permission_manager=permission_manager)
80
80
  self._cache = {}
81
81
  self._gitignore_cache = {}
82
82
 
@@ -257,9 +257,7 @@ class FindTool(BaseTool):
257
257
  # Resolve path
258
258
  root_path = Path(path).resolve()
259
259
  if not root_path.exists():
260
- return MCPResourceDocument(
261
- data={"error": f"Path does not exist: {path}", "results": []}
262
- )
260
+ return MCPResourceDocument(data={"error": f"Path does not exist: {path}", "results": []})
263
261
 
264
262
  # Get ignore patterns
265
263
  ignore_patterns = set()
@@ -270,9 +268,7 @@ class FindTool(BaseTool):
270
268
  min_size_bytes = self._parse_size(min_size) if min_size else None
271
269
  max_size_bytes = self._parse_size(max_size) if max_size else None
272
270
  modified_after_ts = self._parse_time(modified_after) if modified_after else None
273
- modified_before_ts = (
274
- self._parse_time(modified_before) if modified_before else None
275
- )
271
+ modified_before_ts = self._parse_time(modified_before) if modified_before else None
276
272
 
277
273
  # Collect matches
278
274
  matches = []
@@ -350,20 +346,14 @@ class FindTool(BaseTool):
350
346
  stats = {
351
347
  "total_found": total_results,
352
348
  "search_time_ms": int((time.time() - start_time) * 1000),
353
- "search_method": (
354
- "ffind" if FFIND_AVAILABLE and not in_content else "python"
355
- ),
349
+ "search_method": ("ffind" if FFIND_AVAILABLE and not in_content else "python"),
356
350
  "root_path": str(root_path),
357
351
  "filters_applied": {
358
352
  "pattern": pattern,
359
353
  "type": type,
360
- "size": (
361
- {"min": min_size, "max": max_size} if min_size or max_size else None
362
- ),
354
+ "size": ({"min": min_size, "max": max_size} if min_size or max_size else None),
363
355
  "modified": (
364
- {"after": modified_after, "before": modified_before}
365
- if modified_after or modified_before
366
- else None
356
+ {"after": modified_after, "before": modified_before} if modified_after or modified_before else None
367
357
  ),
368
358
  "max_depth": max_depth,
369
359
  "gitignore": respect_gitignore,
@@ -594,9 +584,7 @@ class FindTool(BaseTool):
594
584
  elif fuzzy:
595
585
  pattern_lower = pattern.lower() if not case_sensitive else pattern
596
586
  matcher = (
597
- lambda name: SequenceMatcher(
598
- None, pattern_lower, name.lower() if not case_sensitive else name
599
- ).ratio()
587
+ lambda name: SequenceMatcher(None, pattern_lower, name.lower() if not case_sensitive else name).ratio()
600
588
  > 0.6
601
589
  )
602
590
  else:
@@ -608,9 +596,7 @@ class FindTool(BaseTool):
608
596
  matcher = lambda name: fnmatch.fnmatch(name, pattern)
609
597
 
610
598
  # Walk directory tree
611
- for dirpath, dirnames, filenames in os.walk(
612
- str(root), followlinks=follow_symlinks
613
- ):
599
+ for dirpath, dirnames, filenames in os.walk(str(root), followlinks=follow_symlinks):
614
600
  # Check depth
615
601
  if max_depth is not None:
616
602
  depth = len(Path(dirpath).relative_to(root).parts)
@@ -621,11 +607,7 @@ class FindTool(BaseTool):
621
607
  # Filter directories to skip
622
608
  if respect_gitignore:
623
609
  dirnames[:] = [
624
- d
625
- for d in dirnames
626
- if not self._should_ignore(
627
- os.path.join(dirpath, d), ignore_patterns
628
- )
610
+ d for d in dirnames if not self._should_ignore(os.path.join(dirpath, d), ignore_patterns)
629
611
  ]
630
612
 
631
613
  # Check directories
@@ -662,9 +644,7 @@ class FindTool(BaseTool):
662
644
  match_found = True
663
645
  elif in_content:
664
646
  # Search in file content
665
- match_found = await self._search_in_file(
666
- full_path, pattern, case_sensitive
667
- )
647
+ match_found = await self._search_in_file(full_path, pattern, case_sensitive)
668
648
  else:
669
649
  match_found = False
670
650
 
@@ -686,9 +666,7 @@ class FindTool(BaseTool):
686
666
 
687
667
  return matches
688
668
 
689
- async def _search_in_file(
690
- self, file_path: str, pattern: str, case_sensitive: bool
691
- ) -> bool:
669
+ async def _search_in_file(self, file_path: str, pattern: str, case_sensitive: bool) -> bool:
692
670
  """Search for pattern in file content."""
693
671
  try:
694
672
  with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
@@ -93,7 +93,7 @@ class UnifiedSearch(BaseTool):
93
93
 
94
94
  1. Find code patterns:
95
95
  search("error handling") # Finds all error handling code
96
- search("TASK|FIX") # Regex search for task markers
96
+ search("TODO|FIXME") # Regex search for TODOs
97
97
  search("async function") # Find async functions
98
98
 
99
99
  2. Find symbols/definitions:
@@ -171,9 +171,7 @@ class UnifiedSearch(BaseTool):
171
171
  # Use vector search for natural language queries
172
172
  indicators = [
173
173
  len(query.split()) > 2, # Multi-word queries
174
- not any(
175
- c in query for c in ["(", ")", "{", "}", "[", "]"]
176
- ), # Not code syntax
174
+ not any(c in query for c in ["(", ")", "{", "}", "[", "]"]), # Not code syntax
177
175
  " " in query, # Has spaces (natural language)
178
176
  not query.startswith("^") and not query.endswith("$"), # Not regex anchors
179
177
  ]
@@ -185,10 +183,7 @@ class UnifiedSearch(BaseTool):
185
183
  indicators = [
186
184
  "class " in query or "function " in query or "def " in query,
187
185
  "import " in query or "from " in query,
188
- any(
189
- kw in query.lower()
190
- for kw in ["method", "function", "class", "interface", "struct"]
191
- ),
186
+ any(kw in query.lower() for kw in ["method", "function", "class", "interface", "struct"]),
192
187
  "::" in query or "->" in query or "." in query, # Member access
193
188
  ]
194
189
  return any(indicators)
@@ -198,9 +193,7 @@ class UnifiedSearch(BaseTool):
198
193
  # Use symbol search for identifiers
199
194
  return (
200
195
  len(query.split()) <= 2 # Short queries
201
- and query.replace("_", "")
202
- .replace("-", "")
203
- .isalnum() # Looks like identifier
196
+ and query.replace("_", "").replace("-", "").isalnum() # Looks like identifier
204
197
  and not " " in query.strip() # Single token
205
198
  )
206
199
 
@@ -243,19 +236,14 @@ class UnifiedSearch(BaseTool):
243
236
  # Search memory for natural language queries or specific references
244
237
  search_memory = MEMORY_AVAILABLE and (
245
238
  self._should_use_vector_search(pattern)
246
- or any(
247
- word in pattern.lower()
248
- for word in ["previous", "discussion", "remember", "last"]
249
- )
239
+ or any(word in pattern.lower() for word in ["previous", "discussion", "remember", "last"])
250
240
  )
251
241
 
252
242
  if enable_text is None:
253
243
  enable_text = True # Always use text search as baseline
254
244
 
255
245
  if enable_vector is None:
256
- enable_vector = (
257
- self._should_use_vector_search(pattern) and VECTOR_SEARCH_AVAILABLE
258
- )
246
+ enable_vector = self._should_use_vector_search(pattern) and VECTOR_SEARCH_AVAILABLE
259
247
 
260
248
  if enable_ast is None:
261
249
  enable_ast = self._should_use_ast_search(pattern) and TREESITTER_AVAILABLE
@@ -277,9 +265,7 @@ class UnifiedSearch(BaseTool):
277
265
  # 1. Text search (ripgrep) - always fast, do first
278
266
  if enable_text:
279
267
  start = time.time()
280
- text_results = await self._text_search(
281
- pattern, path, include, exclude, max_results_per_type, context_lines
282
- )
268
+ text_results = await self._text_search(pattern, path, include, exclude, max_results_per_type, context_lines)
283
269
  search_stats["time_ms"]["text"] = int((time.time() - start) * 1000)
284
270
  search_stats["search_types_used"].append("text")
285
271
  all_results.extend(text_results)
@@ -287,9 +273,7 @@ class UnifiedSearch(BaseTool):
287
273
  # 2. AST search - for code structure
288
274
  if enable_ast and TREESITTER_AVAILABLE:
289
275
  start = time.time()
290
- ast_results = await self._ast_search(
291
- pattern, path, include, exclude, max_results_per_type, context_lines
292
- )
276
+ ast_results = await self._ast_search(pattern, path, include, exclude, max_results_per_type, context_lines)
293
277
  search_stats["time_ms"]["ast"] = int((time.time() - start) * 1000)
294
278
  search_stats["search_types_used"].append("ast")
295
279
  all_results.extend(ast_results)
@@ -297,9 +281,7 @@ class UnifiedSearch(BaseTool):
297
281
  # 3. Symbol search - for definitions
298
282
  if enable_symbol:
299
283
  start = time.time()
300
- symbol_results = await self._symbol_search(
301
- pattern, path, include, exclude, max_results_per_type
302
- )
284
+ symbol_results = await self._symbol_search(pattern, path, include, exclude, max_results_per_type)
303
285
  search_stats["time_ms"]["symbol"] = int((time.time() - start) * 1000)
304
286
  search_stats["search_types_used"].append("symbol")
305
287
  all_results.extend(symbol_results)
@@ -317,9 +299,7 @@ class UnifiedSearch(BaseTool):
317
299
  # 5. File search - for finding files by name/pattern
318
300
  if search_files:
319
301
  start = time.time()
320
- file_results = await self._file_search(
321
- pattern, path, include, exclude, max_results_per_type
322
- )
302
+ file_results = await self._file_search(pattern, path, include, exclude, max_results_per_type)
323
303
  search_stats["time_ms"]["files"] = int((time.time() - start) * 1000)
324
304
  search_stats["search_types_used"].append("files")
325
305
  all_results.extend(file_results)
@@ -327,9 +307,7 @@ class UnifiedSearch(BaseTool):
327
307
  # 6. Memory search - for knowledge base and previous discussions
328
308
  if search_memory:
329
309
  start = time.time()
330
- memory_results = await self._memory_search(
331
- pattern, max_results_per_type, context_lines
332
- )
310
+ memory_results = await self._memory_search(pattern, max_results_per_type, context_lines)
333
311
  search_stats["time_ms"]["memory"] = int((time.time() - start) * 1000)
334
312
  search_stats["search_types_used"].append("memory")
335
313
  all_results.extend(memory_results)
@@ -428,9 +406,7 @@ class UnifiedSearch(BaseTool):
428
406
 
429
407
  if not self.ripgrep_available:
430
408
  # Fallback to Python implementation
431
- return await self._python_text_search(
432
- pattern, path, include, exclude, max_results, context_lines
433
- )
409
+ return await self._python_text_search(pattern, path, include, exclude, max_results, context_lines)
434
410
 
435
411
  # Build ripgrep command
436
412
  cmd = ["rg", "--json", "--max-count", str(max_results)]
@@ -547,17 +523,9 @@ class UnifiedSearch(BaseTool):
547
523
  file_path=str(file_path),
548
524
  line_number=line_num,
549
525
  column=0,
550
- match_text=(
551
- lines[line_num - 1]
552
- if 0 < line_num <= len(lines)
553
- else ""
554
- ),
555
- context_before=lines[
556
- max(0, line_num - context_lines - 1) : line_num - 1
557
- ],
558
- context_after=lines[
559
- line_num : min(len(lines), line_num + context_lines)
560
- ],
526
+ match_text=(lines[line_num - 1] if 0 < line_num <= len(lines) else ""),
527
+ context_before=lines[max(0, line_num - context_lines - 1) : line_num - 1],
528
+ context_after=lines[line_num : min(len(lines), line_num + context_lines)],
561
529
  match_type="ast",
562
530
  score=0.9,
563
531
  node_type="ast_match",
@@ -650,10 +618,7 @@ class UnifiedSearch(BaseTool):
650
618
  context_before=[],
651
619
  context_after=[],
652
620
  match_type="vector",
653
- score=1.0
654
- - search_results["distances"][0][
655
- i
656
- ], # Convert distance to similarity
621
+ score=1.0 - search_results["distances"][0][i], # Convert distance to similarity
657
622
  semantic_context=metadata.get("context", ""),
658
623
  )
659
624
  results.append(result)
@@ -712,9 +677,7 @@ class UnifiedSearch(BaseTool):
712
677
 
713
678
  return results
714
679
 
715
- async def _memory_search(
716
- self, query: str, max_results: int, context_lines: int
717
- ) -> List[SearchResult]:
680
+ async def _memory_search(self, query: str, max_results: int, context_lines: int) -> List[SearchResult]:
718
681
  """Search in memory/knowledge base."""
719
682
  results = []
720
683
 
@@ -748,9 +711,7 @@ class UnifiedSearch(BaseTool):
748
711
  file_path=virtual_path,
749
712
  line_number=1,
750
713
  column=0,
751
- match_text=(
752
- content[:200] + "..." if len(content) > 200 else content
753
- ),
714
+ match_text=(content[:200] + "..." if len(content) > 200 else content),
754
715
  context_before=[],
755
716
  context_after=[],
756
717
  match_type="memory",
@@ -793,9 +754,7 @@ class UnifiedSearch(BaseTool):
793
754
 
794
755
  return unique
795
756
 
796
- def _rank_results(
797
- self, results: List[SearchResult], query: str
798
- ) -> List[SearchResult]:
757
+ def _rank_results(self, results: List[SearchResult], query: str) -> List[SearchResult]:
799
758
  """Rank results by relevance."""
800
759
  # Simple ranking based on:
801
760
  # 1. Match type score
@@ -808,16 +767,11 @@ class UnifiedSearch(BaseTool):
808
767
  result.score *= 1.2
809
768
 
810
769
  # Path relevance (prefer non-test, non-vendor files)
811
- if any(
812
- skip in result.file_path for skip in ["test", "vendor", "node_modules"]
813
- ):
770
+ if any(skip in result.file_path for skip in ["test", "vendor", "node_modules"]):
814
771
  result.score *= 0.8
815
772
 
816
773
  # Prefer definition files
817
- if any(
818
- pattern in result.file_path
819
- for pattern in ["index.", "main.", "api.", "types."]
820
- ):
774
+ if any(pattern in result.file_path for pattern in ["index.", "main.", "api.", "types."]):
821
775
  result.score *= 1.1
822
776
 
823
777
  # Sort by score descending, then by file path
@@ -955,16 +909,12 @@ class CodeIndexer:
955
909
  # Batch embed and store
956
910
  if documents:
957
911
  embeddings = self.embedder.encode(documents).tolist()
958
- self.collection.add(
959
- embeddings=embeddings, documents=documents, metadatas=metadatas, ids=ids
960
- )
912
+ self.collection.add(embeddings=embeddings, documents=documents, metadatas=metadatas, ids=ids)
961
913
 
962
- def _split_code_intelligently(
963
- self, content: str, file_path: Path
964
- ) -> List[Dict[str, Any]]:
914
+ def _split_code_intelligently(self, content: str, file_path: Path) -> List[Dict[str, Any]]:
965
915
  """Split code into meaningful chunks."""
966
- # Using line-based splitting for compatibility with all file types
967
- # AST parsing would only work for specific languages
916
+ # Simple line-based splitting for now
917
+ # TODO: Use AST for better splitting
968
918
  chunks = []
969
919
  lines = content.split("\n")
970
920
 
@@ -976,9 +926,7 @@ class CodeIndexer:
976
926
  current_chunk.append(line)
977
927
 
978
928
  # Split on function/class definitions or every 50 lines
979
- if len(current_chunk) >= 50 or any(
980
- kw in line for kw in ["def ", "function ", "class ", "interface "]
981
- ):
929
+ if len(current_chunk) >= 50 or any(kw in line for kw in ["def ", "function ", "class ", "interface "]):
982
930
  if current_chunk:
983
931
  chunks.append(
984
932
  {
@@ -992,9 +940,7 @@ class CodeIndexer:
992
940
 
993
941
  # Add remaining
994
942
  if current_chunk:
995
- chunks.append(
996
- {"text": "\n".join(current_chunk), "line": current_line, "type": "code"}
997
- )
943
+ chunks.append({"text": "\n".join(current_chunk), "line": current_line, "type": "code"})
998
944
 
999
945
  return chunks
1000
946
 
@@ -13,6 +13,7 @@ from hanzo_mcp.tools.shell.zsh_tool import zsh_tool, shell_tool
13
13
 
14
14
  # Import tools
15
15
  from hanzo_mcp.tools.shell.bash_tool import bash_tool
16
+ from hanzo_mcp.tools.shell.run_tool import run_tool
16
17
  from hanzo_mcp.tools.common.permissions import PermissionManager
17
18
  from hanzo_mcp.tools.shell.process_tool import process_tool
18
19
 
@@ -40,17 +41,19 @@ def get_shell_tools(
40
41
  bash_tool.permission_manager = permission_manager
41
42
  zsh_tool.permission_manager = permission_manager
42
43
  shell_tool.permission_manager = permission_manager
44
+ run_tool.permission_manager = permission_manager
43
45
  npx_tool.permission_manager = permission_manager
44
46
  uvx_tool.permission_manager = permission_manager
45
47
 
46
48
  # Note: StreamingCommandTool is abstract and shouldn't be instantiated directly
47
49
  # It's used as a base class for other streaming tools
48
50
 
49
- # Return shell_tool first (smart default), then specific shells
51
+ # Return run_tool first (simplified command execution), then shell_tool (smart default), then specific shells
50
52
  return [
53
+ run_tool, # Simplified run command with auto-backgrounding
51
54
  shell_tool, # Smart shell (prefers zsh if available)
52
- zsh_tool, # Explicit zsh
53
- bash_tool, # Explicit bash
55
+ zsh_tool, # Explicit zsh
56
+ bash_tool, # Explicit bash
54
57
  npx_tool,
55
58
  uvx_tool,
56
59
  process_tool,
@@ -62,16 +65,25 @@ def get_shell_tools(
62
65
  def register_shell_tools(
63
66
  mcp_server: FastMCP,
64
67
  permission_manager: PermissionManager,
68
+ enabled_tools: dict[str, bool] | None = None,
65
69
  ) -> list[BaseTool]:
66
70
  """Register all shell tools with the MCP server.
67
71
 
68
72
  Args:
69
73
  mcp_server: The FastMCP server instance
70
74
  permission_manager: Permission manager for access control
75
+ enabled_tools: Optional dict of tool names to enable/disable
71
76
 
72
77
  Returns:
73
78
  List of registered tools
74
79
  """
75
- tools = get_shell_tools(permission_manager)
80
+ all_tools = get_shell_tools(permission_manager)
81
+
82
+ # Filter tools based on enabled_tools if provided
83
+ if enabled_tools is not None:
84
+ tools = [tool for tool in all_tools if enabled_tools.get(tool.name, True)]
85
+ else:
86
+ tools = all_tools
87
+
76
88
  ToolRegistry.register_tools(mcp_server, tools)
77
89
  return tools
@@ -19,9 +19,7 @@ class AutoBackgroundExecutor:
19
19
  # Default timeout before auto-backgrounding (2 minutes)
20
20
  DEFAULT_TIMEOUT = 120.0
21
21
 
22
- def __init__(
23
- self, process_manager: ProcessManager, timeout: float = DEFAULT_TIMEOUT
24
- ):
22
+ def __init__(self, process_manager: ProcessManager, timeout: float = DEFAULT_TIMEOUT):
25
23
  """Initialize the auto-background executor.
26
24
 
27
25
  Args:
@@ -148,9 +146,7 @@ class AutoBackgroundExecutor:
148
146
  task.cancel()
149
147
 
150
148
  # Continue reading output in background
151
- asyncio.create_task(
152
- self._background_reader(process, process_id, log_file)
153
- )
149
+ asyncio.create_task(self._background_reader(process, process_id, log_file))
154
150
 
155
151
  # Return status message
156
152
  elapsed = time.time() - start_time
@@ -63,11 +63,7 @@ class CommandResult:
63
63
  Returns:
64
64
  True if the command succeeded, False otherwise
65
65
  """
66
- return (
67
- self.return_code == 0
68
- and self.status == BashCommandStatus.COMPLETED
69
- and not self.error_message
70
- )
66
+ return self.return_code == 0 and self.status == BashCommandStatus.COMPLETED and not self.error_message
71
67
 
72
68
  @property
73
69
  def is_running(self) -> bool:
@@ -171,13 +171,11 @@ class BaseProcessTool(BaseTool):
171
171
  process_env.update(env)
172
172
 
173
173
  # Execute with auto-backgrounding
174
- output, was_backgrounded, process_id = (
175
- await self.auto_background_executor.execute_with_auto_background(
176
- cmd_args=cmd_args,
177
- tool_name=self.get_tool_name(),
178
- cwd=cwd,
179
- env=process_env,
180
- )
174
+ output, was_backgrounded, process_id = await self.auto_background_executor.execute_with_auto_background(
175
+ cmd_args=cmd_args,
176
+ tool_name=self.get_tool_name(),
177
+ cwd=cwd,
178
+ env=process_env,
181
179
  )
182
180
 
183
181
  if was_backgrounded: