hanzo-mcp 0.5.2__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 +66 -35
- 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 +2 -2
- 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 +1 -1
- hanzo_mcp/tools/common/thinking_tool.py +3 -5
- hanzo_mcp/tools/common/tool_disable.py +1 -1
- hanzo_mcp/tools/common/tool_enable.py +1 -1
- hanzo_mcp/tools/common/tool_list.py +49 -52
- 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 +1 -1
- hanzo_mcp/tools/database/graph.py +482 -0
- hanzo_mcp/tools/database/graph_add.py +1 -1
- hanzo_mcp/tools/database/graph_query.py +1 -1
- hanzo_mcp/tools/database/graph_remove.py +1 -1
- hanzo_mcp/tools/database/graph_search.py +1 -1
- hanzo_mcp/tools/database/graph_stats.py +1 -1
- hanzo_mcp/tools/database/sql.py +411 -0
- hanzo_mcp/tools/database/sql_query.py +1 -1
- hanzo_mcp/tools/database/sql_search.py +1 -1
- hanzo_mcp/tools/database/sql_stats.py +1 -1
- hanzo_mcp/tools/editor/neovim_command.py +1 -1
- hanzo_mcp/tools/editor/neovim_edit.py +1 -1
- hanzo_mcp/tools/editor/neovim_session.py +1 -1
- hanzo_mcp/tools/filesystem/__init__.py +42 -13
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +4 -4
- 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 +1 -1
- hanzo_mcp/tools/filesystem/git_search.py +1 -1
- 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 +711 -0
- 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 +4 -0
- hanzo_mcp/tools/llm/consensus_tool.py +1 -1
- hanzo_mcp/tools/llm/llm_manage.py +1 -1
- hanzo_mcp/tools/llm/llm_tool.py +1 -1
- hanzo_mcp/tools/llm/llm_unified.py +851 -0
- hanzo_mcp/tools/llm/provider_tools.py +1 -1
- hanzo_mcp/tools/mcp/__init__.py +4 -0
- hanzo_mcp/tools/mcp/mcp_add.py +1 -1
- hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -1
- hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
- hanzo_mcp/tools/shell/__init__.py +20 -42
- 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 +1 -1
- hanzo_mcp/tools/shell/npx.py +1 -1
- hanzo_mcp/tools/shell/npx_background.py +1 -1
- hanzo_mcp/tools/shell/npx_unified.py +101 -0
- hanzo_mcp/tools/shell/open.py +107 -0
- hanzo_mcp/tools/shell/pkill.py +1 -1
- hanzo_mcp/tools/shell/process_unified.py +131 -0
- hanzo_mcp/tools/shell/processes.py +1 -1
- hanzo_mcp/tools/shell/run_background.py +1 -1
- 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 +1 -1
- hanzo_mcp/tools/shell/uvx_background.py +1 -1
- 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 +1 -1
- hanzo_mcp/tools/vector/index_tool.py +1 -1
- 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 +1 -1
- 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.2.dist-info/METADATA +0 -276
- hanzo_mcp-0.5.2.dist-info/RECORD +0 -106
- hanzo_mcp-0.5.2.dist-info/entry_points.txt +0 -2
- {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Process management tool."""
|
|
2
|
+
|
|
3
|
+
import signal
|
|
4
|
+
from typing import Optional, override
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
7
|
+
|
|
8
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
9
|
+
from hanzo_mcp.tools.shell.base_process import ProcessManager
|
|
10
|
+
from mcp.server import FastMCP
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProcessTool(BaseTool):
|
|
14
|
+
"""Tool for process management."""
|
|
15
|
+
|
|
16
|
+
name = "process"
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initialize the process tool."""
|
|
20
|
+
super().__init__()
|
|
21
|
+
self.process_manager = ProcessManager()
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
@override
|
|
25
|
+
def description(self) -> str:
|
|
26
|
+
"""Get the tool description."""
|
|
27
|
+
return """Manage background processes. Actions: list (default), kill, logs.
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
process
|
|
31
|
+
process --action list
|
|
32
|
+
process --action kill --id npx_abc123
|
|
33
|
+
process --action logs --id uvx_def456
|
|
34
|
+
process --action logs --id bash_ghi789 --lines 50"""
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
async def run(
|
|
38
|
+
self,
|
|
39
|
+
ctx: MCPContext,
|
|
40
|
+
action: str = "list",
|
|
41
|
+
id: Optional[str] = None,
|
|
42
|
+
signal_type: str = "TERM",
|
|
43
|
+
lines: int = 100,
|
|
44
|
+
) -> str:
|
|
45
|
+
"""Manage background processes.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
ctx: MCP context
|
|
49
|
+
action: Action to perform (list, kill, logs)
|
|
50
|
+
id: Process ID (for kill/logs actions)
|
|
51
|
+
signal_type: Signal type for kill (TERM, KILL, INT)
|
|
52
|
+
lines: Number of log lines to show
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Action result
|
|
56
|
+
"""
|
|
57
|
+
if action == "list":
|
|
58
|
+
processes = self.process_manager.list_processes()
|
|
59
|
+
if not processes:
|
|
60
|
+
return "No background processes running"
|
|
61
|
+
|
|
62
|
+
output = ["Background processes:"]
|
|
63
|
+
for proc_id, info in processes.items():
|
|
64
|
+
status = "running" if info["running"] else f"stopped (exit code: {info.get('return_code', 'unknown')})"
|
|
65
|
+
output.append(f"- {proc_id}: PID {info['pid']} - {status}")
|
|
66
|
+
if info.get("log_file"):
|
|
67
|
+
output.append(f" Log: {info['log_file']}")
|
|
68
|
+
|
|
69
|
+
return "\n".join(output)
|
|
70
|
+
|
|
71
|
+
elif action == "kill":
|
|
72
|
+
if not id:
|
|
73
|
+
return "Error: Process ID required for kill action"
|
|
74
|
+
|
|
75
|
+
process = self.process_manager.get_process(id)
|
|
76
|
+
if not process:
|
|
77
|
+
return f"Process {id} not found"
|
|
78
|
+
|
|
79
|
+
# Map signal names to signal numbers
|
|
80
|
+
signal_map = {
|
|
81
|
+
"TERM": signal.SIGTERM,
|
|
82
|
+
"KILL": signal.SIGKILL,
|
|
83
|
+
"INT": signal.SIGINT,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
sig = signal_map.get(signal_type.upper(), signal.SIGTERM)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
process.send_signal(sig)
|
|
90
|
+
return f"Sent {signal_type} signal to process {id} (PID: {process.pid})"
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return f"Failed to kill process {id}: {e}"
|
|
93
|
+
|
|
94
|
+
elif action == "logs":
|
|
95
|
+
if not id:
|
|
96
|
+
return "Error: Process ID required for logs action"
|
|
97
|
+
|
|
98
|
+
log_file = self.process_manager.get_log_file(id)
|
|
99
|
+
if not log_file or not log_file.exists():
|
|
100
|
+
return f"No log file found for process {id}"
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
with open(log_file, "r") as f:
|
|
104
|
+
log_lines = f.readlines()
|
|
105
|
+
|
|
106
|
+
# Get last N lines
|
|
107
|
+
if len(log_lines) > lines:
|
|
108
|
+
log_lines = log_lines[-lines:]
|
|
109
|
+
|
|
110
|
+
output = [f"Logs for process {id} (last {lines} lines):"]
|
|
111
|
+
output.append("-" * 50)
|
|
112
|
+
output.extend(line.rstrip() for line in log_lines)
|
|
113
|
+
|
|
114
|
+
return "\n".join(output)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return f"Error reading logs: {e}"
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
return f"Unknown action: {action}. Use 'list', 'kill', or 'logs'"
|
|
120
|
+
|
|
121
|
+
def register(self, server: FastMCP) -> None:
|
|
122
|
+
"""Register the tool with the MCP server."""
|
|
123
|
+
server.tool(name=self.name, description=self.description)(self.call)
|
|
124
|
+
|
|
125
|
+
async def call(self, **kwargs) -> str:
|
|
126
|
+
"""Call the tool with arguments."""
|
|
127
|
+
return await self.run(None, **kwargs)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Create tool instance
|
|
131
|
+
process_tool = ProcessTool()
|
|
@@ -4,7 +4,7 @@ import psutil
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
6
6
|
|
|
7
|
-
from fastmcp import Context as MCPContext
|
|
7
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
8
8
|
from pydantic import Field
|
|
9
9
|
|
|
10
10
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -9,7 +9,7 @@ from datetime import datetime
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
11
11
|
|
|
12
|
-
from fastmcp import Context as MCPContext
|
|
12
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
13
13
|
from pydantic import Field
|
|
14
14
|
|
|
15
15
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -5,9 +5,8 @@ This module provides the RunCommandTool for running shell commands.
|
|
|
5
5
|
|
|
6
6
|
from typing import Annotated, Any, 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.common.base import handle_connection_errors
|
|
@@ -345,8 +344,8 @@ Important:
|
|
|
345
344
|
time_out: TimeOut,
|
|
346
345
|
is_input: IsInput,
|
|
347
346
|
blocking: Blocking,
|
|
347
|
+
ctx: MCPContext
|
|
348
348
|
) -> str:
|
|
349
|
-
ctx = get_context()
|
|
350
349
|
return await tool_self.call(
|
|
351
350
|
ctx,
|
|
352
351
|
command=command,
|
|
@@ -6,9 +6,8 @@ This module provides the RunCommandTool for running shell commands on Windows.
|
|
|
6
6
|
import os
|
|
7
7
|
from typing import Annotated, Any, 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.common.base import handle_connection_errors
|
|
@@ -317,8 +316,8 @@ Important:
|
|
|
317
316
|
default=True,
|
|
318
317
|
),
|
|
319
318
|
] = True,
|
|
319
|
+
ctx: MCPContext
|
|
320
320
|
) -> str:
|
|
321
|
-
ctx = get_context()
|
|
322
321
|
return await tool_self.call(
|
|
323
322
|
ctx,
|
|
324
323
|
command=command,
|
hanzo_mcp/tools/shell/uvx.py
CHANGED
|
@@ -4,7 +4,7 @@ import subprocess
|
|
|
4
4
|
import shutil
|
|
5
5
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
6
6
|
|
|
7
|
-
from fastmcp import Context as MCPContext
|
|
7
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
8
8
|
from pydantic import Field
|
|
9
9
|
|
|
10
10
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -5,7 +5,7 @@ import shutil
|
|
|
5
5
|
import uuid
|
|
6
6
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
7
7
|
|
|
8
|
-
from fastmcp import Context as MCPContext
|
|
8
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
9
9
|
from pydantic import Field
|
|
10
10
|
|
|
11
11
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -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)
|
|
@@ -6,7 +6,7 @@ import time
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Annotated, TypedDict, Unpack, final, override
|
|
8
8
|
|
|
9
|
-
from fastmcp import Context as MCPContext
|
|
9
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
10
10
|
from pydantic import Field
|
|
11
11
|
|
|
12
12
|
from hanzo_mcp.tools.common.base import BaseTool
|