hanzo-mcp 0.3.8__py3-none-any.whl → 0.5.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 (93) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +118 -170
  3. hanzo_mcp/cli_enhanced.py +438 -0
  4. hanzo_mcp/config/__init__.py +19 -0
  5. hanzo_mcp/config/settings.py +449 -0
  6. hanzo_mcp/config/tool_config.py +197 -0
  7. hanzo_mcp/prompts/__init__.py +117 -0
  8. hanzo_mcp/prompts/compact_conversation.py +77 -0
  9. hanzo_mcp/prompts/create_release.py +38 -0
  10. hanzo_mcp/prompts/project_system.py +120 -0
  11. hanzo_mcp/prompts/project_todo_reminder.py +111 -0
  12. hanzo_mcp/prompts/utils.py +286 -0
  13. hanzo_mcp/server.py +117 -99
  14. hanzo_mcp/tools/__init__.py +121 -33
  15. hanzo_mcp/tools/agent/__init__.py +8 -11
  16. hanzo_mcp/tools/agent/agent_tool.py +290 -224
  17. hanzo_mcp/tools/agent/prompt.py +16 -13
  18. hanzo_mcp/tools/agent/tool_adapter.py +9 -9
  19. hanzo_mcp/tools/common/__init__.py +17 -16
  20. hanzo_mcp/tools/common/base.py +79 -110
  21. hanzo_mcp/tools/common/batch_tool.py +330 -0
  22. hanzo_mcp/tools/common/config_tool.py +396 -0
  23. hanzo_mcp/tools/common/context.py +26 -292
  24. hanzo_mcp/tools/common/permissions.py +12 -12
  25. hanzo_mcp/tools/common/thinking_tool.py +153 -0
  26. hanzo_mcp/tools/common/validation.py +1 -63
  27. hanzo_mcp/tools/filesystem/__init__.py +97 -57
  28. hanzo_mcp/tools/filesystem/base.py +32 -24
  29. hanzo_mcp/tools/filesystem/content_replace.py +114 -107
  30. hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
  31. hanzo_mcp/tools/filesystem/edit.py +279 -0
  32. hanzo_mcp/tools/filesystem/grep.py +458 -0
  33. hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
  34. hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
  35. hanzo_mcp/tools/filesystem/read.py +255 -0
  36. hanzo_mcp/tools/filesystem/unified_search.py +689 -0
  37. hanzo_mcp/tools/filesystem/write.py +156 -0
  38. hanzo_mcp/tools/jupyter/__init__.py +41 -29
  39. hanzo_mcp/tools/jupyter/base.py +66 -57
  40. hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
  41. hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
  42. hanzo_mcp/tools/shell/__init__.py +29 -20
  43. hanzo_mcp/tools/shell/base.py +87 -45
  44. hanzo_mcp/tools/shell/bash_session.py +731 -0
  45. hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
  46. hanzo_mcp/tools/shell/command_executor.py +435 -384
  47. hanzo_mcp/tools/shell/run_command.py +284 -131
  48. hanzo_mcp/tools/shell/run_command_windows.py +328 -0
  49. hanzo_mcp/tools/shell/session_manager.py +196 -0
  50. hanzo_mcp/tools/shell/session_storage.py +325 -0
  51. hanzo_mcp/tools/todo/__init__.py +66 -0
  52. hanzo_mcp/tools/todo/base.py +319 -0
  53. hanzo_mcp/tools/todo/todo_read.py +148 -0
  54. hanzo_mcp/tools/todo/todo_write.py +378 -0
  55. hanzo_mcp/tools/vector/__init__.py +99 -0
  56. hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
  57. hanzo_mcp/tools/vector/git_ingester.py +482 -0
  58. hanzo_mcp/tools/vector/infinity_store.py +731 -0
  59. hanzo_mcp/tools/vector/mock_infinity.py +162 -0
  60. hanzo_mcp/tools/vector/project_manager.py +361 -0
  61. hanzo_mcp/tools/vector/vector_index.py +116 -0
  62. hanzo_mcp/tools/vector/vector_search.py +225 -0
  63. hanzo_mcp-0.5.1.dist-info/METADATA +276 -0
  64. hanzo_mcp-0.5.1.dist-info/RECORD +68 -0
  65. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/WHEEL +1 -1
  66. hanzo_mcp/tools/agent/base_provider.py +0 -73
  67. hanzo_mcp/tools/agent/litellm_provider.py +0 -45
  68. hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
  69. hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
  70. hanzo_mcp/tools/agent/provider_registry.py +0 -120
  71. hanzo_mcp/tools/common/error_handling.py +0 -86
  72. hanzo_mcp/tools/common/logging_config.py +0 -115
  73. hanzo_mcp/tools/common/session.py +0 -91
  74. hanzo_mcp/tools/common/think_tool.py +0 -123
  75. hanzo_mcp/tools/common/version_tool.py +0 -120
  76. hanzo_mcp/tools/filesystem/edit_file.py +0 -287
  77. hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
  78. hanzo_mcp/tools/filesystem/read_files.py +0 -199
  79. hanzo_mcp/tools/filesystem/search_content.py +0 -275
  80. hanzo_mcp/tools/filesystem/write_file.py +0 -162
  81. hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
  82. hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
  83. hanzo_mcp/tools/project/__init__.py +0 -64
  84. hanzo_mcp/tools/project/analysis.py +0 -886
  85. hanzo_mcp/tools/project/base.py +0 -66
  86. hanzo_mcp/tools/project/project_analyze.py +0 -173
  87. hanzo_mcp/tools/shell/run_script.py +0 -215
  88. hanzo_mcp/tools/shell/script_tool.py +0 -244
  89. hanzo_mcp-0.3.8.dist-info/METADATA +0 -196
  90. hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
  91. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/entry_points.txt +0 -0
  92. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/licenses/LICENSE +0 -0
  93. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,156 @@
