hanzo-mcp 0.6.12__py3-none-any.whl → 0.7.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 (117) hide show
  1. hanzo_mcp/__init__.py +2 -2
  2. hanzo_mcp/analytics/__init__.py +5 -0
  3. hanzo_mcp/analytics/posthog_analytics.py +364 -0
  4. hanzo_mcp/cli.py +5 -5
  5. hanzo_mcp/cli_enhanced.py +7 -7
  6. hanzo_mcp/cli_plugin.py +91 -0
  7. hanzo_mcp/config/__init__.py +1 -1
  8. hanzo_mcp/config/settings.py +70 -7
  9. hanzo_mcp/config/tool_config.py +20 -6
  10. hanzo_mcp/dev_server.py +3 -3
  11. hanzo_mcp/prompts/project_system.py +1 -1
  12. hanzo_mcp/server.py +40 -3
  13. hanzo_mcp/server_enhanced.py +69 -0
  14. hanzo_mcp/tools/__init__.py +140 -31
  15. hanzo_mcp/tools/agent/__init__.py +85 -4
  16. hanzo_mcp/tools/agent/agent_tool.py +104 -6
  17. hanzo_mcp/tools/agent/agent_tool_v2.py +459 -0
  18. hanzo_mcp/tools/agent/clarification_protocol.py +220 -0
  19. hanzo_mcp/tools/agent/clarification_tool.py +68 -0
  20. hanzo_mcp/tools/agent/claude_cli_tool.py +125 -0
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +508 -0
  22. hanzo_mcp/tools/agent/cli_agent_base.py +191 -0
  23. hanzo_mcp/tools/agent/code_auth.py +436 -0
  24. hanzo_mcp/tools/agent/code_auth_tool.py +194 -0
  25. hanzo_mcp/tools/agent/codex_cli_tool.py +123 -0
  26. hanzo_mcp/tools/agent/critic_tool.py +376 -0
  27. hanzo_mcp/tools/agent/gemini_cli_tool.py +128 -0
  28. hanzo_mcp/tools/agent/grok_cli_tool.py +128 -0
  29. hanzo_mcp/tools/agent/iching_tool.py +380 -0
  30. hanzo_mcp/tools/agent/network_tool.py +273 -0
  31. hanzo_mcp/tools/agent/prompt.py +62 -20
  32. hanzo_mcp/tools/agent/review_tool.py +433 -0
  33. hanzo_mcp/tools/agent/swarm_tool.py +535 -0
  34. hanzo_mcp/tools/agent/swarm_tool_v2.py +594 -0
  35. hanzo_mcp/tools/common/__init__.py +15 -1
  36. hanzo_mcp/tools/common/base.py +5 -4
  37. hanzo_mcp/tools/common/batch_tool.py +103 -11
  38. hanzo_mcp/tools/common/config_tool.py +2 -2
  39. hanzo_mcp/tools/common/context.py +2 -2
  40. hanzo_mcp/tools/common/context_fix.py +26 -0
  41. hanzo_mcp/tools/common/critic_tool.py +196 -0
  42. hanzo_mcp/tools/common/decorators.py +208 -0
  43. hanzo_mcp/tools/common/enhanced_base.py +106 -0
  44. hanzo_mcp/tools/common/fastmcp_pagination.py +369 -0
  45. hanzo_mcp/tools/common/forgiving_edit.py +243 -0
  46. hanzo_mcp/tools/common/mode.py +116 -0
  47. hanzo_mcp/tools/common/mode_loader.py +105 -0
  48. hanzo_mcp/tools/common/paginated_base.py +230 -0
  49. hanzo_mcp/tools/common/paginated_response.py +307 -0
  50. hanzo_mcp/tools/common/pagination.py +226 -0
  51. hanzo_mcp/tools/common/permissions.py +1 -1
  52. hanzo_mcp/tools/common/personality.py +936 -0
  53. hanzo_mcp/tools/common/plugin_loader.py +287 -0
  54. hanzo_mcp/tools/common/stats.py +4 -4
  55. hanzo_mcp/tools/common/tool_list.py +4 -1
  56. hanzo_mcp/tools/common/truncate.py +101 -0
  57. hanzo_mcp/tools/common/validation.py +1 -1
  58. hanzo_mcp/tools/config/__init__.py +3 -1
  59. hanzo_mcp/tools/config/config_tool.py +1 -1
  60. hanzo_mcp/tools/config/mode_tool.py +209 -0
  61. hanzo_mcp/tools/database/__init__.py +1 -1
  62. hanzo_mcp/tools/editor/__init__.py +1 -1
  63. hanzo_mcp/tools/filesystem/__init__.py +48 -14
  64. hanzo_mcp/tools/filesystem/ast_multi_edit.py +562 -0
  65. hanzo_mcp/tools/filesystem/batch_search.py +3 -3
  66. hanzo_mcp/tools/filesystem/diff.py +2 -2
  67. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +338 -0
  68. hanzo_mcp/tools/filesystem/rules_tool.py +235 -0
  69. hanzo_mcp/tools/filesystem/{unified_search.py → search_tool.py} +12 -12
  70. hanzo_mcp/tools/filesystem/{symbols_unified.py → symbols_tool.py} +104 -5
  71. hanzo_mcp/tools/filesystem/watch.py +3 -2
  72. hanzo_mcp/tools/jupyter/__init__.py +2 -2
  73. hanzo_mcp/tools/jupyter/jupyter.py +1 -1
  74. hanzo_mcp/tools/llm/__init__.py +3 -3
  75. hanzo_mcp/tools/llm/llm_tool.py +648 -143
  76. hanzo_mcp/tools/lsp/__init__.py +5 -0
  77. hanzo_mcp/tools/lsp/lsp_tool.py +512 -0
  78. hanzo_mcp/tools/mcp/__init__.py +2 -2
  79. hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
  80. hanzo_mcp/tools/memory/__init__.py +76 -0
  81. hanzo_mcp/tools/memory/knowledge_tools.py +518 -0
  82. hanzo_mcp/tools/memory/memory_tools.py +456 -0
  83. hanzo_mcp/tools/search/__init__.py +6 -0
  84. hanzo_mcp/tools/search/find_tool.py +581 -0
  85. hanzo_mcp/tools/search/unified_search.py +953 -0
  86. hanzo_mcp/tools/shell/__init__.py +11 -6
  87. hanzo_mcp/tools/shell/auto_background.py +203 -0
  88. hanzo_mcp/tools/shell/base_process.py +57 -29
  89. hanzo_mcp/tools/shell/bash_session_executor.py +1 -1
  90. hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +18 -34
  91. hanzo_mcp/tools/shell/command_executor.py +2 -2
  92. hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +16 -33
  93. hanzo_mcp/tools/shell/open.py +2 -2
  94. hanzo_mcp/tools/shell/{process_unified.py → process_tool.py} +1 -1
  95. hanzo_mcp/tools/shell/run_command_windows.py +1 -1
  96. hanzo_mcp/tools/shell/streaming_command.py +594 -0
  97. hanzo_mcp/tools/shell/uvx.py +47 -2
  98. hanzo_mcp/tools/shell/uvx_background.py +47 -2
  99. hanzo_mcp/tools/shell/{uvx_unified.py → uvx_tool.py} +16 -33
  100. hanzo_mcp/tools/todo/__init__.py +14 -19
  101. hanzo_mcp/tools/todo/todo.py +22 -1
  102. hanzo_mcp/tools/vector/__init__.py +1 -1
  103. hanzo_mcp/tools/vector/infinity_store.py +2 -2
  104. hanzo_mcp/tools/vector/project_manager.py +1 -1
  105. hanzo_mcp/types.py +23 -0
  106. hanzo_mcp-0.7.0.dist-info/METADATA +516 -0
  107. hanzo_mcp-0.7.0.dist-info/RECORD +180 -0
  108. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/entry_points.txt +1 -0
  109. hanzo_mcp/tools/common/palette.py +0 -344
  110. hanzo_mcp/tools/common/palette_loader.py +0 -108
  111. hanzo_mcp/tools/config/palette_tool.py +0 -179
  112. hanzo_mcp/tools/llm/llm_unified.py +0 -851
  113. hanzo_mcp-0.6.12.dist-info/METADATA +0 -339
  114. hanzo_mcp-0.6.12.dist-info/RECORD +0 -135
  115. hanzo_mcp-0.6.12.dist-info/licenses/LICENSE +0 -21
  116. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/WHEEL +0 -0
  117. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,338 @@
