hanzo-mcp 0.8.11__py3-none-any.whl → 0.8.13__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 (153) hide show
  1. hanzo_mcp/__init__.py +1 -3
  2. hanzo_mcp/analytics/posthog_analytics.py +3 -9
  3. hanzo_mcp/bridge.py +9 -25
  4. hanzo_mcp/cli.py +6 -15
  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 +1 -3
  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 +2 -6
  17. hanzo_mcp/tools/__init__.py +10 -24
  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 +5 -15
  21. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +14 -41
  22. hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
  23. hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
  24. hanzo_mcp/tools/agent/cli_tools.py +75 -74
  25. hanzo_mcp/tools/agent/code_auth.py +1 -3
  26. hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
  27. hanzo_mcp/tools/agent/critic_tool.py +8 -24
  28. hanzo_mcp/tools/agent/iching_tool.py +12 -36
  29. hanzo_mcp/tools/agent/network_tool.py +7 -18
  30. hanzo_mcp/tools/agent/prompt.py +1 -5
  31. hanzo_mcp/tools/agent/review_tool.py +10 -25
  32. hanzo_mcp/tools/agent/swarm_alias.py +1 -3
  33. hanzo_mcp/tools/agent/swarm_tool.py +9 -29
  34. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +11 -39
  35. hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
  36. hanzo_mcp/tools/common/batch_tool.py +15 -45
  37. hanzo_mcp/tools/common/config_tool.py +9 -28
  38. hanzo_mcp/tools/common/context.py +1 -3
  39. hanzo_mcp/tools/common/critic_tool.py +1 -3
  40. hanzo_mcp/tools/common/decorators.py +2 -6
  41. hanzo_mcp/tools/common/enhanced_base.py +2 -6
  42. hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
  43. hanzo_mcp/tools/common/forgiving_edit.py +9 -28
  44. hanzo_mcp/tools/common/mode.py +1 -5
  45. hanzo_mcp/tools/common/paginated_base.py +3 -11
  46. hanzo_mcp/tools/common/paginated_response.py +10 -30
  47. hanzo_mcp/tools/common/pagination.py +3 -9
  48. hanzo_mcp/tools/common/permissions.py +3 -9
  49. hanzo_mcp/tools/common/personality.py +9 -34
  50. hanzo_mcp/tools/common/plugin_loader.py +3 -15
  51. hanzo_mcp/tools/common/stats.py +6 -18
  52. hanzo_mcp/tools/common/thinking_tool.py +1 -3
  53. hanzo_mcp/tools/common/tool_disable.py +2 -6
  54. hanzo_mcp/tools/common/tool_list.py +2 -6
  55. hanzo_mcp/tools/common/validation.py +1 -3
  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/filesystem/__init__.py +2 -3
  73. hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
  74. hanzo_mcp/tools/filesystem/base.py +4 -12
  75. hanzo_mcp/tools/filesystem/batch_search.py +35 -115
  76. hanzo_mcp/tools/filesystem/content_replace.py +4 -12
  77. hanzo_mcp/tools/filesystem/diff.py +2 -10
  78. hanzo_mcp/tools/filesystem/directory_tree.py +9 -27
  79. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +5 -15
  80. hanzo_mcp/tools/filesystem/edit.py +6 -18
  81. hanzo_mcp/tools/filesystem/find.py +3 -9
  82. hanzo_mcp/tools/filesystem/find_files.py +2 -6
  83. hanzo_mcp/tools/filesystem/git_search.py +9 -24
  84. hanzo_mcp/tools/filesystem/grep.py +9 -27
  85. hanzo_mcp/tools/filesystem/multi_edit.py +6 -18
  86. hanzo_mcp/tools/filesystem/read.py +8 -26
  87. hanzo_mcp/tools/filesystem/rules_tool.py +6 -17
  88. hanzo_mcp/tools/filesystem/search_tool.py +18 -62
  89. hanzo_mcp/tools/filesystem/symbols_tool.py +5 -15
  90. hanzo_mcp/tools/filesystem/tree.py +1 -3
  91. hanzo_mcp/tools/filesystem/watch.py +1 -3
  92. hanzo_mcp/tools/filesystem/write.py +1 -3
  93. hanzo_mcp/tools/jupyter/base.py +6 -20
  94. hanzo_mcp/tools/jupyter/jupyter.py +4 -12
  95. hanzo_mcp/tools/jupyter/notebook_edit.py +11 -35
  96. hanzo_mcp/tools/jupyter/notebook_read.py +2 -6
  97. hanzo_mcp/tools/llm/consensus_tool.py +8 -24
  98. hanzo_mcp/tools/llm/llm_manage.py +2 -6
  99. hanzo_mcp/tools/llm/llm_tool.py +17 -58
  100. hanzo_mcp/tools/llm/llm_unified.py +18 -59
  101. hanzo_mcp/tools/llm/provider_tools.py +1 -3
  102. hanzo_mcp/tools/lsp/lsp_tool.py +5 -17
  103. hanzo_mcp/tools/mcp/mcp_add.py +1 -3
  104. hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
  105. hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
  106. hanzo_mcp/tools/memory/__init__.py +10 -27
  107. hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
  108. hanzo_mcp/tools/memory/memory_tools.py +6 -18
  109. hanzo_mcp/tools/search/find_tool.py +10 -32
  110. hanzo_mcp/tools/search/unified_search.py +24 -78
  111. hanzo_mcp/tools/shell/__init__.py +2 -2
  112. hanzo_mcp/tools/shell/auto_background.py +2 -6
  113. hanzo_mcp/tools/shell/base.py +1 -5
  114. hanzo_mcp/tools/shell/base_process.py +5 -7
  115. hanzo_mcp/tools/shell/bash_session.py +7 -24
  116. hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
  117. hanzo_mcp/tools/shell/bash_tool.py +3 -7
  118. hanzo_mcp/tools/shell/command_executor.py +26 -79
  119. hanzo_mcp/tools/shell/logs.py +4 -16
  120. hanzo_mcp/tools/shell/npx.py +2 -8
  121. hanzo_mcp/tools/shell/npx_tool.py +1 -3
  122. hanzo_mcp/tools/shell/pkill.py +4 -12
  123. hanzo_mcp/tools/shell/process_tool.py +2 -8
  124. hanzo_mcp/tools/shell/processes.py +5 -17
  125. hanzo_mcp/tools/shell/run_background.py +1 -3
  126. hanzo_mcp/tools/shell/run_command.py +1 -3
  127. hanzo_mcp/tools/shell/run_command_windows.py +1 -3
  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/todo/todo_read.py +3 -9
  137. hanzo_mcp/tools/todo/todo_write.py +6 -18
  138. hanzo_mcp/tools/vector/__init__.py +3 -9
  139. hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
  140. hanzo_mcp/tools/vector/git_ingester.py +10 -30
  141. hanzo_mcp/tools/vector/index_tool.py +3 -9
  142. hanzo_mcp/tools/vector/infinity_store.py +7 -27
  143. hanzo_mcp/tools/vector/mock_infinity.py +1 -3
  144. hanzo_mcp/tools/vector/project_manager.py +4 -12
  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.11.dist-info → hanzo_mcp-0.8.13.dist-info}/METADATA +2 -2
  149. hanzo_mcp-0.8.13.dist-info/RECORD +193 -0
  150. hanzo_mcp-0.8.11.dist-info/RECORD +0 -193
  151. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.8.13.dist-info}/WHEEL +0 -0
  152. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.8.13.dist-info}/entry_points.txt +0 -0
  153. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.8.13.dist-info}/top_level.txt +0 -0
