hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.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 (178) hide show
  1. hanzo_mcp/__init__.py +6 -0
  2. hanzo_mcp/__main__.py +1 -1
  3. hanzo_mcp/analytics/__init__.py +2 -2
  4. hanzo_mcp/analytics/posthog_analytics.py +76 -82
  5. hanzo_mcp/cli.py +31 -36
  6. hanzo_mcp/cli_enhanced.py +94 -72
  7. hanzo_mcp/cli_plugin.py +27 -17
  8. hanzo_mcp/config/__init__.py +2 -2
  9. hanzo_mcp/config/settings.py +112 -88
  10. hanzo_mcp/config/tool_config.py +32 -34
  11. hanzo_mcp/dev_server.py +66 -67
  12. hanzo_mcp/prompts/__init__.py +94 -12
  13. hanzo_mcp/prompts/enhanced_prompts.py +809 -0
  14. hanzo_mcp/prompts/example_custom_prompt.py +6 -5
  15. hanzo_mcp/prompts/project_todo_reminder.py +0 -1
  16. hanzo_mcp/prompts/tool_explorer.py +10 -7
  17. hanzo_mcp/server.py +17 -21
  18. hanzo_mcp/server_enhanced.py +15 -22
  19. hanzo_mcp/tools/__init__.py +56 -28
  20. hanzo_mcp/tools/agent/__init__.py +16 -19
  21. hanzo_mcp/tools/agent/agent.py +82 -65
  22. hanzo_mcp/tools/agent/agent_tool.py +152 -122
  23. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
  24. hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
  25. hanzo_mcp/tools/agent/clarification_tool.py +11 -10
  26. hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
  27. hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
  28. hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
  29. hanzo_mcp/tools/agent/code_auth.py +102 -107
  30. hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
  31. hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
  32. hanzo_mcp/tools/agent/critic_tool.py +86 -73
  33. hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
  34. hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
  35. hanzo_mcp/tools/agent/iching_tool.py +404 -139
  36. hanzo_mcp/tools/agent/network_tool.py +89 -73
  37. hanzo_mcp/tools/agent/prompt.py +2 -1
  38. hanzo_mcp/tools/agent/review_tool.py +101 -98
  39. hanzo_mcp/tools/agent/swarm_alias.py +87 -0
  40. hanzo_mcp/tools/agent/swarm_tool.py +246 -161
  41. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
  42. hanzo_mcp/tools/agent/tool_adapter.py +21 -11
  43. hanzo_mcp/tools/common/__init__.py +1 -1
  44. hanzo_mcp/tools/common/base.py +3 -5
  45. hanzo_mcp/tools/common/batch_tool.py +46 -39
  46. hanzo_mcp/tools/common/config_tool.py +120 -84
  47. hanzo_mcp/tools/common/context.py +1 -5
  48. hanzo_mcp/tools/common/context_fix.py +5 -3
  49. hanzo_mcp/tools/common/critic_tool.py +4 -8
  50. hanzo_mcp/tools/common/decorators.py +58 -56
  51. hanzo_mcp/tools/common/enhanced_base.py +29 -32
  52. hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
  53. hanzo_mcp/tools/common/forgiving_edit.py +91 -87
  54. hanzo_mcp/tools/common/mode.py +15 -17
  55. hanzo_mcp/tools/common/mode_loader.py +27 -24
  56. hanzo_mcp/tools/common/paginated_base.py +61 -53
  57. hanzo_mcp/tools/common/paginated_response.py +72 -79
  58. hanzo_mcp/tools/common/pagination.py +50 -53
  59. hanzo_mcp/tools/common/permissions.py +4 -4
  60. hanzo_mcp/tools/common/personality.py +186 -138
  61. hanzo_mcp/tools/common/plugin_loader.py +54 -54
  62. hanzo_mcp/tools/common/stats.py +65 -47
  63. hanzo_mcp/tools/common/test_helpers.py +31 -0
  64. hanzo_mcp/tools/common/thinking_tool.py +4 -8
  65. hanzo_mcp/tools/common/tool_disable.py +17 -12
  66. hanzo_mcp/tools/common/tool_enable.py +13 -14
  67. hanzo_mcp/tools/common/tool_list.py +36 -28
  68. hanzo_mcp/tools/common/truncate.py +23 -23
  69. hanzo_mcp/tools/config/__init__.py +4 -4
  70. hanzo_mcp/tools/config/config_tool.py +42 -29
  71. hanzo_mcp/tools/config/index_config.py +37 -34
  72. hanzo_mcp/tools/config/mode_tool.py +175 -55
  73. hanzo_mcp/tools/database/__init__.py +15 -12
  74. hanzo_mcp/tools/database/database_manager.py +77 -75
  75. hanzo_mcp/tools/database/graph.py +137 -91
  76. hanzo_mcp/tools/database/graph_add.py +30 -18
  77. hanzo_mcp/tools/database/graph_query.py +178 -102
  78. hanzo_mcp/tools/database/graph_remove.py +33 -28
  79. hanzo_mcp/tools/database/graph_search.py +97 -75
  80. hanzo_mcp/tools/database/graph_stats.py +91 -59
  81. hanzo_mcp/tools/database/sql.py +107 -79
  82. hanzo_mcp/tools/database/sql_query.py +30 -24
  83. hanzo_mcp/tools/database/sql_search.py +29 -25
  84. hanzo_mcp/tools/database/sql_stats.py +47 -35
  85. hanzo_mcp/tools/editor/neovim_command.py +25 -28
  86. hanzo_mcp/tools/editor/neovim_edit.py +21 -23
  87. hanzo_mcp/tools/editor/neovim_session.py +60 -54
  88. hanzo_mcp/tools/filesystem/__init__.py +31 -30
  89. hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
  90. hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
  91. hanzo_mcp/tools/filesystem/base.py +1 -1
  92. hanzo_mcp/tools/filesystem/batch_search.py +316 -224
  93. hanzo_mcp/tools/filesystem/content_replace.py +4 -4
  94. hanzo_mcp/tools/filesystem/diff.py +71 -59
  95. hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
  96. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
  97. hanzo_mcp/tools/filesystem/edit.py +4 -4
  98. hanzo_mcp/tools/filesystem/find.py +173 -80
  99. hanzo_mcp/tools/filesystem/find_files.py +73 -52
  100. hanzo_mcp/tools/filesystem/git_search.py +157 -104
  101. hanzo_mcp/tools/filesystem/grep.py +8 -8
  102. hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
  103. hanzo_mcp/tools/filesystem/read.py +12 -10
  104. hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
  105. hanzo_mcp/tools/filesystem/search_tool.py +263 -207
  106. hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
  107. hanzo_mcp/tools/filesystem/tree.py +35 -33
  108. hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
  109. hanzo_mcp/tools/filesystem/watch.py +37 -36
  110. hanzo_mcp/tools/filesystem/write.py +4 -8
  111. hanzo_mcp/tools/jupyter/__init__.py +4 -4
  112. hanzo_mcp/tools/jupyter/base.py +4 -5
  113. hanzo_mcp/tools/jupyter/jupyter.py +67 -47
  114. hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
  115. hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
  116. hanzo_mcp/tools/llm/__init__.py +5 -7
  117. hanzo_mcp/tools/llm/consensus_tool.py +72 -52
  118. hanzo_mcp/tools/llm/llm_manage.py +101 -60
  119. hanzo_mcp/tools/llm/llm_tool.py +226 -166
  120. hanzo_mcp/tools/llm/provider_tools.py +25 -26
  121. hanzo_mcp/tools/lsp/__init__.py +1 -1
  122. hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
  123. hanzo_mcp/tools/mcp/__init__.py +2 -3
  124. hanzo_mcp/tools/mcp/mcp_add.py +27 -25
  125. hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
  126. hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
  127. hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
  128. hanzo_mcp/tools/memory/__init__.py +39 -21
  129. hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
  130. hanzo_mcp/tools/memory/memory_tools.py +90 -108
  131. hanzo_mcp/tools/search/__init__.py +7 -2
  132. hanzo_mcp/tools/search/find_tool.py +297 -212
  133. hanzo_mcp/tools/search/unified_search.py +366 -314
  134. hanzo_mcp/tools/shell/__init__.py +8 -7
  135. hanzo_mcp/tools/shell/auto_background.py +56 -49
  136. hanzo_mcp/tools/shell/base.py +1 -1
  137. hanzo_mcp/tools/shell/base_process.py +75 -75
  138. hanzo_mcp/tools/shell/bash_session.py +2 -2
  139. hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
  140. hanzo_mcp/tools/shell/bash_tool.py +24 -31
  141. hanzo_mcp/tools/shell/command_executor.py +12 -12
  142. hanzo_mcp/tools/shell/logs.py +43 -33
  143. hanzo_mcp/tools/shell/npx.py +13 -13
  144. hanzo_mcp/tools/shell/npx_background.py +24 -21
  145. hanzo_mcp/tools/shell/npx_tool.py +18 -22
  146. hanzo_mcp/tools/shell/open.py +19 -21
  147. hanzo_mcp/tools/shell/pkill.py +31 -26
  148. hanzo_mcp/tools/shell/process_tool.py +32 -32
  149. hanzo_mcp/tools/shell/processes.py +57 -58
  150. hanzo_mcp/tools/shell/run_background.py +24 -25
  151. hanzo_mcp/tools/shell/run_command.py +5 -5
  152. hanzo_mcp/tools/shell/run_command_windows.py +5 -5
  153. hanzo_mcp/tools/shell/session_storage.py +3 -3
  154. hanzo_mcp/tools/shell/streaming_command.py +141 -126
  155. hanzo_mcp/tools/shell/uvx.py +24 -25
  156. hanzo_mcp/tools/shell/uvx_background.py +35 -33
  157. hanzo_mcp/tools/shell/uvx_tool.py +18 -22
  158. hanzo_mcp/tools/todo/__init__.py +6 -2
  159. hanzo_mcp/tools/todo/todo.py +50 -37
  160. hanzo_mcp/tools/todo/todo_read.py +5 -8
  161. hanzo_mcp/tools/todo/todo_write.py +5 -7
  162. hanzo_mcp/tools/vector/__init__.py +40 -28
  163. hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
  164. hanzo_mcp/tools/vector/git_ingester.py +170 -179
  165. hanzo_mcp/tools/vector/index_tool.py +96 -44
  166. hanzo_mcp/tools/vector/infinity_store.py +283 -228
  167. hanzo_mcp/tools/vector/mock_infinity.py +39 -40
  168. hanzo_mcp/tools/vector/project_manager.py +88 -78
  169. hanzo_mcp/tools/vector/vector.py +59 -42
  170. hanzo_mcp/tools/vector/vector_index.py +30 -27
  171. hanzo_mcp/tools/vector/vector_search.py +64 -45
  172. hanzo_mcp/types.py +6 -4
  173. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.1.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,15 @@
