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
@@ -3,23 +3,18 @@
3
3
  import os
4
4
  import time
5
5
  import fnmatch
6
- import re
7
- from typing import List, Optional, Dict, Any, Set
8
- from pathlib import Path
9
- from dataclasses import dataclass
10
6
  import subprocess
11
- import json
7
+ from typing import Any, Set, Dict, List, Optional
8
+ from pathlib import Path
12
9
  from datetime import datetime
13
- from difflib import SequenceMatcher
10
+ from dataclasses import dataclass
14
11
 
15
- from hanzo_mcp.tools.common.base import BaseTool
16
- from hanzo_mcp.tools.common.paginated_response import AutoPaginatedResponse
17
- from hanzo_mcp.tools.common.decorators import with_context_normalization
18
12
  from hanzo_mcp.types import MCPResourceDocument
13
+ from hanzo_mcp.tools.common.base import BaseTool
19
14
 
20
15
  # Check if ffind command is available
21
16
  try:
22
- subprocess.run(['ffind', '--version'], capture_output=True, check=True)
17
+ subprocess.run(["ffind", "--version"], capture_output=True, check=True)
23
18
  FFIND_AVAILABLE = True
24
19
  except (subprocess.CalledProcessError, FileNotFoundError):
25
20
  FFIND_AVAILABLE = False
@@ -28,6 +23,7 @@ except (subprocess.CalledProcessError, FileNotFoundError):
28
23
  @dataclass
29
24
  class FileMatch:
30
25
  """Represents a found file."""
26
+
31
27
  path: str
32
28
  name: str
33
29
  size: int
@@ -35,7 +31,7 @@ class FileMatch:
35
31
  is_dir: bool
36
32
  extension: str
37
33
  depth: int
38
-
34
+
39
35
  def to_dict(self) -> Dict[str, Any]:
40
36
  return {
41
37
  "path": self.path,
@@ -44,17 +40,17 @@ class FileMatch:
44
40
  "modified": datetime.fromtimestamp(self.modified).isoformat(),
45
41
  "is_dir": self.is_dir,
46
42
  "extension": self.extension,
47
- "depth": self.depth
43
+ "depth": self.depth,
48
44
  }
49
45
 
50
46
 
51
47
  class FindTool(BaseTool):
52
48
  """Fast file and directory finding tool.
53
-
49
+
54
50
  This tool is optimized for quickly finding files and directories by name,
55
51
  pattern, or attributes. It uses ffind (when available) for blazing fast
56
52
  performance and falls back to optimized Python implementation.
57
-
53
+
58
54
  Key features:
59
55
  - Lightning fast file discovery
60
56
  - Smart pattern matching (glob, regex, fuzzy)
@@ -63,7 +59,7 @@ class FindTool(BaseTool):
63
59
  - Built-in caching for repeated searches
64
60
  - Respects .gitignore by default
65
61
  """
66
-
62
+
67
63
  name = "find"
68
64
  description = """Find files and directories by name, pattern, or attributes.
69
65
 
@@ -78,138 +74,162 @@ class FindTool(BaseTool):
78
74
  This is the primary tool for discovering files in a project. Use it before
79
75
  reading or searching within files.
80
76
  """
81
-
77
+
82
78
  def __init__(self):
83
79
  super().__init__()
84
80
  self._cache = {}
85
81
  self._gitignore_cache = {}
86
-
82
+
87
83
  def _parse_size(self, size_str: str) -> int:
88
84
  """Parse human-readable size to bytes."""
89
85
  # Order matters - check longer units first
90
86
  units = [
91
- ('TB', 1024**4), ('GB', 1024**3), ('MB', 1024**2), ('KB', 1024),
92
- ('T', 1024**4), ('G', 1024**3), ('M', 1024**2), ('K', 1024), ('B', 1)
87
+ ("TB", 1024**4),
88
+ ("GB", 1024**3),
89
+ ("MB", 1024**2),
90
+ ("KB", 1024),
91
+ ("T", 1024**4),
92
+ ("G", 1024**3),
93
+ ("M", 1024**2),
94
+ ("K", 1024),
95
+ ("B", 1),
93
96
  ]
