{"record":{"id":"b7c27ab91f15e887","repo":"CoplayDev/unity-mcp","slug":"replace-range-out-of-bounds","errorCode":null,"errorMessage":"replace_range out of bounds","messagePattern":"replace_range out of bounds","errorType":"validation","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"Server/src/services/tools/script_apply_edits.py","lineNumber":434,"sourceCode":"            match = _find_best_anchor_match(\n                anchor, text, flags, bool(edit.get(\"prefer_last\", True)))\n            if not match:\n                if edit.get(\"allow_noop\", True):\n                    continue\n                raise RuntimeError(f\"anchor not found: {anchor}\")\n            idx = match.start() if position == \"before\" else match.end()\n            text = text[:idx] + insert_text + text[idx:]\n        elif op == \"replace_range\":\n            start_line = int(edit.get(\"startLine\", 1))\n            start_col = int(edit.get(\"startCol\", 1))\n            end_line = int(edit.get(\"endLine\", start_line))\n            end_col = int(edit.get(\"endCol\", 1))\n            replacement = edit.get(\"text\", \"\")\n            lines = text.splitlines(keepends=True)\n            max_line = len(lines) + 1  # 1-based, exclusive end\n            if (start_line < 1 or end_line < start_line or end_line > max_line\n                    or start_col < 1 or end_col < 1):\n                raise RuntimeError(\"replace_range out of bounds\")\n\n            def index_of(line: int, col: int) -> int:\n                if line <= len(lines):\n                    return sum(len(l) for l in lines[: line - 1]) + (col - 1)\n                return sum(len(l) for l in lines)\n            a = index_of(start_line, start_col)\n            b = index_of(end_line, end_col)\n            text = text[:a] + replacement + text[b:]\n        elif op == \"regex_replace\":\n            pattern = edit.get(\"pattern\", \"\")\n            repl = edit.get(\"replacement\", \"\")\n            # Translate $n backrefs (our input) to Python \\g<n>\n            repl_py = re.sub(r\"\\$(\\d+)\", r\"\\\\g<\\1>\", repl)\n            count = int(edit.get(\"count\", 0))  # 0 = replace all\n            flags = re.MULTILINE\n            if edit.get(\"ignore_case\"):\n                flags |= re.IGNORECASE\n            text = re.sub(pattern, repl_py, text, count=count, flags=flags)","sourceCodeStart":416,"sourceCodeEnd":452,"githubUrl":"https://github.com/CoplayDev/unity-mcp/blob/c21bf496bca87d54e75bad048563c3adb1782081/Server/src/services/tools/script_apply_edits.py#L416-L452","documentation":"In script_apply_edits, the replace_range operation validates 1-based line/column coordinates before splicing. The bounds check rejects: start_line < 1, end_line < start_line, end_line > len(lines)+1 (exclusive end allows appending), or any column < 1. If any condition fails, RuntimeError fires. The coordinates refer to the file text BEFORE this edit is applied (edits apply sequentially to the evolving text buffer).","triggerScenarios":"An edit specifies endLine beyond the file length; startCol or endCol is 0 (columns are 1-based); endLine < startLine (inverted range); the file was shortened by a prior edit in the same batch, making previously-valid coordinates stale; a caller uses 0-based line numbers.","commonSituations":"An AI computed line numbers from a stale file read; a prior edit in the same batch deleted lines, shifting indices; the caller uses 0-based indexing (this tool is 1-based); an edit targets the end of file but endLine exceeds len(lines)+1.","solutions":["Re-read the file and recompute line/col coordinates against the current content.","Remember coordinates are 1-based: the first line is 1, the first column is 1 (not 0).","endLine is exclusive and may be len(lines)+1 to append; do not exceed that.","If stacking multiple edits, apply them in an order that does not invalidate later coordinates, or recompute after each."],"exampleFix":"// before (0-based, stale)\nedits=[{\"op\": \"replace_range\", \"startLine\": 0, \"startCol\": 0, \"endLine\": 5, \"endCol\": 0, \"text\": \"// replaced\\n\"}]\n// after (1-based, validated against current file)\nedits=[{\"op\": \"replace_range\", \"startLine\": 1, \"startCol\": 1, \"endLine\": 6, \"endCol\": 1, \"text\": \"// replaced\\n\"}]","handlingStrategy":"validation","validationCode":"def validate_replace_range(edit: dict, text: str) -> None:\n    lines = text.splitlines(keepends=True)\n    max_line = len(lines) + 1\n    start_line = int(edit.get(\"startLine\", 1))\n    start_col = int(edit.get(\"startCol\", 1))\n    end_line = int(edit.get(\"endLine\", start_line))\n    end_col = int(edit.get(\"endCol\", 1))\n    if (start_line < 1 or end_line < start_line or end_line > max_line\n            or start_col < 1 or end_col < 1):\n        raise ValueError(f\"replace_range out of bounds: file has {len(lines)} lines, \"\n                         f\"requested start={start_line}:{start_col} end={end_line}:{end_col}\")\n\n# Call with the CURRENT text before script_apply_edits:\nfor edit in edits:\n    if edit.get(\"op\") == \"replace_range\":\n        validate_replace_range(edit, current_text)","typeGuard":"def is_in_bounds_replace_range(edit: dict, text: str) -> bool:\n    lines = text.splitlines(keepends=True)\n    max_line = len(lines) + 1\n    sl = int(edit.get(\"startLine\", 1)); sc = int(edit.get(\"startCol\", 1))\n    el = int(edit.get(\"endLine\", sl)); ec = int(edit.get(\"endCol\", 1))\n    return not (sl < 1 or el < sl or el > max_line or sc < 1 or ec < 1)","tryCatchPattern":null,"preventionTips":["Use 1-based line and column numbers (first line is 1, first column is 1).","Re-read the file and recompute coordinates if any prior edit may have shifted lines.","endLine is exclusive and may be len(lines)+1 for append; never exceed that."],"tags":["script-edit","replace-range","out-of-bounds","coordinates","mcp-tool"],"backgroundTag":null,"analyzedSha":"c21bf496bca87d54e75bad048563c3adb1782081","analyzedAt":"2026-08-13T17:36:56.095Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}