@@ -21,10 +21,10 @@ from ...core.model_registry import registry
21
21
 
22
22
  class UnifiedCLITool(BaseTool, CLIAgent):
23
23
  """Unified CLI tool that combines BaseTool and CLIAgent functionality.
24
-
24
+
25
25
  MRO: BaseTool first for proper method resolution order.
26
26
  """
27
-
27
+
28
28
  def __init__(
29
29
  self,
30
30
  name: str,
@@ -34,7 +34,7 @@ class UnifiedCLITool(BaseTool, CLIAgent):
34
34
  permission_manager: Optional[PermissionManager] = None,
35
35
  ):
36
36
  """Initialize unified CLI tool.
37
-
37
+
38
38
  Args:
39
39
  name: Tool name
40
40
  description: Tool description
@@ -45,47 +45,47 @@ class UnifiedCLITool(BaseTool, CLIAgent):
45
45
  # Initialize CLIAgent with config
46
46
  config = AgentConfig(model=default_model)
47
47
  CLIAgent.__init__(self, config)
48
-
48
+
49
49
  # Store tool metadata
50
50
  self._name = name
51
51
  self._description = description
52
52
  self._cli_command = cli_command
53
53
  self.permission_manager = permission_manager
54
-
54
+
55
55
  @property
56
56
  def name(self) -> str:
57
57
  return self._name
58
-
58
+
59
59
  @property
60
60
  def description(self) -> str:
61
61
  return self._description
62
-
62
+
63
63
  @property
64
64
  def cli_command(self) -> str:
65
65
  return self._cli_command
66
-
66
+
67
67
  def build_command(self, prompt: str, **kwargs: Any) -> List[str]:
68
68
  """Build the CLI command with model-specific formatting.
