hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.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 (178) hide show
  1. hanzo_mcp/__init__.py +6 -0
  2. hanzo_mcp/__main__.py +1 -1
  3. hanzo_mcp/analytics/__init__.py +2 -2
  4. hanzo_mcp/analytics/posthog_analytics.py +76 -82
  5. hanzo_mcp/cli.py +31 -36
  6. hanzo_mcp/cli_enhanced.py +94 -72
  7. hanzo_mcp/cli_plugin.py +27 -17
  8. hanzo_mcp/config/__init__.py +2 -2
  9. hanzo_mcp/config/settings.py +112 -88
  10. hanzo_mcp/config/tool_config.py +32 -34
  11. hanzo_mcp/dev_server.py +66 -67
  12. hanzo_mcp/prompts/__init__.py +94 -12
  13. hanzo_mcp/prompts/enhanced_prompts.py +809 -0
  14. hanzo_mcp/prompts/example_custom_prompt.py +6 -5
  15. hanzo_mcp/prompts/project_todo_reminder.py +0 -1
  16. hanzo_mcp/prompts/tool_explorer.py +10 -7
  17. hanzo_mcp/server.py +17 -21
  18. hanzo_mcp/server_enhanced.py +15 -22
  19. hanzo_mcp/tools/__init__.py +56 -28
  20. hanzo_mcp/tools/agent/__init__.py +16 -19
  21. hanzo_mcp/tools/agent/agent.py +82 -65
  22. hanzo_mcp/tools/agent/agent_tool.py +152 -122
  23. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
  24. hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
  25. hanzo_mcp/tools/agent/clarification_tool.py +11 -10
  26. hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
  27. hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
  28. hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
  29. hanzo_mcp/tools/agent/code_auth.py +102 -107
  30. hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
  31. hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
  32. hanzo_mcp/tools/agent/critic_tool.py +86 -73
  33. hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
  34. hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
  35. hanzo_mcp/tools/agent/iching_tool.py +404 -139
  36. hanzo_mcp/tools/agent/network_tool.py +89 -73
  37. hanzo_mcp/tools/agent/prompt.py +2 -1
  38. hanzo_mcp/tools/agent/review_tool.py +101 -98
  39. hanzo_mcp/tools/agent/swarm_alias.py +87 -0
  40. hanzo_mcp/tools/agent/swarm_tool.py +246 -161
  41. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
  42. hanzo_mcp/tools/agent/tool_adapter.py +21 -11
  43. hanzo_mcp/tools/common/__init__.py +1 -1
  44. hanzo_mcp/tools/common/base.py +3 -5
  45. hanzo_mcp/tools/common/batch_tool.py +46 -39
  46. hanzo_mcp/tools/common/config_tool.py +120 -84
  47. hanzo_mcp/tools/common/context.py +1 -5
  48. hanzo_mcp/tools/common/context_fix.py +5 -3
  49. hanzo_mcp/tools/common/critic_tool.py +4 -8
  50. hanzo_mcp/tools/common/decorators.py +58 -56
  51. hanzo_mcp/tools/common/enhanced_base.py +29 -32
  52. hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
  53. hanzo_mcp/tools/common/forgiving_edit.py +91 -87
  54. hanzo_mcp/tools/common/mode.py +15 -17
  55. hanzo_mcp/tools/common/mode_loader.py +27 -24
  56. hanzo_mcp/tools/common/paginated_base.py +61 -53
  57. hanzo_mcp/tools/common/paginated_response.py +72 -79
  58. hanzo_mcp/tools/common/pagination.py +50 -53
  59. hanzo_mcp/tools/common/permissions.py +4 -4
  60. hanzo_mcp/tools/common/personality.py +186 -138
  61. hanzo_mcp/tools/common/plugin_loader.py +54 -54
  62. hanzo_mcp/tools/common/stats.py +65 -47
  63. hanzo_mcp/tools/common/test_helpers.py +31 -0
  64. hanzo_mcp/tools/common/thinking_tool.py +4 -8
  65. hanzo_mcp/tools/common/tool_disable.py +17 -12
  66. hanzo_mcp/tools/common/tool_enable.py +13 -14
  67. hanzo_mcp/tools/common/tool_list.py +36 -28
  68. hanzo_mcp/tools/common/truncate.py +23 -23
  69. hanzo_mcp/tools/config/__init__.py +4 -4
  70. hanzo_mcp/tools/config/config_tool.py +42 -29
  71. hanzo_mcp/tools/config/index_config.py +37 -34
  72. hanzo_mcp/tools/config/mode_tool.py +175 -55
  73. hanzo_mcp/tools/database/__init__.py +15 -12
  74. hanzo_mcp/tools/database/database_manager.py +77 -75
  75. hanzo_mcp/tools/database/graph.py +137 -91
  76. hanzo_mcp/tools/database/graph_add.py +30 -18
  77. hanzo_mcp/tools/database/graph_query.py +178 -102
  78. hanzo_mcp/tools/database/graph_remove.py +33 -28
  79. hanzo_mcp/tools/database/graph_search.py +97 -75
  80. hanzo_mcp/tools/database/graph_stats.py +91 -59
  81. hanzo_mcp/tools/database/sql.py +107 -79
  82. hanzo_mcp/tools/database/sql_query.py +30 -24
  83. hanzo_mcp/tools/database/sql_search.py +29 -25
  84. hanzo_mcp/tools/database/sql_stats.py +47 -35
  85. hanzo_mcp/tools/editor/neovim_command.py +25 -28
  86. hanzo_mcp/tools/editor/neovim_edit.py +21 -23
  87. hanzo_mcp/tools/editor/neovim_session.py +60 -54
  88. hanzo_mcp/tools/filesystem/__init__.py +31 -30
  89. hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
  90. hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
  91. hanzo_mcp/tools/filesystem/base.py +1 -1
  92. hanzo_mcp/tools/filesystem/batch_search.py +316 -224
  93. hanzo_mcp/tools/filesystem/content_replace.py +4 -4
  94. hanzo_mcp/tools/filesystem/diff.py +71 -59
  95. hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
  96. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
  97. hanzo_mcp/tools/filesystem/edit.py +4 -4
  98. hanzo_mcp/tools/filesystem/find.py +173 -80
  99. hanzo_mcp/tools/filesystem/find_files.py +73 -52
  100. hanzo_mcp/tools/filesystem/git_search.py +157 -104
  101. hanzo_mcp/tools/filesystem/grep.py +8 -8
  102. hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
  103. hanzo_mcp/tools/filesystem/read.py +12 -10
  104. hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
  105. hanzo_mcp/tools/filesystem/search_tool.py +263 -207
  106. hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
  107. hanzo_mcp/tools/filesystem/tree.py +35 -33
  108. hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
  109. hanzo_mcp/tools/filesystem/watch.py +37 -36
  110. hanzo_mcp/tools/filesystem/write.py +4 -8
  111. hanzo_mcp/tools/jupyter/__init__.py +4 -4
  112. hanzo_mcp/tools/jupyter/base.py +4 -5
  113. hanzo_mcp/tools/jupyter/jupyter.py +67 -47
  114. hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
  115. hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
  116. hanzo_mcp/tools/llm/__init__.py +5 -7
  117. hanzo_mcp/tools/llm/consensus_tool.py +72 -52
  118. hanzo_mcp/tools/llm/llm_manage.py +101 -60
  119. hanzo_mcp/tools/llm/llm_tool.py +226 -166
  120. hanzo_mcp/tools/llm/provider_tools.py +25 -26
  121. hanzo_mcp/tools/lsp/__init__.py +1 -1
  122. hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
  123. hanzo_mcp/tools/mcp/__init__.py +2 -3
  124. hanzo_mcp/tools/mcp/mcp_add.py +27 -25
  125. hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
  126. hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
  127. hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
  128. hanzo_mcp/tools/memory/__init__.py +39 -21
  129. hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
  130. hanzo_mcp/tools/memory/memory_tools.py +90 -108
  131. hanzo_mcp/tools/search/__init__.py +7 -2
  132. hanzo_mcp/tools/search/find_tool.py +297 -212
  133. hanzo_mcp/tools/search/unified_search.py +366 -314
  134. hanzo_mcp/tools/shell/__init__.py +8 -7
  135. hanzo_mcp/tools/shell/auto_background.py +56 -49
  136. hanzo_mcp/tools/shell/base.py +1 -1
  137. hanzo_mcp/tools/shell/base_process.py +75 -75
  138. hanzo_mcp/tools/shell/bash_session.py +2 -2
  139. hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
  140. hanzo_mcp/tools/shell/bash_tool.py +24 -31
  141. hanzo_mcp/tools/shell/command_executor.py +12 -12
  142. hanzo_mcp/tools/shell/logs.py +43 -33
  143. hanzo_mcp/tools/shell/npx.py +13 -13
  144. hanzo_mcp/tools/shell/npx_background.py +24 -21
  145. hanzo_mcp/tools/shell/npx_tool.py +18 -22
  146. hanzo_mcp/tools/shell/open.py +19 -21
  147. hanzo_mcp/tools/shell/pkill.py +31 -26
  148. hanzo_mcp/tools/shell/process_tool.py +32 -32
  149. hanzo_mcp/tools/shell/processes.py +57 -58
  150. hanzo_mcp/tools/shell/run_background.py +24 -25
  151. hanzo_mcp/tools/shell/run_command.py +5 -5
  152. hanzo_mcp/tools/shell/run_command_windows.py +5 -5
  153. hanzo_mcp/tools/shell/session_storage.py +3 -3
  154. hanzo_mcp/tools/shell/streaming_command.py +141 -126
  155. hanzo_mcp/tools/shell/uvx.py +24 -25
  156. hanzo_mcp/tools/shell/uvx_background.py +35 -33
  157. hanzo_mcp/tools/shell/uvx_tool.py +18 -22
  158. hanzo_mcp/tools/todo/__init__.py +6 -2
  159. hanzo_mcp/tools/todo/todo.py +50 -37
  160. hanzo_mcp/tools/todo/todo_read.py +5 -8
  161. hanzo_mcp/tools/todo/todo_write.py +5 -7
  162. hanzo_mcp/tools/vector/__init__.py +40 -28
  163. hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
  164. hanzo_mcp/tools/vector/git_ingester.py +170 -179
  165. hanzo_mcp/tools/vector/index_tool.py +96 -44
  166. hanzo_mcp/tools/vector/infinity_store.py +283 -228
  167. hanzo_mcp/tools/vector/mock_infinity.py +39 -40
  168. hanzo_mcp/tools/vector/project_manager.py +88 -78
  169. hanzo_mcp/tools/vector/vector.py +59 -42
  170. hanzo_mcp/tools/vector/vector_index.py +30 -27
  171. hanzo_mcp/tools/vector/vector_search.py +64 -45
  172. hanzo_mcp/types.py +6 -4
  173. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.1.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/top_level.txt +0 -0
