hanzo-mcp 0.5.1__py3-none-any.whl → 0.6.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 (118) 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 +168 -6
  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 +9 -4
  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 +261 -0
  19. hanzo_mcp/tools/common/thinking_tool.py +3 -5
  20. hanzo_mcp/tools/common/tool_disable.py +144 -0
  21. hanzo_mcp/tools/common/tool_enable.py +182 -0
  22. hanzo_mcp/tools/common/tool_list.py +260 -0
  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 +71 -0
  28. hanzo_mcp/tools/database/database_manager.py +246 -0
  29. hanzo_mcp/tools/database/graph.py +482 -0
  30. hanzo_mcp/tools/database/graph_add.py +257 -0
  31. hanzo_mcp/tools/database/graph_query.py +536 -0
  32. hanzo_mcp/tools/database/graph_remove.py +267 -0
  33. hanzo_mcp/tools/database/graph_search.py +348 -0
  34. hanzo_mcp/tools/database/graph_stats.py +345 -0
  35. hanzo_mcp/tools/database/sql.py +411 -0
  36. hanzo_mcp/tools/database/sql_query.py +229 -0
  37. hanzo_mcp/tools/database/sql_search.py +296 -0
  38. hanzo_mcp/tools/database/sql_stats.py +254 -0
  39. hanzo_mcp/tools/editor/__init__.py +11 -0
  40. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  41. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  42. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  43. hanzo_mcp/tools/filesystem/__init__.py +52 -13
  44. hanzo_mcp/tools/filesystem/base.py +1 -1
  45. hanzo_mcp/tools/filesystem/batch_search.py +812 -0
  46. hanzo_mcp/tools/filesystem/content_replace.py +3 -5
  47. hanzo_mcp/tools/filesystem/diff.py +193 -0
  48. hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
  49. hanzo_mcp/tools/filesystem/edit.py +3 -5
  50. hanzo_mcp/tools/filesystem/find.py +443 -0
  51. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  52. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  53. hanzo_mcp/tools/filesystem/grep.py +2 -2
  54. hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
  55. hanzo_mcp/tools/filesystem/read.py +17 -5
  56. hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
  57. hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
  58. hanzo_mcp/tools/filesystem/tree.py +268 -0
  59. hanzo_mcp/tools/filesystem/unified_search.py +465 -443
  60. hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
  61. hanzo_mcp/tools/filesystem/watch.py +174 -0
  62. hanzo_mcp/tools/filesystem/write.py +3 -5
  63. hanzo_mcp/tools/jupyter/__init__.py +9 -12
  64. hanzo_mcp/tools/jupyter/base.py +1 -1
  65. hanzo_mcp/tools/jupyter/jupyter.py +326 -0
  66. hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
  67. hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
  68. hanzo_mcp/tools/llm/__init__.py +31 -0
  69. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  70. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  71. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  72. hanzo_mcp/tools/llm/llm_unified.py +851 -0
  73. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  74. hanzo_mcp/tools/mcp/__init__.py +15 -0
  75. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  76. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  77. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  78. hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
  79. hanzo_mcp/tools/shell/__init__.py +21 -23
  80. hanzo_mcp/tools/shell/base.py +1 -1
  81. hanzo_mcp/tools/shell/base_process.py +303 -0
  82. hanzo_mcp/tools/shell/bash_unified.py +134 -0
  83. hanzo_mcp/tools/shell/logs.py +265 -0
  84. hanzo_mcp/tools/shell/npx.py +194 -0
  85. hanzo_mcp/tools/shell/npx_background.py +254 -0
  86. hanzo_mcp/tools/shell/npx_unified.py +101 -0
  87. hanzo_mcp/tools/shell/open.py +107 -0
  88. hanzo_mcp/tools/shell/pkill.py +262 -0
  89. hanzo_mcp/tools/shell/process_unified.py +131 -0
  90. hanzo_mcp/tools/shell/processes.py +279 -0
  91. hanzo_mcp/tools/shell/run_background.py +326 -0
  92. hanzo_mcp/tools/shell/run_command.py +3 -4
  93. hanzo_mcp/tools/shell/run_command_windows.py +3 -4
  94. hanzo_mcp/tools/shell/uvx.py +187 -0
  95. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  96. hanzo_mcp/tools/shell/uvx_unified.py +101 -0
  97. hanzo_mcp/tools/todo/__init__.py +1 -1
  98. hanzo_mcp/tools/todo/base.py +1 -1
  99. hanzo_mcp/tools/todo/todo.py +265 -0
  100. hanzo_mcp/tools/todo/todo_read.py +3 -5
  101. hanzo_mcp/tools/todo/todo_write.py +3 -5
  102. hanzo_mcp/tools/vector/__init__.py +6 -1
  103. hanzo_mcp/tools/vector/git_ingester.py +3 -0
  104. hanzo_mcp/tools/vector/index_tool.py +358 -0
  105. hanzo_mcp/tools/vector/infinity_store.py +98 -0
  106. hanzo_mcp/tools/vector/project_manager.py +27 -5
  107. hanzo_mcp/tools/vector/vector.py +311 -0
  108. hanzo_mcp/tools/vector/vector_index.py +1 -1
  109. hanzo_mcp/tools/vector/vector_search.py +12 -7
  110. hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
  111. hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
  112. hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
  113. hanzo_mcp-0.5.1.dist-info/METADATA +0 -276
  114. hanzo_mcp-0.5.1.dist-info/RECORD +0 -68
  115. hanzo_mcp-0.5.1.dist-info/entry_points.txt +0 -2
  116. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
  117. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
  118. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,260 @@
