hanzo-mcp 0.7.6__py3-none-any.whl → 0.8.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (178) hide show
  1. hanzo_mcp/__init__.py +7 -1
  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.6.dist-info → hanzo_mcp-0.8.0.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.0.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.6.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,16 @@
1
1
  """Run Python packages with uvx."""
2
2
 
3
- import subprocess
4
3
  import shutil
5
- from typing import Annotated, Optional, TypedDict, Unpack, final, override
4
+ import subprocess
5
+ from typing import Unpack, Optional, Annotated, TypedDict, final, override
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
  from hanzo_mcp.tools.common.permissions import PermissionManager
13
13
 
14
-
15
14
  Package = Annotated[
16
15
  str,
17
16
  Field(
@@ -130,28 +129,27 @@ For long-running servers, use uvx_background instead.
130
129
  # Check if uvx is available
131
130
  if not shutil.which("uvx"):
132
131
  await tool_ctx.info("uvx not found, attempting to install...")
133
-
132
+
134
133
  # Try to auto-install uvx
135
134
  install_cmd = "curl -LsSf https://astral.sh/uv/install.sh | sh"
136
-
135
+
137
136
  try:
138
137
  # Run installation
139
138
  install_result = subprocess.run(
140
- install_cmd,
141
- shell=True,
142
- capture_output=True,
143
- text=True,
144
- timeout=60
139
+ install_cmd, shell=True, capture_output=True, text=True, timeout=60
145
140
  )
146
-
141
+
147
142
  if install_result.returncode == 0:
148
143
  await tool_ctx.info("uvx installed successfully!")
149
-
144
+
150
145
  # Add to PATH for current session
151
146
  import os
147
+
152
148
  home = os.path.expanduser("~")
153
- os.environ["PATH"] = f"{home}/.cargo/bin:{os.environ.get('PATH', '')}"
154
-
149
+ os.environ["PATH"] = (
150
+ f"{home}/.cargo/bin:{os.environ.get('PATH', '')}"
151
+ )
152
+
155
153
  # Check again
156
154
  if not shutil.which("uvx"):
157
155
  return """Error: uvx installed but not found in PATH.
@@ -170,7 +168,7 @@ Or on macOS:
170
168
  brew install uv
171
169
 
172
170
  Error details: {install_result.stderr}"""
173
-
171
+
174
172
  except subprocess.TimeoutExpired:
175
173
  return """Error: Installation timed out. Install uvx manually with:
176
174
  curl -LsSf https://astral.sh/uv/install.sh | sh"""
@@ -182,16 +180,17 @@ curl -LsSf https://astral.sh/uv/install.sh | sh"""
182
180
 
183
181
  # Build command
184
182
  cmd = ["uvx"]
185
-
183
+
186
184
  if python_version:
187
185
  cmd.extend(["--python", python_version])
188
-
186
+
189
187
  cmd.append(package)
190
-
188
+
191
189
  # Add package arguments
192
190
  if args:
193
191
  # Split args properly (basic parsing)
194
192
  import shlex
193
+
195
194
  cmd.extend(shlex.split(args))
196
195
 
197
196
  await tool_ctx.info(f"Running: {' '.join(cmd)}")
@@ -199,11 +198,7 @@ curl -LsSf https://astral.sh/uv/install.sh | sh"""
199
198
  try:
200
199
  # Execute command
201
200
  result = subprocess.run(
202
- cmd,
203
- capture_output=True,
204
- text=True,
205
- timeout=timeout,
206
- check=True
201
+ cmd, capture_output=True, text=True, timeout=timeout, check=True
207
202
  )
208
203
 
209
204
  output = []
@@ -212,7 +207,11 @@ curl -LsSf https://astral.sh/uv/install.sh | sh"""
212
207
  if result.stderr:
213
208
  output.append(f"\nSTDERR:\n{result.stderr}")
214
209
 
215
- return "\n".join(output) if output else "Command completed successfully with no output."
210
+ return (
211
+ "\n".join(output)
212
+ if output
213
+ else "Command completed successfully with no output."
214
+ )
216
215
 
217
216
  except subprocess.TimeoutExpired:
218
217
  return f"Error: Command timed out after {timeout} seconds. Use uvx_background for long-running processes."
@@ -1,19 +1,18 @@
1
1
  """Run Python packages in background with uvx."""
2
2
 
3
- import subprocess
4
- import shutil
5
3
  import uuid
6
- from typing import Annotated, Optional, TypedDict, Unpack, final, override
4
+ import shutil
5
+ import subprocess
6
+ from typing import Unpack, Optional, Annotated, TypedDict, final, override
7
7
 
8
- from mcp.server.fastmcp import Context as MCPContext
9
8
  from pydantic import Field
9
+ from mcp.server.fastmcp import Context as MCPContext
10
10
 
11
11
  from hanzo_mcp.tools.common.base import BaseTool
12
12
  from hanzo_mcp.tools.common.context import create_tool_context
13
13
  from hanzo_mcp.tools.common.permissions import PermissionManager
14
14
  from hanzo_mcp.tools.shell.run_background import BackgroundProcess, RunBackgroundTool
15
15
 
16
-
17
16
  Package = Annotated[
18
17
  str,
19
18
  Field(
@@ -151,28 +150,27 @@ Use 'processes' to list running processes and 'pkill' to stop them.
151
150
  # Check if uvx is available
152
151
  if not shutil.which("uvx"):
153
152
  await tool_ctx.info("uvx not found, attempting to install...")
154
-
153
+
155
154
  # Try to auto-install uvx
156
155
  install_cmd = "curl -LsSf https://astral.sh/uv/install.sh | sh"
157
-
156
+
158
157
  try:
159
158
  # Run installation
160
159
  install_result = subprocess.run(
161
- install_cmd,
162
- shell=True,
163
- capture_output=True,
164
- text=True,
165
- timeout=60
160
+ install_cmd, shell=True, capture_output=True, text=True, timeout=60
166
161
  )
167
-
162
+
168
163
  if install_result.returncode == 0:
169
164
  await tool_ctx.info("uvx installed successfully!")
170
-
165
+
171
166
  # Add to PATH for current session
172
167
  import os
168
+
173
169
  home = os.path.expanduser("~")
174
- os.environ["PATH"] = f"{home}/.cargo/bin:{os.environ.get('PATH', '')}"
175
-
170
+ os.environ["PATH"] = (
171
+ f"{home}/.cargo/bin:{os.environ.get('PATH', '')}"
172
+ )
173
+
176
174
  # Check again
177
175
  if not shutil.which("uvx"):
178
176
  return """Error: uvx installed but not found in PATH.
@@ -191,7 +189,7 @@ Or on macOS:
191
189
  brew install uv
192
190
 
193
191
  Error details: {install_result.stderr}"""
194
-
192
+
195
193
  except subprocess.TimeoutExpired:
196
194
  return """Error: Installation timed out. Install uvx manually with:
197
195
  curl -LsSf https://astral.sh/uv/install.sh | sh"""
@@ -203,25 +201,27 @@ curl -LsSf https://astral.sh/uv/install.sh | sh"""
203
201
 
204
202
  # Build command
205
203
  cmd = ["uvx"]
206
-
204
+
207
205
  if python_version:
208
206
  cmd.extend(["--python", python_version])
209
-
207
+
210
208
  cmd.append(package)
211
-
209
+
212
210
  # Add package arguments
213
211
  if args:
214
212
  # Split args properly (basic parsing)
215
213
  import shlex
214
+
216
215
  cmd.extend(shlex.split(args))
217
216
 
218
217
  # Generate process ID
219
218
  process_id = str(uuid.uuid4())[:8]
220
-
219
+
221
220
  # Prepare log file if needed
222
221
  log_file = None
223
222
  if log_output:
224
223
  from pathlib import Path
224
+
225
225
  log_dir = Path.home() / ".hanzo" / "logs"
226
226
  log_dir.mkdir(parents=True, exist_ok=True)
227
227
  log_file = log_dir / f"{name}_{process_id}.log"
@@ -237,7 +237,7 @@ curl -LsSf https://astral.sh/uv/install.sh | sh"""
237
237
  stdout=f,
238
238
  stderr=subprocess.STDOUT,
239
239
  cwd=working_dir,
240
- start_new_session=True
240
+ start_new_session=True,
241
241
  )
242
242
  else:
243
243
  process = subprocess.Popen(
@@ -245,7 +245,7 @@ curl -LsSf https://astral.sh/uv/install.sh | sh"""
245
245
  stdout=subprocess.DEVNULL,
246
246
  stderr=subprocess.DEVNULL,
247
247
  cwd=working_dir,
248
- start_new_session=True
248
+ start_new_session=True,
249
249
  )
250
250
 
251
251
  # Create background process object
@@ -255,7 +255,7 @@ curl -LsSf https://astral.sh/uv/install.sh | sh"""
255
255
  name=name,
256
256
  process=process,
257
257
  log_file=str(log_file) if log_file else None,
258
- working_dir=working_dir
258
+ working_dir=working_dir,
259
259
  )
260
260
 
261
261
  # Register with RunBackgroundTool
@@ -269,19 +269,21 @@ curl -LsSf https://astral.sh/uv/install.sh | sh"""
269
269
  f" PID: {process.pid}",
270
270
  f" Command: {' '.join(cmd)}",
271
271
  ]
272
-
272
+
273
273
  if working_dir:
274
274
  output.append(f" Working Dir: {working_dir}")
275
-
275
+
276
276
  if log_file:
277
277
  output.append(f" Log: {log_file}")
278
-
279
- output.extend([
280
- "",
281
- "Use 'processes' to list running processes.",
282
- f"Use 'logs --process-id {process_id}' to view output.",
283
- f"Use 'pkill --process-id {process_id}' to stop."
284
- ])
278
+
279
+ output.extend(
280
+ [
281
+ "",
282
+ "Use 'processes' to list running processes.",
283
+ f"Use 'logs --process-id {process_id}' to view output.",
284
+ f"Use 'pkill --process-id {process_id}' to stop.",
285
+ ]
286
+ )
285
287
 
286
288
  return "\n".join(output)
287
289
 
@@ -1,19 +1,19 @@
1
1
  """UVX tool for both sync and background execution."""
2
2
 
3
- from pathlib import Path
4
3
  from typing import Optional, override
4
+ from pathlib import Path
5
5
 
6
+ from mcp.server import FastMCP
6
7
  from mcp.server.fastmcp import Context as MCPContext
7
8
 
8
9
  from hanzo_mcp.tools.shell.base_process import BaseBinaryTool
9
- from mcp.server import FastMCP
10
10
 
11
11
 
12
12
  class UvxTool(BaseBinaryTool):
13
13
  """Tool for running uvx commands."""
14
-
14
+
15
15
  name = "uvx"
16
-
16
+
17
17
  @property
18
18
  @override
19
19
  def description(self) -> str:
@@ -27,12 +27,12 @@ uvx ruff check .
27
27
  uvx mkdocs serve # Auto-backgrounds after 2 minutes
28
28
  uvx black --check src/
29
29
  uvx jupyter lab --port 8888 # Auto-backgrounds if needed"""
30
-
30
+
31
31
  @override
32
32
  def get_binary_name(self) -> str:
33
33
  """Get the binary name."""
34
34
  return "uvx"
35
-
35
+
36
36
  @override
37
37
  async def run(
38
38
  self,
@@ -43,57 +43,53 @@ uvx jupyter lab --port 8888 # Auto-backgrounds if needed"""
43
43
  python: Optional[str] = None,
44
44
  ) -> str:
45
45
  """Run a uvx command with auto-backgrounding.
46
-
46
+
47
47
  Args:
48
48
  ctx: MCP context
49
49
  package: Python package to run
50
50
  args: Additional arguments
51
51
  cwd: Working directory
52
52
  python: Python version constraint
53
-
53
+
54
54
  Returns:
55
55
  Command output or background status
56
56
  """
57
57
  # Prepare working directory
58
58
  work_dir = Path(cwd).resolve() if cwd else Path.cwd()
59
-
59
+
60
60
  # Prepare flags
61
61
  flags = []
62
62
  if python:
63
63
  flags.extend(["--python", python])
64
-
64
+
65
65
  # Build full command
66
66
  full_args = args.split() if args else []
67
-
67
+
68
68
  # Always use execute_sync which now has auto-backgrounding
69
69
  return await self.execute_sync(
70
70
  package,
71
71
  cwd=work_dir,
72
72
  flags=flags,
73
73
  args=full_args,
74
- timeout=None # Let auto-backgrounding handle timeout
74
+ timeout=None, # Let auto-backgrounding handle timeout
75
75
  )
76
76
 
77
77
  def register(self, server: FastMCP) -> None:
78
78
  """Register the tool with the MCP server."""
79
79
  tool_self = self
80
-
80
+
81
81
  @server.tool(name=self.name, description=self.description)
82
82
  async def uvx(
83
83
  ctx: MCPContext,
84
84
  package: str,
85
85
  args: str = "",
86
86
  cwd: Optional[str] = None,
87
- python: Optional[str] = None
87
+ python: Optional[str] = None,
88
88
  ) -> str:
89
89
  return await tool_self.run(
90
- ctx,
91
- package=package,
92
- args=args,
93
- cwd=cwd,
94
- python=python
90
+ ctx, package=package, args=args, cwd=cwd, python=python
95
91
  )
96
-
92
+
97
93
  async def call(self, ctx: MCPContext, **params) -> str:
98
94
  """Call the tool with arguments."""
99
95
  return await self.run(
@@ -101,9 +97,9 @@ uvx jupyter lab --port 8888 # Auto-backgrounds if needed"""
101
97
  package=params["package"],
102
98
  args=params.get("args", ""),
103
99
  cwd=params.get("cwd"),
104
- python=params.get("python")
100
+ python=params.get("python"),
105
101
  )
106
102
 
107
103
 
108
104
  # Create tool instance
109
- uvx_tool = UvxTool()
105
+ uvx_tool = UvxTool()
@@ -6,8 +6,8 @@ within Claude Desktop sessions.
6
6
 
7
7
  from mcp.server import FastMCP
8
8
 
9
- from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
10
9
  from hanzo_mcp.tools.todo.todo import TodoTool
10
+ from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
11
11
 
12
12
  # Export all tool classes
13
13
  __all__ = [
@@ -51,7 +51,11 @@ def register_todo_tools(
51
51
  if enabled_tools:
52
52
  # Use individual tool configuration
53
53
  # Support both old names and new name for backward compatibility
54
- if enabled_tools.get("todo", True) or enabled_tools.get("todo_read", True) or enabled_tools.get("todo_write", True):
54
+ if (
55
+ enabled_tools.get("todo", True)
56
+ or enabled_tools.get("todo_read", True)
57
+ or enabled_tools.get("todo_write", True)
58
+ ):
55
59
  tools.append(TodoTool())
56
60
  else:
57
61
  # Use all tools (backward compatibility)
@@ -1,17 +1,23 @@
1
1
  """Unified todo tool."""
2
2
 
3
- from typing import Annotated, TypedDict, Unpack, final, override, Optional, List, Dict, Any
4
- import json
5
3
  import uuid
4
+ from typing import (
5
+ Any,
6
+ Dict,
7
+ Unpack,
8
+ Optional,
9
+ Annotated,
10
+ TypedDict,
11
+ final,
12
+ override,
13
+ )
6
14
  from datetime import datetime
7
- from pathlib import Path
8
15
 
9
- from mcp.server.fastmcp import Context as MCPContext
10
16
  from pydantic import Field
17
+ from mcp.server.fastmcp import Context as MCPContext
11
18
 
12
19
  from hanzo_mcp.tools.todo.base import TodoBaseTool
13
20
 
14
-
15
21
  # Parameter types
16
22
  Action = Annotated[
17
23
  str,
@@ -64,6 +70,7 @@ Filter = Annotated[
64
70
 
65
71
  class TodoParams(TypedDict, total=False):
66
72
  """Parameters for todo tool."""
73
+
67
74
  action: str
68
75
  content: Optional[str]
69
76
  id: Optional[str]
@@ -107,7 +114,7 @@ todo --filter in_progress
107
114
 
108
115
  # Extract action
109
116
  action = params.get("action", "list")
110
-
117
+
111
118
  # Route to appropriate handler
112
119
  if action == "list":
113
120
  return await self._handle_list(params.get("filter"), tool_ctx)
@@ -125,16 +132,16 @@ todo --filter in_progress
125
132
  async def _handle_list(self, filter_status: Optional[str], tool_ctx) -> str:
126
133
  """List todos."""
127
134
  todos = self.read_todos()
128
-
135
+
129
136
  if not todos:
130
137
  return "No todos found. Use 'todo \"Your task here\"' to add one."
131
-
138
+
132
139
  # Apply filter if specified
133
140
  if filter_status:
134
141
  todos = [t for t in todos if t.get("status") == filter_status]
135
142
  if not todos:
136
143
  return f"No todos with status '{filter_status}'"
137
-
144
+
138
145
  # Group by status
139
146
  by_status = {}
140
147
  for todo in todos:
@@ -142,21 +149,27 @@ todo --filter in_progress
142
149
  if status not in by_status:
143
150
  by_status[status] = []
144
151
  by_status[status].append(todo)
145
-
152
+
146
153
  # Format output
147
154
  output = ["=== Todo List ==="]
148
-
155
+
149
156
  # Show in order: in_progress, pending, completed
150
157
  for status in ["in_progress", "pending", "completed"]:
151
158
  if status in by_status:
152
159
  output.append(f"\n{status.replace('_', ' ').title()}:")
153
160
  for todo in by_status[status]:
154
- priority_icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(todo.get("priority", "medium"), "⚪")
155
- output.append(f"{priority_icon} [{todo['id'][:8]}] {todo['content']}")
156
-
161
+ priority_icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(
162
+ todo.get("priority", "medium"), "⚪"
163
+ )
164
+ output.append(
165
+ f"{priority_icon} [{todo['id'][:8]}] {todo['content']}"
166
+ )
167
+
157
168
  # Summary
158
- output.append(f"\nTotal: {len(todos)} | In Progress: {len(by_status.get('in_progress', []))} | Pending: {len(by_status.get('pending', []))} | Completed: {len(by_status.get('completed', []))}")
159
-
169
+ output.append(
170
+ f"\nTotal: {len(todos)} | In Progress: {len(by_status.get('in_progress', []))} | Pending: {len(by_status.get('pending', []))} | Completed: {len(by_status.get('completed', []))}"
171
+ )
172
+
160
173
  return "\n".join(output)
161
174
 
162
175
  async def _handle_add(self, params: Dict[str, Any], tool_ctx) -> str:
@@ -164,9 +177,9 @@ todo --filter in_progress
164
177
  content = params.get("content")
165
178
  if not content:
166
179
  return "Error: content is required for add action"
167
-
180
+
168
181
  todos = self.read_todos()
169
-
182
+
170
183
  new_todo = {
171
184
  "id": str(uuid.uuid4()),
172
185
  "content": content,
@@ -174,10 +187,10 @@ todo --filter in_progress
174
187
  "priority": params.get("priority", "medium"),
175
188
  "created_at": datetime.now().isoformat(),
176
189
  }
177
-
190
+
178
191
  todos.append(new_todo)
179
192
  self.write_todos(todos)
180
-
193
+
181
194
  await tool_ctx.info(f"Added todo: {content}")
182
195
  return f"Added todo [{new_todo['id'][:8]}]: {content}"
183
196
 
@@ -186,19 +199,19 @@ todo --filter in_progress
186
199
  todo_id = params.get("id")
187
200
  if not todo_id:
188
201
  return "Error: id is required for update action"
189
-
202
+
190
203
  todos = self.read_todos()
191
-
204
+
192
205
  # Find todo (support partial ID match)
193
206
  todo_found = None
194
207
  for todo in todos:
195
208
  if todo["id"].startswith(todo_id):
196
209
  todo_found = todo
197
210
  break
198
-
211
+
199
212
  if not todo_found:
200
213
  return f"Error: Todo with ID '{todo_id}' not found"
201
-
214
+
202
215
  # Update fields
203
216
  if params.get("content"):
204
217
  todo_found["content"] = params["content"]
@@ -206,11 +219,11 @@ todo --filter in_progress
206
219
  todo_found["status"] = params["status"]
207
220
  if params.get("priority"):
208
221
  todo_found["priority"] = params["priority"]
209
-
222
+
210
223
  todo_found["updated_at"] = datetime.now().isoformat()
211
-
224
+
212
225
  self.write_todos(todos)
213
-
226
+
214
227
  await tool_ctx.info(f"Updated todo: {todo_found['content']}")
215
228
  return f"Updated todo [{todo_found['id'][:8]}]: {todo_found['content']} (status: {todo_found['status']})"
216
229
 
@@ -218,44 +231,44 @@ todo --filter in_progress
218
231
  """Remove todo."""
219
232
  if not todo_id:
220
233
  return "Error: id is required for remove action"
221
-
234
+
222
235
  todos = self.read_todos()
223
-
236
+
224
237
  # Find and remove (support partial ID match)
225
238
  removed = None
226
239
  for i, todo in enumerate(todos):
227
240
  if todo["id"].startswith(todo_id):
228
241
  removed = todos.pop(i)
229
242
  break
230
-
243
+
231
244
  if not removed:
232
245
  return f"Error: Todo with ID '{todo_id}' not found"
233
-
246
+
234
247
  self.write_todos(todos)
235
-
248
+
236
249
  await tool_ctx.info(f"Removed todo: {removed['content']}")
237
250
  return f"Removed todo [{removed['id'][:8]}]: {removed['content']}"
238
251
 
239
252
  async def _handle_clear(self, filter_status: Optional[str], tool_ctx) -> str:
240
253
  """Clear todos."""
241
254
  todos = self.read_todos()
242
-
255
+
243
256
  if filter_status:
244
257
  # Clear only todos with specific status
245
258
  original_count = len(todos)
246
259
  todos = [t for t in todos if t.get("status") != filter_status]
247
260
  removed_count = original_count - len(todos)
248
-
261
+
249
262
  if removed_count == 0:
250
263
  return f"No todos with status '{filter_status}' to clear"
251
-
264
+
252
265
  self.write_todos(todos)
253
266
  return f"Cleared {removed_count} todo(s) with status '{filter_status}'"
254
267
  else:
255
268
  # Clear all
256
269
  if not todos:
257
270
  return "No todos to clear"
258
-
271
+
259
272
  count = len(todos)
260
273
  self.write_todos([])
261
274
  return f"Cleared all {count} todo(s)"
@@ -283,4 +296,4 @@ todo --filter in_progress
283
296
  status=status,
284
297
  priority=priority,
285
298
  filter=filter,
286
- )
299
+ )
@@ -4,13 +4,13 @@ This module provides the TodoRead tool for reading the current todo list for a s
4
4
  """
5
5
 
6
6
  import json
7
- from typing import Annotated, TypedDict, Unpack, final, override
7
+ from typing import Unpack, Annotated, TypedDict, final, override
8
8
 
9
- from mcp.server.fastmcp import Context as MCPContext
10
- from mcp.server import FastMCP
11
9
  from pydantic import Field
10
+ from mcp.server import FastMCP
11
+ from mcp.server.fastmcp import Context as MCPContext
12
12
 
13
- from hanzo_mcp.tools.todo.base import TodoBaseTool, TodoStorage
13
+ from hanzo_mcp.tools.todo.base import TodoStorage, TodoBaseTool
14
14
 
15
15
  SessionId = Annotated[
16
16
  str | int | float,
@@ -139,8 +139,5 @@ Usage:
139
139
  tool_self = self # Create a reference to self for use in the closure
140
140
 
141
141
  @mcp_server.tool(name=self.name, description=self.description)
142
- async def todo_read(
143
- session_id: SessionId,
144
- ctx: MCPContext
145
- ) -> str:
142
+ async def todo_read(session_id: SessionId, ctx: MCPContext) -> str:
146
143
  return await tool_self.call(ctx, session_id=session_id)