69
-
69
+
70
70
  Args:
71
71
  prompt: The prompt
72
72
  **kwargs: Additional parameters
73
-
73
+
74
74
  Returns:
75
75
  Command arguments list
76
76
  """
77
77
  command = [self.cli_command]
78
-
78
+
79
79
  # Get model config from registry
80
80
  model_config = registry.get(self.config.model)
81
-
81
+
82
82
  # Handle different CLI tool formats
83
83
  if self.cli_command == "claude":
84
84
  if model_config:
85
85
  command.extend(["--model", model_config.full_name])
86
86
  # Claude takes prompt via stdin
87
87
  return command
88
-
88
+
89
89
  elif self.cli_command == "openai":
90
90
  # OpenAI CLI format
91
91
  command.extend(["api", "chat.completions.create"])
@@ -93,14 +93,14 @@ class UnifiedCLITool(BaseTool, CLIAgent):
93
93
  command.extend(["-m", model_config.full_name])
94
94
  command.extend(["-g", "user", prompt])
95
95
  return command
96
-
96
+
97
97
  elif self.cli_command in ["gemini", "grok"]:
98
98
  # Simple format: command --model MODEL prompt
99
99
  if model_config:
100
100
  command.extend(["--model", model_config.full_name])
101
101
  command.append(prompt)
102
102
  return command
103
-
103
+
104
104
  elif self.cli_command == "openhands":
105
105
  # OpenHands format
106
106
  command.extend(["run", prompt])
@@ -109,7 +109,7 @@ class UnifiedCLITool(BaseTool, CLIAgent):
109
109
  if self.config.working_dir:
110
110
  command.extend(["--workspace", str(self.config.working_dir)])
111
111
  return command
112
-
112
+
113
113
  elif self.cli_command == "hanzo":
114
114
  # Hanzo dev format
115
115
  command.append("dev")
@@ -117,13 +117,13 @@ class UnifiedCLITool(BaseTool, CLIAgent):
117
117
  command.extend(["--model", model_config.full_name])
118
118
  command.extend(["--prompt", prompt])
119
119
  return command
120
-
120
+
121
121
  elif self.cli_command == "cline":
122
122
  # Cline format
123
123
  command.append(prompt)
124
124
  command.append("--no-interactive")
125
125
  return command
126
-
126
+
127
127
  elif self.cli_command == "aider":
128
128
  # Aider format
129
129
  if model_config:
@@ -131,24 +131,24 @@ class UnifiedCLITool(BaseTool, CLIAgent):
131
131
  command.extend(["--message", prompt])
132
132
  command.extend(["--yes", "--no-stream"])
133
133
  return command
134
-
134
+
135
135
  elif self.cli_command == "ollama":
136
136
  # Ollama format for local models
137
137
  command.extend(["run", self.config.model.replace("ollama/", "")])
138
138
  command.append(prompt)
139
139
  return command
140
-
140
+
141
141
  # Default format
142
142
  command.append(prompt)
143
143
  return command
144
-
144
+
145
145
  async def call(self, ctx: Context[Any, Any, Any], **params: Any) -> str:
146
146
  """Execute the CLI tool via MCP interface.
147
-
147
+
148
148
  Args:
149
149
  ctx: MCP context
150
150
  **params: Tool parameters
151
-
151
+
152
152
  Returns:
153
153
  Execution result
154
154
  """
@@ -159,23 +159,23 @@ class UnifiedCLITool(BaseTool, CLIAgent):
159
159
  self.config.working_dir = Path(params["working_dir"])
160
160
  if params.get("timeout"):
161
161
  self.config.timeout = params["timeout"]
162
-
162
+
163
163
  # Execute using base agent
164
164
  result = await self.execute(
165
165
  params.get("prompt", ""),
166
166
  context=ctx,
167
167
  )
168
-
168
+
169
169
  return result.content
170
-
170
+
171
171
  def register(self, mcp_server: FastMCP) -> None:
172
172
  """Register this tool with the MCP server.
173
-
173
+
174
174
  Args:
175
175
  mcp_server: The FastMCP server instance
176
176
  """
177
177
  tool_self = self
178
-
178
+
179
179
  @mcp_server.tool(name=self.name, description=self.description)
180
180
  async def tool_wrapper(
181
181
  prompt: str,
@@ -195,15 +195,15 @@ class UnifiedCLITool(BaseTool, CLIAgent):
195
195
 
196
196
  def create_cli_tools(permission_manager: Optional[PermissionManager] = None) -> Dict[str, UnifiedCLITool]:
197
197
  """Create all CLI tools with unified implementation.
