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.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/cli.py +32 -0
- hanzo_mcp/dev_server.py +246 -0
- hanzo_mcp/prompts/__init__.py +1 -1
- hanzo_mcp/prompts/project_system.py +43 -7
- hanzo_mcp/server.py +5 -1
- hanzo_mcp/tools/__init__.py +168 -6
- hanzo_mcp/tools/agent/__init__.py +1 -1
- hanzo_mcp/tools/agent/agent.py +401 -0
- hanzo_mcp/tools/agent/agent_tool.py +3 -4
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +9 -4
- hanzo_mcp/tools/common/batch_tool.py +3 -5
- hanzo_mcp/tools/common/config_tool.py +1 -1
- hanzo_mcp/tools/common/context.py +1 -1
- hanzo_mcp/tools/common/palette.py +344 -0
- hanzo_mcp/tools/common/palette_loader.py +108 -0
- hanzo_mcp/tools/common/stats.py +261 -0
- hanzo_mcp/tools/common/thinking_tool.py +3 -5
- hanzo_mcp/tools/common/tool_disable.py +144 -0
- hanzo_mcp/tools/common/tool_enable.py +182 -0
- hanzo_mcp/tools/common/tool_list.py +260 -0
- hanzo_mcp/tools/config/__init__.py +10 -0
- hanzo_mcp/tools/config/config_tool.py +212 -0
- hanzo_mcp/tools/config/index_config.py +176 -0
- hanzo_mcp/tools/config/palette_tool.py +166 -0
- hanzo_mcp/tools/database/__init__.py +71 -0
- hanzo_mcp/tools/database/database_manager.py +246 -0
- hanzo_mcp/tools/database/graph.py +482 -0
- hanzo_mcp/tools/database/graph_add.py +257 -0
- hanzo_mcp/tools/database/graph_query.py +536 -0
- hanzo_mcp/tools/database/graph_remove.py +267 -0
- hanzo_mcp/tools/database/graph_search.py +348 -0
- hanzo_mcp/tools/database/graph_stats.py +345 -0
- hanzo_mcp/tools/database/sql.py +411 -0
- hanzo_mcp/tools/database/sql_query.py +229 -0
- hanzo_mcp/tools/database/sql_search.py +296 -0
- hanzo_mcp/tools/database/sql_stats.py +254 -0
- hanzo_mcp/tools/editor/__init__.py +11 -0
- hanzo_mcp/tools/editor/neovim_command.py +272 -0
- hanzo_mcp/tools/editor/neovim_edit.py +290 -0
- hanzo_mcp/tools/editor/neovim_session.py +356 -0
- hanzo_mcp/tools/filesystem/__init__.py +52 -13
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +812 -0
- hanzo_mcp/tools/filesystem/content_replace.py +3 -5
- hanzo_mcp/tools/filesystem/diff.py +193 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
- hanzo_mcp/tools/filesystem/edit.py +3 -5
- hanzo_mcp/tools/filesystem/find.py +443 -0
- hanzo_mcp/tools/filesystem/find_files.py +348 -0
- hanzo_mcp/tools/filesystem/git_search.py +505 -0
- hanzo_mcp/tools/filesystem/grep.py +2 -2
- hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
- hanzo_mcp/tools/filesystem/read.py +17 -5
- hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
- hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
- hanzo_mcp/tools/filesystem/tree.py +268 -0
- hanzo_mcp/tools/filesystem/unified_search.py +465 -443
- hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
- hanzo_mcp/tools/filesystem/watch.py +174 -0
- hanzo_mcp/tools/filesystem/write.py +3 -5
- hanzo_mcp/tools/jupyter/__init__.py +9 -12
- hanzo_mcp/tools/jupyter/base.py +1 -1
- hanzo_mcp/tools/jupyter/jupyter.py +326 -0
- hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
- hanzo_mcp/tools/llm/__init__.py +31 -0
- hanzo_mcp/tools/llm/consensus_tool.py +351 -0
- hanzo_mcp/tools/llm/llm_manage.py +413 -0
- hanzo_mcp/tools/llm/llm_tool.py +346 -0
- hanzo_mcp/tools/llm/llm_unified.py +851 -0
- hanzo_mcp/tools/llm/provider_tools.py +412 -0
- hanzo_mcp/tools/mcp/__init__.py +15 -0
- hanzo_mcp/tools/mcp/mcp_add.py +263 -0
- hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
- hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
- hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
- hanzo_mcp/tools/shell/__init__.py +21 -23
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +303 -0
- hanzo_mcp/tools/shell/bash_unified.py +134 -0
- hanzo_mcp/tools/shell/logs.py +265 -0
- hanzo_mcp/tools/shell/npx.py +194 -0
- hanzo_mcp/tools/shell/npx_background.py +254 -0
- hanzo_mcp/tools/shell/npx_unified.py +101 -0
- hanzo_mcp/tools/shell/open.py +107 -0
- hanzo_mcp/tools/shell/pkill.py +262 -0
- hanzo_mcp/tools/shell/process_unified.py +131 -0
- hanzo_mcp/tools/shell/processes.py +279 -0
- hanzo_mcp/tools/shell/run_background.py +326 -0
- hanzo_mcp/tools/shell/run_command.py +3 -4
- hanzo_mcp/tools/shell/run_command_windows.py +3 -4
- hanzo_mcp/tools/shell/uvx.py +187 -0
- hanzo_mcp/tools/shell/uvx_background.py +249 -0
- hanzo_mcp/tools/shell/uvx_unified.py +101 -0
- hanzo_mcp/tools/todo/__init__.py +1 -1
- hanzo_mcp/tools/todo/base.py +1 -1
- hanzo_mcp/tools/todo/todo.py +265 -0
- hanzo_mcp/tools/todo/todo_read.py +3 -5
- hanzo_mcp/tools/todo/todo_write.py +3 -5
- hanzo_mcp/tools/vector/__init__.py +6 -1
- hanzo_mcp/tools/vector/git_ingester.py +3 -0
- hanzo_mcp/tools/vector/index_tool.py +358 -0
- hanzo_mcp/tools/vector/infinity_store.py +98 -0
- hanzo_mcp/tools/vector/project_manager.py +27 -5
- hanzo_mcp/tools/vector/vector.py +311 -0
- hanzo_mcp/tools/vector/vector_index.py +1 -1
- hanzo_mcp/tools/vector/vector_search.py +12 -7
- hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
- hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
- hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
- hanzo_mcp-0.5.1.dist-info/METADATA +0 -276
- hanzo_mcp-0.5.1.dist-info/RECORD +0 -68
- hanzo_mcp-0.5.1.dist-info/entry_points.txt +0 -2
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {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()
|
hanzo_mcp/tools/todo/__init__.py
CHANGED
|
@@ -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
|
|
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
|
hanzo_mcp/tools/todo/base.py
CHANGED
|
@@ -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
|
|
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
|
|
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)
|