@@ -1,26 +1,25 @@
1
1
  """Watch files for changes."""
2
2
 
3
+ import time
3
4
  import asyncio
4
- import os
5
- from pathlib import Path
6
5
  from typing import override
7
- import time
6
+ from pathlib import Path
8
7
  from datetime import datetime
9
8
 
9
+ from mcp.server import FastMCP
10
10
  from mcp.server.fastmcp import Context as MCPContext
11
11
 
12
12
  from hanzo_mcp.tools.common.base import BaseTool
13
- from mcp.server import FastMCP
14
13
 
15
14
 
16
15
  class WatchTool(BaseTool):
17
16
  """Tool for watching files for changes."""
18
17
 
19
18
  name = "watch"
20
-
19
+
21
20
  def register(self, server: FastMCP) -> None:
22
21
  """Register the tool with the MCP server."""
23
-
22
+
24
23
  @server.tool(name=self.name, description=self.description)
25
24
  async def watch_handler(
26
25
  ctx: MCPContext,
@@ -29,7 +28,7 @@ class WatchTool(BaseTool):
29
28
  interval: int = 1,
30
29
  recursive: bool = True,
31
30
  exclude: str = "",
32
- duration: int = 30
31
+ duration: int = 30,
33
32
  ) -> str:
34
33
  """Handle watch tool calls."""