198
-
198
+
199
199
  Args:
200
200
  permission_manager: Permission manager for access control
201
-
201
+
202
202
  Returns:
203
203
  Dictionary of tool name to tool instance
204
204
  """
205
205
  tools = {}
206
-
206
+
207
207
  # Define all tools with their configurations
208
208
  tool_configs = [
209
209
  ("claude", "Execute Claude CLI for AI assistance", "claude", "claude"),
@@ -217,7 +217,7 @@ def create_cli_tools(permission_manager: Optional[PermissionManager] = None) ->
217
217
  ("cline", "Execute Cline for autonomous coding", "cline", "claude"),
218
218
  ("aider", "Execute Aider for AI pair programming", "aider", "gpt-4-turbo"),
219
219
  ]
220
-
220
+
221
221
  for name, description, cli_command, default_model in tool_configs:
222
222
  tools[name] = UnifiedCLITool(
223
223
  name=name,
@@ -226,7 +226,7 @@ def create_cli_tools(permission_manager: Optional[PermissionManager] = None) ->
226
226
  default_model=default_model,
227
227
  permission_manager=permission_manager,
228
228
  )
229
-
229
+
230
230
  return tools
231
231
 
232
232
 
@@ -235,20 +235,20 @@ def register_cli_tools(
235
235
  permission_manager: Optional[PermissionManager] = None,
236
236
  ) -> List[BaseTool]:
237
237
  """Register all CLI tools with the MCP server.
238
-
238
+
239
239
  Args:
240
240
  mcp_server: The FastMCP server instance
241
241
  permission_manager: Permission manager for access control
242
-
242
+
243
243
  Returns:
244
244
  List of registered CLI tools