94
-
97
+
95
98
  size_str = size_str.upper().strip()
96
99
  for unit, multiplier in units:
97
100
  if size_str.endswith(unit):
98
- num_str = size_str[:-len(unit)].strip()
101
+ num_str = size_str[: -len(unit)].strip()
99
102
  if num_str:
100
103
  try:
101
104
  return int(float(num_str) * multiplier)
102
105
  except ValueError:
103
106
  return 0
104
-
107
+
105
108
  try:
106
109
  return int(size_str)
107
110
  except ValueError:
108
111
  return 0
109
-
112
+
110
113
  def _parse_time(self, time_str: str) -> float:
111
114
  """Parse human-readable time to timestamp."""
112
115
  import re
113
116
  from datetime import datetime, timedelta
114
-
117
+
115
118
  # Handle relative times like "1 day ago", "2 hours ago"
116
- match = re.match(r'(\d+)\s*(second|minute|hour|day|week|month|year)s?\s*ago', time_str.lower())
119
+ match = re.match(
120
+ r"(\d+)\s*(second|minute|hour|day|week|month|year)s?\s*ago",
121
+ time_str.lower(),
122
+ )
117
123
  if match:
118
124
  amount = int(match.group(1))
119
125
  unit = match.group(2)
120
-
121
- if unit == 'second':
126
+
127
+ if unit == "second":
122
128
  delta = timedelta(seconds=amount)
123
- elif unit == 'minute':
129
+ elif unit == "minute":
124
130
  delta = timedelta(minutes=amount)
125
- elif unit == 'hour':
131
+ elif unit == "hour":
126
132
  delta = timedelta(hours=amount)
127
- elif unit == 'day':
133
+ elif unit == "day":
128
134
  delta = timedelta(days=amount)
129
- elif unit == 'week':
135
+ elif unit == "week":
130
136
  delta = timedelta(weeks=amount)
131
- elif unit == 'month':
137
+ elif unit == "month":
132
138
  delta = timedelta(days=amount * 30) # Approximate
133
- elif unit == 'year':
139
+ elif unit == "year":
134
140
  delta = timedelta(days=amount * 365) # Approximate
135
-
141
+
136
142
  return (datetime.now() - delta).timestamp()
137
-
143
+
138
144
  # Try parsing as date
139
145
  try:
140
146
  return datetime.fromisoformat(time_str).timestamp()
141
- except:
147
+ except Exception:
142
148
  return datetime.now().timestamp()
143
-
149
+
144
150
  def _load_gitignore(self, root: str) -> Set[str]:
145
151
  """Load and parse .gitignore patterns."""
146
152
  if root in self._gitignore_cache:
147
153
  return self._gitignore_cache[root]
148
-
154
+
149
155
  patterns = set()
150
- gitignore_path = Path(root) / '.gitignore'
151
-
156
+ gitignore_path = Path(root) / ".gitignore"
157
+
152
158
  if gitignore_path.exists():
153
159
  try:
154
- with open(gitignore_path, 'r') as f:
160
+ with open(gitignore_path, "r") as f:
155
161
  for line in f:
156
162
  line = line.strip()
157
- if line and not line.startswith('#'):
163
+ if line and not line.startswith("#"):
158
164
  patterns.add(line)
159
- except:
165
+ except Exception:
160
166
  pass
161
-
167
+
162
168
  # Add common ignore patterns
163
- patterns.update([
164
- '*.pyc', '__pycache__', '.git', '.svn', '.hg',
165
- 'node_modules', '.env', '.venv', 'venv',
166
- '*.swp', '*.swo', '.DS_Store', 'Thumbs.db'
167
- ])
168
-
169
+ patterns.update(
170
+ [
171
+ "*.pyc",
172
+ "__pycache__",
173
+ ".git",
174
+ ".svn",
175
+ ".hg",
176
+ "node_modules",
177
+ ".env",
178
+ ".venv",
179
+ "venv",
180
+ "*.swp",
181
+ "*.swo",
182
+ ".DS_Store",
183
+ "Thumbs.db",
184
+ ]
185
+ )
186
+
169
187
  self._gitignore_cache[root] = patterns
170
188
  return patterns
