hanzo-mcp 0.3.8__py3-none-any.whl → 0.5.0__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 (87) 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 +388 -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 +105 -32
  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/context.py +26 -292
  23. hanzo_mcp/tools/common/permissions.py +12 -12
  24. hanzo_mcp/tools/common/thinking_tool.py +153 -0
  25. hanzo_mcp/tools/common/validation.py +1 -63
  26. hanzo_mcp/tools/filesystem/__init__.py +88 -57
  27. hanzo_mcp/tools/filesystem/base.py +32 -24
  28. hanzo_mcp/tools/filesystem/content_replace.py +114 -107
  29. hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
  30. hanzo_mcp/tools/filesystem/edit.py +279 -0
  31. hanzo_mcp/tools/filesystem/grep.py +458 -0
  32. hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
  33. hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
  34. hanzo_mcp/tools/filesystem/read.py +255 -0
  35. hanzo_mcp/tools/filesystem/write.py +156 -0
  36. hanzo_mcp/tools/jupyter/__init__.py +41 -29
  37. hanzo_mcp/tools/jupyter/base.py +66 -57
  38. hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
  39. hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
  40. hanzo_mcp/tools/shell/__init__.py +29 -20
  41. hanzo_mcp/tools/shell/base.py +87 -45
  42. hanzo_mcp/tools/shell/bash_session.py +731 -0
  43. hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
  44. hanzo_mcp/tools/shell/command_executor.py +435 -384
  45. hanzo_mcp/tools/shell/run_command.py +284 -131
  46. hanzo_mcp/tools/shell/run_command_windows.py +328 -0
  47. hanzo_mcp/tools/shell/session_manager.py +196 -0
  48. hanzo_mcp/tools/shell/session_storage.py +325 -0
  49. hanzo_mcp/tools/todo/__init__.py +66 -0
  50. hanzo_mcp/tools/todo/base.py +319 -0
  51. hanzo_mcp/tools/todo/todo_read.py +148 -0
  52. hanzo_mcp/tools/todo/todo_write.py +378 -0
  53. hanzo_mcp/tools/vector/__init__.py +95 -0
  54. hanzo_mcp/tools/vector/infinity_store.py +365 -0
  55. hanzo_mcp/tools/vector/project_manager.py +361 -0
  56. hanzo_mcp/tools/vector/vector_index.py +115 -0
  57. hanzo_mcp/tools/vector/vector_search.py +215 -0
  58. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/METADATA +33 -1
  59. hanzo_mcp-0.5.0.dist-info/RECORD +63 -0
  60. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/WHEEL +1 -1
  61. hanzo_mcp/tools/agent/base_provider.py +0 -73
  62. hanzo_mcp/tools/agent/litellm_provider.py +0 -45
  63. hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
  64. hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
  65. hanzo_mcp/tools/agent/provider_registry.py +0 -120
  66. hanzo_mcp/tools/common/error_handling.py +0 -86
  67. hanzo_mcp/tools/common/logging_config.py +0 -115
  68. hanzo_mcp/tools/common/session.py +0 -91
  69. hanzo_mcp/tools/common/think_tool.py +0 -123
  70. hanzo_mcp/tools/common/version_tool.py +0 -120
  71. hanzo_mcp/tools/filesystem/edit_file.py +0 -287
  72. hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
  73. hanzo_mcp/tools/filesystem/read_files.py +0 -199
  74. hanzo_mcp/tools/filesystem/search_content.py +0 -275
  75. hanzo_mcp/tools/filesystem/write_file.py +0 -162
  76. hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
  77. hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
  78. hanzo_mcp/tools/project/__init__.py +0 -64
  79. hanzo_mcp/tools/project/analysis.py +0 -886
  80. hanzo_mcp/tools/project/base.py +0 -66
  81. hanzo_mcp/tools/project/project_analyze.py +0 -173
  82. hanzo_mcp/tools/shell/run_script.py +0 -215
  83. hanzo_mcp/tools/shell/script_tool.py +0 -244
  84. hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
  85. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/entry_points.txt +0 -0
  86. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/licenses/LICENSE +0 -0
  87. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/top_level.txt +0 -0
@@ -5,33 +5,92 @@ This module provides the ContentReplaceTool for replacing text patterns in files
5
5
 
6
6
  import fnmatch
7
7
  from pathlib import Path
8
- from typing import Any, final, override
8
+ from typing import Annotated, TypedDict, Unpack, final, override
9
9
 
10
- from mcp.server.fastmcp import Context as MCPContext
11
- from mcp.server.fastmcp import FastMCP
10
+ from fastmcp import Context as MCPContext
11
+ from fastmcp import FastMCP
12
+ from fastmcp.server.dependencies import get_context
13
+ from pydantic import Field
12
14
 
13
15
  from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
14
16
 