35
34
  return await self.run(
@@ -41,7 +40,7 @@ class WatchTool(BaseTool):
41
40
  exclude=exclude,
42
41
  duration=duration,
43
42
  )
44
-
43
+
45
44
  async def call(self, ctx: MCPContext, **params) -> str:
46
45
  """Call the tool with arguments."""
47
46
  return await self.run(
@@ -51,7 +50,7 @@ class WatchTool(BaseTool):
51
50
  interval=params.get("interval", 1),
52
51
  recursive=params.get("recursive", True),
53
52
  exclude=params.get("exclude", ""),
54
- duration=params.get("duration", 30)
53
+ duration=params.get("duration", 30),
55
54
  )
56
55
 
57
56
  @property
@@ -68,8 +67,8 @@ watch . --recursive --exclude "__pycache__"
68
67
 
69
68
  @override
70
69
  async def run(
71
- self,
72
- ctx: MCPContext,
70
+ self,
71
+ ctx: MCPContext,
73
72
  path: str,
74
73
  pattern: str = "*",
75
74
  interval: int = 1,
@@ -78,7 +77,7 @@ watch . --recursive --exclude "__pycache__"
78
77
  duration: int = 30,
79
78
  ) -> str:
80
79
  """Watch files for changes.
81
-
80
+
82
81
  Args:
83
82
  ctx: MCP context
84
83
  path: Path to watch (file or directory)
@@ -87,23 +86,23 @@ watch . --recursive --exclude "__pycache__"
87
86
  recursive: Watch subdirectories (default: True)
88
87
  exclude: Patterns to exclude (comma-separated)
89
88
  duration: Max watch duration in seconds (default: 30)
90
-
89
+
91
90
  Returns:
92
91
  Report of file changes
93
92
  """
94
93
  watch_path = Path(path).expanduser().resolve()
95
-
94
+
96
95
  if not watch_path.exists():