245
245
  """
246
246
  tools = create_cli_tools(permission_manager)
247
-
247
+
248
248
  # Register each tool
249
249
  for tool in tools.values():
250
250
  tool.register(mcp_server)
251
-
251
+
252
252
  return list(tools.values())
253
253
 
254
254
 
@@ -256,4 +256,4 @@ __all__ = [
256
256
  "UnifiedCLITool",
257
257
  "create_cli_tools",
258
258
  "register_cli_tools",
259
- ]
259
+ ]
@@ -177,30 +177,22 @@ Not available: think,write,edit,multi_edit,notebook_edit
177
177
 
178
178
  # Validate required parameters
179
179
  if not description:
180
- await tool_ctx.error(
181
- "Parameter 'description' is required but was None or empty"
182
- )
180
+ await tool_ctx.error("Parameter 'description' is required but was None or empty")
183
181
  return "Error: Parameter 'description' is required but was None or empty"
184
182
 
185
183
  if not invocations:
186
- await tool_ctx.error(
187
- "Parameter 'invocations' is required but was None or empty"
188
- )
184
+ await tool_ctx.error("Parameter 'invocations' is required but was None or empty")
189
185
  return "Error: Parameter 'invocations' is required but was None or empty"
190
186
 
191
187
  if not isinstance(invocations, list) or len(invocations) == 0:
192
188
  await tool_ctx.error("Parameter 'invocations' must be a non-empty list")
193
189
  return "Error: Parameter 'invocations' must be a non-empty list"
194
190
 
195
- await tool_ctx.info(
196
- f"Executing batch operation: {description} ({len(invocations)} invocations)"
197
- )
191
+ await tool_ctx.info(f"Executing batch operation: {description} ({len(invocations)} invocations)")
198
192
 
199
193
  # Execute all tool invocations in parallel
200
194
  tasks: list[asyncio.Future[dict[str, Any]]] = []
201
- invocation_map: dict[asyncio.Future[dict[str, Any]], dict[str, Any]] = (
202
- {}
203
- ) # Map task Future to invocation
195
+ invocation_map: dict[asyncio.Future[dict[str, Any]], dict[str, Any]] = {} # Map task Future to invocation
204
196
 
205
197
  for i, invocation in enumerate(invocations):
206
198
  # Extract tool name and input from invocation
@@ -213,9 +205,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
213
205
  await tool_ctx.error(error_message)
214
206
  # Add direct result for this invocation
215
207
  tasks.append(asyncio.Future())
216
- tasks[-1].set_result(
217
- {"invocation": invocation, "result": f"Error: {error_message}"}
218
- )
208
+ tasks[-1].set_result({"invocation": invocation, "result": f"Error: {error_message}"})
219
209
  invocation_map[tasks[-1]] = invocation
220
210
  continue
221
211
 
@@ -225,9 +215,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
225
215
  await tool_ctx.error(error_message)
226
216
  # Add direct result for this invocation
227
217
  tasks.append(asyncio.Future())
228
- tasks[-1].set_result(
229
- {"invocation": invocation, "result": f"Error: {error_message}"}
230
- )
218
+ tasks[-1].set_result({"invocation": invocation, "result": f"Error: {error_message}"})
231
219
  invocation_map[tasks[-1]] = invocation
232
220
  continue
233
221
 
@@ -237,9 +225,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
237
225
  await tool_ctx.info(f"Creating task for tool: {tool_name}")
238
226
 
239
227
  # Create coroutine for this tool execution
240
- async def execute_tool(
241
- tool_obj: BaseTool, tool_name: str, tool_input: dict[str, Any]
242
- ):
228
+ async def execute_tool(tool_obj: BaseTool, tool_name: str, tool_input: dict[str, Any]):
243
229
  try:
244
230
  await tool_ctx.info(f"Executing tool: {tool_name}")
245
231
  result = await tool_obj.call(ctx, **tool_input)
@@ -265,9 +251,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
265
251
  await tool_ctx.error(error_message)
266
252
  # Add direct result for this invocation
267
253
  tasks.append(asyncio.Future())
268
- tasks[-1].set_result(
269
- {"invocation": invocation, "result": f"Error: {error_message}"}
270
- )
254
+ tasks[-1].set_result({"invocation": invocation, "result": f"Error: {error_message}"})
271
255
  invocation_map[tasks[-1]] = invocation
272
256
 
273
257
  # Wait for all tasks to complete
@@ -284,9 +268,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
284
268
  tool_name: str = invocation.get("tool_name", "unknown")
285
269
  error_message = f"Unexpected error in tool '{tool_name}': {str(e)}"
286
270
  await tool_ctx.error(error_message)
287
- results.append(
288
- {"invocation": invocation, "result": f"Error: {error_message}"}
289
- )
271
+ results.append({"invocation": invocation, "result": f"Error: {error_message}"})
290
272
 
291
273
  # Extract cursor if provided
292
274
  cursor = params.get("cursor")
@@ -314,9 +296,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
314
296
  )
315
297
 
316
298
  # Create paginated response with token awareness
317
- paginated_response = create_paginated_response(
318
- formatted_results, cursor=cursor, use_token_limit=True
319
- )
299
+ paginated_response = create_paginated_response(formatted_results, cursor=cursor, use_token_limit=True)
320
300
 
321
301
  # Convert paginated response to string format for MCP
322
302
  if isinstance(paginated_response, dict) and "items" in paginated_response:
@@ -326,13 +306,9 @@ Not available: think,write,edit,multi_edit,notebook_edit
326
306
  # Add header
327
307
  result_parts.append(f"=== Batch operation: {description} ===")
328
308
  result_parts.append(f"Total invocations: {len(invocations)}")
329
- result_parts.append(
330
- f"Showing results: {len(paginated_response['items'])} of {len(results)}"
331
- )
309
+ result_parts.append(f"Showing results: {len(paginated_response['items'])} of {len(results)}")
332
310
  if paginated_response.get("hasMore"):
333
- result_parts.append(
334
- f"More results available - use cursor: {paginated_response.get('nextCursor')}"
335
- )
311
+ result_parts.append(f"More results available - use cursor: {paginated_response.get('nextCursor')}")
336
312
  result_parts.append("")
337
313
 
338
314
  # Format each result
@@ -352,12 +328,8 @@ Not available: think,write,edit,multi_edit,notebook_edit
352
328
 
353
329
  # If there's a next cursor, we need to preserve it in the response
354
330
  # For now, append it as a note at the end
355
- if paginated_response.get("hasMore") and paginated_response.get(
356
- "nextCursor"
357
- ):
358
- formatted_output += (
359
- f"\n\n[To continue, use cursor: {paginated_response['nextCursor']}]"
360
- )
331
+ if paginated_response.get("hasMore") and paginated_response.get("nextCursor"):
332
+ formatted_output += f"\n\n[To continue, use cursor: {paginated_response['nextCursor']}]"
361
333
 
362
334
  await tool_ctx.info(
363
335
  f"Batch operation '{description}' completed with {len(paginated_response['items'])} results"
@@ -424,6 +396,4 @@ Not available: think,write,edit,multi_edit,notebook_edit
424
396
  cursor: Cursor,
425
397
  ctx: MCPContext,
426
398
  ) -> str:
427
- return await tool_self.call(
428
- ctx, description=description, invocations=invocations, cursor=cursor
429
- )
399
+ return await tool_self.call(ctx, description=description, invocations=invocations, cursor=cursor)
@@ -87,17 +87,11 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
87
87
 
88
88
  try:
89
89
  if action == "get":
90
- return await self._get_config(
91
- scope, setting_path, project_name, project_path
92
- )
90
+ return await self._get_config(scope, setting_path, project_name, project_path)
93
91
  elif action == "set":
94
- return await self._set_config(
95
- scope, setting_path, value, project_name, project_path
96
- )
92
+ return await self._set_config(scope, setting_path, value, project_name, project_path)
97
93
  elif action == "add_server":
98
- return await self._add_mcp_server(
99
- server_name, server_config, scope, project_name
100
- )
94
+ return await self._add_mcp_server(server_name, server_config, scope, project_name)
101
95
  elif action == "remove_server":
102
96
  return await self._remove_mcp_server(server_name, scope, project_name)
103
97
  elif action == "enable_server":
@@ -158,10 +152,7 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
158
152
  "agent": settings.agent.__dict__,
159
153
  "vector_store": settings.vector_store.__dict__,
160
154
  "hub_enabled": settings.hub_enabled,
161
- "mcp_servers": {
162
- name: server.__dict__
163
- for name, server in settings.mcp_servers.items()
164
- },
155
+ "mcp_servers": {name: server.__dict__ for name, server in settings.mcp_servers.items()},
165
156
  "current_project": settings.current_project,
166
157
  "projects": list(settings.projects.keys()),
167
158
  }
@@ -239,9 +230,7 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
239
230
  else:
240
231
  return f"Error: MCP server '{server_name}' already exists"
241
232
 
242
- async def _remove_mcp_server(
243
- self, server_name: Optional[str], scope: str, project_name: Optional[str]
244
- ) -> str:
233
+ async def _remove_mcp_server(self, server_name: Optional[str], scope: str, project_name: Optional[str]) -> str:
245
234
  """Remove an MCP server."""
246
235
  if not server_name:
247
236
  return "Error: server_name is required"
@@ -254,9 +243,7 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
254
243
  else:
255
244
  return f"Error: MCP server '{server_name}' not found"
256
245
 
257
- async def _enable_mcp_server(
258
- self, server_name: Optional[str], scope: str, project_name: Optional[str]
259
- ) -> str:
246
+ async def _enable_mcp_server(self, server_name: Optional[str], scope: str, project_name: Optional[str]) -> str:
260
247
  """Enable an MCP server."""
261
248
  if not server_name:
262
249
  return "Error: server_name is required"
@@ -269,9 +256,7 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
269
256
  else:
270
257
  return f"Error: MCP server '{server_name}' not found"
271
258
 
272
- async def _disable_mcp_server(
273
- self, server_name: Optional[str], scope: str, project_name: Optional[str]
274
- ) -> str:
259
+ async def _disable_mcp_server(self, server_name: Optional[str], scope: str, project_name: Optional[str]) -> str:
275
260
  """Disable an MCP server."""
276
261
  if not server_name:
277
262
  return "Error: server_name is required"
@@ -297,9 +282,7 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
297
282
  else:
298
283
  return f"Error: MCP server '{server_name}' not found"
299
284
 
300
- async def _add_project(
301
- self, project_name: Optional[str], project_path: Optional[str]
302
- ) -> str:
285
+ async def _add_project(self, project_name: Optional[str], project_path: Optional[str]) -> str:
303
286
  """Add a project configuration."""
304
287
  if not project_path:
305
288
  return "Error: project_path is required"
@@ -325,9 +308,7 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
325
308
  else:
326
309
  return f"Error: Project '{project_name}' already exists"
327
310
 
328
- async def _set_current_project(
329
- self, project_name: Optional[str], project_path: Optional[str]
330
- ) -> str:
311
+ async def _set_current_project(self, project_name: Optional[str], project_path: Optional[str]) -> str:
331
312
  """Set the current active project."""
332
313
  settings = load_settings()
333
314
 
@@ -67,9 +67,7 @@ class ToolContext:
67
67
  """