1
+ """Write file tool implementation.
2
+
3
+ This module provides the Write tool for creating or overwriting files.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from typing import Annotated, TypedDict, Unpack, final, override
8
+
9
+ from fastmcp import Context as MCPContext
10
+ from fastmcp import FastMCP
11
+ from fastmcp.server.dependencies import get_context
12
+ from pydantic import Field
13
+
14
+ from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
15
+
16
+ FilePath = Annotated[
17
+ str,
18
+ Field(
19
+ description="The absolute path to the file to write (must be absolute, not relative)",
20
+ min_length=1,
21
+ ),
22
+ ]
23
+
24
+ Content = Annotated[
25
+ str,
26
+ Field(
27
+ description="The content to write to the file",
28
+ min_length=1,
29
+ ),
30
+ ]
31
+
32
+
33
+ class WriteToolParams(TypedDict):
34
+ """Parameters for the Write tool.
35
+
36
+ Attributes:
37
+ file_path: The absolute path to the file to write (must be absolute, not relative)
38
+ content: The content to write to the file
39
+ """
40
+
41
+ file_path: FilePath
42
+ content: Content
43
+
44
+
45
+ @final
46
+ class Write(FilesystemBaseTool):
47
+ """Tool for writing file contents."""
48
+
49
+ @property
50
+ @override
51
+ def name(self) -> str:
52
+ """Get the tool name.
53
+
54
+ Returns:
55
+ Tool name
56
+ """
57
+ return "write"
58
+
59
+ @property
60
+ @override
61
+ def description(self) -> str:
62
+ """Get the tool description.
63
+
64
+ Returns:
65
+ Tool description
66
+ """
67
+ return """Writes a file to the local filesystem.
68
+
69
+ Usage:
70
+ - This tool will overwrite the existing file if there is one at the provided path.
71
+ - If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.
72
+ - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
73
+ - NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User."""
74
+
75
+ @override
76
+ async def call(
77
+ self,
78
+ ctx: MCPContext,
79
+ **params: Unpack[WriteToolParams],
80
+ ) -> str:
81
+ """Execute the tool with the given parameters.
82
+
83
+ Args:
84
+ ctx: MCP context
85
+ **params: Tool parameters
86
+
87
+ Returns:
88
+ Tool result
89
+ """
90
+ tool_ctx = self.create_tool_context(ctx)
91
+ self.set_tool_context_info(tool_ctx)
92
+
93
+ # Extract parameters
94
+ file_path: FilePath = params["file_path"]
95
+ content: Content = params["content"]
96
+
97
+ # Validate parameters
98
+ path_validation = self.validate_path(file_path)
99
+ if path_validation.is_error:
100
+ await tool_ctx.error(path_validation.error_message)
101
+ return f"Error: {path_validation.error_message}"
102
+
103
+ await tool_ctx.info(f"Writing file: {file_path}")
104
+
105
+ # Check if file is allowed to be written
106
+ allowed, error_msg = await self.check_path_allowed(file_path, tool_ctx)
107
+ if not allowed:
108
+ return error_msg
109
+
110
+ # Additional check already verified by is_path_allowed above
111
+ await tool_ctx.info(f"Writing file: {file_path}")
112
+
113
+ try:
114
+ path_obj = Path(file_path)
115
+
116
+ # Check if parent directory is allowed
117
+ parent_dir = str(path_obj.parent)
118
+ if not self.is_path_allowed(parent_dir):
119
+ await tool_ctx.error(f"Parent directory not allowed: {parent_dir}")
120
+ return f"Error: Parent directory not allowed: {parent_dir}"
121
+
122
+ # Create parent directories if they don't exist
123
+ path_obj.parent.mkdir(parents=True, exist_ok=True)
124
+
125
+ # Write the file
126
+ with open(path_obj, "w", encoding="utf-8") as f:
127
+ f.write(content)
128
+
129
+ await tool_ctx.info(
130
+ f"Successfully wrote file: {file_path} ({len(content)} bytes)"
131
+ )
132
+ return f"Successfully wrote file: {file_path} ({len(content)} bytes)"
133
+ except Exception as e:
134
+ await tool_ctx.error(f"Error writing file: {str(e)}")
135
+ return f"Error writing file: {str(e)}"
136
+
137
+ @override
138
+ def register(self, mcp_server: FastMCP) -> None:
139
+ """Register this tool with the MCP server.
140
+
141
+ Creates a wrapper function with explicitly defined parameters that match
142
+ the tool's parameter schema and registers it with the MCP server.
143
+
144
+ Args:
145
+ mcp_server: The FastMCP server instance
146
+ """
147
+ tool_self = self # Create a reference to self for use in the closure
148
+
149
+ @mcp_server.tool(name=self.name, description=self.description)
150
+ async def write(
151
+ ctx: MCPContext,
152
+ file_path: FilePath,
153
+ content: Content,
154
+ ) -> str:
155
+ ctx = get_context()
156
+ return await tool_self.call(ctx, file_path=file_path, content=content)
@@ -4,73 +4,85 @@ This package provides tools for working with Jupyter notebooks (.ipynb files),
4
4
  including reading and editing notebook cells.
5
5
  """