97
96
  raise ValueError(f"Path does not exist: {watch_path}")
98
-
97
+
99
98
  # Parse exclude patterns
100
99
  exclude_patterns = [p.strip() for p in exclude.split(",") if p.strip()]
101
-
100
+
102
101
  # Track file states
103
102
  file_states = {}
104
103
  changes = []
105
104
  start_time = time.time()
106
-
105
+
107
106
  def should_exclude(file_path: Path) -> bool:
108
107
  """Check if file should be excluded."""
109
108
  for pattern in exclude_patterns:
@@ -112,17 +111,17 @@ watch . --recursive --exclude "__pycache__"
112
111
  if file_path.match(pattern):
113
112
  return True
114
113
  return False
115
-
114
+
116
115
  def get_files() -> dict[Path, float]:
117
116
  """Get all matching files with their modification times."""
118
117
  files = {}
119
-
118
+
120
119
  if watch_path.is_file():
121
120
  # Watching a single file
122
121
  if not should_exclude(watch_path):
123
122
  try:
124
123
  files[watch_path] = watch_path.stat().st_mtime
125
- except:
124
+ except Exception:
126
125
  pass
127
126
  else:
128
127
  # Watching a directory
@@ -130,33 +129,33 @@ watch . --recursive --exclude "__pycache__"
130
129
  paths = watch_path.rglob(pattern)
131
130
  else:
132
131
  paths = watch_path.glob(pattern)
133
-
132
+
134
133
  for file_path in paths:
135
134
  if file_path.is_file() and not should_exclude(file_path):
136
135
  try:
137
136
  files[file_path] = file_path.stat().st_mtime
138
- except:
137
+ except Exception:
139
138
  pass
140
-
139
+
141
140
  return files
142
-
141
+
143
142
  # Initial scan
144
143
  file_states = get_files()
145
144
  initial_count = len(file_states)
146
-
145
+
147
146
  output = [f"Watching {watch_path} (pattern: {pattern})"]
148
147
  output.append(f"Found {initial_count} files to monitor")
149
148
  if exclude_patterns:
150
149
  output.append(f"Excluding: {', '.join(exclude_patterns)}")
151
150
  output.append(f"Monitoring for {duration} seconds...\n")
152
-
151
+
153
152
  # Monitor for changes
154
153
  try:
155
154
  while (time.time() - start_time) < duration:
156
155
  await asyncio.sleep(interval)
157
-
156
+
158
157
  current_files = get_files()
159
-
158
+
160
159
  # Check for new files
161
160
  for file_path, mtime in current_files.items():
162
161
  if file_path not in file_states:
@@ -164,7 +163,7 @@ watch . --recursive --exclude "__pycache__"
164
163
  change = f"[{timestamp}] CREATED: {file_path.relative_to(watch_path.parent)}"
165
164
  changes.append(change)
166
165
  output.append(change)
167
-
166
+
168
167
  # Check for deleted files
169
168
  for file_path in list(file_states.keys()):
170
169
  if file_path not in current_files:
@@ -173,7 +172,7 @@ watch . --recursive --exclude "__pycache__"
173
172
  changes.append(change)
174
173
  output.append(change)
175
174
  del file_states[file_path]
176
-
175
+
177
176
  # Check for modified files
178
177
  for file_path, mtime in current_files.items():
179
178
  if file_path in file_states and mtime != file_states[file_path]:
@@ -182,21 +181,23 @@ watch . --recursive --exclude "__pycache__"
182
181
  changes.append(change)
183
182
  output.append(change)
184
183
  file_states[file_path] = mtime
185
-
184
+
186
185
  # Update file states for new files
187
186
  for file_path, mtime in current_files.items():
188
187
  if file_path not in file_states:
189
188
  file_states[file_path] = mtime
190
-
189
+
191
190
  except asyncio.CancelledError:
192
191
  output.append("\nWatch cancelled")
193
-
192
+
194
193
  # Summary
195
- output.append(f"\nWatch completed after {int(time.time() - start_time)} seconds")
194
+ output.append(
195
+ f"\nWatch completed after {int(time.time() - start_time)} seconds"
196
+ )
196
197
  output.append(f"Total changes detected: {len(changes)}")
197
-
198
+
198
199
  return "\n".join(output)
199
200
 
200
201
 
201
202
  # Create tool instance
202
- watch_tool = WatchTool()
203
+ watch_tool = WatchTool()
@@ -3,12 +3,12 @@
3
3
  This module provides the Write tool for creating or overwriting files.