17
+ Pattern = Annotated[
18
+ str,
19
+ Field(
20
+ description="Text pattern to search for in files",
21
+ min_length=1,
22
+ ),
23
+ ]
24
+
25
+ Replacement = Annotated[
26
+ str,
27
+ Field(
28
+ description="Text to replace the pattern with (can be empty string)",
29
+ ),
30
+ ]
31
+
32
+ SearchPath = Annotated[
33
+ str,
34
+ Field(
35
+ description="Path to file or directory to search in",
36
+ min_length=1,
37
+ ),
38
+ ]
39
+
40
+ FilePattern = Annotated[
41
+ str,
42
+ Field(
43
+ description="File name pattern to match (default: all files)",
44
+ default="*",
45
+ ),
46
+ ]
47
+
48
+ DryRun = Annotated[
49
+ bool,
50
+ Field(
51
+ description="If True, only preview changes without modifying files",
52
+ default=False,
53
+ ),
54
+ ]
55
+
56
+
57
+ class ContentReplaceToolParams(TypedDict):
58
+ """Parameters for the ContentReplaceTool.
59
+
60
+ Attributes:
61
+ pattern: Text pattern to search for in files
62
+ replacement: Text to replace the pattern with (can be empty string)
63
+ path: Path to file or directory to search in
64
+ file_pattern: File name pattern to match (default: all files)
65
+ dry_run: If True, only preview changes without modifying files
66
+ """
67
+
68
+ pattern: Pattern
69
+ replacement: Replacement
70
+ path: SearchPath
71
+ file_pattern: FilePattern
72
+ dry_run: DryRun
73
+
15
74
 
16
75
  @final
17
76
  class ContentReplaceTool(FilesystemBaseTool):
18
77
  """Tool for replacing text patterns in files."""
19
-
78
+
20
79
  @property
21
80
  @override
22
81
  def name(self) -> str:
23
82
  """Get the tool name.
24
-
83
+
25
84
  Returns:
26
85
  Tool name
27
86
  """
28
87
  return "content_replace"
29
-
88
+
30
89
  @property
31
90
  @override
32
91
  def description(self) -> str:
33
92
  """Get the tool description.
34
-
93
+
35
94
  Returns:
36
95
  Tool description
37
96
  """
@@ -41,97 +100,30 @@ Searches for text patterns across all files in the specified directory
41
100
  that match the file pattern and replaces them with the specified text.
42
101
  Can be run in dry-run mode to preview changes without applying them.
43
102
  Only works within allowed directories."""
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
- "pattern": {
56
- "title": "Pattern",
57
- "type": "string"
58
- },
59
- "replacement": {
60
- "title": "Replacement",
61
- "type": "string"
62
- },
63
- "path": {
64
- "title": "Path",
65
- "type": "string"
66
- },
67
- "file_pattern": {
68
- "default": "*",
69
- "title": "File Pattern",
70
- "type": "string"
71
- },
72
- "dry_run": {
73
- "default": False,
74
- "title": "Dry Run",
75
- "type": "boolean"
76
- }
77
- },
78
- "required": ["pattern", "replacement", "path"],
79
- "title": "content_replaceArguments",
80
- "type": "object"
81
- }
82
-
83
- @property
84
- @override
85
- def required(self) -> list[str]:
86
- """Get the list of required parameter names.
87
-
88
- Returns:
89
- List of required parameter names
90
- """
91
- return ["pattern", "replacement", "path"]
92
-
103
+
93
104
  @override
94
- async def call(self, ctx: MCPContext, **params: Any) -> str:
105
+ async def call(
106
+ self,
107
+ ctx: MCPContext,
108
+ **params: Unpack[ContentReplaceToolParams],
109
+ ) -> str:
95
110
  """Execute the tool with the given parameters.
96
-
111
+
97
112
  Args:
98
113
  ctx: MCP context
99
114
  **params: Tool parameters
100
-
115
+
101
116
  Returns:
102
117
  Tool result
103
118
  """
104
119
  tool_ctx = self.create_tool_context(ctx)
105
-
120
+
106
121
  # Extract parameters
107
- pattern = params.get("pattern")
108
- replacement = params.get("replacement")
109
- path = params.get("path")
122
+ pattern: Pattern = params["pattern"]
123
+ replacement: Replacement = params["replacement"]
124
+ path: SearchPath = params["path"]
110
125
  file_pattern = params.get("file_pattern", "*") # Default to all files
111
126
  dry_run = params.get("dry_run", False) # Default to False
112
-
113
- # Validate required parameters
114
- if not pattern:
115
- await tool_ctx.error("Parameter 'pattern' is required but was None")
116
- return "Error: Parameter 'pattern' is required but was None"
117
-
118
- if pattern.strip() == "":
119
- await tool_ctx.error("Parameter 'pattern' cannot be empty")
120
- return "Error: Parameter 'pattern' cannot be empty"
121
-
122
- if replacement is None:
123
- await tool_ctx.error("Parameter 'replacement' is required but was None")
124
- return "Error: Parameter 'replacement' is required but was None"
125
-
126
- if not path:
127
- await tool_ctx.error("Parameter 'path' is required but was None")
128
- return "Error: Parameter 'path' is required but was None"
129
-
130
- if path.strip() == "":
131
- await tool_ctx.error("Parameter 'path' cannot be empty")
132
- return "Error: Parameter 'path' cannot be empty"
133
-
134
- # Note: replacement can be an empty string as sometimes you want to delete the pattern
135
127
 
136
128
  path_validation = self.validate_path(path)
137
129
  if path_validation.is_error:
@@ -168,32 +160,38 @@ Only works within allowed directories."""
168
160
  # Process based on whether path is a file or directory