171
-
189
+
172
190
  def _should_ignore(self, path: str, ignore_patterns: Set[str]) -> bool:
173
191
  """Check if path should be ignored."""
174
192
  path_obj = Path(path)
175
-
193
+
176
194
  for pattern in ignore_patterns:
177
195
  # Check against full path and basename
178
196
  if fnmatch.fnmatch(path_obj.name, pattern):
179
197
  return True
180
198
  if fnmatch.fnmatch(str(path_obj), pattern):
181
199
  return True
182
-
200
+
183
201
  # Check if any parent directory matches
184
202
  for parent in path_obj.parents:
185
203
  if fnmatch.fnmatch(parent.name, pattern):
186
204
  return True
187
-
205
+
188
206
  return False
189
-
190
- async def run(self,
191
- pattern: str = "*",
192
- path: str = ".",
193
- type: Optional[str] = None, # "file", "dir", "any"
194
- min_size: Optional[str] = None,
195
- max_size: Optional[str] = None,
196
- modified_after: Optional[str] = None,
197
- modified_before: Optional[str] = None,
198
- max_depth: Optional[int] = None,
199
- case_sensitive: bool = False,
200
- regex: bool = False,
201
- fuzzy: bool = False,
202
- in_content: bool = False,
203
- follow_symlinks: bool = False,
204
- respect_gitignore: bool = True,
205
- max_results: int = 1000,
206
- sort_by: str = "path", # "path", "name", "size", "modified"
207
- reverse: bool = False,
208
- page_size: int = 100,
209
- page: int = 1,
210
- **kwargs) -> MCPResourceDocument:
207
+
208
+ async def run(
209
+ self,
210
+ pattern: str = "*",
211
+ path: str = ".",
212
+ type: Optional[str] = None, # "file", "dir", "any"
213
+ min_size: Optional[str] = None,
214
+ max_size: Optional[str] = None,
215
+ modified_after: Optional[str] = None,
216
+ modified_before: Optional[str] = None,
217
+ max_depth: Optional[int] = None,
218
+ case_sensitive: bool = False,
219
+ regex: bool = False,
220
+ fuzzy: bool = False,
221
+ in_content: bool = False,
222
+ follow_symlinks: bool = False,
223
+ respect_gitignore: bool = True,
224
+ max_results: int = 1000,
225
+ sort_by: str = "path", # "path", "name", "size", "modified"
226
+ reverse: bool = False,
227
+ page_size: int = 100,
228
+ page: int = 1,
229
+ **kwargs,
230
+ ) -> MCPResourceDocument:
211
231
  """Find files and directories.
212
-
232
+
213
233
  Args:
214
234
  pattern: Search pattern (glob by default, regex if regex=True)
215
235
  path: Root directory to search from
@@ -231,44 +251,62 @@ class FindTool(BaseTool):
231
251
  page_size: Results per page
232
252
  page: Page number
233
253
  """
234
-
254
+
235
255
  start_time = time.time()
236
-
256
+
237
257
  # Resolve path
238
258
  root_path = Path(path).resolve()
239
259
  if not root_path.exists():
240
- return MCPResourceDocument(data={
241
- "error": f"Path does not exist: {path}",
242
- "results": []
243
- })
244
-
260
+ return MCPResourceDocument(
261
+ data={"error": f"Path does not exist: {path}", "results": []}
262
+ )
263
+
245
264
  # Get ignore patterns
246
265
  ignore_patterns = set()
247
266
  if respect_gitignore:
248
267
  ignore_patterns = self._load_gitignore(str(root_path))
249
-
268
+
250
269
  # Parse filters
251
270
  min_size_bytes = self._parse_size(min_size) if min_size else None
252
271
  max_size_bytes = self._parse_size(max_size) if max_size else None
253
272
  modified_after_ts = self._parse_time(modified_after) if modified_after else None
254
- modified_before_ts = self._parse_time(modified_before) if modified_before else None
255
-
273
+ modified_before_ts = (
274
+ self._parse_time(modified_before) if modified_before else None
275
+ )
276
+
256
277
  # Collect matches
257
278
  matches = []
258
-
279
+
259
280
  if FFIND_AVAILABLE and not in_content:
260
281
  # Use ffind for fast file discovery