4
4
  """
5
5
 
6
+ from typing import Unpack, Annotated, TypedDict, final, override
6
7
  from pathlib import Path
7
- from typing import Annotated, TypedDict, Unpack, final, override
8
8
 
9
- from mcp.server.fastmcp import Context as MCPContext
10
- from mcp.server import FastMCP
11
9
  from pydantic import Field
10
+ from mcp.server import FastMCP
11
+ from mcp.server.fastmcp import Context as MCPContext
12
12
 
13
13
  from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
14
14
 
@@ -146,9 +146,5 @@ Usage:
146
146
  tool_self = self # Create a reference to self for use in the closure
147
147
 
148
148
  @mcp_server.tool(name=self.name, description=self.description)
149
- async def write(
150
- file_path: FilePath,
151
- content: Content,
152
- ctx: MCPContext
153
- ) -> str:
149
+ async def write(file_path: FilePath, content: Content, ctx: MCPContext) -> str:
154
150
  return await tool_self.call(ctx, file_path=file_path, content=content)
@@ -7,8 +7,8 @@ including reading and editing notebook cells.
7
7
  from mcp.server import FastMCP
8
8
 
9
9
  from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
10
- from hanzo_mcp.tools.common.permissions import PermissionManager
11
10
  from hanzo_mcp.tools.jupyter.jupyter import JupyterTool
11
+ from hanzo_mcp.tools.common.permissions import PermissionManager
12
12
 
13
13
  # Export all tool classes
14
14
  __all__ = [
@@ -68,10 +68,10 @@ def register_jupyter_tools(
68
68
  "notebook_read": JupyterTool,
69
69
  "notebook_edit": JupyterTool,
70
70
  }
71
-
71
+
72
72
  tools = []
73
73
  added_classes = set() # Track which tool classes have been added
74
-
74
+
75
75
  if enabled_tools:
76
76
  # Use individual tool configuration
77
77
  for tool_name, enabled in enabled_tools.items():
@@ -84,6 +84,6 @@ def register_jupyter_tools(
84
84
  else:
85
85
  # Use all tools (backward compatibility)
86
86
  tools = get_jupyter_tools(permission_manager)
87
-
87
+
88
88
  ToolRegistry.register_tools(mcp_server, tools)
89
89
  return tools
@@ -4,17 +4,16 @@ This module provides common functionality for Jupyter notebook tools, including
4
4
  cell processing, and output formatting.
5
5
  """
6
6
 
7
- from abc import ABC
8
- import json
9
7
  import re
10
- from pathlib import Path
8
+ import json
9
+ from abc import ABC
11
10
  from typing import Any, final
11
+ from pathlib import Path
12
12
 
13
13
  from mcp.server.fastmcp import Context as MCPContext
14
14
 
15
- from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
16
15
  from hanzo_mcp.tools.common.context import ToolContext, create_tool_context
17
-
16
+ from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
18
17
 
19
18
  # Pattern to match ANSI escape sequences
20
19
  ANSI_ESCAPE_PATTERN = re.compile(r"\x1B\[[0-9;]*[a-zA-Z]")
@@ -1,16 +1,23 @@
1
1
  """Unified Jupyter notebook tool."""
2
2
 
3
- from typing import Annotated, TypedDict, Unpack, final, override, Optional, List, Dict, Any
4
- import json
5
- import nbformat
3
+ from typing import (
4
+ Any,
5
+ Dict,
6
+ Unpack,
7
+ Optional,
8
+ Annotated,
9
+ TypedDict,
10
+ final,
11
+ override,
12
+ )
6
13
  from pathlib import Path
7
14
 
8
- from mcp.server.fastmcp import Context as MCPContext
15
+ import nbformat
9
16
  from pydantic import Field
17
+ from mcp.server.fastmcp import Context as MCPContext
10
18
 
11
19
  from hanzo_mcp.tools.jupyter.base import JupyterBaseTool
12
20
 
13
-
14
21
  # Parameter types