1
+ """Paginated directory tree tool implementation.
2
+
3
+ This module provides a paginated version of DirectoryTreeTool that supports
4
+ MCP cursor-based pagination for large directory structures.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Annotated, Any, Dict, List, Optional, TypedDict, Unpack, final, override
9
+
10
+ from mcp.server.fastmcp import Context as MCPContext
11
+ from mcp.server import FastMCP
12
+ from pydantic import Field
13
+
14
+ from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
15
+ from hanzo_mcp.tools.common.pagination import (
16
+ CursorManager,
17
+ PaginatedResponse,
18
+ paginate_list
19
+ )
20
+
21
+ DirectoryPath = Annotated[
22
+ str,
23
+ Field(
24
+ description="The path to the directory to view",
25
+ title="Path",
26
+ ),
27
+ ]
28
+
29
+ Depth = Annotated[
30
+ int,
31
+ Field(
32
+ default=3,
33
+ description="The maximum depth to traverse (0 for unlimited)",
34
+ title="Depth",
35
+ ),
36
+ ]
37
+
38
+ IncludeFiltered = Annotated[
39
+ bool,
40
+ Field(
41
+ default=False,
42
+ description="Include directories that are normally filtered",
43
+ title="Include Filtered",
44
+ ),
45
+ ]
46
+
47
+ PageSize = Annotated[
48
+ int,
49
+ Field(
50
+ default=100,
51
+ description="Number of entries per page",
52
+ title="Page Size",
53
+ ),
54
+ ]
55
+
56
+ Cursor = Annotated[
57
+ Optional[str],
58
+ Field(
59
+ default=None,
60
+ description="Pagination cursor for continuing from previous request",
61
+ title="Cursor",
62
+ ),
63
+ ]
64
+
65
+
66
+ class DirectoryTreePaginatedParams(TypedDict):
67
+ """Parameters for the paginated DirectoryTreeTool.
68
+
69
+ Attributes:
70
+ path: The path to the directory to view
71
+ depth: The maximum depth to traverse (0 for unlimited)
72
+ include_filtered: Include directories that are normally filtered
73
+ page_size: Number of entries per page
74
+ cursor: Pagination cursor
75
+ """
76
+
77
+ path: DirectoryPath
78
+ depth: Depth
79
+ include_filtered: IncludeFiltered
80
+ page_size: PageSize
81
+ cursor: Cursor
82
+
83
+
84
+ @final
85
+ class DirectoryTreePaginatedTool(FilesystemBaseTool):
86
+ """Tool for viewing directory structure as a tree with pagination support."""
87
+
88
+ @property
89
+ @override
90
+ def name(self) -> str:
91
+ """Get the tool name."""
92
+ return "directory_tree_paginated"
93
+
94
+ @property
95
+ @override
96
+ def description(self) -> str:
97
+ """Get the tool description."""
98
+ return """Get a paginated recursive tree view of files and directories.
99
+
100
+ This is a paginated version of directory_tree that supports cursor-based pagination
101
+ for large directory structures. Returns a structured view with files and subdirectories.
102
+
103
+ Directories are marked with trailing slashes. Common development directories like
104
+ .git, node_modules, and venv are noted but not traversed unless explicitly requested.
105
+
106
+ Use the cursor field to continue from where the previous request left off.
107
+ Returns nextCursor if more entries are available."""
108
+
109
+ @override
110
+ async def call(
111
+ self,
112
+ ctx: MCPContext,
113
+ **params: Unpack[DirectoryTreePaginatedParams],
114
+ ) -> Dict[str, Any]:
115
+ """Execute the tool with the given parameters.
116
+
117
+ Args:
118
+ ctx: MCP context
119
+ **params: Tool parameters
120
+
121
+ Returns:
122
+ Dictionary with entries and optional nextCursor
123
+ """
124
+ tool_ctx = self.create_tool_context(ctx)
125
+
126
+ # Extract parameters
127
+ path: DirectoryPath = params["path"]
128
+ depth = params.get("depth", 3)
129
+ include_filtered = params.get("include_filtered", False)
130
+ page_size = params.get("page_size", 100)
131
+ cursor = params.get("cursor", None)
132
+
133
+ # Validate cursor if provided
134
+ if cursor:
135
+ cursor_data = CursorManager.parse_cursor(cursor)
136
+ if not cursor_data:
137
+ await tool_ctx.error("Invalid cursor provided")
138
+ return {"error": "Invalid cursor"}
139
+
140
+ # Validate path parameter
141
+ path_validation = self.validate_path(path)
142
+ if path_validation.is_error:
143
+ await tool_ctx.error(path_validation.error_message)
144
+ return {"error": path_validation.error_message}
145
+
146
+ await tool_ctx.info(
147
+ f"Getting paginated directory tree: {path} (depth: {depth}, page_size: {page_size})"
148
+ )
149
+
150
+ # Check if path is allowed
151
+ allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
152
+ if not allowed:
153
+ return {"error": error_msg}
154
+
155
+ try:
156
+ dir_path = Path(path)
157
+
158
+ # Check if path exists
159
+ exists, error_msg = await self.check_path_exists(path, tool_ctx)
160
+ if not exists:
161
+ return {"error": error_msg}
162
+
163
+ # Check if path is a directory
164
+ is_dir, error_msg = await self.check_is_directory(path, tool_ctx)
165
+ if not is_dir:
166
+ return {"error": error_msg}
167
+
168
+ # Define filtered directories
169
+ FILTERED_DIRECTORIES = {
170
+ ".git",
171
+ "node_modules",
172
+ ".venv",
173
+ "venv",
174
+ "__pycache__",
175
+ ".pytest_cache",
176
+ ".idea",
177
+ ".vs",
178
+ ".vscode",
179
+ "dist",
180
+ "build",
181
+ "target",
182
+ ".ruff_cache",
183
+ ".llm-context",
184
+ }
185
+
186
+ # Check if a directory should be filtered
187
+ def should_filter(current_path: Path) -> bool:
188
+ if str(current_path.absolute()) == str(dir_path.absolute()):
189
+ return False
190
+ return (
191
+ current_path.name in FILTERED_DIRECTORIES and not include_filtered
192
+ )
193
+
194
+ # Collect all entries in a flat list for pagination
195
+ all_entries: List[Dict[str, Any]] = []
196
+
197
+ # Build the tree and collect entries
198
+ def collect_entries(
199
+ current_path: Path,
200
+ current_depth: int = 0,
201
+ parent_path: str = ""
202
+ ) -> None:
203
+ """Collect entries in a flat list for pagination."""
204
+ if not self.is_path_allowed(str(current_path)):
205
+ return
206
+
207
+ try:
208
+ # Sort entries: directories first, then files alphabetically
209
+ entries = sorted(
210
+ current_path.iterdir(),
211
+ key=lambda x: (not x.is_dir(), x.name)
212
+ )
213
+
214
+ for entry in entries:
215
+ if not self.is_path_allowed(str(entry)):
216
+ continue
217
+
218
+ # Calculate relative path for display
219
+ relative_path = f"{parent_path}/{entry.name}" if parent_path else entry.name
220
+
221
+ if entry.is_dir():
222
+ entry_data: Dict[str, Any] = {
223
+ "path": relative_path,
224
+ "type": "directory",
225
+ "depth": current_depth,
226
+ }
227
+
228
+ # Check if we should filter this directory
229
+ if should_filter(entry):
230
+ entry_data["skipped"] = "filtered-directory"
231
+ all_entries.append(entry_data)
232
+ continue
233
+
234
+ # Check depth limit
235
+ if depth > 0 and current_depth >= depth:
236
+ entry_data["skipped"] = "depth-limit"
237
+ all_entries.append(entry_data)
238
+ continue
239
+
240
+ # Add directory entry
241
+ all_entries.append(entry_data)
242
+
243
+ # Process children recursively
244
+ collect_entries(
245
+ entry,
246
+ current_depth + 1,
247
+ relative_path
248
+ )
249
+ else:
250
+ # Add file entry
251
+ if depth <= 0 or current_depth < depth:
252
+ all_entries.append({
253
+ "path": relative_path,
254
+ "type": "file",
255
+ "depth": current_depth,
256
+ })
257
+
258
+ except Exception as e:
259
+ await tool_ctx.warning(f"Error processing {current_path}: {str(e)}")
260
+
261
+ # Collect all entries
262
+ await tool_ctx.info("Collecting directory entries...")
263
+ collect_entries(dir_path)
264
+
265
+ # Paginate the results
266
+ paginated = paginate_list(all_entries, cursor, page_size)
267
+
268
+ # Format the paginated entries for display
269
+ formatted_entries = []
270
+ for entry in paginated.items:
271
+ indent = " " * entry["depth"]
272
+ if entry["type"] == "directory":
273
+ if "skipped" in entry:
274
+ formatted_entries.append({
275
+ "entry": f"{indent}{entry['path'].split('/')[-1]}/ [skipped - {entry['skipped']}]",
276
+ "type": "directory",
277
+ "skipped": entry.get("skipped")
278
+ })
279
+ else:
280
+ formatted_entries.append({
281
+ "entry": f"{indent}{entry['path'].split('/')[-1]}/",
282
+ "type": "directory"
283
+ })
284
+ else:
285
+ formatted_entries.append({
286
+ "entry": f"{indent}{entry['path'].split('/')[-1]}",
287
+ "type": "file"
288
+ })
289
+
290
+ # Build response
291
+ response = {
292
+ "entries": formatted_entries,
293
+ "total_collected": len(all_entries),
294
+ "page_size": page_size,
295
+ "current_page_count": len(formatted_entries)
296
+ }
297
+
298
+ # Add next cursor if available
299
+ if paginated.next_cursor:
300
+ response["nextCursor"] = paginated.next_cursor
301
+
302
+ await tool_ctx.info(
303
+ f"Returning page with {len(formatted_entries)} entries"
304
+ f"{' (more available)' if paginated.next_cursor else ' (end of results)'}"
305
+ )
306
+
307
+ return response
308
+
309
+ except Exception as e:
310
+ await tool_ctx.error(f"Error generating directory tree: {str(e)}")
311
+ return {"error": f"Error generating directory tree: {str(e)}"}
312
+
313
+ @override
314
+ def register(self, mcp_server: FastMCP) -> None:
315
+ """Register this paginated directory tree tool with the MCP server."""
316
+ tool_self = self
317
+
318
+ @mcp_server.tool(name=self.name, description=self.description)
319
+ async def directory_tree_paginated(
320
+ path: DirectoryPath,
321
+ depth: Depth = 3,
322
+ include_filtered: IncludeFiltered = False,
323
+ page_size: PageSize = 100,
324
+ cursor: Cursor = None,
325
+ ctx: MCPContext = None,
326
+ ) -> Dict[str, Any]:
327
+ return await tool_self.call(
328
+ ctx,
329
+ path=path,
330
+ depth=depth,
331
+ include_filtered=include_filtered,
332
+ page_size=page_size,
333
+ cursor=cursor,
334
+ )
335
+
336
+
337
+ # Create the tool instance
338
+ directory_tree_paginated_tool = DirectoryTreePaginatedTool()
@@ -0,0 +1,235 @@
1
+ """Rules tool implementation.
2
+
3
+ This module provides the RulesTool for reading local preferences from .cursor rules
4
+ or .claude code configuration files.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Annotated, TypedDict, Unpack, final, override, Optional
11
+
12
+ from mcp.server.fastmcp import Context as MCPContext
13
+ from mcp.server import FastMCP
14
+ from pydantic import Field
15
+
16
+ from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
17
+
18
+
19
+ SearchPath = Annotated[
20
+ str,
21
+ Field(
22
+ description="Directory path to search for configuration files (defaults to current directory)",
23
+ default=".",
24
+ ),
25
+ ]
26
+
27
+
28
+ class RulesToolParams(TypedDict, total=False):
29
+ """Parameters for the RulesTool.
30
+
31
+ Attributes:
32
+ path: Directory path to search for configuration files
33
+ """
34
+
35
+ path: str
36
+
37
+
38
+ @final
39
+ class RulesTool(FilesystemBaseTool):
40
+ """Tool for reading local preferences from configuration files."""
41
+
42
+ @property
43
+ @override
44
+ def name(self) -> str:
45
+ """Get the tool name.
46
+
47
+ Returns:
48
+ Tool name
49
+ """
50
+ return "rules"
51
+
52
+ @property
53
+ @override
54
+ def description(self) -> str:
55
+ """Get the tool description.
56
+
57
+ Returns:
58
+ Tool description
59
+ """
60
+ return """Read local preferences and rules from .cursor/rules or .claude/code configuration files.
61
+
62
+ This tool searches for and reads configuration files that contain project-specific
63
+ preferences, coding standards, and rules for AI assistants.
64
+
65
+ Searches for (in order of priority):
66
+ 1. .cursorrules in current directory
67
+ 2. .cursor/rules in current directory
68
+ 3. .claude/code.md in current directory
69
+ 4. .claude/rules.md in current directory
70
+ 5. Recursively searches parent directories up to project root
71
+
72
+ Usage:
73
+ rules # Search from current directory
74
+ rules --path /project # Search from specific directory
75
+
76
+ The tool returns the contents of all found configuration files to help
77
+ understand project-specific requirements and preferences."""
78
+
79
+ @override
80
+ async def call(
81
+ self,
82
+ ctx: MCPContext,
83
+ **params: Unpack[RulesToolParams],
84
+ ) -> str:
85
+ """Execute the tool with the given parameters.
86
+
87
+ Args:
88
+ ctx: MCP context
89
+ **params: Tool parameters
90
+
91
+ Returns:
92
+ Tool result
93
+ """
94
+ tool_ctx = self.create_tool_context(ctx)
95
+ self.set_tool_context_info(tool_ctx)
96
+
97
+ # Extract parameters
98
+ search_path = params.get("path", ".")
99
+
100
+ # Validate path
101
+ path_validation = self.validate_path(search_path)
102
+ if not path_validation.is_valid:
103
+ await tool_ctx.error(f"Invalid path: {path_validation.error_message}")
104
+ return f"Error: Invalid path: {path_validation.error_message}"
105
+
106
+ # Check permissions
107
+ is_allowed, error_message = await self.check_path_allowed(search_path, tool_ctx)
108
+ if not is_allowed:
109
+ return error_message
110
+
111
+ # Check existence
112
+ is_exists, error_message = await self.check_path_exists(search_path, tool_ctx)
113
+ if not is_exists:
114
+ return error_message
115
+
116
+ # Convert to Path object
117
+ start_path = Path(search_path).resolve()
118
+
119
+ # Configuration files to search for
120
+ config_files = [
121
+ ".cursorrules",
122
+ ".cursor/rules",
123
+ ".cursor/rules.md",
124
+ ".claude/code.md",
125
+ ".claude/rules.md",
126
+ ".claude/config.md",
127
+ ]
128
+
129
+ found_configs = []
130
+
131
+ # Search in current directory and parent directories
132
+ current_path = start_path
133
+ while True:
134
+ for config_file in config_files:
135
+ config_path = current_path / config_file
136
+
137
+ # Check if file exists and we have permission
138
+ if config_path.exists() and config_path.is_file():
139
+ try:
140
+ # Check permissions for this specific file
141
+ if self.is_path_allowed(str(config_path)):
142
+ with open(config_path, "r", encoding="utf-8") as f:
143
+ content = f.read()
144
+
145
+ found_configs.append({
146
+ "path": str(config_path),
147
+ "relative_path": str(config_path.relative_to(start_path)),
148
+ "content": content,
149
+ "size": len(content)
150
+ })
151
+
152
+ await tool_ctx.info(f"Found configuration: {config_path}")
153
+ except Exception as e:
154
+ await tool_ctx.warning(f"Could not read {config_path}: {str(e)}")
155
+
156
+ # Check if we've reached the root or a git repository root
157
+ if current_path.parent == current_path:
158
+ break
159
+
160
+ # Check if this is a git repository root
161
+ if (current_path / ".git").exists():
162
+ # Search one more time in the git root before stopping
163
+ if current_path != start_path:
164
+ for config_file in config_files:
165
+ config_path = current_path / config_file
166
+ if config_path.exists() and config_path.is_file() and str(config_path) not in [c["path"] for c in found_configs]:
167
+ try:
168
+ if self.is_path_allowed(str(config_path)):
169
+ with open(config_path, "r", encoding="utf-8") as f:
170
+ content = f.read()
171
+
172
+ found_configs.append({
173
+ "path": str(config_path),
174
+ "relative_path": str(config_path.relative_to(start_path)),
175
+ "content": content,
176
+ "size": len(content)
177
+ })
178
+
179
+ await tool_ctx.info(f"Found configuration: {config_path}")
180
+ except Exception as e:
181
+ await tool_ctx.warning(f"Could not read {config_path}: {str(e)}")
182
+ break
183
+
184
+ # Move to parent directory
185
+ parent = current_path.parent
186
+
187
+ # Check if parent is still within allowed paths
188
+ if not self.is_path_allowed(str(parent)):
189
+ await tool_ctx.info(f"Stopped at directory boundary: {parent}")
190
+ break
191
+
192
+ current_path = parent
193
+
194
+ # Format results
195
+ if not found_configs:
196
+ return f"""No configuration files found.
197
+
198
+ Searched for:
199
+ {chr(10).join('- ' + cf for cf in config_files)}
200
+
201
+ Starting from: {start_path}
202
+
203
+ To create project rules, create one of these files with your preferences:
204
+ - .cursorrules: For Cursor IDE rules
205
+ - .cursor/rules: Alternative Cursor location
206
+ - .claude/code.md: For Claude-specific coding preferences
207
+ - .claude/rules.md: For general Claude interaction rules"""
208
+
209
+ # Build output
210
+ output = [f"=== Found {len(found_configs)} Configuration File(s) ===\n"]
211
+
212
+ for i, config in enumerate(found_configs, 1):
213
+ output.append(f"--- [{i}] {config['path']} ({config['size']} bytes) ---")
214
+ output.append(config['content'])
215
+ output.append("") # Empty line between configs
216
+
217
+ output.append(f"\nSearched from: {start_path}")
218
+
219
+ return "\n".join(output)
220
+
221
+ @override
222
+ def register(self, mcp_server: FastMCP) -> None:
223
+ """Register this rules tool with the MCP server.
224
+
225
+ Args:
226
+ mcp_server: The FastMCP server instance
227
+ """
228
+ tool_self = self # Create a reference to self for use in the closure
229
+
230
+ @mcp_server.tool(name=self.name, description=self.description)
231
+ async def rules(
232
+ path: SearchPath = ".",
233
+ ctx: MCPContext = None,
234
+ ) -> str:
235
+ return await tool_self.call(ctx, path=path)
@@ -1,4 +1,4 @@
1
- """Unified search tool that runs multiple search types in parallel.
1
+ """Search tool that runs multiple search types in parallel.
2
2
 