6
6
 
7
- from mcp.server.fastmcp import FastMCP
7
+ from fastmcp import FastMCP
8
8
 
9
9
  from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
10
- from hanzo_mcp.tools.common.context import DocumentContext
11
10
  from hanzo_mcp.tools.common.permissions import PermissionManager
12
- from hanzo_mcp.tools.jupyter.edit_notebook import EditNotebookTool
13
- from hanzo_mcp.tools.jupyter.read_notebook import ReadNotebookTool
11
+ from hanzo_mcp.tools.jupyter.notebook_edit import NoteBookEditTool
12
+ from hanzo_mcp.tools.jupyter.notebook_read import NotebookReadTool
14
13
 
15
14
  # Export all tool classes
16
15
  __all__ = [
17
- "ReadNotebookTool",
18
- "EditNotebookTool",
16
+ "NotebookReadTool",
17
+ "NoteBookEditTool",
19
18
  "get_jupyter_tools",
20
19
  "register_jupyter_tools",
21
20
  ]
22
21
 
22
+
23
23
  def get_read_only_jupyter_tools(
24
- document_context: DocumentContext, permission_manager: PermissionManager
24
+ permission_manager: PermissionManager,
25
25
  ) -> list[BaseTool]:
26
26
  """Create instances of read only Jupyter notebook tools.
27
-
27
+
28
28
  Args:
29
- document_context: Document context for tracking file contents
30
29
  permission_manager: Permission manager for access control
31
-
30
+
32
31
  Returns:
33
32
  List of Jupyter notebook tool instances
34
33
  """
35
34
  return [
36
- ReadNotebookTool(document_context, permission_manager),
35
+ NotebookReadTool(permission_manager),
37
36
  ]
38
37
 
39
38
 
40
- def get_jupyter_tools(
41
- document_context: DocumentContext, permission_manager: PermissionManager
42
- ) -> list[BaseTool]:
39
+ def get_jupyter_tools(permission_manager: PermissionManager) -> list[BaseTool]:
43
40
  """Create instances of all Jupyter notebook tools.
44
-
41
+
45
42
  Args:
46
- document_context: Document context for tracking file contents
47
43
  permission_manager: Permission manager for access control
48
-
44
+
49
45
  Returns:
50
46
  List of Jupyter notebook tool instances
51
47
  """