261
282
  matches = await self._find_with_ffind(
262
- pattern, root_path, type, case_sensitive, regex, fuzzy,
263
- max_depth, follow_symlinks, respect_gitignore, ignore_patterns
283
+ pattern,
284
+ root_path,
285
+ type,
286
+ case_sensitive,
287
+ regex,
288
+ fuzzy,
289
+ max_depth,
290
+ follow_symlinks,
291
+ respect_gitignore,
292
+ ignore_patterns,
264
293
  )
265
294
  else:
266
295
  # Fall back to Python implementation
267
296
  matches = await self._find_with_python(
268
- pattern, root_path, type, case_sensitive, regex, fuzzy,
269
- in_content, max_depth, follow_symlinks, respect_gitignore, ignore_patterns
297
+ pattern,
298
+ root_path,
299
+ type,
300
+ case_sensitive,
301
+ regex,
302
+ fuzzy,
303
+ in_content,
304
+ max_depth,
305
+ follow_symlinks,
306
+ respect_gitignore,
307
+ ignore_patterns,
270
308
  )
271
-
309
+
272
310
  # Apply filters
273
311
  filtered_matches = []
274
312
  for match in matches:
@@ -277,18 +315,18 @@ class FindTool(BaseTool):
277
315
  continue
278
316
  if max_size_bytes and match.size > max_size_bytes:
279
317
  continue
280
-
318
+
281
319
  # Time filters
282
320
  if modified_after_ts and match.modified < modified_after_ts:
283
321
  continue
284
322
  if modified_before_ts and match.modified > modified_before_ts:
285
323
  continue
286
-
324
+
287
325
  filtered_matches.append(match)
288
-
326
+
289
327
  if len(filtered_matches) >= max_results:
290
328
  break
291
-
329
+
292
330
  # Sort results
293
331
  if sort_by == "name":
294
332
  filtered_matches.sort(key=lambda m: m.name, reverse=reverse)
@@ -298,54 +336,63 @@ class FindTool(BaseTool):
298
336
  filtered_matches.sort(key=lambda m: m.modified, reverse=reverse)
299
337
  else: # path
300
338
  filtered_matches.sort(key=lambda m: m.path, reverse=reverse)
301
-
339
+
302
340
  # Paginate
303
341
  total_results = len(filtered_matches)
304
342
  start_idx = (page - 1) * page_size
305
343
  end_idx = start_idx + page_size
306
344
  page_results = filtered_matches[start_idx:end_idx]
307
-
345
+
308
346
  # Format results
309
347
  formatted_results = [match.to_dict() for match in page_results]
310
-
348
+
311
349
  # Statistics
312
350
  stats = {
313
351
  "total_found": total_results,
314
352
  "search_time_ms": int((time.time() - start_time) * 1000),
315
- "search_method": "ffind" if FFIND_AVAILABLE and not in_content else "python",
353
+ "search_method": (
354
+ "ffind" if FFIND_AVAILABLE and not in_content else "python"
355
+ ),
316
356
  "root_path": str(root_path),
317
357
  "filters_applied": {
318
358
  "pattern": pattern,
319
359
  "type": type,
320
- "size": {"min": min_size, "max": max_size} if min_size or max_size else None,
321
- "modified": {"after": modified_after, "before": modified_before} if modified_after or modified_before else None,
360
+ "size": (
361
+ {"min": min_size, "max": max_size} if min_size or max_size else None
362
+ ),
363
+ "modified": (
364
+ {"after": modified_after, "before": modified_before}
365
+ if modified_after or modified_before
366
+ else None
367
+ ),
322
368
  "max_depth": max_depth,
323
- "gitignore": respect_gitignore
324
- }
325
- }
326
-
327
- return MCPResourceDocument(data={
328
- "results": formatted_results,
329
- "pagination": {
330
- "page": page,
331
- "page_size": page_size,
332
- "total_results": total_results,
333
- "total_pages": (total_results + page_size - 1) // page_size,
334
- "has_next": end_idx < total_results,
335
- "has_prev": page > 1
369
+ "gitignore": respect_gitignore,
336
370
  },
337
- "statistics": stats
338
- })
339
-
371
+ }
372
+
373
+ return MCPResourceDocument(
374
+ data={
375
+ "results": formatted_results,
376
+ "pagination": {
377
+ "page": page,
378
+ "page_size": page_size,
379
+ "total_results": total_results,
380
+ "total_pages": (total_results + page_size - 1) // page_size,
381
+ "has_next": end_idx < total_results,
382
+ "has_prev": page > 1,
383
+ },
384
+ "statistics": stats,
385
+ }
386
+ )
387
+
340
388
  async def call(self, **kwargs) -> str:
341
389
  """Tool interface for MCP - converts result to JSON string."""
342
390
  result = await self.run(**kwargs)
343
391
  return result.to_json_string()
344
-
392
+
345
393
  def register(self, mcp_server) -> None:
346
394
  """Register tool with MCP server."""
347
- from mcp.server import FastMCP
348
-
395
+
349
396
  @mcp_server.tool(name=self.name, description=self.description)
350
397
  async def find_handler(
351
398
  pattern: str,
@@ -388,91 +435,102 @@ class FindTool(BaseTool):
388
435
  page_size=page_size,
389
436
  page=page,
390
437
  )
391
-
392
- async def _find_with_ffind(self,
393
- pattern: str,
394
- root: Path,
395
- file_type: Optional[str],
396
- case_sensitive: bool,
397
- regex: bool,
398
- fuzzy: bool,
399
- max_depth: Optional[int],
400
- follow_symlinks: bool,
401
- respect_gitignore: bool,
402
- ignore_patterns: Set[str]) -> List[FileMatch]:
438
+
439
+ async def _find_with_ffind(
440
+ self,
441
+ pattern: str,
442
+ root: Path,
443
+ file_type: Optional[str],
444
+ case_sensitive: bool,
445
+ regex: bool,
446
+ fuzzy: bool,
447
+ max_depth: Optional[int],
448
+ follow_symlinks: bool,
449
+ respect_gitignore: bool,
450
+ ignore_patterns: Set[str],
451
+ ) -> List[FileMatch]:
403
452
  """Use ffind for fast file discovery."""
404
453
  matches = []
405
-
454
+
406
455
  # Configure ffind
407
456
  ffind_args = {
408
- 'path': str(root),
409
- 'pattern': pattern,
410
- 'regex': regex,
411
- 'case_sensitive': case_sensitive,
412
- 'follow_symlinks': follow_symlinks,
457
+ "path": str(root),
458
+ "pattern": pattern,
459
+ "regex": regex,
460
+ "case_sensitive": case_sensitive,
461
+ "follow_symlinks": follow_symlinks,
413
462
  }
414
-
463
+
415
464
  if fuzzy:
416
- ffind_args['fuzzy'] = True
417
-
465
+ ffind_args["fuzzy"] = True
466
+
418
467
  if max_depth:
419
- ffind_args['max_depth'] = max_depth
420
-
468
+ ffind_args["max_depth"] = max_depth
469
+
421
470
  try:
422
471
  # Build ffind command
423
- cmd = ['ffind']
424
-
472
+ cmd = ["ffind"]
473
+
425
474
  if not case_sensitive:
426
- cmd.append('-i')
427
-
475
+ cmd.append("-i")
476
+
428
477
  if regex:
429
- cmd.append('-E')
430
-
478
+ cmd.append("-E")
479
+
431
480
  if fuzzy:
432
- cmd.append('-f')
433
-
481
+ cmd.append("-f")
482
+
434
483
  if follow_symlinks:
435
- cmd.append('-L')
436
-
484
+ cmd.append("-L")
485
+
437
486
  if max_depth:
438
- cmd.extend(['-D', str(max_depth)])
439
-
440
- # Add pattern and path
441
- cmd.append(pattern)
487
+ cmd.extend(["-D", str(max_depth)])
488
+
489
+ # Add path and pattern (ffind expects directory first)
442
490
  cmd.append(str(root))
443
-
491
+ cmd.append(pattern)
492
+
444
493
  # Run ffind command
445
494
  result = subprocess.run(cmd, capture_output=True, text=True, check=False)
446
-
495
+
447
496
  if result.returncode != 0:
448
497
  # Fall back to Python implementation on error
449
498
  return await self._find_with_python(
450
- pattern, root, file_type, case_sensitive, regex, fuzzy,
451
- False, max_depth, follow_symlinks, respect_gitignore, ignore_patterns
499
+ pattern,
500
+ root,
501
+ file_type,
502
+ case_sensitive,
503
+ regex,
504
+ fuzzy,
505
+ False,
506
+ max_depth,
507
+ follow_symlinks,
508
+ respect_gitignore,
509
+ ignore_patterns,
452
510
  )
453
-
511
+
454
512
  # Parse results
455
- results = result.stdout.strip().split('\n') if result.stdout else []
456
-
513
+ results = result.stdout.strip().split("\n") if result.stdout else []
514
+
457
515
  for path in results:
458
516
  if not path: # Skip empty lines
459
517
  continue
460
-
518
+
461
519
  # Check ignore patterns
462
520
  if self._should_ignore(path, ignore_patterns):
463
521
  continue
464
-
522
+
465
523
  # Get file info
466
524
  try:
467
525
  stat = os.stat(path)
468
526
  is_dir = os.path.isdir(path)
469
-
527
+
470
528
  # Apply type filter
471
529
  if file_type == "file" and is_dir:
472
530
  continue
473
531
  if file_type == "dir" and not is_dir:
474
532
  continue
475
-
533
+
476
534
  match = FileMatch(
477
535
  path=path,
478
536
  name=os.path.basename(path),
@@ -480,40 +538,51 @@ class FindTool(BaseTool):
480
538
  modified=stat.st_mtime,
481
539
  is_dir=is_dir,
482
540
  extension=Path(path).suffix,
483
- depth=len(Path(path).relative_to(root).parts)
541
+ depth=len(Path(path).relative_to(root).parts),
484
542
  )
485
543
  matches.append(match)
486
-
544
+
487
545
  except OSError:
488
546
  continue
489
-
490
- except Exception as e:
547
+
548
+ except Exception:
491
549
  # Fall back to Python implementation
492
550
  return await self._find_with_python(
493
- pattern, root, file_type, case_sensitive, regex, False,
494
- False, max_depth, follow_symlinks, respect_gitignore, ignore_patterns
551
+ pattern,
552
+ root,
553
+ file_type,
554
+ case_sensitive,
555
+ regex,
556
+ False,
557
+ False,
558
+ max_depth,
559
+ follow_symlinks,
560
+ respect_gitignore,
561
+ ignore_patterns,
495
562
  )
496
-
563
+
497
564
  return matches
498
-
499
- async def _find_with_python(self,
500
- pattern: str,
501
- root: Path,
502
- file_type: Optional[str],
503
- case_sensitive: bool,
504
- regex: bool,
505
- fuzzy: bool,
506
- in_content: bool,
507
- max_depth: Optional[int],
508
- follow_symlinks: bool,
509
- respect_gitignore: bool,
510
- ignore_patterns: Set[str]) -> List[FileMatch]:
565
+
566
+ async def _find_with_python(
567
+ self,
568
+ pattern: str,
569
+ root: Path,
570
+ file_type: Optional[str],
571
+ case_sensitive: bool,
572
+ regex: bool,
573
+ fuzzy: bool,
574
+ in_content: bool,
575
+ max_depth: Optional[int],
576
+ follow_symlinks: bool,
577
+ respect_gitignore: bool,
578
+ ignore_patterns: Set[str],
579
+ ) -> List[FileMatch]:
511
580
  """Python implementation of file finding."""
512
581
  matches = []
513
-
582
+
514
583
  import re
515
584
  from difflib import SequenceMatcher
516
-
585
+
517
586
  # Prepare pattern matcher
518
587
  if regex:
519
588
  flags = 0 if case_sensitive else re.IGNORECASE
@@ -524,8 +593,12 @@ class FindTool(BaseTool):
524
593
  matcher = lambda name: pattern in name
525
594
  elif fuzzy:
526
595
  pattern_lower = pattern.lower() if not case_sensitive else pattern
