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.

Files changed (114) 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 +66 -35
  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 +2 -2
  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 +1 -1
  19. hanzo_mcp/tools/common/thinking_tool.py +3 -5
  20. hanzo_mcp/tools/common/tool_disable.py +1 -1
  21. hanzo_mcp/tools/common/tool_enable.py +1 -1
  22. hanzo_mcp/tools/common/tool_list.py +49 -52
  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 +1 -1
  28. hanzo_mcp/tools/database/graph.py +482 -0
  29. hanzo_mcp/tools/database/graph_add.py +1 -1
  30. hanzo_mcp/tools/database/graph_query.py +1 -1
  31. hanzo_mcp/tools/database/graph_remove.py +1 -1
  32. hanzo_mcp/tools/database/graph_search.py +1 -1
  33. hanzo_mcp/tools/database/graph_stats.py +1 -1
  34. hanzo_mcp/tools/database/sql.py +411 -0
  35. hanzo_mcp/tools/database/sql_query.py +1 -1
  36. hanzo_mcp/tools/database/sql_search.py +1 -1
  37. hanzo_mcp/tools/database/sql_stats.py +1 -1
  38. hanzo_mcp/tools/editor/neovim_command.py +1 -1
  39. hanzo_mcp/tools/editor/neovim_edit.py +1 -1
  40. hanzo_mcp/tools/editor/neovim_session.py +1 -1
  41. hanzo_mcp/tools/filesystem/__init__.py +42 -13
  42. hanzo_mcp/tools/filesystem/base.py +1 -1
  43. hanzo_mcp/tools/filesystem/batch_search.py +4 -4
  44. hanzo_mcp/tools/filesystem/content_replace.py +3 -5
  45. hanzo_mcp/tools/filesystem/diff.py +193 -0
  46. hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
  47. hanzo_mcp/tools/filesystem/edit.py +3 -5
  48. hanzo_mcp/tools/filesystem/find.py +443 -0
  49. hanzo_mcp/tools/filesystem/find_files.py +1 -1
  50. hanzo_mcp/tools/filesystem/git_search.py +1 -1
  51. hanzo_mcp/tools/filesystem/grep.py +2 -2
  52. hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
  53. hanzo_mcp/tools/filesystem/read.py +17 -5
  54. hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
  55. hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
  56. hanzo_mcp/tools/filesystem/tree.py +268 -0
  57. hanzo_mcp/tools/filesystem/unified_search.py +711 -0
  58. hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
  59. hanzo_mcp/tools/filesystem/watch.py +174 -0
  60. hanzo_mcp/tools/filesystem/write.py +3 -5
  61. hanzo_mcp/tools/jupyter/__init__.py +9 -12
  62. hanzo_mcp/tools/jupyter/base.py +1 -1
  63. hanzo_mcp/tools/jupyter/jupyter.py +326 -0
  64. hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
  65. hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
  66. hanzo_mcp/tools/llm/__init__.py +4 -0
  67. hanzo_mcp/tools/llm/consensus_tool.py +1 -1
  68. hanzo_mcp/tools/llm/llm_manage.py +1 -1
  69. hanzo_mcp/tools/llm/llm_tool.py +1 -1
  70. hanzo_mcp/tools/llm/llm_unified.py +851 -0
  71. hanzo_mcp/tools/llm/provider_tools.py +1 -1
  72. hanzo_mcp/tools/mcp/__init__.py +4 -0
  73. hanzo_mcp/tools/mcp/mcp_add.py +1 -1
  74. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  75. hanzo_mcp/tools/mcp/mcp_stats.py +1 -1
  76. hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
  77. hanzo_mcp/tools/shell/__init__.py +20 -42
  78. hanzo_mcp/tools/shell/base.py +1 -1
  79. hanzo_mcp/tools/shell/base_process.py +303 -0
  80. hanzo_mcp/tools/shell/bash_unified.py +134 -0
  81. hanzo_mcp/tools/shell/logs.py +1 -1
  82. hanzo_mcp/tools/shell/npx.py +1 -1
  83. hanzo_mcp/tools/shell/npx_background.py +1 -1
  84. hanzo_mcp/tools/shell/npx_unified.py +101 -0
  85. hanzo_mcp/tools/shell/open.py +107 -0
  86. hanzo_mcp/tools/shell/pkill.py +1 -1
  87. hanzo_mcp/tools/shell/process_unified.py +131 -0
  88. hanzo_mcp/tools/shell/processes.py +1 -1
  89. hanzo_mcp/tools/shell/run_background.py +1 -1
  90. hanzo_mcp/tools/shell/run_command.py +3 -4
  91. hanzo_mcp/tools/shell/run_command_windows.py +3 -4
  92. hanzo_mcp/tools/shell/uvx.py +1 -1
  93. hanzo_mcp/tools/shell/uvx_background.py +1 -1
  94. hanzo_mcp/tools/shell/uvx_unified.py +101 -0
  95. hanzo_mcp/tools/todo/__init__.py +1 -1
  96. hanzo_mcp/tools/todo/base.py +1 -1
  97. hanzo_mcp/tools/todo/todo.py +265 -0
  98. hanzo_mcp/tools/todo/todo_read.py +3 -5
  99. hanzo_mcp/tools/todo/todo_write.py +3 -5
  100. hanzo_mcp/tools/vector/__init__.py +1 -1
  101. hanzo_mcp/tools/vector/index_tool.py +1 -1
  102. hanzo_mcp/tools/vector/project_manager.py +27 -5
  103. hanzo_mcp/tools/vector/vector.py +311 -0
  104. hanzo_mcp/tools/vector/vector_index.py +1 -1
  105. hanzo_mcp/tools/vector/vector_search.py +1 -1
  106. hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
  107. hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
  108. hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
  109. hanzo_mcp-0.5.2.dist-info/METADATA +0 -276
  110. hanzo_mcp-0.5.2.dist-info/RECORD +0 -106
  111. hanzo_mcp-0.5.2.dist-info/entry_points.txt +0 -2
  112. {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
  113. {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
  114. {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 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.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 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.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,
@@ -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()
@@ -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)
@@ -9,7 +9,7 @@ Supported backends:
9
9
 
10
10
  from hanzo_mcp.tools.common.base import BaseTool
11
11
  from hanzo_mcp.tools.common.permissions import PermissionManager
12
- from fastmcp import FastMCP
12
+ from mcp.server import FastMCP
13
13
 
14
14
  # Try to import vector dependencies
15
15
  try:
@@ -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