52
48
  return [
53
- ReadNotebookTool(document_context, permission_manager),
54
- EditNotebookTool(document_context, permission_manager),
49
+ NotebookReadTool(permission_manager),
50
+ NoteBookEditTool(permission_manager),
55
51
  ]
56
52
 
57
53
 
58
54
  def register_jupyter_tools(
59
55
  mcp_server: FastMCP,
60
- document_context: DocumentContext,
61
56
  permission_manager: PermissionManager,
62
- disable_write_tools: bool = False,
63
- ) -> None:
64
- """Register all Jupyter notebook tools with the MCP server.
65
-
57
+ enabled_tools: dict[str, bool] | None = None,
58
+ ) -> list[BaseTool]:
59
+ """Register Jupyter notebook tools with the MCP server.
60
+
66
61
  Args:
67
62
  mcp_server: The FastMCP server instance
68
- document_context: Document context for tracking file contents
69
63
  permission_manager: Permission manager for access control
70
- disable_write_tools: Whether to disable write/edit tools (default: False)
64
+ enabled_tools: Dictionary of individual tool enable states (default: None)
65
+
66
+ Returns:
67
+ List of registered tools
71
68
  """
72
- if disable_write_tools:
73
- tools = get_read_only_jupyter_tools(document_context, permission_manager)
69
+ # Define tool mapping
70
+ tool_classes = {
71
+ "notebook_read": NotebookReadTool,
72
+ "notebook_edit": NoteBookEditTool,
73
+ }
74
+
75
+ tools = []
76
+
77
+ if enabled_tools:
78
+ # Use individual tool configuration
79
+ for tool_name, enabled in enabled_tools.items():
80
+ if enabled and tool_name in tool_classes:
81
+ tool_class = tool_classes[tool_name]
82
+ tools.append(tool_class(permission_manager))
74
83
  else:
75
- tools = get_jupyter_tools(document_context, permission_manager)
84
+ # Use all tools (backward compatibility)
85
+ tools = get_jupyter_tools(permission_manager)
86
+
76
87
  ToolRegistry.register_tools(mcp_server, tools)
88
+ return tools
@@ -10,35 +10,36 @@ import re
10
10
  from pathlib import Path
11
11
  from typing import Any, final
12
12
 
13
- from mcp.server.fastmcp import Context as MCPContext
13
+ from fastmcp import Context as MCPContext
14
14
 
15
15
  from hanzo_mcp.tools.common.base import FileSystemTool
16
16
  from hanzo_mcp.tools.common.context import ToolContext, create_tool_context
17
17
 
18
18
 
19
19
  # Pattern to match ANSI escape sequences
20
- ANSI_ESCAPE_PATTERN = re.compile(r'\x1B\[[0-9;]*[a-zA-Z]')
20
+ ANSI_ESCAPE_PATTERN = re.compile(r"\x1B\[[0-9;]*[a-zA-Z]")
21
+
21
22
 
22
23
  # Function to clean ANSI escape codes from text
23
24
  def clean_ansi_escapes(text: str) -> str:
24
25
  """Remove ANSI escape sequences from text.
25
-
26
+
26
27
  Args:
27
28
  text: Text containing ANSI escape sequences
28
-
29
+
29
30
  Returns:
30
31
  Text with ANSI escape sequences removed
31
32
  """
32
- return ANSI_ESCAPE_PATTERN.sub('', text)
33
+ return ANSI_ESCAPE_PATTERN.sub("", text)
33
34
 
34
35
 
35
36
  @final
36
37
  class NotebookOutputImage:
37
38
  """Representation of an image output in a notebook cell."""
38
-
39
+
39
40
  def __init__(self, image_data: str, media_type: str):
40
41
  """Initialize a notebook output image.
41
-
42
+
42
43
  Args:
43
44
  image_data: Base64-encoded image data
44
45
  media_type: Media type of the image (e.g., "image/png")
@@ -50,15 +51,15 @@ class NotebookOutputImage:
50
51
  @final
51
52
  class NotebookCellOutput:
52
53
  """Representation of an output from a notebook cell."""