68
68
  return self._mcp_context.client_id
69
69
 
70
- async def set_tool_info(
71
- self, tool_name: str, execution_id: str | None = None
72
- ) -> None:
70
+ async def set_tool_info(self, tool_name: str, execution_id: str | None = None) -> None:
73
71
  """Set information about the currently executing tool.
74
72
 
75
73
  Args:
@@ -153,9 +153,7 @@ Recommendations:
153
153
 
154
154
  # Validate required analysis parameter
155
155
  if not analysis:
156
- await tool_ctx.error(
157
- "Parameter 'analysis' is required but was None or empty"
158
- )
156
+ await tool_ctx.error("Parameter 'analysis' is required but was None or empty")
159
157
  return "Error: Parameter 'analysis' is required but was None or empty"
160
158
 
161
159
  if analysis.strip() == "":
@@ -120,9 +120,7 @@ def _is_valid_context(ctx: Any) -> bool:
120
120
  )
121
121
 
122
122
 
123
- def mcp_tool(
124
- server: Any, name: str | None = None, description: str | None = None
125
- ) -> Callable[[F], F]:
123
+ def mcp_tool(server: Any, name: str | None = None, description: str | None = None) -> Callable[[F], F]:
126
124
  """Enhanced MCP tool decorator that includes context normalization.