169
161
  if input_path.is_file():
170
162
  # Single file search
171
- if file_pattern == "*" or fnmatch.fnmatch(input_path.name, file_pattern):
163
+ if file_pattern == "*" or fnmatch.fnmatch(
164
+ input_path.name, file_pattern
165
+ ):
172
166
  matching_files.append(input_path)
173
167
  await tool_ctx.info(f"Searching single file: {path}")
174
168
  else:
175
- await tool_ctx.info(f"File does not match pattern '{file_pattern}': {path}")
169
+ await tool_ctx.info(
170
+ f"File does not match pattern '{file_pattern}': {path}"
171
+ )
176
172
  return f"File does not match pattern '{file_pattern}': {path}"
177
173
  elif input_path.is_dir():
178
174
  # Directory search - optimized file finding
179
175
  await tool_ctx.info(f"Finding files in directory: {path}")
180
-
176
+
181
177
  # Keep track of allowed paths for filtering
182
178
  allowed_paths: set[str] = set()
183
-
179
+
184
180
  # Collect all allowed paths first for faster filtering
185
181
  for entry in input_path.rglob("*"):
186
182
  entry_path = str(entry)
187
183
  if self.is_path_allowed(entry_path):
188
184
  allowed_paths.add(entry_path)
189
-
185
+
190
186
  # Find matching files efficiently
191
187
  for entry in input_path.rglob("*"):
192
188
  entry_path = str(entry)
193
189
  if entry_path in allowed_paths and entry.is_file():
194
- if file_pattern == "*" or fnmatch.fnmatch(entry.name, file_pattern):
190
+ if file_pattern == "*" or fnmatch.fnmatch(
191
+ entry.name, file_pattern
192
+ ):
195
193
  matching_files.append(entry)
196
-
194
+
197
195
  await tool_ctx.info(f"Found {len(matching_files)} matching files")
198
196
  else:
199
197
  # This shouldn't happen since we already checked for existence
@@ -236,17 +234,11 @@ Only works within allowed directories."""
236
234
  with open(file_path, "w", encoding="utf-8") as f:
237
235
  f.write(new_content)
238
236
 
239
- # Update document context
240
- self.document_context.update_document(
241
- str(file_path), new_content
242
- )
243
237
  except UnicodeDecodeError:
244
238
  # Skip binary files
245
239
  continue
246
240
  except Exception as e:
247
- await tool_ctx.warning(
248
- f"Error processing {file_path}: {str(e)}"
249
- )
241
+ await tool_ctx.warning(f"Error processing {file_path}: {str(e)}")
250
242
 
251
243
  # Final progress report
252
244
  await tool_ctx.report_progress(total_files, total_files)
@@ -269,19 +261,34 @@ Only works within allowed directories."""
269
261
  except Exception as e:
270
262
  await tool_ctx.error(f"Error replacing content: {str(e)}")
271
263
  return f"Error replacing content: {str(e)}"
272
-
264
+
273
265
  @override
274
266
  def register(self, mcp_server: FastMCP) -> None:
275
267
  """Register this content replace tool with the MCP server.
276
-
268
+
277
269
  Creates a wrapper function with explicitly defined parameters that match
278
270
  the tool's parameter schema and registers it with the MCP server.
279
-
271
+
280
272
  Args:
281
273
  mcp_server: The FastMCP server instance
282
274
  """
283
275
  tool_self = self # Create a reference to self for use in the closure
284
-
285
- @mcp_server.tool(name=self.name, description=self.mcp_description)
286
- async def content_replace(ctx: MCPContext, pattern: str, replacement: str, path: str, file_pattern: str = "*", dry_run: bool = False) -> str:
287
- return await tool_self.call(ctx, pattern=pattern, replacement=replacement, path=path, file_pattern=file_pattern, dry_run=dry_run)
276
+
277
+ @mcp_server.tool(name=self.name, description=self.description)
278
+ async def content_replace(
279
+ ctx: MCPContext,
280
+ pattern: Pattern,
281
+ replacement: Replacement,
282
+ path: SearchPath,
283
+ file_pattern: FilePattern,
284
+ dry_run: DryRun,
285
+ ) -> str:
286
+ ctx = get_context()
287
+ return await tool_self.call(
288
+ ctx,
289
+ pattern=pattern,
290
+ replacement=replacement,
291
+ path=path,
292
+ file_pattern=file_pattern,
293
+ dry_run=dry_run,
294
+ )