hanzo-mcp 0.5.1__py3-none-any.whl → 0.6.1__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (118) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +32 -0
  3. hanzo_mcp/dev_server.py +246 -0
  4. hanzo_mcp/prompts/__init__.py +1 -1
  5. hanzo_mcp/prompts/project_system.py +43 -7
  6. hanzo_mcp/server.py +5 -1
  7. hanzo_mcp/tools/__init__.py +168 -6
  8. hanzo_mcp/tools/agent/__init__.py +1 -1
  9. hanzo_mcp/tools/agent/agent.py +401 -0
  10. hanzo_mcp/tools/agent/agent_tool.py +3 -4
  11. hanzo_mcp/tools/common/__init__.py +1 -1
  12. hanzo_mcp/tools/common/base.py +9 -4
  13. hanzo_mcp/tools/common/batch_tool.py +3 -5
  14. hanzo_mcp/tools/common/config_tool.py +1 -1
  15. hanzo_mcp/tools/common/context.py +1 -1
  16. hanzo_mcp/tools/common/palette.py +344 -0
  17. hanzo_mcp/tools/common/palette_loader.py +108 -0
  18. hanzo_mcp/tools/common/stats.py +261 -0
  19. hanzo_mcp/tools/common/thinking_tool.py +3 -5
  20. hanzo_mcp/tools/common/tool_disable.py +144 -0
  21. hanzo_mcp/tools/common/tool_enable.py +182 -0
  22. hanzo_mcp/tools/common/tool_list.py +260 -0
  23. hanzo_mcp/tools/config/__init__.py +10 -0
  24. hanzo_mcp/tools/config/config_tool.py +212 -0
  25. hanzo_mcp/tools/config/index_config.py +176 -0
  26. hanzo_mcp/tools/config/palette_tool.py +166 -0
  27. hanzo_mcp/tools/database/__init__.py +71 -0
  28. hanzo_mcp/tools/database/database_manager.py +246 -0
  29. hanzo_mcp/tools/database/graph.py +482 -0
  30. hanzo_mcp/tools/database/graph_add.py +257 -0
  31. hanzo_mcp/tools/database/graph_query.py +536 -0
  32. hanzo_mcp/tools/database/graph_remove.py +267 -0
  33. hanzo_mcp/tools/database/graph_search.py +348 -0
  34. hanzo_mcp/tools/database/graph_stats.py +345 -0
  35. hanzo_mcp/tools/database/sql.py +411 -0
  36. hanzo_mcp/tools/database/sql_query.py +229 -0
  37. hanzo_mcp/tools/database/sql_search.py +296 -0
  38. hanzo_mcp/tools/database/sql_stats.py +254 -0
  39. hanzo_mcp/tools/editor/__init__.py +11 -0
  40. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  41. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  42. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  43. hanzo_mcp/tools/filesystem/__init__.py +52 -13
  44. hanzo_mcp/tools/filesystem/base.py +1 -1
  45. hanzo_mcp/tools/filesystem/batch_search.py +812 -0
  46. hanzo_mcp/tools/filesystem/content_replace.py +3 -5
  47. hanzo_mcp/tools/filesystem/diff.py +193 -0
  48. hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
  49. hanzo_mcp/tools/filesystem/edit.py +3 -5
  50. hanzo_mcp/tools/filesystem/find.py +443 -0
  51. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  52. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  53. hanzo_mcp/tools/filesystem/grep.py +2 -2
  54. hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
  55. hanzo_mcp/tools/filesystem/read.py +17 -5
  56. hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
  57. hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
  58. hanzo_mcp/tools/filesystem/tree.py +268 -0
  59. hanzo_mcp/tools/filesystem/unified_search.py +465 -443
  60. hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
  61. hanzo_mcp/tools/filesystem/watch.py +174 -0
  62. hanzo_mcp/tools/filesystem/write.py +3 -5
  63. hanzo_mcp/tools/jupyter/__init__.py +9 -12
  64. hanzo_mcp/tools/jupyter/base.py +1 -1
  65. hanzo_mcp/tools/jupyter/jupyter.py +326 -0
  66. hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
  67. hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
  68. hanzo_mcp/tools/llm/__init__.py +31 -0
  69. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  70. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  71. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  72. hanzo_mcp/tools/llm/llm_unified.py +851 -0
  73. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  74. hanzo_mcp/tools/mcp/__init__.py +15 -0
  75. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  76. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  77. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  78. hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
  79. hanzo_mcp/tools/shell/__init__.py +21 -23
  80. hanzo_mcp/tools/shell/base.py +1 -1
  81. hanzo_mcp/tools/shell/base_process.py +303 -0
  82. hanzo_mcp/tools/shell/bash_unified.py +134 -0
  83. hanzo_mcp/tools/shell/logs.py +265 -0
  84. hanzo_mcp/tools/shell/npx.py +194 -0
  85. hanzo_mcp/tools/shell/npx_background.py +254 -0
  86. hanzo_mcp/tools/shell/npx_unified.py +101 -0
  87. hanzo_mcp/tools/shell/open.py +107 -0
  88. hanzo_mcp/tools/shell/pkill.py +262 -0
  89. hanzo_mcp/tools/shell/process_unified.py +131 -0
  90. hanzo_mcp/tools/shell/processes.py +279 -0
  91. hanzo_mcp/tools/shell/run_background.py +326 -0
  92. hanzo_mcp/tools/shell/run_command.py +3 -4
  93. hanzo_mcp/tools/shell/run_command_windows.py +3 -4
  94. hanzo_mcp/tools/shell/uvx.py +187 -0
  95. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  96. hanzo_mcp/tools/shell/uvx_unified.py +101 -0
  97. hanzo_mcp/tools/todo/__init__.py +1 -1
  98. hanzo_mcp/tools/todo/base.py +1 -1
  99. hanzo_mcp/tools/todo/todo.py +265 -0
  100. hanzo_mcp/tools/todo/todo_read.py +3 -5
  101. hanzo_mcp/tools/todo/todo_write.py +3 -5
  102. hanzo_mcp/tools/vector/__init__.py +6 -1
  103. hanzo_mcp/tools/vector/git_ingester.py +3 -0
  104. hanzo_mcp/tools/vector/index_tool.py +358 -0
  105. hanzo_mcp/tools/vector/infinity_store.py +98 -0
  106. hanzo_mcp/tools/vector/project_manager.py +27 -5
  107. hanzo_mcp/tools/vector/vector.py +311 -0
  108. hanzo_mcp/tools/vector/vector_index.py +1 -1
  109. hanzo_mcp/tools/vector/vector_search.py +12 -7
  110. hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
  111. hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
  112. hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
  113. hanzo_mcp-0.5.1.dist-info/METADATA +0 -276
  114. hanzo_mcp-0.5.1.dist-info/RECORD +0 -68
  115. hanzo_mcp-0.5.1.dist-info/entry_points.txt +0 -2
  116. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
  117. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
  118. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,249 @@