1
1
  """Enable tools dynamically."""
2
2
 
3
3
  import json
4
- from typing import Annotated, TypedDict, Unpack, final, override
4
+ from typing import Unpack, Annotated, TypedDict, final, override
5
5
  from pathlib import Path
6
6
 
7
- from mcp.server.fastmcp import Context as MCPContext
8
7
  from pydantic import Field
8
+ from mcp.server.fastmcp import Context as MCPContext
9
9
 
10
10
  from hanzo_mcp.tools.common.base import BaseTool
11
11
  from hanzo_mcp.tools.common.context import create_tool_context
12
12
 
13
-
14
13
  ToolName = Annotated[
15
14
  str,
16
15
  Field(
@@ -38,7 +37,7 @@ class ToolEnableParams(TypedDict, total=False):
38
37
  @final
39
38
  class ToolEnableTool(BaseTool):
40
39
  """Tool for enabling other tools dynamically."""
41
-
40
+
42
41
  # Class variable to track enabled/disabled tools
43
42
  _tool_states = {}
44
43
  _config_file = Path.home() / ".hanzo" / "mcp" / "tool_states.json"
@@ -55,7 +54,7 @@ class ToolEnableTool(BaseTool):
55
54
  """Load tool states from config file."""
56
55
  if cls._config_file.exists():
57
56
  try:
58
- with open(cls._config_file, 'r') as f:
57
+ with open(cls._config_file, "r") as f:
59
58
  cls._tool_states = json.load(f)
60
59
  except Exception:
61
60
  cls._tool_states = {}
@@ -67,16 +66,16 @@ class ToolEnableTool(BaseTool):
67
66
  def _save_states(cls):
68
67
  """Save tool states to config file."""
69
68
  cls._config_file.parent.mkdir(parents=True, exist_ok=True)
70
- with open(cls._config_file, 'w') as f:
69
+ with open(cls._config_file, "w") as f:
71
70
  json.dump(cls._tool_states, f, indent=2)
72
71
 
73
72
  @classmethod
74
73
  def is_tool_enabled(cls, tool_name: str) -> bool:
75
74
  """Check if a tool is enabled.
76
-
75
+
77
76
  Args:
78
77
  tool_name: Name of the tool
79
-
78
+
80
79
  Returns:
81
80
  True if enabled (default), False if explicitly disabled
82
81
  """
@@ -84,7 +83,7 @@ class ToolEnableTool(BaseTool):
84
83
  if not cls._initialized:
85
84
  cls._load_states()
86
85
  cls._initialized = True
87
-
86
+
88
87
  # Default to enabled if not in states
89
88
  return cls._tool_states.get(tool_name, True)
90
89
 
@@ -146,13 +145,13 @@ Use 'tool_list' to see all available tools and their status.
146
145
 
147
146
  # Check current state
148
147
  was_enabled = self.is_tool_enabled(tool_name)
149
-
148
+
150
149
  if was_enabled:
151
150
  return f"Tool '{tool_name}' is already enabled."
152
151
 
153
152
  # Enable the tool
154
153
  self._tool_states[tool_name] = True
155
-
154
+
156
155
  # Persist if requested
157
156
  if persist:
158
157
  self._save_states()
@@ -165,16 +164,16 @@ Use 'tool_list' to see all available tools and their status.
165
164
  "",
166
165
  "The tool is now available for use.",
167
166
  ]
168
-
167
+
169
168
  if not persist:
170
169
  output.append("Note: This change is temporary and will be lost on restart.")
171
-
170
+
172
171
  # Count enabled/disabled tools
173
172
  disabled_count = sum(1 for enabled in self._tool_states.values() if not enabled)
174
173
  if disabled_count > 0:
175
174
  output.append(f"\nCurrently disabled tools: {disabled_count}")
176
175
  output.append("Use 'tool_list --disabled' to see them.")
177
-
176
+
178
177
  return "\n".join(output)
179
178
 
180
179
  def register(self, mcp_server) -> None:
@@ -1,15 +1,14 @@
1
1
  """List all available tools and their status."""
2
2
 
3
- from typing import Annotated, TypedDict, Unpack, final, override, Optional
3
+ from typing import Unpack, Optional, Annotated, TypedDict, final, override
4
4
 
5
- from mcp.server.fastmcp import Context as MCPContext
6
5
  from pydantic import Field
6
+ from mcp.server.fastmcp import Context as MCPContext
7
7
 
8
8
  from hanzo_mcp.tools.common.base import BaseTool
9
9
  from hanzo_mcp.tools.common.context import create_tool_context
10
10
  from hanzo_mcp.tools.common.tool_enable import ToolEnableTool
11
11
 
12
-
13
12
  ShowDisabled = Annotated[
14
13
  bool,
15
14
  Field(
@@ -46,7 +45,7 @@ class ToolListParams(TypedDict, total=False):
46
45
  @final
47
46
  class ToolListTool(BaseTool):
48
47
  """Tool for listing all available tools and their status."""
49
-
48
+
50
49
  # Tool information organized by category
51
50
  TOOL_INFO = {
52
51
  "filesystem": [
@@ -80,7 +79,10 @@ class ToolListTool(BaseTool):
80
79
  ("llm", "LLM interface (query/consensus/list/models/enable/disable)"),
81
80
  ("agent", "AI agents (run/start/call/stop/list with A2A support)"),
82
81
  ("swarm", "Parallel agent execution across multiple files"),
83
- ("hierarchical_swarm", "Hierarchical agent teams with Claude Code integration"),
82
+ (
83
+ "hierarchical_swarm",
84
+ "Hierarchical agent teams with Claude Code integration",
85
+ ),
84
86
  ("mcp", "MCP servers (list/add/remove/enable/disable/restart)"),
85
87
  ],
86
88
  "config": [
@@ -171,7 +173,7 @@ Use 'tool_enable' and 'tool_disable' to change tool status.
171
173
  List of tools and their status
172
174
  """
173
175
  tool_ctx = create_tool_context(ctx)
174
- await tool_ctx.set_tool_info(self.name)
176
+ tool_ctx.set_tool_info(self.name)
175
177
 
176
178
  # Extract parameters
177
179
  show_disabled = params.get("show_disabled", False)
@@ -180,9 +182,9 @@ Use 'tool_enable' and 'tool_disable' to change tool status.
180
182
 
181
183
  # Get all tool states
182
184
  all_states = ToolEnableTool.get_all_states()
183
-
185
+
184
186
  output = []
185
-
187
+
186
188
  # Header
187
189
  if show_disabled:
188
190
  output.append("=== Disabled Tools ===")
@@ -190,56 +192,62 @@ Use 'tool_enable' and 'tool_disable' to change tool status.
190
192
  output.append("=== Enabled Tools ===")
191
193
  else:
192
194
  output.append("=== All Available Tools ===")
193
-
195
+
194
196
  if category_filter:
195
197
  output.append(f"Category: {category_filter}")
196
-
198
+
197
199
  output.append("")
198
-
200
+
199
201
  # Count statistics
200
202
  total_tools = 0
201
203
  disabled_count = 0
202
204
  shown_count = 0
203
-
205
+
204
206
  # Iterate through categories
205
- categories = [category_filter] if category_filter and category_filter in self.TOOL_INFO else self.TOOL_INFO.keys()
206
-
207
+ categories = (
208
+ [category_filter]
209
+ if category_filter and category_filter in self.TOOL_INFO
210
+ else self.TOOL_INFO.keys()
211
+ )
212
+
207
213
  for category in categories:
208
214
  if category not in self.TOOL_INFO:
209
215
  continue
210
-
216
+
211
217
  category_tools = self.TOOL_INFO[category]
212
218
  category_shown = []
213
-
219
+
214
220
  for tool_name, description in category_tools:
215
221
  total_tools += 1
216
222
  is_enabled = ToolEnableTool.is_tool_enabled(tool_name)
217
-
223
+
218
224
  if not is_enabled:
219
225
  disabled_count += 1
220
-
226
+
221
227
  # Apply filters
222
228
  if show_disabled and is_enabled:
223
229
  continue
224
230
  if show_enabled and not is_enabled:
225
231
  continue
226
-
232
+
227
233
  status = "✅" if is_enabled else "❌"
228
234
  category_shown.append((tool_name, description, status))
229
235
  shown_count += 1
230
-
236
+
231
237
  # Show category if it has tools
232
238
  if category_shown:
233
239
  output.append(f"=== {category.title()} Tools ===")
234
-
240
+
235
241
  # Find max tool name length for alignment
236
242
  max_name_len = max(len(name) for name, _, _ in category_shown)
237
-
243
+
238
244
  for tool_name, description, status in category_shown:
239
- output.append(f"{status} {tool_name.ljust(max_name_len)} - {description}")
240
-
245
+ output.append(
246
+ f"{status} {tool_name.ljust(max_name_len)} - {description}"
247
+ )
248
+
241
249
  output.append("")
242
-
250
+
243
251
  # Summary
244
252
  if not show_disabled and not show_enabled:
245
253
  output.append("=== Summary ===")
@@ -248,14 +256,14 @@ Use 'tool_enable' and 'tool_disable' to change tool status.
248
256
  output.append(f"Disabled: {disabled_count}")
249
257
  else:
250
258
  output.append(f"Showing {shown_count} tool(s)")
251
-
259
+
252
260
  if disabled_count > 0 and not show_disabled:
253
261
  output.append("\nUse 'tool_list --show-disabled' to see disabled tools.")
254
262
  output.append("Use 'tool_enable --tool <name>' to enable a tool.")
255
-
263
+
256
264
  if show_disabled:
257
265
  output.append("\nUse 'tool_enable --tool <name>' to enable these tools.")
258
-
266
+
259
267
  return "\n".join(output)
260
268
 
261
269
  def register(self, mcp_server) -> None:
@@ -8,11 +8,11 @@ import tiktoken
8
8
 
9
9
  def estimate_tokens(text: str, model: str = "gpt-4") -> int:
10
10
  """Estimate the number of tokens in a text string.
11
-
11
+
12
12
  Args:
13
13
  text: The text to estimate tokens for
14
14
  model: The model to use for token estimation (default: gpt-4)
15
-
15
+
16
16
  Returns:
17
17
  Estimated number of tokens
18
18
  """
@@ -22,80 +22,80 @@ def estimate_tokens(text: str, model: str = "gpt-4") -> int:
22
22
  except KeyError:
23
23
  # Fall back to cl100k_base which is used by newer models
24
24
  encoding = tiktoken.get_encoding("cl100k_base")
25
-
25
+
26
26
  return len(encoding.encode(text))
27
27
 
28
28
 
29
29
  def truncate_response(
30
- response: str,
30
+ response: str,
31
31
  max_tokens: int = 20000,
32
- truncation_message: str = "\n\n[Response truncated due to length. Please use pagination, filtering, or limit parameters to see more.]"
32
+ truncation_message: str = "\n\n[Response truncated due to length. Please use pagination, filtering, or limit parameters to see more.]",
33
33
  ) -> str:
34
34
  """Truncate a response to fit within token limits.
35
-
35
+
36
36
  Args:
37
37
  response: The response text to truncate
38
38
  max_tokens: Maximum number of tokens allowed (default: 20000)
39
39
  truncation_message: Message to append when truncating
40
-
40
+
41
41
  Returns:
42
42
  Truncated response if needed, original response otherwise
43
43
  """
44
44
  # Quick check - if response is short, no need to count tokens
45
45
  if len(response) < max_tokens * 2: # Rough estimate: 1 token ≈ 2-4 chars
46
46
  return response
47
-
47
+
48
48
  # Estimate tokens
49
49
  token_count = estimate_tokens(response)
50
-
50
+
51
51
  # If within limit, return as-is
52
52
  if token_count <= max_tokens:
53
53
  return response
54
-
54
+
55
55
  # Need to truncate
56
56
  # Binary search to find the right truncation point
57
57
  left, right = 0, len(response)
58
58
  truncation_msg_tokens = estimate_tokens(truncation_message)
59
59
  target_tokens = max_tokens - truncation_msg_tokens
60
-
60
+
61
61
  while left < right - 1:
62
62
  mid = (left + right) // 2
63
63
  mid_tokens = estimate_tokens(response[:mid])
64
-
64
+
65
65
  if mid_tokens <= target_tokens:
66
66
  left = mid
67
67
  else:
68
68
  right = mid
69
-
69
+
70
70
  # Find a good break point (newline or space)
71
71
  truncate_at = left
72
72
  for i in range(min(100, left), -1, -1):
73
- if response[left - i] in '\n ':
73
+ if response[left - i] in "\n ":
74
74
  truncate_at = left - i
75
75
  break
76
-
76
+
77
77
  return response[:truncate_at] + truncation_message
78
78
 
79
79
 
80
80
  def truncate_lines(
81
81
  response: str,
82
82
  max_lines: int = 1000,
83
- truncation_message: str = "\n\n[Response truncated to {max_lines} lines. Please use pagination or filtering to see more.]"
83
+ truncation_message: str = "\n\n[Response truncated to {max_lines} lines. Please use pagination or filtering to see more.]",
84
84
  ) -> str:
85
85
  """Truncate a response by number of lines.
86
-
86
+
87
87
  Args:
88
88
  response: The response text to truncate
89
89
  max_lines: Maximum number of lines allowed (default: 1000)
90
90
  truncation_message: Message template to append when truncating
91
-
91
+
92
92
  Returns:
93
93
  Truncated response if needed, original response otherwise
94
94
  """
95
- lines = response.split('\n')
96
-
95
+ lines = response.split("\n")
96
+
97
97
  if len(lines) <= max_lines:
98
98
  return response
99
-
100
- truncated = '\n'.join(lines[:max_lines])
101
- return truncated + truncation_message.format(max_lines=max_lines)
99
+
100
+ truncated = "\n".join(lines[:max_lines])
101
+ return truncated + truncation_message.format(max_lines=max_lines)
@@ -1,12 +1,12 @@
1
1
  """Configuration tools for Hanzo AI."""
2
2
 
3
- from hanzo_mcp.tools.config.config_tool import ConfigTool
4
- from hanzo_mcp.tools.config.index_config import IndexConfig, IndexScope
5
3
  from hanzo_mcp.tools.config.mode_tool import mode_tool
4
+ from hanzo_mcp.tools.config.config_tool import ConfigTool
5
+ from hanzo_mcp.tools.config.index_config import IndexScope, IndexConfig
6
6
 
7
7
  __all__ = [
8
8
  "ConfigTool",
9
- "IndexConfig",
9
+ "IndexConfig",
10
10
  "IndexScope",
11
11
  "mode_tool",
12
- ]
12
+ ]
@@ -3,17 +3,14 @@
3
3
  Git-style config tool for managing settings.
4
4
  """
5
5
 
6
- from typing import Annotated, TypedDict, Unpack, final, override, Optional, Dict, Any
7
- from pathlib import Path
8
- import json
6
+ from typing import Unpack, Optional, Annotated, TypedDict, final, override
9
7
 
10
- from mcp.server.fastmcp import Context as MCPContext
11
8
  from pydantic import Field
9
+ from mcp.server.fastmcp import Context as MCPContext
12
10
 
13
11
  from hanzo_mcp.tools.common.base import BaseTool
14
12
  from hanzo_mcp.tools.common.permissions import PermissionManager
15
- from hanzo_mcp.tools.config.index_config import IndexConfig, IndexScope
16
-
13
+ from hanzo_mcp.tools.config.index_config import IndexScope, IndexConfig
17
14
 
18
15
  # Parameter types
19
16
  Action = Annotated[
@@ -59,6 +56,7 @@ ConfigPath = Annotated[
59
56
 
60
57
  class ConfigParams(TypedDict, total=False):
61
58
  """Parameters for config tool."""
59
+
62
60
  action: str
63
61
  key: Optional[str]
64
62
  value: Optional[str]
@@ -69,7 +67,7 @@ class ConfigParams(TypedDict, total=False):
69
67
  @final
70
68
  class ConfigTool(BaseTool):
71
69
  """Git-style configuration management tool."""
72
-
70
+
73
71
  def __init__(self, permission_manager: PermissionManager):
74
72
  """Initialize config tool."""
75
73
  super().__init__(permission_manager)
@@ -101,14 +99,14 @@ config --action toggle index.scope --path ./project"""
101
99
  ) -> str:
102
100
  """Execute config operation."""
103
101
  tool_ctx = self.create_tool_context(ctx)
104
-
102
+
105
103
  # Extract parameters
106
104
  action = params.get("action", "get")
107
105
  key = params.get("key")
108
106
  value = params.get("value")
109
107
  scope = params.get("scope", "local")
110
108
  path = params.get("path")
111
-
109
+
112
110
  # Route to handler
113
111
  if action == "get":
114
112
  return await self._handle_get(key, scope, path, tool_ctx)
@@ -121,41 +119,52 @@ config --action toggle index.scope --path ./project"""
121
119
  else:
122
120
  return f"Error: Unknown action '{action}'. Valid actions: get, set, list, toggle"
123
121
 
124
- async def _handle_get(self, key: Optional[str], scope: str, path: Optional[str], tool_ctx) -> str:
122
+ async def _handle_get(
123
+ self, key: Optional[str], scope: str, path: Optional[str], tool_ctx
124
+ ) -> str:
125
125
  """Get configuration value."""
126
126
  if not key:
127
127
  return "Error: key required for get action"
128
-
128
+
129
129
  # Handle index scope
130
130
  if key == "index.scope":
131
131
  current_scope = self.index_config.get_scope(path)
132
132
  return f"index.scope={current_scope.value}"
133
-
133
+
134
134
  # Handle tool-specific settings
135
135
  if "." in key:
136
136
  tool, setting = key.split(".", 1)
137
137
  if setting == "enabled":
138
138
  enabled = self.index_config.is_indexing_enabled(tool)
139
139
  return f"{key}={enabled}"
140
-
140
+
141
141
  return f"Unknown key: {key}"
142
142
 
143
- async def _handle_set(self, key: Optional[str], value: Optional[str], scope: str, path: Optional[str], tool_ctx) -> str:
143
+ async def _handle_set(
144
+ self,
145
+ key: Optional[str],
146
+ value: Optional[str],
147
+ scope: str,
148
+ path: Optional[str],
149
+ tool_ctx,
150
+ ) -> str:
144
151
  """Set configuration value."""
145
152
  if not key:
146
153
  return "Error: key required for set action"
147
154
  if not value:
148
155
  return "Error: value required for set action"
149
-
156
+
150
157
  # Handle index scope
151
158
  if key == "index.scope":
152
159
  try:
153
160
  new_scope = IndexScope(value)
154
- self.index_config.set_scope(new_scope, path if scope == "local" else None)
161
+ self.index_config.set_scope(
162
+ new_scope, path if scope == "local" else None
163
+ )
155
164
  return f"Set {key}={value} ({'project' if path else 'global'})"
156
165
  except ValueError:
157
166
  return f"Error: Invalid scope value '{value}'. Valid: project, global, auto"
158
-
167
+
159
168
  # Handle tool-specific settings
160
169
  if "." in key:
161
170
  tool, setting = key.split(".", 1)
@@ -163,40 +172,44 @@ config --action toggle index.scope --path ./project"""
163
172
  enabled = value.lower() in ["true", "yes", "1", "on"]
164
173
  self.index_config.set_indexing_enabled(tool, enabled)
165
174
  return f"Set {key}={enabled}"
166
-
175
+
167
176
  return f"Unknown key: {key}"
168
177
 
169
178
  async def _handle_list(self, scope: str, path: Optional[str], tool_ctx) -> str:
170
179
  """List all configuration."""
171
180
  status = self.index_config.get_status()
172
-
181
+
173
182
  output = ["=== Configuration ==="]
174
183
  output.append(f"\nDefault scope: {status['default_scope']}")
175
-
184
+
176
185
  if path:
177
186
  current_scope = self.index_config.get_scope(path)
178
187
  output.append(f"Current path scope: {current_scope.value}")
179
-
188
+
180
189
  output.append(f"\nProjects with custom config: {status['project_count']}")
181
-
190
+
182
191
  output.append("\nTool settings:")
183
192
  for tool, settings in status["tools"].items():
184
193
  output.append(f" {tool}:")
185
194
  output.append(f" enabled: {settings['enabled']}")
186
195
  output.append(f" per_project: {settings['per_project']}")
187
-
196
+
188
197
  return "\n".join(output)
189
198
 
190
- async def _handle_toggle(self, key: Optional[str], scope: str, path: Optional[str], tool_ctx) -> str:
199
+ async def _handle_toggle(
200
+ self, key: Optional[str], scope: str, path: Optional[str], tool_ctx
201
+ ) -> str:
191
202
  """Toggle configuration value."""
192
203
  if not key:
193
204
  return "Error: key required for toggle action"
194
-
205
+
195
206
  # Handle index scope toggle
196
207
  if key == "index.scope":
197
- new_scope = self.index_config.toggle_scope(path if scope == "local" else None)
208
+ new_scope = self.index_config.toggle_scope(
209
+ path if scope == "local" else None
210
+ )
198
211
  return f"Toggled index.scope to {new_scope.value}"
199
-
212
+
200
213
  # Handle tool enable/disable toggle
201
214
  if "." in key:
202
215
  tool, setting = key.split(".", 1)
@@ -204,9 +217,9 @@ config --action toggle index.scope --path ./project"""
204
217
  current = self.index_config.is_indexing_enabled(tool)
205
218
  self.index_config.set_indexing_enabled(tool, not current)
206
219
  return f"Toggled {key} to {not current}"
207
-
220
+
208
221
  return f"Cannot toggle key: {key}"
209
222
 
210
223
  def register(self, mcp_server) -> None:
211
224
  """Register this tool with the MCP server."""
212
- pass
225
+ pass