53
-
54
+
54
55
  def __init__(
55
- self,
56
+ self,
56
57
  output_type: str,
57
58
  text: str | None = None,
58
- image: NotebookOutputImage | None = None
59
+ image: NotebookOutputImage | None = None,
59
60
  ):
60
61
  """Initialize a notebook cell output.
61
-
62
+
62
63
  Args:
63
64
  output_type: Type of output
64
65
  text: Text output (if any)
@@ -72,7 +73,7 @@ class NotebookCellOutput:
72
73
  @final
73
74
  class NotebookCellSource:
74
75
  """Representation of a source cell from a notebook."""
75
-
76
+
76
77
  def __init__(
77
78
  self,
78
79
  cell_index: int,
@@ -80,10 +81,10 @@ class NotebookCellSource:
80
81
  source: str,
81
82
  language: str,
82
83
  execution_count: int | None = None,
83
- outputs: list[NotebookCellOutput] | None = None
84
+ outputs: list[NotebookCellOutput] | None = None,
84
85
  ):
85
86
  """Initialize a notebook cell source.
86
-
87
+
87
88
  Args:
88
89
  cell_index: Index of the cell in the notebook
89
90
  cell_type: Type of cell (code or markdown)
@@ -100,81 +101,87 @@ class NotebookCellSource:
100
101
  self.outputs = outputs or []
101
102
 
102
103
 
103
- class JupyterBaseTool(FileSystemTool,ABC):
104
+ class JupyterBaseTool(FileSystemTool, ABC):
104
105
  """Base class for Jupyter notebook tools.
105
-
106
+
106
107
  Provides common functionality for working with Jupyter notebooks, including
107
108
  parsing, cell extraction, and output formatting.
108
109
  """
109
-
110
+
110
111
  def create_tool_context(self, ctx: MCPContext) -> ToolContext:
111
112
  """Create a tool context with the tool name.
112
-
113
+
113
114
  Args:
114
115
  ctx: MCP context
115
-
116
+
116
117
  Returns:
117
118
  Tool context
118
119
  """
119
120
  tool_ctx = create_tool_context(ctx)
120
121
  return tool_ctx
121
-
122
+
122
123
  def set_tool_context_info(self, tool_ctx: ToolContext) -> None:
123
124
  """Set the tool info on the context.
124
-
125
+
125
126
  Args:
126
127
  tool_ctx: Tool context
127
128
  """
128
129
  tool_ctx.set_tool_info(self.name)
129
-
130
- async def parse_notebook(self, file_path: Path) -> tuple[dict[str, Any], list[NotebookCellSource]]:
130
+
131
+ async def parse_notebook(
132
+ self, file_path: Path
133
+ ) -> tuple[dict[str, Any], list[NotebookCellSource]]:
131
134
  """Parse a Jupyter notebook file.
132
-
135
+
133
136
  Args:
134
137
  file_path: Path to the notebook file
135
-
138
+
136
139
  Returns:
137
140
  Tuple of (notebook_data, processed_cells)
138
141
  """
139
142
  with open(file_path, "r", encoding="utf-8") as f:
140
143
  content = f.read()
141
144
  notebook = json.loads(content)
142
-
145
+
143
146
  # Get notebook language
144
- language = notebook.get("metadata", {}).get("language_info", {}).get("name", "python")
147
+ language = (
148
+ notebook.get("metadata", {}).get("language_info", {}).get("name", "python")
149
+ )
145
150
  cells = notebook.get("cells", [])
146
151
  processed_cells = []
147
152
 
148
153
  for i, cell in enumerate(cells):
149
154
  cell_type = cell.get("cell_type", "code")
150
-
155
+
151
156
  # Skip if not code or markdown
152
157
  if cell_type not in ["code", "markdown"]:
153
158
  continue
154
-
159
+
155
160
  # Get source
156
161
  source = cell.get("source", "")
157
162
  if isinstance(source, list):
158
163
  source = "".join(source)
159
-
164
+
160
165
  # Get execution count for code cells
161
166
  execution_count = None
162
167
  if cell_type == "code":
163
168
  execution_count = cell.get("execution_count")
164
-
169
+
165
170
  # Process outputs for code cells
166
171
  outputs = []
167
172
  if cell_type == "code" and "outputs" in cell:
168
173
  for output in cell["outputs"]:
169
174
  output_type = output.get("output_type", "")
170
-
175
+
171
176
  # Process different output types
172
177
  if output_type == "stream":
173
178
  text = output.get("text", "")
174
179
  if isinstance(text, list):