3
3
  This tool consolidates all search capabilities and runs them concurrently:
4
4
  - grep: Fast pattern/regex search using ripgrep
@@ -41,7 +41,7 @@ class SearchType(Enum):
41
41
 
42
42
  @dataclass
43
43
  class SearchResult:
44
- """Unified search result from any search type."""
44
+ """Search result from any search type."""
45
45
  file_path: str
46
46
  line_number: Optional[int]
47
47
  content: str
@@ -133,7 +133,7 @@ IncludeContext = Annotated[
133
133
 
134
134
 
135
135
  class UnifiedSearchParams(TypedDict):
136
- """Parameters for unified search."""
136
+ """Parameters for search."""
137
137
  pattern: Pattern
138
138
  path: SearchPath
139
139
  include: Include
@@ -147,12 +147,12 @@ class UnifiedSearchParams(TypedDict):
147
147
 
148
148
 
149
149
  @final
150
- class UnifiedSearchTool(FilesystemBaseTool):
151
- """Unified search tool that runs multiple search types in parallel."""
150
+ class SearchTool(FilesystemBaseTool):
151
+ """Search tool that runs multiple search types in parallel."""
152
152
 
153
153
  def __init__(self, permission_manager: PermissionManager,
154
154
  project_manager: Optional[ProjectVectorManager] = None):
155
- """Initialize the unified search tool.
155
+ """Initialize the search tool.
156
156
 
157
157
  Args:
158
158
  permission_manager: Permission manager for access control
@@ -175,13 +175,13 @@ class UnifiedSearchTool(FilesystemBaseTool):
175
175
  @override
176
176
  def name(self) -> str:
177
177
  """Get the tool name."""
178
- return "unified_search"
178
+ return "search"
179
179
 
180
180
  @property
181
181
  @override
182
182
  def description(self) -> str:
183
183
  """Get the tool description."""
184
- return """Unified search that runs multiple search strategies in parallel.
184
+ return """Search that runs multiple search strategies in parallel.
185
185
 
186
186
  Automatically runs the most appropriate search types based on your pattern:
187
187
  - Pattern matching (grep) for exact text/regex
@@ -527,7 +527,7 @@ This is the recommended search tool for comprehensive results."""
527
527
  ctx: MCPContext,
528
528
  **params: Unpack[UnifiedSearchParams],
529
529
  ) -> str:
530
- """Execute unified search across all enabled search types."""
530
+ """Execute search across all enabled search types."""
531
531
  import time
532
532
  start_time = time.time()
533
533
 
@@ -559,7 +559,7 @@ This is the recommended search tool for comprehensive results."""
559
559
  # Analyze pattern to determine best search strategies
560
560
  pattern_analysis = self._analyze_pattern(pattern)
561
561
 
562
- await tool_ctx.info(f"Starting unified search for '{pattern}' in {path}")
562
+ await tool_ctx.info(f"Starting search for '{pattern}' in {path}")
563
563
 
564
564
  # Build list of search tasks based on enabled types and pattern analysis
565
565
  search_tasks = []
@@ -679,11 +679,11 @@ This is the recommended search tool for comprehensive results."""
679
679
 
680
680
  @override
681
681
  def register(self, mcp_server: FastMCP) -> None:
682
- """Register the unified search tool with the MCP server."""
682
+ """Register the search tool with the MCP server."""
683
683
  tool_self = self
684
684
 
685
685
  @mcp_server.tool(name=self.name, description=self.description)
686
- async def unified_search(
686
+ async def search(
687
687
  ctx: MCPContext,
688
688
  pattern: Pattern,
689
689
  path: SearchPath = ".",