1
+ """Run Python packages in background with uvx."""
2
+
3
+ import subprocess
4
+ import shutil
5
+ import uuid
6
+ from typing import Annotated, Optional, TypedDict, Unpack, final, override
7
+
8
+ from mcp.server.fastmcp import Context as MCPContext
9
+ from pydantic import Field
10
+
11
+ from hanzo_mcp.tools.common.base import BaseTool
12
+ from hanzo_mcp.tools.common.context import create_tool_context
13
+ from hanzo_mcp.tools.common.permissions import PermissionManager
14
+ from hanzo_mcp.tools.shell.run_background import BackgroundProcess, RunBackgroundTool
15
+
16
+
17
+ Package = Annotated[
18
+ str,
19
+ Field(
20
+ description="Package name to run (e.g., 'streamlit', 'jupyter-lab', 'mkdocs')",
21
+ min_length=1,
22
+ ),
23
+ ]
24
+
25
+ Args = Annotated[
26
+ Optional[str],
27
+ Field(
28
+ description="Arguments to pass to the package",
29
+ default=None,
30
+ ),
31
+ ]
32
+
33
+ Name = Annotated[
34
+ Optional[str],
35
+ Field(
36
+ description="Process name for identification",
37
+ default=None,
38
+ ),
39
+ ]
40
+
41
+ PythonVersion = Annotated[
42
+ Optional[str],
43
+ Field(
44
+ description="Python version to use (e.g., '3.11', '3.12')",
45
+ default=None,
46
+ ),
47
+ ]
48
+
49
+ LogOutput = Annotated[
50
+ bool,
51
+ Field(
52
+ description="Log output to file in ~/.hanzo/logs",
53
+ default=True,
54
+ ),
55
+ ]
56
+
57
+ WorkingDir = Annotated[
58
+ Optional[str],
59
+ Field(
60
+ description="Working directory for the process",
61
+ default=None,
62
+ ),
63
+ ]
64
+
65
+
66
+ class UvxBackgroundParams(TypedDict, total=False):
67
+ """Parameters for uvx background tool."""
68
+
69
+ package: str
70
+ args: Optional[str]
71
+ name: Optional[str]
72
+ python_version: Optional[str]
73
+ log_output: bool
74
+ working_dir: Optional[str]
75
+
76
+
77
+ @final
78
+ class UvxBackgroundTool(BaseTool):
79
+ """Tool for running Python packages in background with uvx."""
80
+
81
+ def __init__(self, permission_manager: PermissionManager):
82
+ """Initialize the uvx background tool.
83
+
84
+ Args:
85
+ permission_manager: Permission manager for access control
86
+ """
87
+ self.permission_manager = permission_manager
88
+
89
+ @property
90
+ @override
91
+ def name(self) -> str:
92
+ """Get the tool name."""
93
+ return "uvx_background"
94
+
95
+ @property
96
+ @override
97
+ def description(self) -> str:
98
+ """Get the tool description."""
99
+ return """Run Python packages in the background using uvx.
100
+
101
+ Perfect for running servers and long-running Python applications.
102
+ The process continues running even after the command returns.
103
+
104
+ Common server packages:
105
+ - streamlit: Data app framework
106
+ - jupyter-lab: Jupyter Lab server
107
+ - mkdocs: Documentation server
108
+ - fastapi: FastAPI with uvicorn
109
+ - flask: Flask development server
110
+ - gradio: ML model demos
111
+ - panel: Data app framework
112
+
113
+ Examples:
114
+ - uvx_background --package streamlit --args "run app.py --port 8501" --name streamlit-app
115
+ - uvx_background --package jupyter-lab --args "--port 8888" --name jupyter
116
+ - uvx_background --package mkdocs --args "serve --dev-addr 0.0.0.0:8000" --name docs
117
+ - uvx_background --package gradio --args "app.py" --name ml-demo
118
+
119
+ Use 'processes' to list running processes and 'pkill' to stop them.
120
+ """
121
+
122
+ @override
123
+ async def call(
124
+ self,
125
+ ctx: MCPContext,
126
+ **params: Unpack[UvxBackgroundParams],
127
+ ) -> str:
128
+ """Execute uvx command in background.
129
+
130
+ Args:
131
+ ctx: MCP context
132
+ **params: Tool parameters
133
+
134
+ Returns:
135
+ Process information
136
+ """
137
+ tool_ctx = create_tool_context(ctx)
138
+ await tool_ctx.set_tool_info(self.name)
139
+
140
+ # Extract parameters
141
+ package = params.get("package")
142
+ if not package:
143
+ return "Error: package is required"
144
+
145
+ args = params.get("args", "")
146
+ name = params.get("name", f"uvx-{package}")
147
+ python_version = params.get("python_version")
148
+ log_output = params.get("log_output", True)
149
+ working_dir = params.get("working_dir")
150
+
151
+ # Check if uvx is available
152
+ if not shutil.which("uvx"):
153
+ return """Error: uvx is not installed. Install it with:
154
+ curl -LsSf https://astral.sh/uv/install.sh | sh
155
+
156
+ Or on macOS:
157
+ brew install uv"""
158
+
159
+ # Build command
160
+ cmd = ["uvx"]
161
+
162
+ if python_version:
163
+ cmd.extend(["--python", python_version])
164
+
165
+ cmd.append(package)
166
+
167
+ # Add package arguments
168
+ if args:
169
+ # Split args properly (basic parsing)
170
+ import shlex
171
+ cmd.extend(shlex.split(args))
172
+
173
+ # Generate process ID
174
+ process_id = str(uuid.uuid4())[:8]
175
+
176
+ # Prepare log file if needed
177
+ log_file = None
178
+ if log_output:
179
+ from pathlib import Path
180
+ log_dir = Path.home() / ".hanzo" / "logs"
181
+ log_dir.mkdir(parents=True, exist_ok=True)
182
+ log_file = log_dir / f"{name}_{process_id}.log"
183
+
184
+ await tool_ctx.info(f"Starting background process: {' '.join(cmd)}")
185
+
186
+ try:
187
+ # Start process
188
+ if log_output and log_file:
189
+ with open(log_file, "w") as f:
190
+ process = subprocess.Popen(
191
+ cmd,
192
+ stdout=f,
193
+ stderr=subprocess.STDOUT,
194
+ cwd=working_dir,
195
+ start_new_session=True
196
+ )
197
+ else:
198
+ process = subprocess.Popen(
199
+ cmd,
200
+ stdout=subprocess.DEVNULL,
201
+ stderr=subprocess.DEVNULL,
202
+ cwd=working_dir,
203
+ start_new_session=True
204
+ )
205
+
206
+ # Create background process object
207
+ bg_process = BackgroundProcess(
208
+ process_id=process_id,
209
+ command=" ".join(cmd),
210
+ name=name,
211
+ process=process,
212
+ log_file=str(log_file) if log_file else None,
213
+ working_dir=working_dir
214
+ )
215
+
216
+ # Register with RunBackgroundTool
217
+ RunBackgroundTool._add_process(bg_process)
218
+
219
+ output = [
220
+ f"Started uvx background process:",
221
+ f" ID: {process_id}",
222
+ f" Name: {name}",
223
+ f" Package: {package}",
224
+ f" PID: {process.pid}",
225
+ f" Command: {' '.join(cmd)}",
226
+ ]
227
+
228
+ if working_dir:
229
+ output.append(f" Working Dir: {working_dir}")
230
+
231
+ if log_file:
232
+ output.append(f" Log: {log_file}")
233
+
234
+ output.extend([
235
+ "",
236
+ "Use 'processes' to list running processes.",
237
+ f"Use 'logs --process-id {process_id}' to view output.",
238
+ f"Use 'pkill --process-id {process_id}' to stop."
239
+ ])
240
+
241
+ return "\n".join(output)
242
+
243
+ except Exception as e:
244
+ await tool_ctx.error(f"Failed to start process: {str(e)}")
245
+ return f"Error starting uvx background process: {str(e)}"
246
+
247
+ def register(self, mcp_server) -> None:
248
+ """Register this tool with the MCP server."""
249
+ pass
@@ -0,0 +1,101 @@
1
+ """UVX tool for both sync and background execution."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, override
5
+
6
+ from mcp.server.fastmcp import Context as MCPContext
7
+
8
+ from hanzo_mcp.tools.shell.base_process import BaseBinaryTool
9
+ from mcp.server import FastMCP
10
+
11
+
12
+ class UvxTool(BaseBinaryTool):
13
+ """Tool for running uvx commands."""
14
+
15
+ name = "uvx"
16
+
17
+ @property
18
+ @override
19
+ def description(self) -> str:
20
+ """Get the tool description."""
21
+ return """Run Python packages with uvx. Actions: run (default), background.
22
+
23
+ Usage:
24
+ uvx ruff check .
25
+ uvx --action background mkdocs serve
26
+ uvx black --check src/
27
+ uvx --action background jupyter lab --port 8888"""
28
+
29
+ @override
30
+ def get_binary_name(self) -> str:
31
+ """Get the binary name."""
32
+ return "uvx"
33
+
34
+ @override
35
+ async def run(
36
+ self,
37
+ ctx: MCPContext,
38
+ package: str,
39
+ args: str = "",
40
+ action: str = "run",
41
+ cwd: Optional[str] = None,
42
+ python: Optional[str] = None,
43
+ ) -> str:
44
+ """Run a uvx command.
45
+
46
+ Args:
47
+ ctx: MCP context
48
+ package: Python package to run
49
+ args: Additional arguments
50
+ action: Action to perform (run, background)
51
+ cwd: Working directory
52
+ python: Python version constraint
53
+
54
+ Returns:
55
+ Command output or process info
56
+ """
57
+ # Prepare working directory
58
+ work_dir = Path(cwd).resolve() if cwd else Path.cwd()
59
+
60
+ # Prepare flags
61
+ flags = []
62
+ if python:
63
+ flags.extend(["--python", python])
64
+
65
+ # Build full command
66
+ full_args = args.split() if args else []
67
+
68
+ if action == "background":
69
+ result = await self.execute_background(
70
+ package,
71
+ cwd=work_dir,
72
+ flags=flags,
73
+ args=full_args
74
+ )
75
+ return (
76
+ f"Started uvx process in background\n"
77
+ f"Process ID: {result['process_id']}\n"
78
+ f"PID: {result['pid']}\n"
79
+ f"Log file: {result['log_file']}"
80
+ )
81
+ else:
82
+ # Default to sync execution
83
+ return await self.execute_sync(
84
+ package,
85
+ cwd=work_dir,
86
+ flags=flags,
87
+ args=full_args,
88
+ timeout=300 # 5 minute timeout for uvx
89
+ )
90
+
91
+ def register(self, server: FastMCP) -> None:
92
+ """Register the tool with the MCP server."""
93
+ server.tool(name=self.name, description=self.description)(self.call)
94
+
95
+ async def call(self, **kwargs) -> str:
96
+ """Call the tool with arguments."""
97
+ return await self.run(None, **kwargs)
98
+
99
+
100
+ # Create tool instance
101
+ uvx_tool = UvxTool()
@@ -4,7 +4,7 @@ This package provides tools for managing todo lists across different Claude Desk
4
4
  using in-memory storage to maintain separate task lists for each conversation.
5
5
  """