127
125
 
128
126
  This decorator combines the standard MCP tool registration with
@@ -192,9 +190,7 @@ def create_tool_handler(server: Any, tool: Any) -> Callable[[], None]:
192
190
  # Apply context normalization
193
191
  normalized = with_context_normalization(func)
194
192
  # Apply original decorator
195
- return original_tool_decorator(name=name, description=description)(
196
- normalized
197
- )
193
+ return original_tool_decorator(name=name, description=description)(normalized)
198
194
 
199
195
  return decorator
200
196
 
@@ -85,9 +85,7 @@ class AutoRegisterTool(BaseTool, ABC):
85
85
  params = list(sig.parameters.items())
86
86
 
87
87
  # Skip 'self' and 'ctx' parameters
88
- tool_params = [
89
- (name, param) for name, param in params if name not in ("self", "ctx")
90
- ]
88
+ tool_params = [(name, param) for name, param in params if name not in ("self", "ctx")]
91
89
 
92
90
  # Create the handler function dynamically
93
91
  async def handler(ctx: MCPContext, **kwargs: Any) -> Any:
@@ -98,6 +96,4 @@ class AutoRegisterTool(BaseTool, ABC):
98
96
  normalized_handler = with_context_normalization(handler)
99
97
 
100
98
  # Register with the server
101
- mcp_server.tool(name=self.name, description=self.description)(
102
- normalized_handler
103
- )
99
+ mcp_server.tool(name=self.name, description=self.description)(normalized_handler)
@@ -143,9 +143,7 @@ class FastMCPPaginator(Generic[T]):
143
143
  cursor_data = CursorData.from_cursor(cursor) if cursor else CursorData()
144
144
 
145
145
  # Use provided page size or default
146
- actual_page_size = min(
147
- page_size or cursor_data.page_size or self.page_size, self.max_page_size
148
- )
146
+ actual_page_size = min(page_size or cursor_data.page_size or self.page_size, self.max_page_size)
149
147
 
150
148
  # Get starting position
151
149
  start_idx = cursor_data.offset
@@ -207,9 +205,7 @@ class FastMCPPaginator(Generic[T]):
207
205
  cursor_data = CursorData.from_cursor(cursor) if cursor else CursorData()
208
206
 
209
207
  # Determine page size
210
- limit = min(
211
- page_size or cursor_data.page_size or self.page_size, self.max_page_size
212
- )
208
+ limit = min(page_size or cursor_data.page_size or self.page_size, self.max_page_size)
213
209
 
214
210
  # Execute query with cursor position
