CoplayDev/unity-mcp · error · RuntimeError
replace_range out of bounds
Error message
replace_range out of bounds
What it means
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).
Source
Thrown at Server/src/services/tools/script_apply_edits.py:434
match = _find_best_anchor_match(
anchor, text, flags, bool(edit.get("prefer_last", True)))
if not match:
if edit.get("allow_noop", True):
continue
raise RuntimeError(f"anchor not found: {anchor}")
idx = match.start() if position == "before" else match.end()
text = text[:idx] + insert_text + text[idx:]
elif op == "replace_range":
start_line = int(edit.get("startLine", 1))
start_col = int(edit.get("startCol", 1))
end_line = int(edit.get("endLine", start_line))
end_col = int(edit.get("endCol", 1))
replacement = edit.get("text", "")
lines = text.splitlines(keepends=True)
max_line = len(lines) + 1 # 1-based, exclusive end
if (start_line < 1 or end_line < start_line or end_line > max_line
or start_col < 1 or end_col < 1):
raise RuntimeError("replace_range out of bounds")
def index_of(line: int, col: int) -> int:
if line <= len(lines):
return sum(len(l) for l in lines[: line - 1]) + (col - 1)
return sum(len(l) for l in lines)
a = index_of(start_line, start_col)
b = index_of(end_line, end_col)
text = text[:a] + replacement + text[b:]
elif op == "regex_replace":
pattern = edit.get("pattern", "")
repl = edit.get("replacement", "")
# Translate $n backrefs (our input) to Python \g<n>
repl_py = re.sub(r"\$(\d+)", r"\\g<\1>", repl)
count = int(edit.get("count", 0)) # 0 = replace all
flags = re.MULTILINE
if edit.get("ignore_case"):
flags |= re.IGNORECASE
text = re.sub(pattern, repl_py, text, count=count, flags=flags)View on GitHub (pinned to c21bf496bc)
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.
Example fix
// before (0-based, stale)
edits=[{"op": "replace_range", "startLine": 0, "startCol": 0, "endLine": 5, "endCol": 0, "text": "// replaced\n"}]
// after (1-based, validated against current file)
edits=[{"op": "replace_range", "startLine": 1, "startCol": 1, "endLine": 6, "endCol": 1, "text": "// replaced\n"}] Defensive patterns
Strategy: validation
Validate before calling
def validate_replace_range(edit: dict, text: str) -> None:
lines = text.splitlines(keepends=True)
max_line = len(lines) + 1
start_line = int(edit.get("startLine", 1))
start_col = int(edit.get("startCol", 1))
end_line = int(edit.get("endLine", start_line))
end_col = int(edit.get("endCol", 1))
if (start_line < 1 or end_line < start_line or end_line > max_line
or start_col < 1 or end_col < 1):
raise ValueError(f"replace_range out of bounds: file has {len(lines)} lines, "
f"requested start={start_line}:{start_col} end={end_line}:{end_col}")
# Call with the CURRENT text before script_apply_edits:
for edit in edits:
if edit.get("op") == "replace_range":
validate_replace_range(edit, current_text) Type guard
def is_in_bounds_replace_range(edit: dict, text: str) -> bool:
lines = text.splitlines(keepends=True)
max_line = len(lines) + 1
sl = int(edit.get("startLine", 1)); sc = int(edit.get("startCol", 1))
el = int(edit.get("endLine", sl)); ec = int(edit.get("endCol", 1))
return not (sl < 1 or el < sl or el > max_line or sc < 1 or ec < 1) Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- op is required; allowed: {allowed}. Use 'op' (aliases accept
- anchor not found: {anchor}
- unknown edit op: {op}; allowed: {allowed}. Use 'op' (aliases
- 'commands' must be a non-empty list of command specification
- batch_execute supports up to {max_commands} commands (config
AI-assisted analysis of CoplayDev/unity-mcp@c21bf496bc (2026-08-13).
Data as JSON: /api/errors/b7c27ab91f15e887.
Report an issue: GitHub.