6
6
 
7
- from fastmcp import FastMCP
7
+ from mcp.server import FastMCP
8
8
 
9
9
  from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
10
10
  from hanzo_mcp.tools.todo.todo_read import TodoReadTool
@@ -9,7 +9,7 @@ import time
9
9
  from abc import ABC
10
10
  from typing import Any, final
11
11
 
12
- from fastmcp import Context as MCPContext
12
+ from mcp.server.fastmcp import Context as MCPContext
13
13
 
14
14
  from hanzo_mcp.tools.common.base import BaseTool
15
15
  from hanzo_mcp.tools.common.context import ToolContext, create_tool_context
@@ -0,0 +1,265 @@
1
+ """Unified todo tool."""
2
+
3
+ from typing import Annotated, TypedDict, Unpack, final, override, Optional, List, Dict, Any
4
+ import json
5
+ import uuid
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ from mcp.server.fastmcp import Context as MCPContext
10
+ from pydantic import Field
11
+
12
+ from hanzo_mcp.tools.todo.base import TodoBaseTool
13
+
14
+
15
+ # Parameter types
16
+ Action = Annotated[
17
+ str,
18
+ Field(
19
+ description="Action to perform: list (default), add, update, remove, clear",
20
+ default="list",
21
+ ),
22
+ ]
23
+
24
+ Content = Annotated[
25
+ Optional[str],
26
+ Field(
27
+ description="Todo content for add/update",
28
+ default=None,
29
+ ),
30
+ ]
31
+
32
+ TodoId = Annotated[
33
+ Optional[str],
34
+ Field(
35
+ description="Todo ID for update/remove",
36
+ default=None,
37
+ ),
38
+ ]
39
+
40
+ Status = Annotated[
41
+ Optional[str],
42
+ Field(
43
+ description="Status: pending, in_progress, completed",
44
+ default="pending",
45
+ ),
46
+ ]
47
+
48
+ Priority = Annotated[
49
+ Optional[str],
50
+ Field(
51
+ description="Priority: high, medium, low",
52
+ default="medium",
53
+ ),
54
+ ]
55
+
56
+ Filter = Annotated[
57
+ Optional[str],
58
+ Field(
59
+ description="Filter todos by status for list action",
60
+ default=None,
61
+ ),
62
+ ]
63
+
64
+
65
+ class TodoParams(TypedDict, total=False):
66
+ """Parameters for todo tool."""
67
+ action: str
68
+ content: Optional[str]
69
+ id: Optional[str]
70
+ status: Optional[str]
71
+ priority: Optional[str]
72
+ filter: Optional[str]
73
+
74
+
75
+ @final
76
+ class TodoTool(TodoBaseTool):
77
+ """Unified todo management tool."""
78
+
79
+ @property
80
+ @override
81
+ def name(self) -> str:
82
+ """Get the tool name."""
83
+ return "todo"
84
+
85
+ @property
86
+ @override
87
+ def description(self) -> str:
88
+ """Get the tool description."""
89
+ return """Manage todos. Actions: list (default), add, update, remove, clear.
90
+
91
+ Usage:
92
+ todo
93
+ todo "Fix the bug in authentication"
94
+ todo --action update --id abc123 --status completed
95
+ todo --action remove --id abc123
96
+ todo --filter in_progress
97
+ """
98
+
99
+ @override
100
+ async def call(
101
+ self,
102
+ ctx: MCPContext,
103
+ **params: Unpack[TodoParams],
104
+ ) -> str:
105
+ """Execute todo operation."""
106
+ tool_ctx = self.create_tool_context(ctx)
107
+
108
+ # Extract action
109
+ action = params.get("action", "list")
110
+
111
+ # Route to appropriate handler
112
+ if action == "list":
113
+ return await self._handle_list(params.get("filter"), tool_ctx)
114
+ elif action == "add":
115
+ return await self._handle_add(params, tool_ctx)
116
+ elif action == "update":
117
+ return await self._handle_update(params, tool_ctx)
118
+ elif action == "remove":
119
+ return await self._handle_remove(params.get("id"), tool_ctx)
120
+ elif action == "clear":
121
+ return await self._handle_clear(params.get("filter"), tool_ctx)
122
+ else:
123
+ return f"Error: Unknown action '{action}'. Valid actions: list, add, update, remove, clear"
124
+
125
+ async def _handle_list(self, filter_status: Optional[str], tool_ctx) -> str:
126
+ """List todos."""
127
+ todos = self.read_todos()
128
+
129
+ if not todos:
130
+ return "No todos found. Use 'todo \"Your task here\"' to add one."
131
+
132
+ # Apply filter if specified
133
+ if filter_status:
134
+ todos = [t for t in todos if t.get("status") == filter_status]
135
+ if not todos:
136
+ return f"No todos with status '{filter_status}'"
137
+
138
+ # Group by status
139
+ by_status = {}
140
+ for todo in todos:
141
+ status = todo.get("status", "pending")
142
+ if status not in by_status:
143
+ by_status[status] = []
144
+ by_status[status].append(todo)
145
+
146
+ # Format output
147
+ output = ["=== Todo List ==="]
148
+
149
+ # Show in order: in_progress, pending, completed
150
+ for status in ["in_progress", "pending", "completed"]:
151
+ if status in by_status:
152
+ output.append(f"\n{status.replace('_', ' ').title()}:")
153
+ 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
+
157
+ # 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
+
160
+ return "\n".join(output)
161
+
162
+ async def _handle_add(self, params: Dict[str, Any], tool_ctx) -> str:
163
+ """Add new todo."""
164
+ content = params.get("content")
165
+ if not content:
166
+ return "Error: content is required for add action"
167
+
168
+ todos = self.read_todos()
169
+
170
+ new_todo = {
171
+ "id": str(uuid.uuid4()),
172
+ "content": content,
173
+ "status": params.get("status", "pending"),
174
+ "priority": params.get("priority", "medium"),
175
+ "created_at": datetime.now().isoformat(),
176
+ }
177
+
178
+ todos.append(new_todo)
179
+ self.write_todos(todos)
180
+
181
+ await tool_ctx.info(f"Added todo: {content}")
182
+ return f"Added todo [{new_todo['id'][:8]}]: {content}"
183
+
184
+ async def _handle_update(self, params: Dict[str, Any], tool_ctx) -> str:
185
+ """Update existing todo."""
186
+ todo_id = params.get("id")
187
+ if not todo_id:
188
+ return "Error: id is required for update action"
189
+
190
+ todos = self.read_todos()
191
+
192
+ # Find todo (support partial ID match)
193
+ todo_found = None
194
+ for todo in todos:
195
+ if todo["id"].startswith(todo_id):
196
+ todo_found = todo
197
+ break
198
+
199
+ if not todo_found:
200
+ return f"Error: Todo with ID '{todo_id}' not found"
201
+
202
+ # Update fields
203
+ if params.get("content"):
204
+ todo_found["content"] = params["content"]
205
+ if params.get("status"):
206
+ todo_found["status"] = params["status"]
207
+ if params.get("priority"):
208
+ todo_found["priority"] = params["priority"]
209
+
210
+ todo_found["updated_at"] = datetime.now().isoformat()
211
+
212
+ self.write_todos(todos)
213
+
214
+ await tool_ctx.info(f"Updated todo: {todo_found['content']}")
215
+ return f"Updated todo [{todo_found['id'][:8]}]: {todo_found['content']} (status: {todo_found['status']})"
216
+
217
+ async def _handle_remove(self, todo_id: Optional[str], tool_ctx) -> str:
218
+ """Remove todo."""
219
+ if not todo_id:
220
+ return "Error: id is required for remove action"
221
+
222
+ todos = self.read_todos()
223
+
224
+ # Find and remove (support partial ID match)
225
+ removed = None
226
+ for i, todo in enumerate(todos):
227
+ if todo["id"].startswith(todo_id):
228
+ removed = todos.pop(i)
229
+ break
230
+
231
+ if not removed:
232
+ return f"Error: Todo with ID '{todo_id}' not found"
233
+
234
+ self.write_todos(todos)
235
+
236
+ await tool_ctx.info(f"Removed todo: {removed['content']}")
237
+ return f"Removed todo [{removed['id'][:8]}]: {removed['content']}"
238
+
239
+ async def _handle_clear(self, filter_status: Optional[str], tool_ctx) -> str:
240
+ """Clear todos."""
241
+ todos = self.read_todos()
242
+
243
+ if filter_status:
244
+ # Clear only todos with specific status
245
+ original_count = len(todos)
246
+ todos = [t for t in todos if t.get("status") != filter_status]
247
+ removed_count = original_count - len(todos)
248
+
249
+ if removed_count == 0:
250
+ return f"No todos with status '{filter_status}' to clear"
251
+
252
+ self.write_todos(todos)
253
+ return f"Cleared {removed_count} todo(s) with status '{filter_status}'"
254
+ else:
255
+ # Clear all
256
+ if not todos:
257
+ return "No todos to clear"
258
+
259
+ count = len(todos)
260
+ self.write_todos([])
261
+ return f"Cleared all {count} todo(s)"
262
+
263
+ def register(self, mcp_server) -> None:
264
+ """Register this tool with the MCP server."""
265
+ pass
@@ -6,9 +6,8 @@ This module provides the TodoRead tool for reading the current todo list for a s
6
6
  import json
