diff --git a/src/mcp_client.py b/src/mcp_client.py index e5c9406f..7f44206c 100644 --- a/src/mcp_client.py +++ b/src/mcp_client.py @@ -328,38 +328,105 @@ def search_files_result(path: str, pattern: str) -> Result[str]: return Result(data="\n".join(lines)) except Exception as e: return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(e), source="mcp.search_files_result", original=e)]) + +def edit_file_result(path: str, old_string: str, new_string: str, replace_all: bool = False) -> Result[str]: + resolved = _resolve_and_check_result(path) + if not resolved.ok: + return Result(data="", errors=resolved.errors) + p = resolved.data + if isinstance(p, NilPath): + return Result(data="", errors=resolved.errors) + if not p.exists(): + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.NOT_FOUND, message=f"file not found: {path}", source="mcp.edit_file_result")]) + if not old_string: + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INVALID_INPUT, message="old_string cannot be empty", source="mcp.edit_file_result")]) + try: + content = p.read_text(encoding="utf-8") + if old_string not in content: + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.NOT_FOUND, message=f"old_string not found in '{path}'", source="mcp.edit_file_result")]) + count = content.count(old_string) + if count > 1 and not replace_all: + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INVALID_INPUT, message=f"Found {count} matches for old_string in '{path}'. Use replace_all=true or provide more context to make it unique.", source="mcp.edit_file_result")]) + if replace_all: + new_content = content.replace(old_string, new_string) + p.write_text(new_content, encoding="utf-8") + return Result(data=f"Successfully replaced {count} occurrences in '{path}'") + new_content = content.replace(old_string, new_string, 1) + p.write_text(new_content, encoding="utf-8") + return Result(data=f"Successfully replaced 1 occurrence in '{path}'") + except Exception as e: + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(e), source="mcp.edit_file_result", original=e)]) + +def get_file_summary_result(path: str) -> Result[str]: + resolved = _resolve_and_check_result(path) + if not resolved.ok: + return Result(data="", errors=resolved.errors) + p = resolved.data + if isinstance(p, NilPath): + return Result(data="", errors=resolved.errors) + if not p.exists(): + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.NOT_FOUND, message=f"file not found: {path}", source="mcp.get_file_summary_result")]) + if not p.is_file(): + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INVALID_INPUT, message=f"not a file: {path}", source="mcp.get_file_summary_result")]) + try: + content = p.read_text(encoding="utf-8") + return Result(data=summarize.summarise_file(p, content)) + except Exception as e: + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(e), source="mcp.get_file_summary_result", original=e)]) + +def get_file_slice_result(path: str, start_line: int, end_line: int) -> Result[str]: + resolved = _resolve_and_check_result(path) + if not resolved.ok: + return Result(data="", errors=resolved.errors) + p = resolved.data + if isinstance(p, NilPath): + return Result(data="", errors=resolved.errors) + if not p.exists(): + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.NOT_FOUND, message=f"file not found: {path}", source="mcp.get_file_slice_result")]) + try: + lines = p.read_text(encoding="utf-8").splitlines(keepends=True) + start_idx = start_line - 1 + end_idx = end_line + return Result(data="".join(lines[start_idx:end_idx])) + except Exception as e: + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(e), source="mcp.get_file_slice_result", original=e)]) + +def set_file_slice_result(path: str, start_line: int, end_line: int, new_content: str) -> Result[str]: + resolved = _resolve_and_check_result(path) + if not resolved.ok: + return Result(data="", errors=resolved.errors) + p = resolved.data + if isinstance(p, NilPath): + return Result(data="", errors=resolved.errors) + if not p.exists(): + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.NOT_FOUND, message=f"file not found: {path}", source="mcp.set_file_slice_result")]) + try: + lines = p.read_text(encoding="utf-8").splitlines(keepends=True) + start_idx = start_line - 1 + end_idx = end_line + if new_content and not new_content.endswith("\n"): + new_content += "\n" + new_lines = new_content.splitlines(keepends=True) if new_content else [] + lines[start_idx:end_idx] = new_lines + p.write_text("".join(lines), encoding="utf-8") + return Result(data=f"Successfully updated lines {start_line}-{end_line} in {path}") + except Exception as e: + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(e), source="mcp.set_file_slice_result", original=e)]) #endregion: Result Variants def edit_file(path: str, old_string: str, new_string: str, replace_all: bool = False) -> str: """ Replace exact string match in a file. Preserves indentation and line endings. Drop-in replacement for native edit tool that destroys 1-space indentation. + + Thin wrapper over edit_file_result; the legacy str shape is preserved + for backward compatibility, but the try/except Exception lives in + the Result variant. """ - p, err = _resolve_and_check(path) - if err: - return err - assert p is not None - if not p.exists(): - return f"ERROR: file not found: {path}" - if not old_string: - return "ERROR: old_string cannot be empty" - try: - content = p.read_text(encoding="utf-8") - if old_string not in content: - return f"ERROR: old_string not found in '{path}'" - count = content.count(old_string) - if count > 1 and not replace_all: - return f"ERROR: Found {count} matches for old_string in '{path}'. Use replace_all=true or provide more context to make it unique." - if replace_all: - new_content = content.replace(old_string, new_string) - p.write_text(new_content, encoding="utf-8") - return f"Successfully replaced {count} occurrences in '{path}'" - else: - new_content = content.replace(old_string, new_string, 1) - p.write_text(new_content, encoding="utf-8") - return f"Successfully replaced 1 occurrence in '{path}'" - except Exception as e: - return f"ERROR editing '{path}': {e}" + resolved = edit_file_result(path, old_string, new_string, replace_all) + if resolved.ok: + return resolved.data + return "; ".join(e.ui_message() for e in resolved.errors) def get_file_summary(path: str) -> str: """