hanzo-mcp 0.8.8__py3-none-any.whl → 0.8.13__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.
- hanzo_mcp/__init__.py +1 -3
- hanzo_mcp/analytics/posthog_analytics.py +4 -17
- hanzo_mcp/bridge.py +9 -25
- hanzo_mcp/cli.py +8 -17
- hanzo_mcp/cli_enhanced.py +5 -14
- hanzo_mcp/cli_plugin.py +3 -9
- hanzo_mcp/config/settings.py +6 -20
- hanzo_mcp/config/tool_config.py +2 -4
- hanzo_mcp/core/base_agent.py +88 -88
- hanzo_mcp/core/model_registry.py +238 -210
- hanzo_mcp/dev_server.py +5 -15
- hanzo_mcp/prompts/__init__.py +2 -6
- hanzo_mcp/prompts/project_todo_reminder.py +3 -9
- hanzo_mcp/prompts/tool_explorer.py +1 -3
- hanzo_mcp/prompts/utils.py +7 -21
- hanzo_mcp/server.py +6 -7
- hanzo_mcp/tools/__init__.py +13 -29
- hanzo_mcp/tools/agent/__init__.py +2 -1
- hanzo_mcp/tools/agent/agent.py +10 -30
- hanzo_mcp/tools/agent/agent_tool.py +6 -17
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +15 -42
- hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
- hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
- hanzo_mcp/tools/agent/cli_tools.py +76 -75
- hanzo_mcp/tools/agent/code_auth.py +1 -3
- hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
- hanzo_mcp/tools/agent/critic_tool.py +8 -24
- hanzo_mcp/tools/agent/iching_tool.py +12 -36
- hanzo_mcp/tools/agent/network_tool.py +7 -18
- hanzo_mcp/tools/agent/prompt.py +1 -5
- hanzo_mcp/tools/agent/review_tool.py +10 -25
- hanzo_mcp/tools/agent/swarm_alias.py +1 -3
- hanzo_mcp/tools/agent/swarm_tool.py +16 -41
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +11 -39
- hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
- hanzo_mcp/tools/common/batch_tool.py +15 -45
- hanzo_mcp/tools/common/config_tool.py +9 -28
- hanzo_mcp/tools/common/context.py +1 -3
- hanzo_mcp/tools/common/critic_tool.py +1 -3
- hanzo_mcp/tools/common/decorators.py +2 -6
- hanzo_mcp/tools/common/enhanced_base.py +2 -6
- hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
- hanzo_mcp/tools/common/forgiving_edit.py +9 -28
- hanzo_mcp/tools/common/mode.py +1 -5
- hanzo_mcp/tools/common/paginated_base.py +3 -11
- hanzo_mcp/tools/common/paginated_response.py +10 -30
- hanzo_mcp/tools/common/pagination.py +3 -9
- hanzo_mcp/tools/common/permissions.py +3 -9
- hanzo_mcp/tools/common/personality.py +9 -34
- hanzo_mcp/tools/common/plugin_loader.py +3 -15
- hanzo_mcp/tools/common/stats.py +7 -19
- hanzo_mcp/tools/common/thinking_tool.py +1 -3
- hanzo_mcp/tools/common/tool_disable.py +2 -6
- hanzo_mcp/tools/common/tool_list.py +2 -6
- hanzo_mcp/tools/common/validation.py +1 -3
- hanzo_mcp/tools/config/config_tool.py +7 -13
- hanzo_mcp/tools/config/index_config.py +1 -3
- hanzo_mcp/tools/config/mode_tool.py +5 -15
- hanzo_mcp/tools/database/database_manager.py +3 -9
- hanzo_mcp/tools/database/graph.py +1 -3
- hanzo_mcp/tools/database/graph_add.py +3 -9
- hanzo_mcp/tools/database/graph_query.py +11 -34
- hanzo_mcp/tools/database/graph_remove.py +3 -9
- hanzo_mcp/tools/database/graph_search.py +6 -20
- hanzo_mcp/tools/database/graph_stats.py +11 -33
- hanzo_mcp/tools/database/sql.py +4 -12
- hanzo_mcp/tools/database/sql_query.py +6 -10
- hanzo_mcp/tools/database/sql_search.py +2 -6
- hanzo_mcp/tools/database/sql_stats.py +5 -15
- hanzo_mcp/tools/editor/neovim_command.py +1 -3
- hanzo_mcp/tools/editor/neovim_session.py +7 -13
- hanzo_mcp/tools/filesystem/__init__.py +2 -3
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
- hanzo_mcp/tools/filesystem/base.py +4 -12
- hanzo_mcp/tools/filesystem/batch_search.py +35 -115
- hanzo_mcp/tools/filesystem/content_replace.py +4 -12
- hanzo_mcp/tools/filesystem/diff.py +2 -10
- hanzo_mcp/tools/filesystem/directory_tree.py +9 -27
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +5 -15
- hanzo_mcp/tools/filesystem/edit.py +6 -18
- hanzo_mcp/tools/filesystem/find.py +3 -9
- hanzo_mcp/tools/filesystem/find_files.py +2 -6
- hanzo_mcp/tools/filesystem/git_search.py +9 -24
- hanzo_mcp/tools/filesystem/grep.py +9 -27
- hanzo_mcp/tools/filesystem/multi_edit.py +6 -18
- hanzo_mcp/tools/filesystem/read.py +8 -26
- hanzo_mcp/tools/filesystem/rules_tool.py +6 -17
- hanzo_mcp/tools/filesystem/search_tool.py +18 -62
- hanzo_mcp/tools/filesystem/symbols_tool.py +5 -15
- hanzo_mcp/tools/filesystem/tree.py +1 -3
- hanzo_mcp/tools/filesystem/watch.py +1 -3
- hanzo_mcp/tools/filesystem/write.py +1 -3
- hanzo_mcp/tools/jupyter/base.py +6 -20
- hanzo_mcp/tools/jupyter/jupyter.py +4 -12
- hanzo_mcp/tools/jupyter/notebook_edit.py +11 -35
- hanzo_mcp/tools/jupyter/notebook_read.py +2 -6
- hanzo_mcp/tools/llm/consensus_tool.py +8 -24
- hanzo_mcp/tools/llm/llm_manage.py +2 -6
- hanzo_mcp/tools/llm/llm_tool.py +17 -58
- hanzo_mcp/tools/llm/llm_unified.py +18 -59
- hanzo_mcp/tools/llm/provider_tools.py +1 -3
- hanzo_mcp/tools/lsp/lsp_tool.py +5 -17
- hanzo_mcp/tools/mcp/mcp_add.py +3 -5
- hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
- hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
- hanzo_mcp/tools/memory/__init__.py +33 -40
- hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
- hanzo_mcp/tools/memory/memory_tools.py +7 -19
- hanzo_mcp/tools/search/find_tool.py +10 -32
- hanzo_mcp/tools/search/unified_search.py +27 -81
- hanzo_mcp/tools/shell/__init__.py +2 -2
- hanzo_mcp/tools/shell/auto_background.py +2 -6
- hanzo_mcp/tools/shell/base.py +1 -5
- hanzo_mcp/tools/shell/base_process.py +5 -7
- hanzo_mcp/tools/shell/bash_session.py +7 -24
- hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
- hanzo_mcp/tools/shell/bash_tool.py +3 -7
- hanzo_mcp/tools/shell/command_executor.py +26 -79
- hanzo_mcp/tools/shell/logs.py +4 -16
- hanzo_mcp/tools/shell/npx.py +2 -8
- hanzo_mcp/tools/shell/npx_tool.py +1 -3
- hanzo_mcp/tools/shell/pkill.py +4 -12
- hanzo_mcp/tools/shell/process_tool.py +2 -8
- hanzo_mcp/tools/shell/processes.py +5 -17
- hanzo_mcp/tools/shell/run_background.py +1 -3
- hanzo_mcp/tools/shell/run_command.py +1 -3
- hanzo_mcp/tools/shell/run_command_windows.py +1 -3
- hanzo_mcp/tools/shell/session_manager.py +2 -6
- hanzo_mcp/tools/shell/session_storage.py +2 -6
- hanzo_mcp/tools/shell/streaming_command.py +7 -23
- hanzo_mcp/tools/shell/uvx.py +4 -14
- hanzo_mcp/tools/shell/uvx_background.py +2 -6
- hanzo_mcp/tools/shell/uvx_tool.py +1 -3
- hanzo_mcp/tools/shell/zsh_tool.py +12 -20
- hanzo_mcp/tools/todo/todo.py +1 -3
- hanzo_mcp/tools/todo/todo_read.py +3 -9
- hanzo_mcp/tools/todo/todo_write.py +6 -18
- hanzo_mcp/tools/vector/__init__.py +3 -9
- hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
- hanzo_mcp/tools/vector/git_ingester.py +10 -30
- hanzo_mcp/tools/vector/index_tool.py +3 -9
- hanzo_mcp/tools/vector/infinity_store.py +11 -30
- hanzo_mcp/tools/vector/mock_infinity.py +159 -0
- hanzo_mcp/tools/vector/project_manager.py +4 -12
- hanzo_mcp/tools/vector/vector.py +2 -6
- hanzo_mcp/tools/vector/vector_index.py +8 -8
- hanzo_mcp/tools/vector/vector_search.py +7 -21
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.8.13.dist-info}/METADATA +2 -2
- hanzo_mcp-0.8.13.dist-info/RECORD +193 -0
- hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.8.13.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.8.13.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.8.13.dist-info}/top_level.txt +0 -0
|
@@ -335,9 +335,7 @@ For database search, use 'sql_search' or 'vector_search'.
|
|
|
335
335
|
return f"No files found matching '{pattern}' (using fallback search)"
|
|
336
336
|
|
|
337
337
|
# Format output
|
|
338
|
-
output = [
|
|
339
|
-
f"Found {len(results)} file(s) matching '{pattern}' (using fallback search):"
|
|
340
|
-
]
|
|
338
|
+
output = [f"Found {len(results)} file(s) matching '{pattern}' (using fallback search):"]
|
|
341
339
|
output.append("")
|
|
342
340
|
|
|
343
341
|
for filepath in sorted(results):
|
|
@@ -346,9 +344,7 @@ For database search, use 'sql_search' or 'vector_search'.
|
|
|
346
344
|
if count >= max_results:
|
|
347
345
|
output.append(f"\n... (showing first {max_results} results)")
|
|
348
346
|
|
|
349
|
-
output.append(
|
|
350
|
-
"\nNote: Install 'ffind' for faster searching: pip install ffind"
|
|
351
|
-
)
|
|
347
|
+
output.append("\nNote: Install 'ffind' for faster searching: pip install ffind")
|
|
352
348
|
|
|
353
349
|
return "\n".join(output)
|
|
354
350
|
|
|
@@ -256,9 +256,7 @@ Examples:
|
|
|
256
256
|
tool_ctx,
|
|
257
257
|
)
|
|
258
258
|
elif search_type == "blame":
|
|
259
|
-
return await self._search_blame(
|
|
260
|
-
abs_path, pattern, case_sensitive, file_pattern, tool_ctx
|
|
261
|
-
)
|
|
259
|
+
return await self._search_blame(abs_path, pattern, case_sensitive, file_pattern, tool_ctx)
|
|
262
260
|
else:
|
|
263
261
|
return f"Unknown search type: {search_type}"
|
|
264
262
|
|
|
@@ -307,9 +305,7 @@ Examples:
|
|
|
307
305
|
elif result.returncode == 1:
|
|
308
306
|
return f"No matches found for pattern: {pattern}"
|
|
309
307
|
else:
|
|
310
|
-
raise subprocess.CalledProcessError(
|
|
311
|
-
result.returncode, cmd, result.stdout, result.stderr
|
|
312
|
-
)
|
|
308
|
+
raise subprocess.CalledProcessError(result.returncode, cmd, result.stdout, result.stderr)
|
|
313
309
|
|
|
314
310
|
async def _search_commits(
|
|
315
311
|
self,
|
|
@@ -353,16 +349,11 @@ Examples:
|
|
|
353
349
|
lines = result.stdout.strip().split("\n")
|
|
354
350
|
if lines and lines[0]:
|
|
355
351
|
await tool_ctx.info(f"Found {len(lines)} commits")
|
|
356
|
-
return (
|
|
357
|
-
f"Found {len(lines)} commits matching '{pattern}':\n\n"
|
|
358
|
-
+ result.stdout
|
|
359
|
-
)
|
|
352
|
+
return f"Found {len(lines)} commits matching '{pattern}':\n\n" + result.stdout
|
|
360
353
|
else:
|
|
361
354
|
return f"No commits found matching: {pattern}"
|
|
362
355
|
else:
|
|
363
|
-
raise subprocess.CalledProcessError(
|
|
364
|
-
result.returncode, cmd, result.stdout, result.stderr
|
|
365
|
-
)
|
|
356
|
+
raise subprocess.CalledProcessError(result.returncode, cmd, result.stdout, result.stderr)
|
|
366
357
|
|
|
367
358
|
async def _search_diff(
|
|
368
359
|
self,
|
|
@@ -387,8 +378,7 @@ Examples:
|
|
|
387
378
|
import re
|
|
388
379
|
|
|
389
380
|
case_insensitive_pattern = "".join(
|
|
390
|
-
f"[{c.upper()}{c.lower()}]" if c.isalpha() else re.escape(c)
|
|
391
|
-
for c in pattern
|
|
381
|
+
f"[{c.upper()}{c.lower()}]" if c.isalpha() else re.escape(c) for c in pattern
|
|
392
382
|
)
|
|
393
383
|
search_flag = f"-G{case_insensitive_pattern}"
|
|
394
384
|
|
|
@@ -415,9 +405,7 @@ Examples:
|
|
|
415
405
|
|
|
416
406
|
if result.returncode == 0 and result.stdout.strip():
|
|
417
407
|
# Parse and highlight matching lines
|
|
418
|
-
output = self._highlight_diff_matches(
|
|
419
|
-
result.stdout, pattern, case_sensitive
|
|
420
|
-
)
|
|
408
|
+
output = self._highlight_diff_matches(result.stdout, pattern, case_sensitive)
|
|
421
409
|
matches = output.count("commit ")
|
|
422
410
|
await tool_ctx.info(f"Found {matches} commits with changes")
|
|
423
411
|
return f"Found {matches} commits with changes matching '{pattern}':\n\n{output}"
|
|
@@ -505,9 +493,8 @@ Examples:
|
|
|
505
493
|
|
|
506
494
|
if all_matches:
|
|
507
495
|
await tool_ctx.info(f"Found {len(all_matches)} matching lines")
|
|
508
|
-
return (
|
|
509
|
-
|
|
510
|
-
+ "\n".join(all_matches[:50])
|
|
496
|
+
return f"Found {len(all_matches)} lines matching '{pattern}':\n\n" + "\n".join(
|
|
497
|
+
all_matches[:50]
|
|
511
498
|
) # Limit output
|
|
512
499
|
else:
|
|
513
500
|
return f"No lines found matching: {pattern}"
|
|
@@ -534,9 +521,7 @@ Examples:
|
|
|
534
521
|
|
|
535
522
|
return f"Found matches for '{pattern}':\n" + "\n".join(output)
|
|
536
523
|
|
|
537
|
-
def _highlight_diff_matches(
|
|
538
|
-
self, diff_output: str, pattern: str, case_sensitive: bool
|
|
539
|
-
) -> str:
|
|
524
|
+
def _highlight_diff_matches(self, diff_output: str, pattern: str, case_sensitive: bool) -> str:
|
|
540
525
|
"""Highlight matching lines in diff output."""
|
|
541
526
|
lines = diff_output.split("\n")
|
|
542
527
|
output = []
|
|
@@ -118,9 +118,7 @@ When you are doing an open ended search that may require multiple rounds of glob
|
|
|
118
118
|
# Special case for tests: direct file path with include pattern that doesn't match
|
|
119
119
|
if Path(path).is_file() and include_pattern and include_pattern != "*":
|
|
120
120
|
if not fnmatch.fnmatch(Path(path).name, include_pattern):
|
|
121
|
-
await tool_ctx.info(
|
|
122
|
-
f"File does not match pattern '{include_pattern}': {path}"
|
|
123
|
-
)
|
|
121
|
+
await tool_ctx.info(f"File does not match pattern '{include_pattern}': {path}")
|
|
124
122
|
return f"File does not match pattern '{include_pattern}': {path}"
|
|
125
123
|
|
|
126
124
|
cmd = ["rg", "--json", pattern]
|
|
@@ -144,9 +142,7 @@ When you are doing an open ended search that may require multiple rounds of glob
|
|
|
144
142
|
|
|
145
143
|
if process.returncode != 0 and process.returncode != 1:
|
|
146
144
|
# rg returns 1 when no matches are found, which is not an error
|
|
147
|
-
await tool_ctx.error(
|
|
148
|
-
f"ripgrep failed with exit code {process.returncode}: {stderr.decode()}"
|
|
149
|
-
)
|
|
145
|
+
await tool_ctx.error(f"ripgrep failed with exit code {process.returncode}: {stderr.decode()}")
|
|
150
146
|
return f"Error executing ripgrep: {stderr.decode()}"
|
|
151
147
|
|
|
152
148
|
# Parse the JSON output
|
|
@@ -182,9 +178,7 @@ When you are doing an open ended search that may require multiple rounds of glob
|
|
|
182
178
|
if data.get("type") == "match":
|
|
183
179
|
path = data.get("data", {}).get("path", {}).get("text", "")
|
|
184
180
|
line_number = data.get("data", {}).get("line_number", 0)
|
|
185
|
-
line_text = (
|
|
186
|
-
data.get("data", {}).get("lines", {}).get("text", "").rstrip()
|
|
187
|
-
)
|
|
181
|
+
line_text = data.get("data", {}).get("lines", {}).get("text", "").rstrip()
|
|
188
182
|
|
|
189
183
|
if path not in file_results:
|
|
190
184
|
file_results[path] = []
|
|
@@ -251,9 +245,7 @@ When you are doing an open ended search that may require multiple rounds of glob
|
|
|
251
245
|
await tool_ctx.info(f"Searching single file: {path}")
|
|
252
246
|
else:
|
|
253
247
|
# File doesn't match the pattern, return immediately
|
|
254
|
-
await tool_ctx.info(
|
|
255
|
-
f"File does not match pattern '{include_pattern}': {path}"
|
|
256
|
-
)
|
|
248
|
+
await tool_ctx.info(f"File does not match pattern '{include_pattern}': {path}")
|
|
257
249
|
return f"File does not match pattern '{include_pattern}': {path}"
|
|
258
250
|
elif input_path.is_dir():
|
|
259
251
|
# Directory search - find all files
|
|
@@ -290,9 +282,7 @@ When you are doing an open ended search that may require multiple rounds of glob
|
|
|
290
282
|
if input_path.is_file():
|
|
291
283
|
await tool_ctx.info(f"Searching file: {path}")
|
|
292
284
|
else:
|
|
293
|
-
await tool_ctx.info(
|
|
294
|
-
f"Searching through {total_files} files in directory"
|
|
295
|
-
)
|
|
285
|
+
await tool_ctx.info(f"Searching through {total_files} files in directory")
|
|
296
286
|
|
|
297
287
|
# Set up for parallel processing
|
|
298
288
|
results: list[str] = []
|
|
@@ -314,18 +304,14 @@ When you are doing an open ended search that may require multiple rounds of glob
|
|
|
314
304
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
315
305
|
for line_num, line in enumerate(f, 1):
|
|
316
306
|
if re.search(pattern, line):
|
|
317
|
-
file_results.append(
|
|
318
|
-
f"{file_path}:{line_num}: {line.rstrip()}"
|
|
319
|
-
)
|
|
307
|
+
file_results.append(f"{file_path}:{line_num}: {line.rstrip()}")
|
|
320
308
|
matches_found += 1
|
|
321
309
|
files_processed += 1
|
|
322
310
|
except UnicodeDecodeError:
|
|
323
311
|
# Skip binary files
|
|
324
312
|
files_processed += 1
|
|
325
313
|
except Exception as e:
|
|
326
|
-
await tool_ctx.warning(
|
|
327
|
-
f"Error reading {file_path}: {str(e)}"
|
|
328
|
-
)
|
|
314
|
+
await tool_ctx.warning(f"Error reading {file_path}: {str(e)}")
|
|
329
315
|
except Exception as e:
|
|
330
316
|
await tool_ctx.warning(f"Error processing {file_path}: {str(e)}")
|
|
331
317
|
|
|
@@ -429,9 +415,7 @@ When you are doing an open ended search that may require multiple rounds of glob
|
|
|
429
415
|
truncation_message="\n\n[Grep results truncated due to token limit. Use more specific patterns or paths to reduce output.]",
|
|
430
416
|
)
|
|
431
417
|
else:
|
|
432
|
-
await tool_ctx.info(
|
|
433
|
-
"ripgrep is not installed, using fallback implementation"
|
|
434
|
-
)
|
|
418
|
+
await tool_ctx.info("ripgrep is not installed, using fallback implementation")
|
|
435
419
|
result = await self.fallback_grep(pattern, path, tool_ctx, include)
|
|
436
420
|
return truncate_response(
|
|
437
421
|
result,
|
|
@@ -462,6 +446,4 @@ When you are doing an open ended search that may require multiple rounds of glob
|
|
|
462
446
|
include: Include,
|
|
463
447
|
) -> str:
|
|
464
448
|
# Use 'include' parameter if provided, otherwise fall back to 'file_pattern'
|
|
465
|
-
return await tool_self.call(
|
|
466
|
-
ctx, pattern=pattern, path=path, include=include
|
|
467
|
-
)
|
|
449
|
+
return await tool_self.call(ctx, pattern=pattern, path=path, include=include)
|
|
@@ -170,15 +170,11 @@ If you want to create a new file, use:
|
|
|
170
170
|
expected_replacements = edit.get("expected_replacements", 1)
|
|
171
171
|
|
|
172
172
|
if old_string is None:
|
|
173
|
-
await tool_ctx.error(
|
|
174
|
-
f"Parameter 'old_string' in edit at index {i} is required but was None"
|
|
175
|
-
)
|
|
173
|
+
await tool_ctx.error(f"Parameter 'old_string' in edit at index {i} is required but was None")
|
|
176
174
|
return f"Error: Parameter 'old_string' in edit at index {i} is required but was None"
|
|
177
175
|
|
|
178
176
|
if new_string is None:
|
|
179
|
-
await tool_ctx.error(
|
|
180
|
-
f"Parameter 'new_string' in edit at index {i} is required but was None"
|
|
181
|
-
)
|
|
177
|
+
await tool_ctx.error(f"Parameter 'new_string' in edit at index {i} is required but was None")
|
|
182
178
|
return f"Error: Parameter 'new_string' in edit at index {i} is required but was None"
|
|
183
179
|
|
|
184
180
|
if (
|
|
@@ -192,12 +188,8 @@ If you want to create a new file, use:
|
|
|
192
188
|
return f"Error: Parameter 'expected_replacements' in edit at index {i} must be a non-negative number"
|
|
193
189
|
|
|
194
190
|
if old_string == new_string:
|
|
195
|
-
await tool_ctx.error(
|
|
196
|
-
|
|
197
|
-
)
|
|
198
|
-
return (
|
|
199
|
-
f"Error: Edit at index {i}: old_string and new_string are identical"
|
|
200
|
-
)
|
|
191
|
+
await tool_ctx.error(f"Edit at index {i}: old_string and new_string are identical")
|
|
192
|
+
return f"Error: Edit at index {i}: old_string and new_string are identical"
|
|
201
193
|
|
|
202
194
|
await tool_ctx.info(f"Applying {len(edits)} edits to file: {file_path}")
|
|
203
195
|
|
|
@@ -261,9 +253,7 @@ If you want to create a new file, use:
|
|
|
261
253
|
|
|
262
254
|
# Check if old_string exists in current content
|
|
263
255
|
if old_string not in current_content:
|
|
264
|
-
edit_index =
|
|
265
|
-
i + 1 if not creation_mode else i + 2
|
|
266
|
-
) # Adjust for display
|
|
256
|
+
edit_index = i + 1 if not creation_mode else i + 2 # Adjust for display
|
|
267
257
|
await tool_ctx.error(
|
|
268
258
|
f"Edit {edit_index}: The specified old_string was not found in the file content"
|
|
269
259
|
)
|
|
@@ -274,9 +264,7 @@ If you want to create a new file, use:
|
|
|
274
264
|
|
|
275
265
|
# Check if the number of occurrences matches expected_replacements
|
|
276
266
|
if occurrences != expected_replacements:
|
|
277
|
-
edit_index =
|
|
278
|
-
i + 1 if not creation_mode else i + 2
|
|
279
|
-
) # Adjust for display
|
|
267
|
+
edit_index = i + 1 if not creation_mode else i + 2 # Adjust for display
|
|
280
268
|
await tool_ctx.error(
|
|
281
269
|
f"Edit {edit_index}: Found {occurrences} occurrences of the specified old_string, but expected {expected_replacements}"
|
|
282
270
|
)
|
|
@@ -118,18 +118,12 @@ Usage:
|
|
|
118
118
|
await tool_ctx.error("Parameter 'file_path' is required but was None")
|
|
119
119
|
return "Error: Parameter 'file_path' is required but was None"
|
|
120
120
|
|
|
121
|
-
await tool_ctx.info(
|
|
122
|
-
f"Reading file: {file_path} (offset: {offset}, limit: {limit})"
|
|
123
|
-
)
|
|
121
|
+
await tool_ctx.info(f"Reading file: {file_path} (offset: {offset}, limit: {limit})")
|
|
124
122
|
|
|
125
123
|
# Check if path is allowed
|
|
126
124
|
if not self.is_path_allowed(file_path):
|
|
127
|
-
await tool_ctx.error(
|
|
128
|
-
|
|
129
|
-
)
|
|
130
|
-
return (
|
|
131
|
-
f"Error: Access denied - path outside allowed directories: {file_path}"
|
|
132
|
-
)
|
|
125
|
+
await tool_ctx.error(f"Access denied - path outside allowed directories: {file_path}")
|
|
126
|
+
return f"Error: Access denied - path outside allowed directories: {file_path}"
|
|
133
127
|
|
|
134
128
|
try:
|
|
135
129
|
file_path_obj = Path(file_path)
|
|
@@ -166,10 +160,7 @@ Usage:
|
|
|
166
160
|
|
|
167
161
|
# Truncate long lines
|
|
168
162
|
if len(line) > self.MAX_LINE_LENGTH:
|
|
169
|
-
line =
|
|
170
|
-
line[: self.MAX_LINE_LENGTH]
|
|
171
|
-
+ self.LINE_TRUNCATION_INDICATOR
|
|
172
|
-
)
|
|
163
|
+
line = line[: self.MAX_LINE_LENGTH] + self.LINE_TRUNCATION_INDICATOR
|
|
173
164
|
|
|
174
165
|
# Add line with line number (1-based)
|
|
175
166
|
lines.append(f"{i + 1:6d} {line.rstrip()}")
|
|
@@ -196,17 +187,12 @@ Usage:
|
|
|
196
187
|
|
|
197
188
|
# Truncate long lines
|
|
198
189
|
if len(line) > self.MAX_LINE_LENGTH:
|
|
199
|
-
line =
|
|
200
|
-
line[: self.MAX_LINE_LENGTH]
|
|
201
|
-
+ self.LINE_TRUNCATION_INDICATOR
|
|
202
|
-
)
|
|
190
|
+
line = line[: self.MAX_LINE_LENGTH] + self.LINE_TRUNCATION_INDICATOR
|
|
203
191
|
|
|
204
192
|
# Add line with line number (1-based)
|
|
205
193
|
lines.append(f"{i + 1:6d} {line.rstrip()}")
|
|
206
194
|
|
|
207
|
-
await tool_ctx.warning(
|
|
208
|
-
f"File read with latin-1 encoding: {file_path}"
|
|
209
|
-
)
|
|
195
|
+
await tool_ctx.warning(f"File read with latin-1 encoding: {file_path}")
|
|
210
196
|
|
|
211
197
|
except Exception:
|
|
212
198
|
await tool_ctx.error(f"Cannot read binary file: {file_path}")
|
|
@@ -236,9 +222,7 @@ Usage:
|
|
|
236
222
|
await tool_ctx.error(f"Error reading file: {str(e)}")
|
|
237
223
|
return f"Error: {str(e)}"
|
|
238
224
|
|
|
239
|
-
async def run(
|
|
240
|
-
self, ctx: MCPContext, file_path: str, offset: int = 0, limit: int = 2000
|
|
241
|
-
) -> str:
|
|
225
|
+
async def run(self, ctx: MCPContext, file_path: str, offset: int = 0, limit: int = 2000) -> str:
|
|
242
226
|
"""Run method for backwards compatibility with test scripts.
|
|
243
227
|
|
|
244
228
|
Args:
|
|
@@ -271,6 +255,4 @@ Usage:
|
|
|
271
255
|
offset: Offset = 0,
|
|
272
256
|
limit: Limit = 2000,
|
|
273
257
|
) -> str:
|
|
274
|
-
return await tool_self.call(
|
|
275
|
-
ctx, file_path=file_path, offset=offset, limit=limit
|
|
276
|
-
)
|
|
258
|
+
return await tool_self.call(ctx, file_path=file_path, offset=offset, limit=limit)
|
|
@@ -142,9 +142,7 @@ understand project-specific requirements and preferences."""
|
|
|
142
142
|
found_configs.append(
|
|
143
143
|
{
|
|
144
144
|
"path": str(config_path),
|
|
145
|
-
"relative_path": str(
|
|
146
|
-
config_path.relative_to(start_path)
|
|
147
|
-
),
|
|
145
|
+
"relative_path": str(config_path.relative_to(start_path)),
|
|
148
146
|
"content": content,
|
|
149
147
|
"size": len(content),
|
|
150
148
|
}
|
|
@@ -152,9 +150,7 @@ understand project-specific requirements and preferences."""
|
|
|
152
150
|
|
|
153
151
|
await tool_ctx.info(f"Found configuration: {config_path}")
|
|
154
152
|
except Exception as e:
|
|
155
|
-
await tool_ctx.warning(
|
|
156
|
-
f"Could not read {config_path}: {str(e)}"
|
|
157
|
-
)
|
|
153
|
+
await tool_ctx.warning(f"Could not read {config_path}: {str(e)}")
|
|
158
154
|
|
|
159
155
|
# Check if we've reached the root or a git repository root
|
|
160
156
|
if current_path.parent == current_path:
|
|
@@ -169,8 +165,7 @@ understand project-specific requirements and preferences."""
|
|
|
169
165
|
if (
|
|
170
166
|
config_path.exists()
|
|
171
167
|
and config_path.is_file()
|
|
172
|
-
and str(config_path)
|
|
173
|
-
not in [c["path"] for c in found_configs]
|
|
168
|
+
and str(config_path) not in [c["path"] for c in found_configs]
|
|
174
169
|
):
|
|
175
170
|
try:
|
|
176
171
|
if self.is_path_allowed(str(config_path)):
|
|
@@ -180,21 +175,15 @@ understand project-specific requirements and preferences."""
|
|
|
180
175
|
found_configs.append(
|
|
181
176
|
{
|
|
182
177
|
"path": str(config_path),
|
|
183
|
-
"relative_path": str(
|
|
184
|
-
config_path.relative_to(start_path)
|
|
185
|
-
),
|
|
178
|
+
"relative_path": str(config_path.relative_to(start_path)),
|
|
186
179
|
"content": content,
|
|
187
180
|
"size": len(content),
|
|
188
181
|
}
|
|
189
182
|
)
|
|
190
183
|
|
|
191
|
-
await tool_ctx.info(
|
|
192
|
-
f"Found configuration: {config_path}"
|
|
193
|
-
)
|
|
184
|
+
await tool_ctx.info(f"Found configuration: {config_path}")
|
|
194
185
|
except Exception as e:
|
|
195
|
-
await tool_ctx.warning(
|
|
196
|
-
f"Could not read {config_path}: {str(e)}"
|
|
197
|
-
)
|
|
186
|
+
await tool_ctx.warning(f"Could not read {config_path}: {str(e)}")
|
|
198
187
|
break
|
|
199
188
|
|
|
200
189
|
# Move to parent directory
|
|
@@ -248,9 +248,7 @@ This is the recommended search tool for comprehensive results."""
|
|
|
248
248
|
) -> List[SearchResult]:
|
|
249
249
|
"""Run grep search and parse results."""
|
|
250
250
|
try:
|
|
251
|
-
result = await self.grep_tool.call(
|
|
252
|
-
tool_ctx.mcp_context, pattern=pattern, path=path, include=include
|
|
253
|
-
)
|
|
251
|
+
result = await self.grep_tool.call(tool_ctx.mcp_context, pattern=pattern, path=path, include=include)
|
|
254
252
|
|
|
255
253
|
results = []
|
|
256
254
|
if "Found" in result and "matches" in result:
|
|
@@ -281,9 +279,7 @@ This is the recommended search tool for comprehensive results."""
|
|
|
281
279
|
await tool_ctx.error(f"Grep search failed: {e}")
|
|
282
280
|
return []
|
|
283
281
|
|
|
284
|
-
async def _run_grep_ast_search(
|
|
285
|
-
self, pattern: str, path: str, tool_ctx, max_results: int
|
|
286
|
-
) -> List[SearchResult]:
|
|
282
|
+
async def _run_grep_ast_search(self, pattern: str, path: str, tool_ctx, max_results: int) -> List[SearchResult]:
|
|
287
283
|
"""Run AST-aware search and parse results."""
|
|
288
284
|
try:
|
|
289
285
|
result = await self.grep_ast_tool.call(
|
|
@@ -317,11 +313,7 @@ This is the recommended search tool for comprehensive results."""
|
|
|
317
313
|
content=content,
|
|
318
314
|
search_type=SearchType.GREP_AST,
|
|
319
315
|
score=0.95, # High score for AST matches
|
|
320
|
-
context=(
|
|
321
|
-
" > ".join(current_context)
|
|
322
|
-
if current_context
|
|
323
|
-
else None
|
|
324
|
-
),
|
|
316
|
+
context=(" > ".join(current_context) if current_context else None),
|
|
325
317
|
)
|
|
326
318
|
)
|
|
327
319
|
|
|
@@ -339,9 +331,7 @@ This is the recommended search tool for comprehensive results."""
|
|
|
339
331
|
await tool_ctx.error(f"AST search failed: {e}")
|
|
340
332
|
return []
|
|
341
333
|
|
|
342
|
-
async def _run_vector_search(
|
|
343
|
-
self, pattern: str, path: str, tool_ctx, max_results: int
|
|
344
|
-
) -> List[SearchResult]:
|
|
334
|
+
async def _run_vector_search(self, pattern: str, path: str, tool_ctx, max_results: int) -> List[SearchResult]:
|
|
345
335
|
"""Run semantic vector search."""
|
|
346
336
|
if not self.vector_tool:
|
|
347
337
|
return []
|
|
@@ -399,9 +389,7 @@ This is the recommended search tool for comprehensive results."""
|
|
|
399
389
|
await tool_ctx.error(f"Vector search failed: {e}")
|
|
400
390
|
return []
|
|
401
391
|
|
|
402
|
-
async def _run_git_search(
|
|
403
|
-
self, pattern: str, path: str, tool_ctx, max_results: int
|
|
404
|
-
) -> List[SearchResult]:
|
|
392
|
+
async def _run_git_search(self, pattern: str, path: str, tool_ctx, max_results: int) -> List[SearchResult]:
|
|
405
393
|
"""Run git history search."""
|
|
406
394
|
try:
|
|
407
395
|
# Search in both content and commits
|
|
@@ -440,11 +428,7 @@ This is the recommended search tool for comprehensive results."""
|
|
|
440
428
|
SearchResult(
|
|
441
429
|
file_path=parts[0].strip(),
|
|
442
430
|
line_number=None,
|
|
443
|
-
content=(
|
|
444
|
-
parts[-1].strip()
|
|
445
|
-
if len(parts) > 2
|
|
446
|
-
else line
|
|
447
|
-
),
|
|
431
|
+
content=(parts[-1].strip() if len(parts) > 2 else line),
|
|
448
432
|
search_type=SearchType.GIT,
|
|
449
433
|
score=0.8, # Good score for git matches
|
|
450
434
|
)
|
|
@@ -460,9 +444,7 @@ This is the recommended search tool for comprehensive results."""
|
|
|
460
444
|
await tool_ctx.error(f"Git search failed: {e}")
|
|
461
445
|
return []
|
|
462
446
|
|
|
463
|
-
async def _run_symbol_search(
|
|
464
|
-
self, pattern: str, path: str, tool_ctx, max_results: int
|
|
465
|
-
) -> List[SearchResult]:
|
|
447
|
+
async def _run_symbol_search(self, pattern: str, path: str, tool_ctx, max_results: int) -> List[SearchResult]:
|
|
466
448
|
"""Search for symbol definitions using grep with specific patterns."""
|
|
467
449
|
try:
|
|
468
450
|
# Create patterns for common symbol definitions
|
|
@@ -477,11 +459,7 @@ This is the recommended search tool for comprehensive results."""
|
|
|
477
459
|
# Run grep searches in parallel for each pattern
|
|
478
460
|
tasks = []
|
|
479
461
|
for sp in symbol_patterns:
|
|
480
|
-
tasks.append(
|
|
481
|
-
self.grep_tool.call(
|
|
482
|
-
tool_ctx.mcp_context, pattern=sp, path=path, include="*"
|
|
483
|
-
)
|
|
484
|
-
)
|
|
462
|
+
tasks.append(self.grep_tool.call(tool_ctx.mcp_context, pattern=sp, path=path, include="*"))
|
|
485
463
|
|
|
486
464
|
grep_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
487
465
|
|
|
@@ -518,9 +496,7 @@ This is the recommended search tool for comprehensive results."""
|
|
|
518
496
|
await tool_ctx.error(f"Symbol search failed: {e}")
|
|
519
497
|
return []
|
|
520
498
|
|
|
521
|
-
def _deduplicate_results(
|
|
522
|
-
self, all_results: List[SearchResult]
|
|
523
|
-
) -> List[SearchResult]:
|
|
499
|
+
def _deduplicate_results(self, all_results: List[SearchResult]) -> List[SearchResult]:
|
|
524
500
|
"""Deduplicate results, keeping the highest scoring version."""
|
|
525
501
|
seen = {}
|
|
526
502
|
|
|
@@ -547,9 +523,7 @@ This is the recommended search tool for comprehensive results."""
|
|
|
547
523
|
}
|
|
548
524
|
|
|
549
525
|
# Sort by score (descending) and then by type priority
|
|
550
|
-
results.sort(
|
|
551
|
-
key=lambda r: (r.score, type_priority.get(r.search_type, 0)), reverse=True
|
|
552
|
-
)
|
|
526
|
+
results.sort(key=lambda r: (r.score, type_priority.get(r.search_type, 0)), reverse=True)
|
|
553
527
|
|
|
554
528
|
return results
|
|
555
529
|
|
|
@@ -599,42 +573,26 @@ This is the recommended search tool for comprehensive results."""
|
|
|
599
573
|
search_names = []
|
|
600
574
|
|
|
601
575
|
if params.get("enable_grep", True) and pattern_analysis["use_grep"]:
|
|
602
|
-
search_tasks.append(
|
|
603
|
-
self._run_grep_search(pattern, path, include, tool_ctx, max_results)
|
|
604
|
-
)
|
|
576
|
+
search_tasks.append(self._run_grep_search(pattern, path, include, tool_ctx, max_results))
|
|
605
577
|
search_names.append("grep")
|
|
606
578
|
|
|
607
579
|
if params.get("enable_grep_ast", True) and pattern_analysis["use_grep_ast"]:
|
|
608
|
-
search_tasks.append(
|
|
609
|
-
self._run_grep_ast_search(pattern, path, tool_ctx, max_results)
|
|
610
|
-
)
|
|
580
|
+
search_tasks.append(self._run_grep_ast_search(pattern, path, tool_ctx, max_results))
|
|
611
581
|
search_names.append("grep_ast")
|
|
612
582
|
|
|
613
|
-
if (
|
|
614
|
-
|
|
615
|
-
and self.vector_tool
|
|
616
|
-
and pattern_analysis["use_vector"]
|
|
617
|
-
):
|
|
618
|
-
search_tasks.append(
|
|
619
|
-
self._run_vector_search(pattern, path, tool_ctx, max_results)
|
|
620
|
-
)
|
|
583
|
+
if params.get("enable_vector", True) and self.vector_tool and pattern_analysis["use_vector"]:
|
|
584
|
+
search_tasks.append(self._run_vector_search(pattern, path, tool_ctx, max_results))
|
|
621
585
|
search_names.append("vector")
|
|
622
586
|
|
|
623
587
|
if params.get("enable_git", True) and pattern_analysis["use_git"]:
|
|
624
|
-
search_tasks.append(
|
|
625
|
-
self._run_git_search(pattern, path, tool_ctx, max_results)
|
|
626
|
-
)
|
|
588
|
+
search_tasks.append(self._run_git_search(pattern, path, tool_ctx, max_results))
|
|
627
589
|
search_names.append("git")
|
|
628
590
|
|
|
629
591
|
if params.get("enable_symbol", True) and pattern_analysis["use_symbol"]:
|
|
630
|
-
search_tasks.append(
|
|
631
|
-
self._run_symbol_search(pattern, path, tool_ctx, max_results)
|
|
632
|
-
)
|
|
592
|
+
search_tasks.append(self._run_symbol_search(pattern, path, tool_ctx, max_results))
|
|
633
593
|
search_names.append("symbol")
|
|
634
594
|
|
|
635
|
-
await tool_ctx.info(
|
|
636
|
-
f"Running {len(search_tasks)} search types in parallel: {', '.join(search_names)}"
|
|
637
|
-
)
|
|
595
|
+
await tool_ctx.info(f"Running {len(search_tasks)} search types in parallel: {', '.join(search_names)}")
|
|
638
596
|
|
|
639
597
|
# Run all searches in parallel
|
|
640
598
|
search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
|
|
@@ -719,9 +677,7 @@ This is the recommended search tool for comprehensive results."""
|
|
|
719
677
|
score_str = f"[{result.search_type.value} {result.score:.2f}]"
|
|
720
678
|
|
|
721
679
|
if result.line_number:
|
|
722
|
-
output.append(
|
|
723
|
-
f" {result.line_number:>4}: {score_str} {result.content}"
|
|
724
|
-
)
|
|
680
|
+
output.append(f" {result.line_number:>4}: {score_str} {result.content}")
|
|
725
681
|
else:
|
|
726
682
|
output.append(f" {score_str} {result.content}")
|
|
727
683
|
|
|
@@ -141,9 +141,7 @@ Finds code structures (functions, classes, methods) with full context."""
|
|
|
141
141
|
# Route to appropriate handler
|
|
142
142
|
if action == "search":
|
|
143
143
|
return await self._handle_search(params, tool_ctx)
|
|
144
|
-
elif
|
|
145
|
-
action == "ast" or action == "grep_ast"
|
|
146
|
-
): # Support both for backward compatibility
|
|
144
|
+
elif action == "ast" or action == "grep_ast": # Support both for backward compatibility
|
|
147
145
|
return await self._handle_ast(params, tool_ctx)
|
|
148
146
|
elif action == "index":
|
|
149
147
|
return await self._handle_index(params, tool_ctx)
|
|
@@ -218,9 +216,7 @@ Finds code structures (functions, classes, methods) with full context."""
|
|
|
218
216
|
output = tc.format()
|
|
219
217
|
else:
|
|
220
218
|
# Just show matching lines
|
|
221
|
-
output = "\n".join(
|
|
222
|
-
[f"{line}: {code.splitlines()[line - 1]}" for line in loi]
|
|
223
|
-
)
|
|
219
|
+
output = "\n".join([f"{line}: {code.splitlines()[line - 1]}" for line in loi])
|
|
224
220
|
|
|
225
221
|
results.append(f"\n{file_path}:\n{output}\n")
|
|
226
222
|
match_count += len(loi)
|
|
@@ -325,9 +321,7 @@ Finds code structures (functions, classes, methods) with full context."""
|
|
|
325
321
|
return f"No matches found for '{pattern}' in {path}"
|
|
326
322
|
|
|
327
323
|
output = [f"=== AST-aware Grep Results for '{pattern}' ==="]
|
|
328
|
-
output.append(
|
|
329
|
-
f"Total matches: {match_count} in {len([r for r in results if '===' in str(r)]) // 4} files\n"
|
|
330
|
-
)
|
|
324
|
+
output.append(f"Total matches: {match_count} in {len([r for r in results if '===' in str(r)]) // 4} files\n")
|
|
331
325
|
output.extend(results)
|
|
332
326
|
|
|
333
327
|
if match_count >= limit:
|
|
@@ -490,16 +484,12 @@ Finds code structures (functions, classes, methods) with full context."""
|
|
|
490
484
|
for root, _, files in os.walk(path_obj):
|
|
491
485
|
for file in files:
|
|
492
486
|
file_path = Path(root) / file
|
|
493
|
-
if file_path.suffix in extensions and self.is_path_allowed(
|
|
494
|
-
str(file_path)
|
|
495
|
-
):
|
|
487
|
+
if file_path.suffix in extensions and self.is_path_allowed(str(file_path)):
|
|
496
488
|
files_to_process.append(str(file_path))
|
|
497
489
|
|
|
498
490
|
return files_to_process
|
|
499
491
|
|
|
500
|
-
def _extract_symbols(
|
|
501
|
-
self, tc: TreeContext, file_path: str
|
|
502
|
-
) -> Dict[str, List[Dict[str, Any]]]:
|
|
492
|
+
def _extract_symbols(self, tc: TreeContext, file_path: str) -> Dict[str, List[Dict[str, Any]]]:
|
|
503
493
|
"""Extract symbols from a TreeContext (placeholder implementation)."""
|
|
504
494
|
# This would need proper tree-sitter queries to extract symbols
|
|
505
495
|
# For now, return empty structure
|
|
@@ -188,9 +188,7 @@ tree --pattern "*.py" --show-size"""
|
|
|
188
188
|
if pattern:
|
|
189
189
|
import fnmatch
|
|
190
190
|
|
|
191
|
-
entries = [
|
|
192
|
-
e for e in entries if fnmatch.fnmatch(e.name, pattern) or e.is_dir()
|
|
193
|
-
]
|
|
191
|
+
entries = [e for e in entries if fnmatch.fnmatch(e.name, pattern) or e.is_dir()]
|
|
194
192
|
|
|
195
193
|
# Filter dirs only
|
|
196
194
|
if dirs_only:
|
|
@@ -191,9 +191,7 @@ watch . --recursive --exclude "__pycache__"
|
|
|
191
191
|
output.append("\nWatch cancelled")
|
|
192
192
|
|
|
193
193
|
# Summary
|
|
194
|
-
output.append(
|
|
195
|
-
f"\nWatch completed after {int(time.time() - start_time)} seconds"
|
|
196
|
-
)
|
|
194
|
+
output.append(f"\nWatch completed after {int(time.time() - start_time)} seconds")
|
|
197
195
|
output.append(f"Total changes detected: {len(changes)}")
|
|
198
196
|
|
|
199
197
|
return "\n".join(output)
|
|
@@ -125,9 +125,7 @@ Usage:
|
|
|
125
125
|
with open(path_obj, "w", encoding="utf-8") as f:
|
|
126
126
|
f.write(content)
|
|
127
127
|
|
|
128
|
-
await tool_ctx.info(
|
|
129
|
-
f"Successfully wrote file: {file_path} ({len(content)} bytes)"
|
|
130
|
-
)
|
|
128
|
+
await tool_ctx.info(f"Successfully wrote file: {file_path} ({len(content)} bytes)")
|
|
131
129
|
return f"Successfully wrote file: {file_path} ({len(content)} bytes)"
|
|
132
130
|
except Exception as e:
|
|
133
131
|
await tool_ctx.error(f"Error writing file: {str(e)}")
|