7
7
  from typing import Annotated, TypedDict, Unpack, final, override
8
8
 
9
- from fastmcp import Context as MCPContext
10
- from fastmcp import FastMCP
11
- from fastmcp.server.dependencies import get_context
9
+ from mcp.server.fastmcp import Context as MCPContext
10
+ from mcp.server import FastMCP
12
11
  from pydantic import Field
13
12
 
14
13
  from hanzo_mcp.tools.todo.base import TodoBaseTool, TodoStorage
@@ -141,8 +140,7 @@ Usage:
141
140
 
142
141
  @mcp_server.tool(name=self.name, description=self.description)
143
142
  async def todo_read(
144
- ctx: MCPContext,
145
143
  session_id: SessionId,
144
+ ctx: MCPContext
146
145
  ) -> str:
147
- ctx = get_context()
148
146
  return await tool_self.call(ctx, session_id=session_id)
@@ -5,9 +5,8 @@ This module provides the TodoWrite tool for creating and managing a structured t
5
5
 
6
6
  from typing import Annotated, Literal, TypedDict, Unpack, final, override
7
7
 
8
- from fastmcp import Context as MCPContext
9
- from fastmcp import FastMCP
10
- from fastmcp.server.dependencies import get_context
8
+ from mcp.server.fastmcp import Context as MCPContext
9
+ from mcp.server import FastMCP
11
10
  from pydantic import Field
12
11
 
13
12
  from hanzo_mcp.tools.todo.base import TodoBaseTool, TodoStorage
@@ -370,9 +369,8 @@ When in doubt, use this tool. Being proactive with task management demonstrates
370
369
 
371
370
  @mcp_server.tool(name=self.name, description=self.description)
372
371
  async def todo_write(
373
- ctx: MCPContext,
374
372
  session_id: SessionId,
375
373
  todos: Todos,
374
+ ctx: MCPContext
376
375
  ) -> str:
377
- ctx = get_context()
378
376
  return await tool_self.call(ctx, session_id=session_id, todos=todos)