215
211
  results = query_func(
@@ -258,9 +254,7 @@ class TokenAwarePaginator:
258
254
  self.max_tokens = max_tokens
259
255
  self.paginator = FastMCPPaginator()
260
256
 
261
- def paginate_by_tokens(
262
- self, items: List[Any], cursor: Optional[str] = None, estimate_func=None
263
- ) -> Dict[str, Any]:
257
+ def paginate_by_tokens(self, items: List[Any], cursor: Optional[str] = None, estimate_func=None) -> Dict[str, Any]:
264
258
  """Paginate items based on token count.
265
259
 
266
260
  Args:
@@ -275,9 +269,7 @@ class TokenAwarePaginator:
275
269
 
276
270
  # Default token estimation
277
271
  if not estimate_func:
278
- estimate_func = lambda x: estimate_tokens(
279
- json.dumps(x) if not isinstance(x, str) else x
280
- )
272
+ estimate_func = lambda x: estimate_tokens(json.dumps(x) if not isinstance(x, str) else x)
281
273
 
282
274
  # Parse cursor
283
275
  cursor_data = CursorData.from_cursor(cursor) if cursor else CursorData()
@@ -42,9 +42,7 @@ class ForgivingEditHelper:
42
42
  return "\n".join(lines)
43
43
 
44
44
  @staticmethod
45
- def find_fuzzy_match(
46
- haystack: str, needle: str, threshold: float = 0.85
47
- ) -> Optional[Tuple[int, int, str]]:
45
+ def find_fuzzy_match(haystack: str, needle: str, threshold: float = 0.85) -> Optional[Tuple[int, int, str]]:
48
46
  """Find a fuzzy match for the needle in the haystack.
49
47
 
50
48
  Args:
@@ -81,13 +79,9 @@ class ForgivingEditHelper:
81
79
 
82
80
  # Find end position by counting lines in needle
83
81
  needle_lines = norm_needle.count("\n") + 1
84
- end_pos = sum(
85
- len(line) + 1 for line in original_lines[: lines_before + needle_lines]
86
- )
82
+ end_pos = sum(len(line) + 1 for line in original_lines[: lines_before + needle_lines])
87
83
 
88
- matched = "\n".join(
89
- original_lines[lines_before : lines_before + needle_lines]
90
- )
84
+ matched = "\n".join(original_lines[lines_before : lines_before + needle_lines])
91
85
  return (start_pos, end_pos - 1, matched)
92
86
 
93
87
  # Try fuzzy matching on lines
@@ -119,9 +113,7 @@ class ForgivingEditHelper:
119
113
  candidate_norm = ForgivingEditHelper.normalize_whitespace(candidate)
120
114
  needle_norm = ForgivingEditHelper.normalize_whitespace(needle)
121
115
 
122
- ratio = difflib.SequenceMatcher(
123
- None, candidate_norm, needle_norm
124
- ).ratio()
116
+ ratio = difflib.SequenceMatcher(None, candidate_norm, needle_norm).ratio()
125
117
 
126
118
  if ratio >= threshold:
127
119
  start_pos = sum(len(l) + 1 for l in haystack_lines[:i])
@@ -130,9 +122,7 @@ class ForgivingEditHelper:
130
122
  return None
131
123
 
132
124
  @staticmethod
133
- def suggest_matches(
134
- haystack: str, needle: str, max_suggestions: int = 3
135
- ) -> List[Tuple[float, str]]:
125
+ def suggest_matches(haystack: str, needle: str, max_suggestions: int = 3) -> List[Tuple[float, str]]:
136
126
  """Suggest possible matches when exact match fails.
137
127
 
138
128
  Args:
@@ -154,9 +144,7 @@ class ForgivingEditHelper:
154
144
  for line in haystack.split("\n"):
155
145
  if line.strip(): # Skip empty lines
156
146
  line_norm = ForgivingEditHelper.normalize_whitespace(line)
157
- ratio = difflib.SequenceMatcher(
158
- None, line_norm, needle_norm
159
- ).ratio()
147
+ ratio = difflib.SequenceMatcher(None, line_norm, needle_norm).ratio()
160
148
  if ratio > 0.5: # Only reasonably similar lines
161
149
  suggestions.append((ratio, line))
162
150
 
@@ -170,9 +158,7 @@ class ForgivingEditHelper:
170
158
  candidate = "\n".join(candidate_lines)
171
159
  candidate_norm = ForgivingEditHelper.normalize_whitespace(candidate)
172
160
 
173
- ratio = difflib.SequenceMatcher(
174
- None, candidate_norm, needle_norm
175
- ).ratio()
161
+ ratio = difflib.SequenceMatcher(None, candidate_norm, needle_norm).ratio()
176
162
  if ratio > 0.5:
177
163
  suggestions.append((ratio, candidate))
178
164
 
@@ -181,9 +167,7 @@ class ForgivingEditHelper:
181
167
  return suggestions[:max_suggestions]
182
168
 
183
169
  @staticmethod
184
- def create_edit_suggestion(
185
- file_content: str, old_string: str, new_string: str
186
- ) -> dict:
170
+ def create_edit_suggestion(file_content: str, old_string: str, new_string: str) -> dict:
187
171
  """Create a helpful edit suggestion when match fails.
188
172
 
189
173
  Args:
@@ -212,10 +196,7 @@ class ForgivingEditHelper:
212
196
  if suggestions:
213
197
  return {
214
198
  "error": "Could not find exact or fuzzy match",
215
- "suggestions": [
216
- {"similarity": f"{score:.0%}", "text": text}
217
- for score, text in suggestions
218
- ],
199
+ "suggestions": [{"similarity": f"{score:.0%}", "text": text} for score, text in suggestions],
219
200
  "hint": "Try using one of these suggestions as old_string",
220
201
  }
221
202