hanzo-mcp 0.5.2__py3-none-any.whl → 0.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

Files changed (114) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +32 -0
  3. hanzo_mcp/dev_server.py +246 -0
  4. hanzo_mcp/prompts/__init__.py +1 -1
  5. hanzo_mcp/prompts/project_system.py +43 -7
  6. hanzo_mcp/server.py +5 -1
  7. hanzo_mcp/tools/__init__.py +66 -35
  8. hanzo_mcp/tools/agent/__init__.py +1 -1
  9. hanzo_mcp/tools/agent/agent.py +401 -0
  10. hanzo_mcp/tools/agent/agent_tool.py +3 -4
  11. hanzo_mcp/tools/common/__init__.py +1 -1
  12. hanzo_mcp/tools/common/base.py +2 -2
  13. hanzo_mcp/tools/common/batch_tool.py +3 -5
  14. hanzo_mcp/tools/common/config_tool.py +1 -1
  15. hanzo_mcp/tools/common/context.py +1 -1
  16. hanzo_mcp/tools/common/palette.py +344 -0
  17. hanzo_mcp/tools/common/palette_loader.py +108 -0
  18. hanzo_mcp/tools/common/stats.py +1 -1
  19. hanzo_mcp/tools/common/thinking_tool.py +3 -5
  20. hanzo_mcp/tools/common/tool_disable.py +1 -1
  21. hanzo_mcp/tools/common/tool_enable.py +1 -1
  22. hanzo_mcp/tools/common/tool_list.py +49 -52
  23. hanzo_mcp/tools/config/__init__.py +10 -0
  24. hanzo_mcp/tools/config/config_tool.py +212 -0
  25. hanzo_mcp/tools/config/index_config.py +176 -0
  26. hanzo_mcp/tools/config/palette_tool.py +166 -0
  27. hanzo_mcp/tools/database/__init__.py +1 -1
  28. hanzo_mcp/tools/database/graph.py +482 -0
  29. hanzo_mcp/tools/database/graph_add.py +1 -1
  30. hanzo_mcp/tools/database/graph_query.py +1 -1
  31. hanzo_mcp/tools/database/graph_remove.py +1 -1
  32. hanzo_mcp/tools/database/graph_search.py +1 -1
  33. hanzo_mcp/tools/database/graph_stats.py +1 -1
  34. hanzo_mcp/tools/database/sql.py +411 -0
  35. hanzo_mcp/tools/database/sql_query.py +1 -1
  36. hanzo_mcp/tools/database/sql_search.py +1 -1
  37. hanzo_mcp/tools/database/sql_stats.py +1 -1
  38. hanzo_mcp/tools/editor/neovim_command.py +1 -1
  39. hanzo_mcp/tools/editor/neovim_edit.py +1 -1
  40. hanzo_mcp/tools/editor/neovim_session.py +1 -1
  41. hanzo_mcp/tools/filesystem/__init__.py +42 -13
  42. hanzo_mcp/tools/filesystem/base.py +1 -1
  43. hanzo_mcp/tools/filesystem/batch_search.py +4 -4
  44. hanzo_mcp/tools/filesystem/content_replace.py +3 -5
  45. hanzo_mcp/tools/filesystem/diff.py +193 -0
  46. hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
  47. hanzo_mcp/tools/filesystem/edit.py +3 -5
  48. hanzo_mcp/tools/filesystem/find.py +443 -0
  49. hanzo_mcp/tools/filesystem/find_files.py +1 -1
  50. hanzo_mcp/tools/filesystem/git_search.py +1 -1
  51. hanzo_mcp/tools/filesystem/grep.py +2 -2
  52. hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
  53. hanzo_mcp/tools/filesystem/read.py +17 -5
  54. hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
  55. hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
  56. hanzo_mcp/tools/filesystem/tree.py +268 -0
  57. hanzo_mcp/tools/filesystem/unified_search.py +711 -0
  58. hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
  59. hanzo_mcp/tools/filesystem/watch.py +174 -0
  60. hanzo_mcp/tools/filesystem/write.py +3 -5
  61. hanzo_mcp/tools/jupyter/__init__.py +9 -12
  62. hanzo_mcp/tools/jupyter/base.py +1 -1
  63. hanzo_mcp/tools/jupyter/jupyter.py +326 -0
  64. hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
  65. hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
  66. hanzo_mcp/tools/llm/__init__.py +4 -0
  67. hanzo_mcp/tools/llm/consensus_tool.py +1 -1
  68. hanzo_mcp/tools/llm/llm_manage.py +1 -1
  69. hanzo_mcp/tools/llm/llm_tool.py +1 -1
  70. hanzo_mcp/tools/llm/llm_unified.py +851 -0
  71. hanzo_mcp/tools/llm/provider_tools.py +1 -1
  72. hanzo_mcp/tools/mcp/__init__.py +4 -0
  73. hanzo_mcp/tools/mcp/mcp_add.py +1 -1
  74. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  75. hanzo_mcp/tools/mcp/mcp_stats.py +1 -1
  76. hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
  77. hanzo_mcp/tools/shell/__init__.py +20 -42
  78. hanzo_mcp/tools/shell/base.py +1 -1
  79. hanzo_mcp/tools/shell/base_process.py +303 -0
  80. hanzo_mcp/tools/shell/bash_unified.py +134 -0
  81. hanzo_mcp/tools/shell/logs.py +1 -1
  82. hanzo_mcp/tools/shell/npx.py +1 -1
  83. hanzo_mcp/tools/shell/npx_background.py +1 -1
  84. hanzo_mcp/tools/shell/npx_unified.py +101 -0
  85. hanzo_mcp/tools/shell/open.py +107 -0
  86. hanzo_mcp/tools/shell/pkill.py +1 -1
  87. hanzo_mcp/tools/shell/process_unified.py +131 -0
  88. hanzo_mcp/tools/shell/processes.py +1 -1
  89. hanzo_mcp/tools/shell/run_background.py +1 -1
  90. hanzo_mcp/tools/shell/run_command.py +3 -4
  91. hanzo_mcp/tools/shell/run_command_windows.py +3 -4
  92. hanzo_mcp/tools/shell/uvx.py +1 -1
  93. hanzo_mcp/tools/shell/uvx_background.py +1 -1
  94. hanzo_mcp/tools/shell/uvx_unified.py +101 -0
  95. hanzo_mcp/tools/todo/__init__.py +1 -1
  96. hanzo_mcp/tools/todo/base.py +1 -1
  97. hanzo_mcp/tools/todo/todo.py +265 -0
  98. hanzo_mcp/tools/todo/todo_read.py +3 -5
  99. hanzo_mcp/tools/todo/todo_write.py +3 -5
  100. hanzo_mcp/tools/vector/__init__.py +1 -1
  101. hanzo_mcp/tools/vector/index_tool.py +1 -1
  102. hanzo_mcp/tools/vector/project_manager.py +27 -5
  103. hanzo_mcp/tools/vector/vector.py +311 -0
  104. hanzo_mcp/tools/vector/vector_index.py +1 -1
  105. hanzo_mcp/tools/vector/vector_search.py +1 -1
  106. hanzo_mcp-0.6.2.dist-info/METADATA +336 -0
  107. hanzo_mcp-0.6.2.dist-info/RECORD +134 -0
  108. hanzo_mcp-0.6.2.dist-info/entry_points.txt +3 -0
  109. hanzo_mcp-0.5.2.dist-info/METADATA +0 -276
  110. hanzo_mcp-0.5.2.dist-info/RECORD +0 -106
  111. hanzo_mcp-0.5.2.dist-info/entry_points.txt +0 -2
  112. {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.2.dist-info}/WHEEL +0 -0
  113. {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.2.dist-info}/licenses/LICENSE +0 -0
  114. {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.2.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,7 @@ import asyncio
8
8
  from concurrent.futures import ThreadPoolExecutor
9
9
 
10
10
  from .infinity_store import InfinityVectorStore, SearchResult
11
+ from hanzo_mcp.tools.config.index_config import IndexConfig, IndexScope
11
12
 
12
13
 
13
14
  @dataclass
@@ -38,12 +39,14 @@ class ProjectVectorManager:
38
39
  self.embedding_model = embedding_model
39
40
  self.dimension = dimension
40
41
 
42
+ # Set up index configuration
43
+ self.index_config = IndexConfig()
44
+
41
45
  # Set up global database path
42
46
  if global_db_path:
43
47
  self.global_db_path = Path(global_db_path)
44
48
  else:
45
- from hanzo_mcp.config.settings import get_config_dir
46
- self.global_db_path = get_config_dir() / "db"
49
+ self.global_db_path = self.index_config.get_index_path("vector")
47
50
 
48
51
  self.global_db_path.mkdir(parents=True, exist_ok=True)
49
52
 
@@ -158,14 +161,25 @@ class ProjectVectorManager:
158
161
  Returns:
159
162
  Vector store instance
160
163
  """
161
- if project_info is None:
164
+ # Check indexing scope
165
+ if project_info:
166
+ scope = self.index_config.get_scope(str(project_info.root_path))
167
+ if scope == IndexScope.GLOBAL:
168
+ # Even for project files, use global store if configured
169
+ return self._get_global_store()
170
+ else:
162
171
  return self._get_global_store()
163
172
 
173
+ # Use project-specific store
164
174
  project_key = str(project_info.root_path)
165
175
 
166
176
  if project_key not in self.vector_stores:
177
+ # Get index path based on configuration
178
+ index_path = self.index_config.get_index_path("vector", str(project_info.root_path))
179
+ index_path.mkdir(parents=True, exist_ok=True)
180
+
167
181
  self.vector_stores[project_key] = InfinityVectorStore(
168
- data_path=str(project_info.db_path),
182
+ data_path=str(index_path),
169
183
  embedding_model=self.embedding_model,
170
184
  dimension=self.dimension,
171
185
  )
@@ -190,10 +204,14 @@ class ProjectVectorManager:
190
204
  Returns:
191
205
  Tuple of (document IDs, project info or None for global)
192
206
  """
207
+ # Check if indexing is enabled
208
+ if not self.index_config.is_indexing_enabled("vector"):
209
+ return [], None
210
+
193
211
  # Find project for this file
194
212
  project_info = self.get_project_for_path(file_path)
195
213
 
196
- # Get appropriate vector store
214
+ # Get appropriate vector store based on scope configuration
197
215
  vector_store = self.get_vector_store(project_info)
198
216
 
199
217
  # Add file metadata
@@ -201,8 +219,12 @@ class ProjectVectorManager:
201
219
  if project_info:
202
220
  file_metadata["project_name"] = project_info.name
203
221
  file_metadata["project_root"] = str(project_info.root_path)
222
+ # Check actual scope used
223
+ scope = self.index_config.get_scope(str(project_info.root_path))
224
+ file_metadata["index_scope"] = scope.value
204
225
  else:
205
226
  file_metadata["project_name"] = "global"
227
+ file_metadata["index_scope"] = "global"
206
228
 
207
229
  # Add file to store
208
230
  doc_ids = vector_store.add_file(
@@ -0,0 +1,311 @@
1
+ """Unified vector store tool."""
2
+
3
+ from typing import Annotated, TypedDict, Unpack, final, override, Optional, List, Dict, Any
4
+ from pathlib import Path
5
+
6
+ from mcp.server.fastmcp import Context as MCPContext
7
+ from pydantic import Field
8
+
9
+ from hanzo_mcp.tools.common.base import BaseTool
10
+ from hanzo_mcp.tools.common.permissions import PermissionManager
11
+ from hanzo_mcp.tools.vector.project_manager import ProjectVectorManager
12
+
13
+
14
+ # Parameter types
15
+ Action = Annotated[
16
+ str,
17
+ Field(
18
+ description="Action: search (default), index, stats, clear",
19
+ default="search",
20
+ ),
21
+ ]
22
+
23
+ Query = Annotated[
24
+ Optional[str],
25
+ Field(
26
+ description="Search query for semantic similarity",
27
+ default=None,
28
+ ),
29
+ ]
30
+
31
+ Path = Annotated[
32
+ Optional[str],
33
+ Field(
34
+ description="Path to index or search within",
35
+ default=".",
36
+ ),
37
+ ]
38
+
39
+ Include = Annotated[
40
+ Optional[str],
41
+ Field(
42
+ description="File pattern to include (e.g., '*.py')",
43
+ default=None,
44
+ ),
45
+ ]
46
+
47
+ Exclude = Annotated[
48
+ Optional[str],
49
+ Field(
50
+ description="File pattern to exclude",
51
+ default=None,
52
+ ),
53
+ ]
54
+
55
+ Limit = Annotated[
56
+ int,
57
+ Field(
58
+ description="Maximum results to return",
59
+ default=10,
60
+ ),
61
+ ]
62
+
63
+ IncludeGit = Annotated[
64
+ bool,
65
+ Field(
66
+ description="Include git history in indexing",
67
+ default=True,
68
+ ),
69
+ ]
70
+
71
+ ForceReindex = Annotated[
72
+ bool,
73
+ Field(
74
+ description="Force reindexing even if up to date",
75
+ default=False,
76
+ ),
77
+ ]
78
+
79
+
80
+ class VectorParams(TypedDict, total=False):
81
+ """Parameters for vector tool."""
82
+ action: str
83
+ query: Optional[str]
84
+ path: Optional[str]
85
+ include: Optional[str]
86
+ exclude: Optional[str]
87
+ limit: int
88
+ include_git: bool
89
+ force_reindex: bool
90
+
91
+
92
+ @final
93
+ class VectorTool(BaseTool):
94
+ """Unified vector store tool for semantic search."""
95
+
96
+ def __init__(self, permission_manager: PermissionManager, project_manager: ProjectVectorManager):
97
+ """Initialize the vector tool."""
98
+ super().__init__(permission_manager)
99
+ self.project_manager = project_manager
100
+
101
+ @property
102
+ @override
103
+ def name(self) -> str:
104
+ """Get the tool name."""
105
+ return "vector"
106
+
107
+ @property
108
+ @override
109
+ def description(self) -> str:
110
+ """Get the tool description."""
111
+ return """Semantic search with embeddings. Actions: search (default), index, stats, clear.
112
+
113
+ Usage:
114
+ vector "find authentication logic"
115
+ vector --action index --path ./src --include "*.py"
116
+ vector --action stats
117
+ vector --action clear --path ./old_code
118
+ """
119
+
120
+ @override
121
+ async def call(
122
+ self,
123
+ ctx: MCPContext,
124
+ **params: Unpack[VectorParams],
125
+ ) -> str:
126
+ """Execute vector operation."""
127
+ tool_ctx = self.create_tool_context(ctx)
128
+
129
+ # Extract action
130
+ action = params.get("action", "search")
131
+
132
+ # Route to appropriate handler
133
+ if action == "search":
134
+ return await self._handle_search(params, tool_ctx)
135
+ elif action == "index":
136
+ return await self._handle_index(params, tool_ctx)
137
+ elif action == "stats":
138
+ return await self._handle_stats(params, tool_ctx)
139
+ elif action == "clear":
140
+ return await self._handle_clear(params, tool_ctx)
141
+ else:
142
+ return f"Error: Unknown action '{action}'. Valid actions: search, index, stats, clear"
143
+
144
+ async def _handle_search(self, params: Dict[str, Any], tool_ctx) -> str:
145
+ """Handle semantic search."""
146
+ query = params.get("query")
147
+ if not query:
148
+ return "Error: query is required for search action"
149
+
150
+ path = params.get("path", ".")
151
+ limit = params.get("limit", 10)
152
+
153
+ # Validate path
154
+ allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
155
+ if not allowed:
156
+ return error_msg
157
+
158
+ try:
159
+ # Determine search scope
160
+ project = self.project_manager.get_project_for_path(path)
161
+ if not project:
162
+ return "Error: No indexed project found for this path. Run 'vector --action index' first."
163
+
164
+ # Search
165
+ await tool_ctx.info(f"Searching for: {query}")
166
+ results = project.search(query, k=limit)
167
+
168
+ if not results:
169
+ return f"No results found for: {query}"
170
+
171
+ # Format results
172
+ output = [f"=== Vector Search Results for '{query}' ==="]
173
+ output.append(f"Found {len(results)} matches\n")
174
+
175
+ for i, result in enumerate(results, 1):
176
+ score = result.get("score", 0)
177
+ file_path = result.get("file_path", "unknown")
178
+ content = result.get("content", "")
179
+ chunk_type = result.get("metadata", {}).get("type", "content")
180
+
181
+ output.append(f"Result {i} - Score: {score:.1%}")
182
+ output.append(f"File: {file_path}")
183
+ if chunk_type != "content":
184
+ output.append(f"Type: {chunk_type}")
185
+ output.append("-" * 60)
186
+
187
+ # Truncate content if too long
188
+ if len(content) > 300:
189
+ content = content[:300] + "..."
190
+ output.append(content)
191
+ output.append("")
192
+
193
+ return "\n".join(output)
194
+
195
+ except Exception as e:
196
+ await tool_ctx.error(f"Search failed: {str(e)}")
197
+ return f"Error during search: {str(e)}"
198
+
199
+ async def _handle_index(self, params: Dict[str, Any], tool_ctx) -> str:
200
+ """Handle indexing files."""
201
+ path = params.get("path", ".")
202
+ include = params.get("include")
203
+ exclude = params.get("exclude")
204
+ include_git = params.get("include_git", True)
205
+ force = params.get("force_reindex", False)
206
+
207
+ # Validate path
208
+ allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
209
+ if not allowed:
210
+ return error_msg
211
+
212
+ try:
213
+ await tool_ctx.info(f"Indexing {path}...")
214
+
215
+ # Get or create project
216
+ project = self.project_manager.get_or_create_project(path)
217
+
218
+ # Index files
219
+ stats = await project.index_directory(
220
+ path,
221
+ include_pattern=include,
222
+ exclude_pattern=exclude,
223
+ force_reindex=force
224
+ )
225
+
226
+ # Index git history if requested
227
+ if include_git and Path(path).joinpath(".git").exists():
228
+ await tool_ctx.info("Indexing git history...")
229
+ git_stats = await project.index_git_history(path)
230
+ stats["git_commits"] = git_stats.get("commits_indexed", 0)
231
+
232
+ # Format output
233
+ output = [f"=== Indexing Complete ==="]
234
+ output.append(f"Path: {path}")
235
+ output.append(f"Files indexed: {stats.get('files_indexed', 0)}")
236
+ output.append(f"Chunks created: {stats.get('chunks_created', 0)}")
237
+ if stats.get("git_commits"):
238
+ output.append(f"Git commits indexed: {stats['git_commits']}")
239
+ output.append(f"Total documents: {project.get_stats().get('total_documents', 0)}")
240
+
241
+ return "\n".join(output)
242
+
243
+ except Exception as e:
244
+ await tool_ctx.error(f"Indexing failed: {str(e)}")
245
+ return f"Error during indexing: {str(e)}"
246
+
247
+ async def _handle_stats(self, params: Dict[str, Any], tool_ctx) -> str:
248
+ """Get vector store statistics."""
249
+ path = params.get("path")
250
+
251
+ try:
252
+ if path:
253
+ # Stats for specific project
254
+ project = self.project_manager.get_project_for_path(path)
255
+ if not project:
256
+ return f"No indexed project found for path: {path}"
257
+
258
+ stats = project.get_stats()
259
+ output = [f"=== Vector Store Stats for {project.name} ==="]
260
+ else:
261
+ # Global stats
262
+ stats = self.project_manager.get_global_stats()
263
+ output = ["=== Global Vector Store Stats ==="]
264
+
265
+ output.append(f"Total documents: {stats.get('total_documents', 0)}")
266
+ output.append(f"Total size: {stats.get('total_size_mb', 0):.1f} MB")
267
+
268
+ if stats.get("projects"):
269
+ output.append(f"\nProjects indexed: {len(stats['projects'])}")
270
+ for proj in stats["projects"]:
271
+ output.append(f" - {proj['name']}: {proj['documents']} docs, {proj['size_mb']:.1f} MB")
272
+
273
+ return "\n".join(output)
274
+
275
+ except Exception as e:
276
+ await tool_ctx.error(f"Failed to get stats: {str(e)}")
277
+ return f"Error getting stats: {str(e)}"
278
+
279
+ async def _handle_clear(self, params: Dict[str, Any], tool_ctx) -> str:
280
+ """Clear vector store."""
281
+ path = params.get("path")
282
+
283
+ if not path:
284
+ return "Error: path is required for clear action"
285
+
286
+ # Validate path
287
+ allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
288
+ if not allowed:
289
+ return error_msg
290
+
291
+ try:
292
+ project = self.project_manager.get_project_for_path(path)
293
+ if not project:
294
+ return f"No indexed project found for path: {path}"
295
+
296
+ # Get stats before clearing
297
+ stats = project.get_stats()
298
+ doc_count = stats.get("total_documents", 0)
299
+
300
+ # Clear
301
+ project.clear()
302
+
303
+ return f"Cleared {doc_count} documents from vector store for {project.name}"
304
+
305
+ except Exception as e:
306
+ await tool_ctx.error(f"Failed to clear: {str(e)}")
307
+ return f"Error clearing vector store: {str(e)}"
308
+
309
+ def register(self, mcp_server) -> None:
310
+ """Register this tool with the MCP server."""
311
+ pass
@@ -3,7 +3,7 @@
3
3
  from typing import Dict, List, Optional, TypedDict, Unpack, final
4
4
  from pathlib import Path
5
5
 
6
- from fastmcp import Context as MCPContext
6
+ from mcp.server.fastmcp import Context as MCPContext
7
7
  from pydantic import Field
8
8
 
9
9
  from hanzo_mcp.tools.common.base import BaseTool
@@ -4,7 +4,7 @@ from typing import Dict, List, Optional, TypedDict, Unpack, final
4
4
  import json
5
5
  import asyncio
6
6
 
7
- from fastmcp import Context as MCPContext
7
+ from mcp.server.fastmcp import Context as MCPContext
8
8
  from pydantic import Field
9
9
 
10
10
  from hanzo_mcp.tools.common.base import BaseTool