hanzo-mcp 0.1.21__py3-none-any.whl → 0.1.30__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 (48) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +81 -10
  3. hanzo_mcp/server.py +42 -11
  4. hanzo_mcp/tools/__init__.py +51 -32
  5. hanzo_mcp/tools/agent/__init__.py +59 -0
  6. hanzo_mcp/tools/agent/agent_tool.py +474 -0
  7. hanzo_mcp/tools/agent/prompt.py +137 -0
  8. hanzo_mcp/tools/agent/tool_adapter.py +75 -0
  9. hanzo_mcp/tools/common/__init__.py +18 -1
  10. hanzo_mcp/tools/common/base.py +216 -0
  11. hanzo_mcp/tools/common/context.py +9 -5
  12. hanzo_mcp/tools/common/permissions.py +7 -3
  13. hanzo_mcp/tools/common/session.py +91 -0
  14. hanzo_mcp/tools/common/thinking_tool.py +123 -0
  15. hanzo_mcp/tools/common/validation.py +1 -1
  16. hanzo_mcp/tools/filesystem/__init__.py +85 -5
  17. hanzo_mcp/tools/filesystem/base.py +113 -0
  18. hanzo_mcp/tools/filesystem/content_replace.py +287 -0
  19. hanzo_mcp/tools/filesystem/directory_tree.py +286 -0
  20. hanzo_mcp/tools/filesystem/edit_file.py +287 -0
  21. hanzo_mcp/tools/filesystem/get_file_info.py +170 -0
  22. hanzo_mcp/tools/filesystem/read_files.py +198 -0
  23. hanzo_mcp/tools/filesystem/search_content.py +275 -0
  24. hanzo_mcp/tools/filesystem/write_file.py +162 -0
  25. hanzo_mcp/tools/jupyter/__init__.py +67 -4
  26. hanzo_mcp/tools/jupyter/base.py +284 -0
  27. hanzo_mcp/tools/jupyter/edit_notebook.py +295 -0
  28. hanzo_mcp/tools/jupyter/notebook_operations.py +73 -113
  29. hanzo_mcp/tools/jupyter/read_notebook.py +165 -0
  30. hanzo_mcp/tools/project/__init__.py +64 -1
  31. hanzo_mcp/tools/project/analysis.py +8 -5
  32. hanzo_mcp/tools/project/base.py +66 -0
  33. hanzo_mcp/tools/project/project_analyze.py +173 -0
  34. hanzo_mcp/tools/shell/__init__.py +58 -1
  35. hanzo_mcp/tools/shell/base.py +148 -0
  36. hanzo_mcp/tools/shell/command_executor.py +198 -317
  37. hanzo_mcp/tools/shell/run_command.py +204 -0
  38. hanzo_mcp/tools/shell/run_script.py +215 -0
  39. hanzo_mcp/tools/shell/script_tool.py +244 -0
  40. {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.dist-info}/METADATA +72 -77
  41. hanzo_mcp-0.1.30.dist-info/RECORD +45 -0
  42. {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.dist-info}/WHEEL +1 -1
  43. {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.dist-info}/licenses/LICENSE +2 -2
  44. hanzo_mcp/tools/common/thinking.py +0 -65
  45. hanzo_mcp/tools/filesystem/file_operations.py +0 -1050
  46. hanzo_mcp-0.1.21.dist-info/RECORD +0 -23
  47. {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.dist-info}/entry_points.txt +0 -0
  48. {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- """Jupyter notebook operations tools for Hanzo Dev MCP.
1
+ """Jupyter notebook operations tools for Hanzo MCP.
2
2
 
3
3
  This module provides tools for reading and editing Jupyter notebook (.ipynb) files.
4
4
  It supports reading notebook cells with their outputs and modifying notebook contents.
@@ -18,20 +18,19 @@ from hanzo_mcp.tools.common.validation import validate_path_parameter
18
18
 
19
19
 
20
20
  # Pattern to match ANSI escape sequences
21
- ANSI_ESCAPE_PATTERN = re.compile(r"\x1B\[[0-9;]*[a-zA-Z]")
22
-
21
+ ANSI_ESCAPE_PATTERN = re.compile(r'\x1B\[[0-9;]*[a-zA-Z]')
23
22
 
24
23
  # Function to clean ANSI escape codes from text
25
24
  def clean_ansi_escapes(text: str) -> str:
26
25
  """Remove ANSI escape sequences from text.
27
-
26
+
28
27
  Args:
29
28
  text: Text containing ANSI escape sequences
30
-
29
+
31
30
  Returns:
32
31
  Text with ANSI escape sequences removed
33
32
  """
34
- return ANSI_ESCAPE_PATTERN.sub("", text)
33
+ return ANSI_ESCAPE_PATTERN.sub('', text)
35
34
 
36
35
 
37
36
  # Type definitions for Jupyter notebooks based on the nbformat spec
@@ -39,15 +38,14 @@ CellType = Literal["code", "markdown"]
39
38
  OutputType = Literal["stream", "display_data", "execute_result", "error"]
40
39
  EditMode = Literal["replace", "insert", "delete"]
41
40
 
42
-
43
41
  # Define a structure for notebook cell outputs
44
42
  @final
45
43
  class NotebookOutputImage:
46
44
  """Representation of an image output in a notebook cell."""
47
-
45
+
48
46
  def __init__(self, image_data: str, media_type: str):
49
47
  """Initialize a notebook output image.
50
-
48
+
51
49
  Args:
52
50
  image_data: Base64-encoded image data
53
51
  media_type: Media type of the image (e.g., "image/png")
@@ -59,15 +57,15 @@ class NotebookOutputImage:
59
57
  @final
60
58
  class NotebookCellOutput:
61
59
  """Representation of an output from a notebook cell."""
62
-
60
+
63
61
  def __init__(
64
- self,
62
+ self,
65
63
  output_type: OutputType,
66
64
  text: str | None = None,
67
- image: NotebookOutputImage | None = None,
65
+ image: NotebookOutputImage | None = None
68
66
  ):
69
67
  """Initialize a notebook cell output.
70
-
68
+
71
69
  Args:
72
70
  output_type: Type of output
73
71
  text: Text output (if any)
@@ -81,7 +79,7 @@ class NotebookCellOutput:
81
79
  @final
82
80
  class NotebookCellSource:
83
81
  """Representation of a source cell from a notebook."""
84
-
82
+
85
83
  def __init__(
86
84
  self,
87
85
  cell_index: int,
@@ -89,10 +87,10 @@ class NotebookCellSource:
89
87
  source: str,
90
88
  language: str,
91
89
  execution_count: int | None = None,
92
- outputs: list[NotebookCellOutput] | None = None,
90
+ outputs: list[NotebookCellOutput] | None = None
93
91
  ):
94
92
  """Initialize a notebook cell source.
95
-
93
+
96
94
  Args:
97
95
  cell_index: Index of the cell in the notebook
98
96
  cell_type: Type of cell (code or markdown)
@@ -136,9 +134,9 @@ class JupyterNotebookTools:
136
134
  async def read_notebook(path: str, ctx: MCPContext) -> str:
137
135
  """Extract and read source code from all cells in a Jupyter notebook.
138
136
 
139
- Reads a Jupyter notebook (.ipynb file) and returns all of the cells with
140
- their outputs. Jupyter notebooks are interactive documents that combine
141
- code, text, and visualizations, commonly used for data analysis and
137
+ Reads a Jupyter notebook (.ipynb file) and returns all of the cells with
138
+ their outputs. Jupyter notebooks are interactive documents that combine
139
+ code, text, and visualizations, commonly used for data analysis and
142
140
  scientific computing.
143
141
 
144
142
  Args:
@@ -163,9 +161,7 @@ class JupyterNotebookTools:
163
161
  await tool_ctx.error(
164
162
  f"Access denied - path outside allowed directories: {path}"
165
163
  )
166
- return (
167
- f"Error: Access denied - path outside allowed directories: {path}"
168
- )
164
+ return f"Error: Access denied - path outside allowed directories: {path}"
169
165
 
170
166
  try:
171
167
  file_path = Path(path)
@@ -199,46 +195,41 @@ class JupyterNotebookTools:
199
195
  self.document_context.add_document(path, content)
200
196
 
201
197
  # Process notebook cells
202
- language = (
203
- notebook.get("metadata", {})
204
- .get("language_info", {})
205
- .get("name", "python")
206
- )
198
+ # Get notebook language
199
+ language = notebook.get("metadata", {}).get("language_info", {}).get("name", "python")
207
200
  cells = notebook.get("cells", [])
208
201
  processed_cells = []
209
202
 
210
203
  for i, cell in enumerate(cells):
211
204
  cell_type = cell.get("cell_type", "code")
212
-
205
+
213
206
  # Skip if not code or markdown
214
207
  if cell_type not in ["code", "markdown"]:
215
208
  continue
216
-
209
+
217
210
  # Get source
218
211
  source = cell.get("source", "")
219
212
  if isinstance(source, list):
220
213
  source = "".join(source)
221
-
214
+
222
215
  # Get execution count for code cells
223
216
  execution_count = None
224
217
  if cell_type == "code":
225
218
  execution_count = cell.get("execution_count")
226
-
219
+
227
220
  # Process outputs for code cells
228
221
  outputs = []
229
222
  if cell_type == "code" and "outputs" in cell:
230
223
  for output in cell["outputs"]:
231
224
  output_type = output.get("output_type", "")
232
-
225
+
233
226
  # Process different output types
234
227
  if output_type == "stream":
235
228
  text = output.get("text", "")
236
229
  if isinstance(text, list):
237
230
  text = "".join(text)
238
- outputs.append(
239
- NotebookCellOutput(output_type="stream", text=text)
240
- )
241
-
231
+ outputs.append(NotebookCellOutput(output_type="stream", text=text))
232
+
242
233
  elif output_type in ["execute_result", "display_data"]:
243
234
  # Process text output
244
235
  text = None
@@ -248,50 +239,46 @@ class JupyterNotebookTools:
248
239
  text = "".join(text_data)
249
240
  else:
250
241
  text = text_data
251
-
242
+
252
243
  # Process image output
253
244
  image = None
254
245
  if "data" in output:
255
246
  if "image/png" in output["data"]:
256
247
  image = NotebookOutputImage(
257
248
  image_data=output["data"]["image/png"],
258
- media_type="image/png",
249
+ media_type="image/png"
259
250
  )
260
251
  elif "image/jpeg" in output["data"]:
261
252
  image = NotebookOutputImage(
262
253
  image_data=output["data"]["image/jpeg"],
263
- media_type="image/jpeg",
254
+ media_type="image/jpeg"
264
255
  )
265
-
256
+
266
257
  outputs.append(
267
258
  NotebookCellOutput(
268
- output_type=output_type, text=text, image=image
259
+ output_type=output_type,
260
+ text=text,
261
+ image=image
269
262
  )
270
263
  )
271
-
264
+
272
265
  elif output_type == "error":
273
266
  # Format error traceback
274
267
  ename = output.get("ename", "")
275
268
  evalue = output.get("evalue", "")
276
269
  traceback = output.get("traceback", [])
277
-
270
+
278
271
  # Handle raw text strings and lists of strings
279
272
  if isinstance(traceback, list):
280
273
  # Clean ANSI escape codes and join the list but preserve the formatting
281
- clean_traceback = [
282
- clean_ansi_escapes(line) for line in traceback
283
- ]
274
+ clean_traceback = [clean_ansi_escapes(line) for line in traceback]
284
275
  traceback_text = "\n".join(clean_traceback)
285
276
  else:
286
277
  traceback_text = clean_ansi_escapes(str(traceback))
287
-
278
+
288
279
  error_text = f"{ename}: {evalue}\n{traceback_text}"
289
- outputs.append(
290
- NotebookCellOutput(
291
- output_type="error", text=error_text
292
- )
293
- )
294
-
280
+ outputs.append(NotebookCellOutput(output_type="error", text=error_text))
281
+
295
282
  # Create cell object
296
283
  processed_cell = NotebookCellSource(
297
284
  cell_index=i,
@@ -299,9 +286,9 @@ class JupyterNotebookTools:
299
286
  source=source,
300
287
  language=language,
301
288
  execution_count=execution_count,
302
- outputs=outputs,
289
+ outputs=outputs
303
290
  )
304
-
291
+
305
292
  processed_cells.append(processed_cell)
306
293
 
307
294
  # Format the notebook content as a readable string
@@ -313,15 +300,13 @@ class JupyterNotebookTools:
313
300
  cell_header += f" (execution_count: {cell.execution_count})"
314
301
  if cell.cell_type == "code" and cell.language != "python":
315
302
  cell_header += f" [{cell.language}]"
316
-
303
+
317
304
  # Add cell to result
318
305
  result.append(f"{cell_header}:")
319
- result.append(
320
- f"```{cell.language if cell.cell_type == 'code' else ''}"
321
- )
306
+ result.append(f"```{cell.language if cell.cell_type == 'code' else ''}")
322
307
  result.append(cell.source)
323
308
  result.append("```")
324
-
309
+
325
310
  # Add outputs if any
326
311
  if cell.outputs:
327
312
  result.append("Outputs:")
@@ -337,15 +322,11 @@ class JupyterNotebookTools:
337
322
  result.append(output.text)
338
323
  result.append("```")
339
324
  if output.image:
340
- result.append(
341
- f"[Image output: {output.image.media_type}]"
342
- )
343
-
325
+ result.append(f"[Image output: {output.image.media_type}]")
326
+
344
327
  result.append("") # Empty line between cells
345
328
 
346
- await tool_ctx.info(
347
- f"Successfully read notebook: {path} ({len(processed_cells)} cells)"
348
- )
329
+ await tool_ctx.info(f"Successfully read notebook: {path} ({len(processed_cells)} cells)")
349
330
  return "\n".join(result)
350
331
  except Exception as e:
351
332
  await tool_ctx.error(f"Error reading notebook: {str(e)}")
@@ -353,12 +334,12 @@ class JupyterNotebookTools:
353
334
 
354
335
  @mcp_server.tool()
355
336
  async def edit_notebook(
356
- path: str,
357
- cell_number: int,
358
- new_source: str,
337
+ path: str,
338
+ cell_number: int,
339
+ new_source: str,
359
340
  ctx: MCPContext,
360
341
  cell_type: CellType | None = None,
361
- edit_mode: EditMode = "replace",
342
+ edit_mode: EditMode = "replace"
362
343
  ) -> str:
363
344
  """Edit a specific cell in a Jupyter notebook.
364
345
 
@@ -403,23 +384,17 @@ class JupyterNotebookTools:
403
384
 
404
385
  # Don't validate new_source for delete mode
405
386
  if edit_mode != "delete" and not new_source:
406
- await tool_ctx.error(
407
- "New source is required for replace or insert operations"
408
- )
387
+ await tool_ctx.error("New source is required for replace or insert operations")
409
388
  return "Error: New source is required for replace or insert operations"
410
389
 
411
- await tool_ctx.info(
412
- f"Editing notebook: {path} (cell: {cell_number}, mode: {edit_mode})"
413
- )
390
+ await tool_ctx.info(f"Editing notebook: {path} (cell: {cell_number}, mode: {edit_mode})")
414
391
 
415
392
  # Check if path is allowed
416
393
  if not self.permission_manager.is_path_allowed(path):
417
394
  await tool_ctx.error(
418
395
  f"Access denied - path outside allowed directories: {path}"
419
396
  )
420
- return (
421
- f"Error: Access denied - path outside allowed directories: {path}"
422
- )
397
+ return f"Error: Access denied - path outside allowed directories: {path}"
423
398
 
424
399
  try:
425
400
  file_path = Path(path)
@@ -451,51 +426,44 @@ class JupyterNotebookTools:
451
426
 
452
427
  # Check cell_number is valid
453
428
  cells = notebook.get("cells", [])
454
-
429
+
455
430
  if edit_mode == "insert":
456
431
  if cell_number > len(cells):
457
- await tool_ctx.error(
458
- f"Cell number {cell_number} is out of bounds for insert (max: {len(cells)})"
459
- )
432
+ await tool_ctx.error(f"Cell number {cell_number} is out of bounds for insert (max: {len(cells)})")
460
433
  return f"Error: Cell number {cell_number} is out of bounds for insert (max: {len(cells)})"
461
434
  else: # replace or delete
462
435
  if cell_number >= len(cells):
463
- await tool_ctx.error(
464
- f"Cell number {cell_number} is out of bounds (max: {len(cells) - 1})"
465
- )
436
+ await tool_ctx.error(f"Cell number {cell_number} is out of bounds (max: {len(cells) - 1})")
466
437
  return f"Error: Cell number {cell_number} is out of bounds (max: {len(cells) - 1})"
467
438
 
468
- language = (
469
- notebook.get("metadata", {})
470
- .get("language_info", {})
471
- .get("name", "python")
472
- )
439
+ # Get notebook language (needed for context but not directly used in this block)
440
+ _ = notebook.get("metadata", {}).get("language_info", {}).get("name", "python")
473
441
 
474
442
  # Perform the requested operation
475
443
  if edit_mode == "replace":
476
444
  # Get the target cell
477
445
  target_cell = cells[cell_number]
478
-
446
+
479
447
  # Store previous contents for reporting
480
448
  old_type = target_cell.get("cell_type", "code")
481
449
  old_source = target_cell.get("source", "")
482
450
  if isinstance(old_source, list):
483
451
  old_source = "".join(old_source)
484
-
452
+
485
453
  # Update source
486
454
  target_cell["source"] = new_source
487
-
455
+
488
456
  # Update type if specified
489
457
  if cell_type is not None:
490
458
  target_cell["cell_type"] = cell_type
491
-
459
+
492
460
  # If changing to markdown, remove code-specific fields
493
461
  if cell_type == "markdown":
494
462
  if "outputs" in target_cell:
495
463
  del target_cell["outputs"]
496
464
  if "execution_count" in target_cell:
497
465
  del target_cell["execution_count"]
498
-
466
+
499
467
  # If code cell, reset execution
500
468
  if target_cell["cell_type"] == "code":
501
469
  target_cell["outputs"] = []
@@ -503,39 +471,33 @@ class JupyterNotebookTools:
503
471
 
504
472
  change_description = f"Replaced cell {cell_number}"
505
473
  if cell_type is not None and cell_type != old_type:
506
- change_description += (
507
- f" (changed type from {old_type} to {cell_type})"
508
- )
474
+ change_description += f" (changed type from {old_type} to {cell_type})"
509
475
 
510
476
  elif edit_mode == "insert":
511
477
  # Create new cell
512
478
  new_cell: dict[str, Any] = {
513
479
  "cell_type": cell_type,
514
480
  "source": new_source,
515
- "metadata": {},
481
+ "metadata": {}
516
482
  }
517
-
483
+
518
484
  # Add code-specific fields
519
485
  if cell_type == "code":
520
486
  new_cell["outputs"] = []
521
487
  new_cell["execution_count"] = None
522
-
488
+
523
489
  # Insert the cell
524
490
  cells.insert(cell_number, new_cell)
525
- change_description = (
526
- f"Inserted new {cell_type} cell at position {cell_number}"
527
- )
491
+ change_description = f"Inserted new {cell_type} cell at position {cell_number}"
528
492
 
529
493
  else: # delete
530
494
  # Store deleted cell info for reporting
531
495
  deleted_cell = cells[cell_number]
532
496
  deleted_type = deleted_cell.get("cell_type", "code")
533
-
497
+
534
498
  # Remove the cell
535
499
  del cells[cell_number]
536
- change_description = (
537
- f"Deleted {deleted_type} cell at position {cell_number}"
538
- )
500
+ change_description = f"Deleted {deleted_type} cell at position {cell_number}"
539
501
 
540
502
  # Write the updated notebook back to file
541
503
  with open(file_path, "w", encoding="utf-8") as f:
@@ -545,9 +507,7 @@ class JupyterNotebookTools:
545
507
  updated_content = json.dumps(notebook, indent=1)
546
508
  self.document_context.update_document(path, updated_content)
547
509
 
548
- await tool_ctx.info(
549
- f"Successfully edited notebook: {path} - {change_description}"
550
- )
510
+ await tool_ctx.info(f"Successfully edited notebook: {path} - {change_description}")
551
511
  return f"Successfully edited notebook: {path} - {change_description}"
552
512
  except Exception as e:
553
513
  await tool_ctx.error(f"Error editing notebook: {str(e)}")
@@ -0,0 +1,165 @@
1
+ """Read notebook tool implementation.
2
+
3
+ This module provides the ReadNotebookTool for reading Jupyter notebook files.
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any, final, override
9
+
10
+ from mcp.server.fastmcp import Context as MCPContext
11
+ from mcp.server.fastmcp import FastMCP
12
+
13
+ from hanzo_mcp.tools.jupyter.base import JupyterBaseTool
14
+
15
+
16
+ @final
17
+ class ReadNotebookTool(JupyterBaseTool):
18
+ """Tool for reading Jupyter notebook files."""
19
+
20
+ @property
21
+ @override
22
+ def name(self) -> str:
23
+ """Get the tool name.
24
+
25
+ Returns:
26
+ Tool name
27
+ """
28
+ return "read_notebook"
29
+
30
+ @property
31
+ @override
32
+ def description(self) -> str:
33
+ """Get the tool description.
34
+
35
+ Returns:
36
+ Tool description
37
+ """
38
+ return """Extract and read source code from all cells in a Jupyter notebook.
39
+
40
+ Reads a Jupyter notebook (.ipynb file) and returns all of the cells with
41
+ their outputs. Jupyter notebooks are interactive documents that combine
42
+ code, text, and visualizations, commonly used for data analysis and
43
+ scientific computing."""
44
+
45
+ @property
46
+ @override
47
+ def parameters(self) -> dict[str, Any]:
48
+ """Get the parameter specifications for the tool.
49
+
50
+ Returns:
51
+ Parameter specifications
52
+ """
53
+ return {
54
+ "properties": {
55
+ "path": {
56
+ "type": "string",
57
+ "description": "path to the Jupyter notebook file"
58
+ }
59
+ },
60
+ "required": ["path"],
61
+ "type": "object"
62
+ }
63
+
64
+ @property
65
+ @override
66
+ def required(self) -> list[str]:
67
+ """Get the list of required parameter names.
68
+
69
+ Returns:
70
+ List of required parameter names
71
+ """
72
+ return ["path"]
73
+
74
+ @override
75
+ async def call(self, ctx: MCPContext, **params: Any) -> str:
76
+ """Execute the tool with the given parameters.
77
+
78
+ Args:
79
+ ctx: MCP context
80
+ **params: Tool parameters
81
+
82
+ Returns:
83
+ Tool result
84
+ """
85
+ tool_ctx = self.create_tool_context(ctx)
86
+ self.set_tool_context_info(tool_ctx)
87
+
88
+ # Extract parameters
89
+ path = params.get("path")
90
+
91
+ if not path:
92
+ await tool_ctx.error("Missing required parameter: path")
93
+ return "Error: Missing required parameter: path"
94
+
95
+ # Validate path parameter
96
+ path_validation = self.validate_path(path)
97
+ if path_validation.is_error:
98
+ await tool_ctx.error(path_validation.error_message)
99
+ return f"Error: {path_validation.error_message}"
100
+
101
+ await tool_ctx.info(f"Reading notebook: {path}")
102
+
103
+ # Check if path is allowed
104
+ if not self.is_path_allowed(path):
105
+ await tool_ctx.error(
106
+ f"Access denied - path outside allowed directories: {path}"
107
+ )
108
+ return f"Error: Access denied - path outside allowed directories: {path}"
109
+
110
+ try:
111
+ file_path = Path(path)
112
+
113
+ if not file_path.exists():
114
+ await tool_ctx.error(f"File does not exist: {path}")
115
+ return f"Error: File does not exist: {path}"
116
+
117
+ if not file_path.is_file():
118
+ await tool_ctx.error(f"Path is not a file: {path}")
119
+ return f"Error: Path is not a file: {path}"
120
+
121
+ # Check file extension
122
+ if file_path.suffix.lower() != ".ipynb":
123
+ await tool_ctx.error(f"File is not a Jupyter notebook: {path}")
124
+ return f"Error: File is not a Jupyter notebook: {path}"
125
+
126
+ # Read and parse the notebook
127
+ try:
128
+ # This will read the file, so we don't need to read it separately
129
+ _, processed_cells = await self.parse_notebook(file_path)
130
+
131
+ # Add to document context
132
+ with open(file_path, "r", encoding="utf-8") as f:
133
+ content = f.read()
134
+ self.document_context.add_document(path, content)
135
+
136
+ # Format the notebook content as a readable string
137
+ result = self.format_notebook_cells(processed_cells)
138
+
139
+ await tool_ctx.info(f"Successfully read notebook: {path} ({len(processed_cells)} cells)")
140
+ return result
141
+ except json.JSONDecodeError:
142
+ await tool_ctx.error(f"Invalid notebook format: {path}")
143
+ return f"Error: Invalid notebook format: {path}"
144
+ except UnicodeDecodeError:
145
+ await tool_ctx.error(f"Cannot read notebook file: {path}")
146
+ return f"Error: Cannot read notebook file: {path}"
147
+ except Exception as e:
148
+ await tool_ctx.error(f"Error reading notebook: {str(e)}")
149
+ return f"Error reading notebook: {str(e)}"
150
+
151
+ @override
152
+ def register(self, mcp_server: FastMCP) -> None:
153
+ """Register this read notebook tool with the MCP server.
154
+
155
+ Creates a wrapper function with explicitly defined parameters that match
156
+ the tool's parameter schema and registers it with the MCP server.
157
+
158
+ Args:
159
+ mcp_server: The FastMCP server instance
160
+ """
161
+ tool_self = self # Create a reference to self for use in the closure
162
+
163
+ @mcp_server.tool(name=self.name, description=self.mcp_description)
164
+ async def read_notebook(path: str, ctx: MCPContext) -> str:
165
+ return await tool_self.call(ctx, path=path)
@@ -1 +1,64 @@
1
- """Project analysis and management tools for Hanzo Dev MCP."""
1
+ """Project analysis tools package for Hanzo MCP.
2
+
3
+ This package provides tools for analyzing project structure and dependencies.
4
+ """
5
+
6
+ from mcp.server.fastmcp import FastMCP
7
+
8
+ from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
9
+ from hanzo_mcp.tools.common.context import DocumentContext
10
+ from hanzo_mcp.tools.common.permissions import PermissionManager
11
+ from hanzo_mcp.tools.project.analysis import ProjectAnalyzer, ProjectManager
12
+ from hanzo_mcp.tools.project.project_analyze import ProjectAnalyzeTool
13
+ from hanzo_mcp.tools.shell.command_executor import CommandExecutor
14
+
15
+ # Export all tool classes
16
+ __all__ = [
17
+ "ProjectAnalyzer",
18
+ "ProjectManager",
19
+ "ProjectAnalyzeTool",
20
+ "get_project_tools",
21
+ "register_project_tools",
22
+ ]
23
+
24
+
25
+ def get_project_tools(
26
+ document_context: DocumentContext,
27
+ permission_manager: PermissionManager,
28
+ command_executor: CommandExecutor,
29
+ ) -> list[BaseTool]:
30
+ """Create instances of all project tools.
31
+
32
+ Args:
33
+ permission_manager: Permission manager for access control
34
+ document_context: Document context for tracking file contents
35
+ command_executor: Command executor for running analysis scripts
36
+
37
+ Returns:
38
+ List of project tool instances
39
+ """
40
+ # Initialize project analyzer and manager
41
+ project_analyzer = ProjectAnalyzer(command_executor)
42
+ project_manager = ProjectManager(document_context, permission_manager, project_analyzer)
43
+
44
+ return [
45
+ ProjectAnalyzeTool(permission_manager, project_manager, project_analyzer),
46
+ ]
47
+
48
+
49
+ def register_project_tools(
50
+ mcp_server: FastMCP,
51
+ permission_manager: PermissionManager,
52
+ document_context: DocumentContext,
53
+ command_executor: CommandExecutor,
54
+ ) -> None:
55
+ """Register all project tools with the MCP server.
56
+
57
+ Args:
58
+ mcp_server: The FastMCP server instance
59
+ permission_manager: Permission manager for access control
60
+ document_context: Document context for tracking file contents
61
+ command_executor: Command executor for running analysis scripts
62
+ """
63
+ tools = get_project_tools(document_context, permission_manager, command_executor)
64
+ ToolRegistry.register_tools(mcp_server, tools)