1
+ """List all available tools and their status."""
2
+
3
+ from typing import Annotated, TypedDict, Unpack, final, override, Optional
4
+
5
+ from mcp.server.fastmcp import Context as MCPContext
6
+ from pydantic import Field
7
+
8
+ from hanzo_mcp.tools.common.base import BaseTool
9
+ from hanzo_mcp.tools.common.context import create_tool_context
10
+ from hanzo_mcp.tools.common.tool_enable import ToolEnableTool
11
+
12
+
13
+ ShowDisabled = Annotated[
14
+ bool,
15
+ Field(
16
+ description="Show only disabled tools",
17
+ default=False,
18
+ ),
19
+ ]
20
+
21
+ ShowEnabled = Annotated[
22
+ bool,
23
+ Field(
24
+ description="Show only enabled tools",
25
+ default=False,
26
+ ),
27
+ ]
28
+
29
+ Category = Annotated[
30
+ Optional[str],
31
+ Field(
32
+ description="Filter by category (filesystem, shell, database, etc.)",
33
+ default=None,
34
+ ),
35
+ ]
36
+
37
+
38
+ class ToolListParams(TypedDict, total=False):
39
+ """Parameters for tool list."""
40
+
41
+ show_disabled: bool
42
+ show_enabled: bool
43
+ category: Optional[str]
44
+
45
+
46
+ @final
47
+ class ToolListTool(BaseTool):
48
+ """Tool for listing all available tools and their status."""
49
+
50
+ # Tool information organized by category
51
+ TOOL_INFO = {
52
+ "filesystem": [
53
+ ("read", "Read contents of files"),
54
+ ("write", "Write contents to files"),
55
+ ("edit", "Edit specific parts of files"),
56
+ ("multi_edit", "Make multiple edits to a file"),
57
+ ("tree", "Directory tree visualization (Unix-style)"),
58
+ ("find", "Find text in files (rg/ag/ack/grep)"),
59
+ ("symbols", "Code symbols search with tree-sitter"),
60
+ ("search", "Unified search (parallel grep/symbols/vector/git)"),
61
+ ("git_search", "Search git history"),
62
+ ("glob", "Find files by name pattern"),
63
+ ("content_replace", "Replace content across files"),
64
+ ],
65
+ "shell": [
66
+ ("run_command", "Execute shell commands (--background option)"),
67
+ ("processes", "List background processes"),
68
+ ("pkill", "Kill background processes"),
69
+ ("logs", "View process logs"),
70
+ ("uvx", "Run Python packages (--background option)"),
71
+ ("npx", "Run Node.js packages (--background option)"),
72
+ ],
73
+ "database": [
74
+ ("sql", "SQLite operations (query/search/schema/stats)"),
75
+ ("graph", "Graph database (query/add/remove/search/stats)"),
76
+ ("vector", "Semantic search (search/index/stats/clear)"),
77
+ ],
78
+ "ai": [
79
+ ("llm", "LLM interface (query/consensus/list/models/enable/disable)"),
80
+ ("agent", "AI agents (run/start/call/stop/list with A2A support)"),
81
+ ("mcp", "MCP servers (list/add/remove/enable/disable/restart)"),
82
+ ],
83
+ "config": [
84
+ ("config", "Git-style configuration (get/set/list/toggle)"),
85
+ ("tool_enable", "Enable tools"),
86
+ ("tool_disable", "Disable tools"),
87
+ ("tool_list", "List all tools (this tool)"),
88
+ ],
89
+ "productivity": [
90
+ ("todo", "Todo management (list/add/update/remove/clear)"),
91
+ ("jupyter", "Jupyter notebooks (read/edit/create/delete/execute)"),
92
+ ("think", "Structured thinking space"),
93
+ ],
94
+ "system": [
95
+ ("stats", "System and resource statistics"),
96
+ ("batch", "Run multiple tools in parallel"),
97
+ ],
98
+ "legacy": [
99
+ ("directory_tree", "Legacy: Use 'tree' instead"),
100
+ ("grep", "Legacy: Use 'find' instead"),
101
+ ("grep_ast", "Legacy: Use 'symbols' instead"),
102
+ ("batch_search", "Legacy: Use 'search' instead"),
103
+ ("find_files", "Legacy: Use 'glob' instead"),
104
+ ("run_background", "Legacy: Use 'run_command --background'"),
105
+ ("uvx_background", "Legacy: Use 'uvx --background'"),
106
+ ("npx_background", "Legacy: Use 'npx --background'"),
107
+ ("sql_query", "Legacy: Use 'sql' instead"),
108
+ ("sql_search", "Legacy: Use 'sql --action search'"),
109
+ ("sql_stats", "Legacy: Use 'sql --action stats'"),
110
+ ("graph_add", "Legacy: Use 'graph --action add'"),
111
+ ("graph_remove", "Legacy: Use 'graph --action remove'"),
112
+ ("graph_query", "Legacy: Use 'graph' instead"),
113
+ ("graph_search", "Legacy: Use 'graph --action search'"),
114
+ ("graph_stats", "Legacy: Use 'graph --action stats'"),
115
+ ("vector_index", "Legacy: Use 'vector --action index'"),
116
+ ("vector_search", "Legacy: Use 'vector' instead"),
117
+ ("dispatch_agent", "Legacy: Use 'agent' instead"),
118
+ ("todo_read", "Legacy: Use 'todo' instead"),
119
+ ("todo_write", "Legacy: Use 'todo --action add/update'"),
120
+ ("notebook_read", "Legacy: Use 'jupyter' instead"),
121
+ ("notebook_edit", "Legacy: Use 'jupyter --action edit'"),
122
+ ],
123
+ }
124
+
125
+ def __init__(self):
126
+ """Initialize the tool list tool."""
127
+ pass
128
+
129
+ @property
130
+ @override
131
+ def name(self) -> str:
132
+ """Get the tool name."""
133
+ return "tool_list"
134
+
135
+ @property
136
+ @override
137
+ def description(self) -> str:
138
+ """Get the tool description."""
139
+ return """List all available tools and their current status.
140
+
141
+ Shows:
142
+ - Tool names and descriptions
143
+ - Whether each tool is enabled or disabled
144
+ - Tools organized by category
145
+
146
+ Examples:
147
+ - tool_list # Show all tools
148
+ - tool_list --show-disabled # Show only disabled tools
149
+ - tool_list --show-enabled # Show only enabled tools
150
+ - tool_list --category shell # Show only shell tools
151
+
152
+ Use 'tool_enable' and 'tool_disable' to change tool status.
153
+ """
154
+
155
+ @override
156
+ async def call(
157
+ self,
158
+ ctx: MCPContext,
159
+ **params: Unpack[ToolListParams],
160
+ ) -> str:
161
+ """List all tools.
162
+
163
+ Args:
164
+ ctx: MCP context
165
+ **params: Tool parameters
166
+
167
+ Returns:
168
+ List of tools and their status
169
+ """
170
+ tool_ctx = create_tool_context(ctx)
171
+ await tool_ctx.set_tool_info(self.name)
172
+
173
+ # Extract parameters
174
+ show_disabled = params.get("show_disabled", False)
175
+ show_enabled = params.get("show_enabled", False)
176
+ category_filter = params.get("category")
177
+
178
+ # Get all tool states
179
+ all_states = ToolEnableTool.get_all_states()
180
+
181
+ output = []
182
+
183
+ # Header
184
+ if show_disabled:
185
+ output.append("=== Disabled Tools ===")
186
+ elif show_enabled:
187
+ output.append("=== Enabled Tools ===")
188
+ else:
189
+ output.append("=== All Available Tools ===")
190
+
191
+ if category_filter:
192
+ output.append(f"Category: {category_filter}")
193
+
194
+ output.append("")
195
+
196
+ # Count statistics
197
+ total_tools = 0
198
+ disabled_count = 0
199
+ shown_count = 0
200
+
201
+ # Iterate through categories
202
+ categories = [category_filter] if category_filter and category_filter in self.TOOL_INFO else self.TOOL_INFO.keys()
203
+
204
+ for category in categories:
205
+ if category not in self.TOOL_INFO:
206
+ continue
207
+
208
+ category_tools = self.TOOL_INFO[category]
209
+ category_shown = []
210
+
211
+ for tool_name, description in category_tools:
212
+ total_tools += 1
213
+ is_enabled = ToolEnableTool.is_tool_enabled(tool_name)
214
+
215
+ if not is_enabled:
216
+ disabled_count += 1
217
+
218
+ # Apply filters
219
+ if show_disabled and is_enabled:
220
+ continue
221
+ if show_enabled and not is_enabled:
222
+ continue
223
+
224
+ status = "✅" if is_enabled else "❌"
225
+ category_shown.append((tool_name, description, status))
226
+ shown_count += 1
227
+
228
+ # Show category if it has tools
229
+ if category_shown:
230
+ output.append(f"=== {category.title()} Tools ===")
231
+
232
+ # Find max tool name length for alignment
233
+ max_name_len = max(len(name) for name, _, _ in category_shown)
234
+
235
+ for tool_name, description, status in category_shown:
236
+ output.append(f"{status} {tool_name.ljust(max_name_len)} - {description}")
237
+
238
+ output.append("")
239
+
240
+ # Summary
241
+ if not show_disabled and not show_enabled:
242
+ output.append("=== Summary ===")
243
+ output.append(f"Total tools: {total_tools}")
244
+ output.append(f"Enabled: {total_tools - disabled_count}")
245
+ output.append(f"Disabled: {disabled_count}")
246
+ else:
247
+ output.append(f"Showing {shown_count} tool(s)")
248
+
249
+ if disabled_count > 0 and not show_disabled:
250
+ output.append("\nUse 'tool_list --show-disabled' to see disabled tools.")
251
+ output.append("Use 'tool_enable --tool <name>' to enable a tool.")
252
+
253
+ if show_disabled:
254
+ output.append("\nUse 'tool_enable --tool <name>' to enable these tools.")
255
+
256
+ return "\n".join(output)
257
+
258
+ def register(self, mcp_server) -> None:
259
+ """Register this tool with the MCP server."""
260
+ pass
@@ -0,0 +1,10 @@
1
+ """Configuration tools for Hanzo MCP."""
2
+
3
+ from hanzo_mcp.tools.config.config_tool import ConfigTool
4
+ from hanzo_mcp.tools.config.index_config import IndexConfig, IndexScope
5
+
6
+ __all__ = [
7
+ "ConfigTool",
8
+ "IndexConfig",
9
+ "IndexScope",
10
+ ]
@@ -0,0 +1,212 @@
1
+ """Configuration tool for Hanzo MCP.
2
+
3
+ Git-style config tool for managing settings.
4
+ """
5
+
6
+ from typing import Annotated, TypedDict, Unpack, final, override, Optional, Dict, Any
7
+ from pathlib import Path
8
+ import json
9
+
10
+ from mcp.server.fastmcp import Context as MCPContext
11
+ from pydantic import Field
12
+
13
+ from hanzo_mcp.tools.common.base import BaseTool
14
+ from hanzo_mcp.tools.common.permissions import PermissionManager
15
+ from hanzo_mcp.tools.config.index_config import IndexConfig, IndexScope
16
+
17
+
18
+ # Parameter types
19
+ Action = Annotated[
20
+ str,
21
+ Field(
22
+ description="Action: get (default), set, list, toggle",
23
+ default="get",
24
+ ),
25
+ ]
26
+
27
+ Key = Annotated[
28
+ Optional[str],
29
+ Field(
30
+ description="Configuration key (e.g., index.scope, vector.enabled)",
31
+ default=None,
32
+ ),
33
+ ]
34
+
35
+ Value = Annotated[
36
+ Optional[str],
37
+ Field(
38
+ description="Configuration value",
39
+ default=None,
40
+ ),
41
+ ]
42
+
43
+ Scope = Annotated[
44
+ str,
45
+ Field(
46
+ description="Config scope: local (project) or global",
47
+ default="local",
48
+ ),
49
+ ]
50
+
51
+ ConfigPath = Annotated[
52
+ Optional[str],
53
+ Field(
54
+ description="Path for project-specific config",
55
+ default=None,
56
+ ),
57
+ ]
58
+
59
+
60
+ class ConfigParams(TypedDict, total=False):
61
+ """Parameters for config tool."""
62
+ action: str
63
+ key: Optional[str]
64
+ value: Optional[str]
65
+ scope: str
66
+ path: Optional[str]
67
+
68
+
69
+ @final
70
+ class ConfigTool(BaseTool):
71
+ """Git-style configuration management tool."""
72
+
73
+ def __init__(self, permission_manager: PermissionManager):
74
+ """Initialize config tool."""
75
+ super().__init__(permission_manager)
76
+ self.index_config = IndexConfig()
77
+
78
+ @property
79
+ @override
80
+ def name(self) -> str:
81
+ """Get the tool name."""
82
+ return "config"
83
+
84
+ @property
85
+ @override
86
+ def description(self) -> str:
87
+ """Get the tool description."""
88
+ return """Git-style configuration. Actions: get (default), set, list, toggle.
89
+
90
+ Usage:
91
+ config index.scope
92
+ config --action set index.scope project
93
+ config --action list
94
+ config --action toggle index.scope --path ./project"""
95
+
96
+ @override
97
+ async def call(
98
+ self,
99
+ ctx: MCPContext,
100
+ **params: Unpack[ConfigParams],
101
+ ) -> str:
102
+ """Execute config operation."""
103
+ tool_ctx = self.create_tool_context(ctx)
104
+
105
+ # Extract parameters
106
+ action = params.get("action", "get")
107
+ key = params.get("key")
108
+ value = params.get("value")
109
+ scope = params.get("scope", "local")
110
+ path = params.get("path")
111
+
112
+ # Route to handler
113
+ if action == "get":
114
+ return await self._handle_get(key, scope, path, tool_ctx)
115
+ elif action == "set":
116
+ return await self._handle_set(key, value, scope, path, tool_ctx)
117
+ elif action == "list":
118
+ return await self._handle_list(scope, path, tool_ctx)
119
+ elif action == "toggle":
120
+ return await self._handle_toggle(key, scope, path, tool_ctx)
121
+ else:
122
+ return f"Error: Unknown action '{action}'. Valid actions: get, set, list, toggle"
123
+
124
+ async def _handle_get(self, key: Optional[str], scope: str, path: Optional[str], tool_ctx) -> str:
125
+ """Get configuration value."""
126
+ if not key:
127
+ return "Error: key required for get action"
128
+
129
+ # Handle index scope
130
+ if key == "index.scope":
131
+ current_scope = self.index_config.get_scope(path)
132
+ return f"index.scope={current_scope.value}"
133
+
134
+ # Handle tool-specific settings
135
+ if "." in key:
136
+ tool, setting = key.split(".", 1)
137
+ if setting == "enabled":
138
+ enabled = self.index_config.is_indexing_enabled(tool)
139
+ return f"{key}={enabled}"
140
+
141
+ return f"Unknown key: {key}"
142
+
143
+ async def _handle_set(self, key: Optional[str], value: Optional[str], scope: str, path: Optional[str], tool_ctx) -> str:
144
+ """Set configuration value."""
145
+ if not key:
146
+ return "Error: key required for set action"
147
+ if not value:
148
+ return "Error: value required for set action"
149
+
150
+ # Handle index scope
151
+ if key == "index.scope":
152
+ try:
153
+ new_scope = IndexScope(value)
154
+ self.index_config.set_scope(new_scope, path if scope == "local" else None)
155
+ return f"Set {key}={value} ({'project' if path else 'global'})"
156
+ except ValueError:
157
+ return f"Error: Invalid scope value '{value}'. Valid: project, global, auto"
158
+
159
+ # Handle tool-specific settings
160
+ if "." in key:
161
+ tool, setting = key.split(".", 1)
162
+ if setting == "enabled":
163
+ enabled = value.lower() in ["true", "yes", "1", "on"]
164
+ self.index_config.set_indexing_enabled(tool, enabled)
165
+ return f"Set {key}={enabled}"
166
+
167
+ return f"Unknown key: {key}"
168
+
169
+ async def _handle_list(self, scope: str, path: Optional[str], tool_ctx) -> str:
170
+ """List all configuration."""
171
+ status = self.index_config.get_status()
172
+
173
+ output = ["=== Configuration ==="]
174
+ output.append(f"\nDefault scope: {status['default_scope']}")
175
+
176
+ if path:
177
+ current_scope = self.index_config.get_scope(path)
178
+ output.append(f"Current path scope: {current_scope.value}")
179
+
180
+ output.append(f"\nProjects with custom config: {status['project_count']}")
181
+
182
+ output.append("\nTool settings:")
183
+ for tool, settings in status["tools"].items():
184
+ output.append(f" {tool}:")
185
+ output.append(f" enabled: {settings['enabled']}")
186
+ output.append(f" per_project: {settings['per_project']}")
187
+
188
+ return "\n".join(output)
189
+
190
+ async def _handle_toggle(self, key: Optional[str], scope: str, path: Optional[str], tool_ctx) -> str:
191
+ """Toggle configuration value."""
192
+ if not key:
193
+ return "Error: key required for toggle action"
194
+
195
+ # Handle index scope toggle
196
+ if key == "index.scope":
197
+ new_scope = self.index_config.toggle_scope(path if scope == "local" else None)
198
+ return f"Toggled index.scope to {new_scope.value}"
199
+
200
+ # Handle tool enable/disable toggle
201
+ if "." in key:
202
+ tool, setting = key.split(".", 1)
203
+ if setting == "enabled":
204
+ current = self.index_config.is_indexing_enabled(tool)
205
+ self.index_config.set_indexing_enabled(tool, not current)
206
+ return f"Toggled {key} to {not current}"
207
+
208
+ return f"Cannot toggle key: {key}"
209
+
210
+ def register(self, mcp_server) -> None:
211
+ """Register this tool with the MCP server."""
212
+ pass
@@ -0,0 +1,176 @@
1
+ """Index configuration for per-project vs global indexing.
2
+
3
+ This module manages indexing configuration for different scopes.
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Dict, Any, Optional
9
+ from enum import Enum
10
+
11
+
12
+ class IndexScope(Enum):
13
+ """Indexing scope options."""
14
+ PROJECT = "project" # Per-project indexing
15
+ GLOBAL = "global" # Global indexing
16
+ AUTO = "auto" # Auto-detect based on git root
17
+
18
+
19
+ class IndexConfig:
20
+ """Manages indexing configuration."""
21
+
22
+ def __init__(self, config_dir: Optional[Path] = None):
23
+ """Initialize index configuration."""
24
+ self.config_dir = config_dir or Path.home() / ".hanzo" / "mcp"
25
+ self.config_file = self.config_dir / "index_config.json"
26
+ self._config = self._load_config()
27
+
28
+ def _load_config(self) -> Dict[str, Any]:
29
+ """Load configuration from disk."""
30
+ if self.config_file.exists():
31
+ try:
32
+ with open(self.config_file, "r") as f:
33
+ return json.load(f)
34
+ except:
35
+ pass
36
+
37
+ # Default configuration
38
+ return {
39
+ "default_scope": IndexScope.AUTO.value,
40
+ "project_configs": {},
41
+ "global_index_paths": [],
42
+ "index_settings": {
43
+ "vector": {
44
+ "enabled": True,
45
+ "auto_index": True,
46
+ "include_git_history": True,
47
+ },
48
+ "symbols": {
49
+ "enabled": True,
50
+ "auto_index": False,
51
+ },
52
+ "sql": {
53
+ "enabled": True,
54
+ "per_project": True,
55
+ },
56
+ "graph": {
57
+ "enabled": True,
58
+ "per_project": True,
59
+ }
60
+ }
61
+ }
62
+
63
+ def save_config(self) -> None:
64
+ """Save configuration to disk."""
65
+ self.config_dir.mkdir(parents=True, exist_ok=True)
66
+ with open(self.config_file, "w") as f:
67
+ json.dump(self._config, f, indent=2)
68
+
69
+ def get_scope(self, path: Optional[str] = None) -> IndexScope:
70
+ """Get indexing scope for a path."""
71
+ if not path:
72
+ return IndexScope(self._config["default_scope"])
73
+
74
+ # Check project-specific config
75
+ project_root = self._find_project_root(path)
76
+ if project_root:
77
+ project_config = self._config["project_configs"].get(str(project_root))
78
+ if project_config and "scope" in project_config:
79
+ return IndexScope(project_config["scope"])
80
+
81
+ # Use default
82
+ scope = IndexScope(self._config["default_scope"])
83
+
84
+ # Handle auto mode
85
+ if scope == IndexScope.AUTO:
86
+ if project_root:
87
+ return IndexScope.PROJECT
88
+ else:
89
+ return IndexScope.GLOBAL
90
+
91
+ return scope
92
+
93
+ def set_scope(self, scope: IndexScope, path: Optional[str] = None) -> None:
94
+ """Set indexing scope."""
95
+ if path:
96
+ # Set for specific project
97
+ project_root = self._find_project_root(path)
98
+ if project_root:
99
+ if str(project_root) not in self._config["project_configs"]:
100
+ self._config["project_configs"][str(project_root)] = {}
101
+ self._config["project_configs"][str(project_root)]["scope"] = scope.value
102
+ else:
103
+ # Set global default
104
+ self._config["default_scope"] = scope.value
105
+
106
+ self.save_config()
107
+
108
+ def get_index_path(self, tool: str, path: Optional[str] = None) -> Path:
109
+ """Get index path for a tool and location."""
110
+ scope = self.get_scope(path)
111
+
112
+ if scope == IndexScope.PROJECT and path:
113
+ project_root = self._find_project_root(path)
114
+ if project_root:
115
+ return Path(project_root) / ".hanzo" / "index" / tool
116
+
117
+ # Global index
118
+ return self.config_dir / "index" / tool
119
+
120
+ def is_indexing_enabled(self, tool: str) -> bool:
121
+ """Check if indexing is enabled for a tool."""
122
+ return self._config["index_settings"].get(tool, {}).get("enabled", True)
123
+
124
+ def set_indexing_enabled(self, tool: str, enabled: bool) -> None:
125
+ """Enable/disable indexing for a tool."""
126
+ if tool not in self._config["index_settings"]:
127
+ self._config["index_settings"][tool] = {}
128
+ self._config["index_settings"][tool]["enabled"] = enabled
129
+ self.save_config()
130
+
131
+ def toggle_scope(self, path: Optional[str] = None) -> IndexScope:
132
+ """Toggle between project and global scope."""
133
+ current = self.get_scope(path)
134
+
135
+ if current == IndexScope.PROJECT:
136
+ new_scope = IndexScope.GLOBAL
137
+ elif current == IndexScope.GLOBAL:
138
+ new_scope = IndexScope.PROJECT
139
+ else: # AUTO
140
+ # Determine what auto resolves to and toggle
141
+ if path and self._find_project_root(path):
142
+ new_scope = IndexScope.GLOBAL
143
+ else:
144
+ new_scope = IndexScope.PROJECT
145
+
146
+ self.set_scope(new_scope, path)
147
+ return new_scope
148
+
149
+ def _find_project_root(self, path: str) -> Optional[Path]:
150
+ """Find project root (git root or similar)."""
151
+ current = Path(path).resolve()
152
+
153
+ # Walk up looking for markers
154
+ markers = [".git", ".hg", "pyproject.toml", "package.json", "Cargo.toml"]
155
+
156
+ while current != current.parent:
157
+ for marker in markers:
158
+ if (current / marker).exists():
159
+ return current
160
+ current = current.parent
161
+
162
+ return None
163
+
164
+ def get_status(self) -> Dict[str, Any]:
165
+ """Get current configuration status."""
166
+ return {
167
+ "default_scope": self._config["default_scope"],
168
+ "project_count": len(self._config["project_configs"]),
169
+ "tools": {
170
+ tool: {
171
+ "enabled": settings.get("enabled", True),
172
+ "per_project": settings.get("per_project", True),
173
+ }
174
+ for tool, settings in self._config["index_settings"].items()
175
+ }
176
+ }