527
- matcher = lambda name: SequenceMatcher(None, pattern_lower,
528
- name.lower() if not case_sensitive else name).ratio() > 0.6
596
+ matcher = (
597
+ lambda name: SequenceMatcher(
598
+ None, pattern_lower, name.lower() if not case_sensitive else name
599
+ ).ratio()
600
+ > 0.6
601
+ )
529
602
  else:
530
603
  # Glob pattern
531
604
  if not case_sensitive:
@@ -533,20 +606,28 @@ class FindTool(BaseTool):
533
606
  matcher = lambda name: fnmatch.fnmatch(name.lower(), pattern)
534
607
  else:
535
608
  matcher = lambda name: fnmatch.fnmatch(name, pattern)
536
-
609
+
537
610
  # Walk directory tree
538
- for dirpath, dirnames, filenames in os.walk(str(root), followlinks=follow_symlinks):
611
+ for dirpath, dirnames, filenames in os.walk(
612
+ str(root), followlinks=follow_symlinks
613
+ ):
539
614
  # Check depth
540
615
  if max_depth is not None:
541
616
  depth = len(Path(dirpath).relative_to(root).parts)
542
617
  if depth > max_depth:
543
618
  dirnames.clear() # Don't recurse deeper
544
619
  continue
545
-
620
+
546
621
  # Filter directories to skip
547
622
  if respect_gitignore:
548
- dirnames[:] = [d for d in dirnames if not self._should_ignore(os.path.join(dirpath, d), ignore_patterns)]
549
-
623
+ dirnames[:] = [
624
+ d
625
+ for d in dirnames
626
+ if not self._should_ignore(
627
+ os.path.join(dirpath, d), ignore_patterns
628
+ )
629
+ ]
630
+
550
631
  # Check directories
551
632
  if file_type != "file":
552
633
  for dirname in dirnames:
@@ -562,29 +643,31 @@ class FindTool(BaseTool):
562
643
  modified=stat.st_mtime,
563
644
  is_dir=True,
564
645
  extension="",
565
- depth=len(Path(full_path).relative_to(root).parts)
646
+ depth=len(Path(full_path).relative_to(root).parts),
566
647
  )
567
648
  matches.append(match)
568
649
  except OSError:
569
650
  continue
570
-
651
+
571
652
  # Check files
572
653
  if file_type != "dir":
573
654
  for filename in filenames:
574
655
  full_path = os.path.join(dirpath, filename)
575
-
656
+
576
657
  if self._should_ignore(full_path, ignore_patterns):
577
658
  continue
578
-
659
+
579
660
  # Match against filename
580
661
  if matcher(filename):
581
662
  match_found = True
582
663
  elif in_content:
583
664
  # Search in file content
584
- match_found = await self._search_in_file(full_path, pattern, case_sensitive)
665
+ match_found = await self._search_in_file(
666
+ full_path, pattern, case_sensitive
667
+ )
585
668
  else:
586
669
  match_found = False
587
-
670
+
588
671
  if match_found:
589
672
  try:
590
673
  stat = os.stat(full_path)
@@ -595,28 +678,30 @@ class FindTool(BaseTool):
595
678
  modified=stat.st_mtime,
596
679
  is_dir=False,
597
680
  extension=Path(filename).suffix,
598
- depth=len(Path(full_path).relative_to(root).parts)
681
+ depth=len(Path(full_path).relative_to(root).parts),
599
682
  )
600
683
  matches.append(match)
601
684
  except OSError:
602
685
  continue
603
-
686
+
604
687
  return matches
605
-
606
- async def _search_in_file(self, file_path: str, pattern: str, case_sensitive: bool) -> bool:
688
+
689
+ async def _search_in_file(
690
+ self, file_path: str, pattern: str, case_sensitive: bool
691
+ ) -> bool:
607
692
  """Search for pattern in file content."""
608
693
  try:
609
- with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
694
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
610
695
  content = f.read()
611
696
  if not case_sensitive:
612
697
  return pattern.lower() in content.lower()
613
698
  else:
614
699
  return pattern in content
615
- except:
700
+ except Exception:
616
701
  return False
617
702
 
618
703
 
619
704
  # Tool registration
620
705
  def create_find_tool():
621
706
  """Factory function to create find tool."""
622
- return FindTool()
707
+ return FindTool()