15
22
  Action = Annotated[
16
23
  str,
@@ -70,6 +77,7 @@ EditMode = Annotated[
70
77
 
71
78
  class NotebookParams(TypedDict, total=False):
72
79
  """Parameters for notebook tool."""
80
+
73
81
  action: str
74
82
  notebook_path: str
75
83
  cell_id: Optional[str]
@@ -114,7 +122,7 @@ jupyter --action create "new.ipynb"
114
122
  # Extract parameters
115
123
  action = params.get("action", "read")
116
124
  notebook_path = params.get("notebook_path")
117
-
125
+
118
126
  if not notebook_path:
119
127
  return "Error: notebook_path is required"
120
128
 
@@ -143,7 +151,9 @@ jupyter --action create "new.ipynb"
143
151
  else:
144
152
  return f"Error: Unknown action '{action}'. Valid actions: read, edit, create, delete, execute"
145
153
 
146
- async def _handle_read(self, notebook_path: str, params: Dict[str, Any], tool_ctx) -> str:
154
+ async def _handle_read(
155
+ self, notebook_path: str, params: Dict[str, Any], tool_ctx
156
+ ) -> str:
147
157
  """Read notebook or specific cell."""
148
158
  exists, error_msg = await self.check_path_exists(notebook_path, tool_ctx)
149
159
  if not exists:
@@ -151,34 +161,36 @@ jupyter --action create "new.ipynb"
151
161
 
152
162
  try:
153
163
  nb = self.read_notebook(notebook_path)
154
-
164
+
155
165
  # Check if specific cell requested
156
166
  cell_id = params.get("cell_id")
157
167
  cell_index = params.get("cell_index")
158
-
168
+
159
169
  if cell_id:
160
170
  # Find cell by ID
161
171
  for i, cell in enumerate(nb.cells):
162
172
  if cell.get("id") == cell_id:
163
173
  return self._format_cell(cell, i)
164
174
  return f"Error: Cell with ID '{cell_id}' not found"
165
-
175
+
166
176
  elif cell_index is not None:
167
177
  # Get cell by index
168
178
  if 0 <= cell_index < len(nb.cells):
169
179
  return self._format_cell(nb.cells[cell_index], cell_index)
170
180
  else:
171
181
  return f"Error: Cell index {cell_index} out of range (notebook has {len(nb.cells)} cells)"
172
-
182
+
173
183
  else:
174
184
  # Return all cells
175
185
  return self.format_notebook(nb)
176
-
186
+
177
187
  except Exception as e:
178
188
  await tool_ctx.error(f"Failed to read notebook: {str(e)}")
179
189
  return f"Error reading notebook: {str(e)}"
180
190
 
181
- async def _handle_edit(self, notebook_path: str, params: Dict[str, Any], tool_ctx) -> str:
191
+ async def _handle_edit(
192
+ self, notebook_path: str, params: Dict[str, Any], tool_ctx
193
+ ) -> str:
182
194
  """Edit notebook cell."""
183
195
  exists, error_msg = await self.check_path_exists(notebook_path, tool_ctx)
184
196
  if not exists:
@@ -186,7 +198,7 @@ jupyter --action create "new.ipynb"
186
198
 
187
199
  source = params.get("source")
188
200
  edit_mode = params.get("edit_mode", "replace")
189
-
201
+
190
202
  # Only require source for non-delete operations
191
203
  if edit_mode != "delete" and not source:
192
204
  return "Error: source is required for edit action"
@@ -196,19 +208,23 @@ jupyter --action create "new.ipynb"
196
208
 
197
209
  try:
198
210
  nb = self.read_notebook(notebook_path)
199
-
211
+
200
212
  if edit_mode == "insert":
201
213
  # Insert new cell
202
- new_cell = nbformat.v4.new_code_cell(source) if cell_type != "markdown" else nbformat.v4.new_markdown_cell(source)
203
-
214
+ new_cell = (
215
+ nbformat.v4.new_code_cell(source)
216
+ if cell_type != "markdown"
217
+ else nbformat.v4.new_markdown_cell(source)
218
+ )
219
+
204
220
  if cell_index is not None:
205
221
  nb.cells.insert(cell_index, new_cell)
206
222
  else:
207
223
  nb.cells.append(new_cell)
208
-
224
+
209
225
  self.write_notebook(nb, notebook_path)
210
- return f"Successfully inserted new cell at index {cell_index if cell_index is not None else len(nb.cells)-1}"
211
-
226
+ return f"Successfully inserted new cell at index {cell_index if cell_index is not None else len(nb.cells) - 1}"
227
+
212
228
  elif edit_mode == "delete":
213
229
  # Delete cell
214
230
  if cell_id:
@@ -218,7 +234,7 @@ jupyter --action create "new.ipynb"
218
234
  self.write_notebook(nb, notebook_path)
219
235
  return f"Successfully deleted cell with ID '{cell_id}'"
220
236
  return f"Error: Cell with ID '{cell_id}' not found"
221
-
237
+
222
238
  elif cell_index is not None:
223
239
  if 0 <= cell_index < len(nb.cells):
224
240
  nb.cells.pop(cell_index)
@@ -228,7 +244,7 @@ jupyter --action create "new.ipynb"
228
244
  return f"Error: Cell index {cell_index} out of range"
229
245
  else:
230
246
  return "Error: cell_id or cell_index required for delete"
231
-
247
+
232
248
  else: # replace
233
249
  # Replace cell content
234
250
  if cell_id:
@@ -240,7 +256,7 @@ jupyter --action create "new.ipynb"
240
256
  self.write_notebook(nb, notebook_path)
241
257
  return f"Successfully updated cell with ID '{cell_id}'"
242
258
  return f"Error: Cell with ID '{cell_id}' not found"
243
-
259
+
244
260
  elif cell_index is not None:
245
261
  if 0 <= cell_index < len(nb.cells):
246
262
  nb.cells[cell_index]["source"] = source
@@ -252,7 +268,7 @@ jupyter --action create "new.ipynb"
252
268
  return f"Error: Cell index {cell_index} out of range"
253
269
  else:
254
270
  return "Error: cell_id or cell_index required for replace"
255
-
271
+
256
272
  except Exception as e:
257
273
  await tool_ctx.error(f"Failed to edit notebook: {str(e)}")
258
274
  return f"Error editing notebook: {str(e)}"
@@ -267,25 +283,27 @@ jupyter --action create "new.ipynb"
267
283
  try:
268
284
  # Create new notebook
269
285
  nb = nbformat.v4.new_notebook()
270
-
286
+
271
287
  # Ensure parent directory exists
272
288
  path.parent.mkdir(parents=True, exist_ok=True)
273
-
289
+
274
290
  # Write notebook
275
291
  self.write_notebook(nb, notebook_path)
276
292
  return f"Successfully created notebook at {notebook_path}"
277
-
293
+
278
294
  except Exception as e:
279
295
  await tool_ctx.error(f"Failed to create notebook: {str(e)}")
280
296
  return f"Error creating notebook: {str(e)}"
281
297
 
282
- async def _handle_delete(self, notebook_path: str, params: Dict[str, Any], tool_ctx) -> str:
298
+ async def _handle_delete(
299
+ self, notebook_path: str, params: Dict[str, Any], tool_ctx
300
+ ) -> str:
283
301
  """Delete notebook or cell."""
284
302
  # If cell specified, delegate to edit with delete mode
285
303
  if params.get("cell_id") or params.get("cell_index") is not None:
286
304
  params["edit_mode"] = "delete"
287
305
  return await self._handle_edit(notebook_path, params, tool_ctx)
288
-
306
+
289
307
  # Otherwise, delete entire notebook
290
308
  exists, error_msg = await self.check_path_exists(notebook_path, tool_ctx)
291
309
  if not exists:
@@ -298,7 +316,9 @@ jupyter --action create "new.ipynb"
298
316
  await tool_ctx.error(f"Failed to delete notebook: {str(e)}")
299
317
  return f"Error deleting notebook: {str(e)}"
300
318
 
301
- async def _handle_execute(self, notebook_path: str, params: Dict[str, Any], tool_ctx) -> str:
319
+ async def _handle_execute(
320
+ self, notebook_path: str, params: Dict[str, Any], tool_ctx
321
+ ) -> str:
302
322
  """Execute notebook cells (placeholder for future implementation)."""
303
323
  return "Error: Cell execution not yet implemented. Use a Jupyter kernel or server for execution."
304
324
 
@@ -313,19 +333,19 @@ jupyter --action create "new.ipynb"
313
333
  if isinstance(source, list):
314
334
  source = "".join(source)
315
335
  output.append(source)
316
-
336
+
317
337
  if cell.get("cell_type") == "code" and cell.get("outputs"):
318
338
  output.append("\nOutputs:")
319
339
  for out in cell.get("outputs", []):
320
340
  out_type = out.get("output_type", "")
321
-
341
+
322
342
  if out_type == "stream":
323
343
  text = out.get("text", "")
324
344
  if isinstance(text, list):
325
345
  text = "".join(text)
326
346
  name = out.get("name", "stdout")
327
347
  output.append(f"[{name}]: {text}")
328
-
348
+
329
349
  elif out_type == "execute_result":
330
350
  exec_count = out.get("execution_count", "?")
331
351
  data = out.get("data", {})
@@ -337,7 +357,7 @@ jupyter --action create "new.ipynb"
337
357
  output.append(f"[Out {exec_count}]: {text_data}")
338
358
  else:
339
359
  output.append(f"[Out {exec_count}]: {data}")
340
-
360
+
341
361
  elif out_type == "error":
342
362
  ename = out.get("ename", "Error")
343
363
  evalue = out.get("evalue", "")
@@ -348,50 +368,50 @@ jupyter --action create "new.ipynb"
348
368
  output.append("Traceback:")
349
369
  for line in traceback:
350
370
  output.append(f" {line}")
351
-
371
+
352
372
  return "\n".join(output)
353
373
 
354
374
  def read_notebook(self, notebook_path: str) -> Any:
355
375
  """Read a notebook from disk using nbformat.
356
-
376
+
357
377
  Args:
358
378
  notebook_path: Path to the notebook file
359
-
379
+
360
380
  Returns:
361
381
  Notebook object
362
382
  """
363
- with open(notebook_path, 'r') as f:
383
+ with open(notebook_path, "r") as f:
364
384
  return nbformat.read(f, as_version=4)
365
-
385
+
366
386
  def write_notebook(self, nb: Any, notebook_path: str) -> None:
367
387
  """Write a notebook to disk using nbformat.
368
-
388
+
369
389
  Args:
370
390
  nb: Notebook object to write
371
391
  notebook_path: Path to write the notebook to
372
392
  """
373
- with open(notebook_path, 'w') as f:
393
+ with open(notebook_path, "w") as f:
374
394
  nbformat.write(nb, f)
375
-
395
+
376
396
  def format_notebook(self, nb: Any) -> str:
377
397
  """Format an entire notebook for display.
378
-
398
+
379
399
  Args:
380
400
  nb: Notebook object
381
-
401
+
382
402
  Returns:
383
403
  Formatted string representation of the notebook
384
404
  """
385
405
  output = []
386
406
  output.append(f"Notebook with {len(nb.cells)} cells")
387
407
  output.append("=" * 50)
388
-
408
+
389
409
  for i, cell in enumerate(nb.cells):
390
410
  output.append("")
391
411
  output.append(self._format_cell(cell, i))
392
-
412
+
393
413
  return "\n".join(output)
394
414
 
395
415
  def register(self, mcp_server) -> None:
396
416
  """Register this tool with the MCP server."""
397
- pass
417
+ pass
@@ -4,12 +4,12 @@ This module provides the NoteBookEditTool for editing Jupyter notebook files.
4
4
  """
5
5
 
6
6
  import json
7
+ from typing import Any, Unpack, Literal, Annotated, TypedDict, final, override
7
8
  from pathlib import Path
8
- from typing import Annotated, Any, Literal, TypedDict, Unpack, final, override
9
9
 
10
- from mcp.server.fastmcp import Context as MCPContext
11
- from mcp.server import FastMCP
12
10
  from pydantic import Field
11
+ from mcp.server import FastMCP
12
+ from mcp.server.fastmcp import Context as MCPContext
13
13
 
14
14
  from hanzo_mcp.tools.jupyter.base import JupyterBaseTool
15
15
 
@@ -305,7 +305,7 @@ class NoteBookEditTool(JupyterBaseTool):
305
305
  new_source: NewSource,
306
306
  cell_type: CellType,
307
307
  edit_mode: EditMode,
308
- ctx: MCPContext
308
+ ctx: MCPContext,
309
309
  ) -> str:
310
310
  return await tool_self.call(
311
311
  ctx,
@@ -4,12 +4,12 @@ This module provides the NotebookReadTool for reading Jupyter notebook files.
4
4
  """
5
5
 
6
6
  import json
7
+ from typing import Unpack, Annotated, TypedDict, final, override
7
8
  from pathlib import Path
8
- from typing import Annotated, TypedDict, Unpack, final, override
9
9
 
10
- from mcp.server.fastmcp import Context as MCPContext
11
- from mcp.server import FastMCP
12
10
  from pydantic import Field
11
+ from mcp.server import FastMCP
12
+ from mcp.server.fastmcp import Context as MCPContext
13
13
 
14
14
  from hanzo_mcp.tools.jupyter.base import JupyterBaseTool
15
15
 
@@ -143,8 +143,5 @@ class NotebookReadTool(JupyterBaseTool):
143
143
  tool_self = self # Create a reference to self for use in the closure
144
144
 
145
145
  @mcp_server.tool(name=self.name, description=self.description)
146
- async def notebook_read(
147
- notebook_path: NotebookPath,
148
- ctx: MCPContext
149
- ) -> str:
146
+ async def notebook_read(notebook_path: NotebookPath, ctx: MCPContext) -> str:
150
147
  return await tool_self.call(ctx, notebook_path=notebook_path)
@@ -1,19 +1,17 @@
1
1
  """LLM tools for Hanzo AI."""
2
2
 
3
- from hanzo_mcp.tools.llm.llm_tool import LLMTool
4
-
5
3
  # Legacy imports for backwards compatibility
6
4
  from hanzo_mcp.tools.llm.llm_tool import LLMTool
7
- from hanzo_mcp.tools.llm.consensus_tool import ConsensusTool
8
5
  from hanzo_mcp.tools.llm.llm_manage import LLMManageTool
6
+ from hanzo_mcp.tools.llm.consensus_tool import ConsensusTool
9
7
  from hanzo_mcp.tools.llm.provider_tools import (
10
- create_provider_tools,
11
- OpenAITool,
12
- AnthropicTool,
13
- GeminiTool,
14
8
  GroqTool,
9
+ GeminiTool,
10
+ OpenAITool,
15
11
  MistralTool,
12
+ AnthropicTool,
16
13
  PerplexityTool,
14
+ create_provider_tools,
17
15
  )
18
16
 
19
17
  __all__ = [