175
180
  text = "".join(text)
176
- outputs.append(NotebookCellOutput(output_type="stream", text=text))
177
-
181
+ outputs.append(
182
+ NotebookCellOutput(output_type="stream", text=text)
183
+ )
184
+
178
185
  elif output_type in ["execute_result", "display_data"]:
179
186
  # Process text output
180
187
  text = None
@@ -184,46 +191,48 @@ class JupyterBaseTool(FileSystemTool,ABC):
184
191
  text = "".join(text_data)
185
192
  else:
186
193
  text = text_data
187
-
194
+
188
195
  # Process image output
189
196
  image = None
190
197
  if "data" in output:
191
198
  if "image/png" in output["data"]:
192
199
  image = NotebookOutputImage(
193
200
  image_data=output["data"]["image/png"],
194
- media_type="image/png"
201
+ media_type="image/png",
195
202
  )
196
203
  elif "image/jpeg" in output["data"]:
197
204
  image = NotebookOutputImage(
198
205
  image_data=output["data"]["image/jpeg"],
199
- media_type="image/jpeg"
206
+ media_type="image/jpeg",
200
207
  )
201
-
208
+
202
209
  outputs.append(
203
210
  NotebookCellOutput(
204
- output_type=output_type,
205
- text=text,
206
- image=image
211
+ output_type=output_type, text=text, image=image
207
212
  )
208
213
  )
209
-
214
+
210
215
  elif output_type == "error":
211
216
  # Format error traceback
212
217
  ename = output.get("ename", "")
213
218
  evalue = output.get("evalue", "")
214
219
  traceback = output.get("traceback", [])
215
-
220
+
216
221
  # Handle raw text strings and lists of strings
217
222
  if isinstance(traceback, list):
218
223
  # Clean ANSI escape codes and join the list but preserve the formatting
219
- clean_traceback = [clean_ansi_escapes(line) for line in traceback]
224
+ clean_traceback = [
225
+ clean_ansi_escapes(line) for line in traceback
226
+ ]
220
227
  traceback_text = "\n".join(clean_traceback)
221
228
  else:
222
229
  traceback_text = clean_ansi_escapes(str(traceback))
223
-
230
+
224
231
  error_text = f"{ename}: {evalue}\n{traceback_text}"
225
- outputs.append(NotebookCellOutput(output_type="error", text=error_text))
226
-
232
+ outputs.append(
233
+ NotebookCellOutput(output_type="error", text=error_text)
234
+ )
235
+
227
236
  # Create cell object
228
237
  processed_cell = NotebookCellSource(
229
238
  cell_index=i,
@@ -231,19 +240,19 @@ class JupyterBaseTool(FileSystemTool,ABC):
231
240
  source=source,
232
241
  language=language,
233
242
  execution_count=execution_count,
234
- outputs=outputs
243
+ outputs=outputs,
235
244
  )
236
-
245
+
237
246
  processed_cells.append(processed_cell)
238
-
247
+
239
248
  return notebook, processed_cells
240
-
249
+
241
250
  def format_notebook_cells(self, cells: list[NotebookCellSource]) -> str:
242
251
  """Format notebook cells as a readable string.
243
-
252
+
244
253
  Args:
245
254
  cells: List of processed notebook cells
246
-
255
+
247
256
  Returns:
248
257
  Formatted string representation of the cells
249
258
  """
@@ -255,13 +264,13 @@ class JupyterBaseTool(FileSystemTool,ABC):
255
264
  cell_header += f" (execution_count: {cell.execution_count})"
256
265
  if cell.cell_type == "code" and cell.language != "python":
257
266
  cell_header += f" [{cell.language}]"
258
-
267
+
259
268
  # Add cell to result
260
269
  result.append(f"{cell_header}:")
261
270
  result.append(f"```{cell.language if cell.cell_type == 'code' else ''}")
262
271
  result.append(cell.source)
263
272
  result.append("```")
264
-
273
+
265
274
  # Add outputs if any
266
275
  if cell.outputs:
267
276
  result.append("Outputs:")
@@ -278,7 +287,7 @@ class JupyterBaseTool(FileSystemTool,ABC):
278
287
  result.append("```")
279
288
  if output.image:
280
289
  result.append(f"[Image output: {output.image.media_type}]")
281
-
290
+
282
291
  result.append("") # Empty line between cells
283
-
292
+
284
293
  return "\n".join(result)