CoplayDev/unity-mcp · error · RuntimeError

anchor not found: {anchor}

Error message

anchor not found: {anchor}

What it means

In script_apply_edits, the anchor_insert operation searches for an anchor pattern in the file text using _find_best_anchor_match (a heuristic regex matcher). If no match is found AND the edit's allow_noop flag is False (or absent, since the default is True), this RuntimeError fires with the unmatched anchor. When allow_noop is True (default), a missing anchor is silently skipped (continue), so this error only fires when the caller explicitly set allow_noop=False.

Source

Thrown at Server/src/services/tools/script_apply_edits.py:421

            if not text.endswith("\n"):
                text += "\n"
            text += append_text
            if not text.endswith("\n"):
                text += "\n"
        elif op == "anchor_insert":
            anchor = edit.get("anchor", "")
            position = (edit.get("position") or "before").lower()
            insert_text = edit.get("text", "")
            flags = re.MULTILINE | (
                re.IGNORECASE if edit.get("ignore_case") else 0)

            # Find the best match using improved heuristics
            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)

View on GitHub (pinned to c21bf496bc)

Solutions

  1. Re-read the current file content and recompute the anchor to match the latest text.
  2. Set allow_noop=true (or omit it — default is True) so a missing anchor is skipped instead of raising.
  3. Enable ignore_case=true if the mismatch is case-related.
  4. Simplify the anchor to a more stable, unique substring (e.g., a method signature line).

Example fix

// before
edits=[{"op": "anchor_insert", "anchor": "class OldName {", "position": "before", "text": "// header\n", "allow_noop": false}]
// after — re-read file, use current anchor, and allow noop
edits=[{"op": "anchor_insert", "anchor": "class NewName {", "position": "before", "text": "// header\n"}]
Defensive patterns

Strategy: validation

Validate before calling

import re

def anchor_exists(anchor: str, text: str, ignore_case: bool = False) -> bool:
    flags = re.MULTILINE | (re.IGNORECASE if ignore_case else 0)
    return re.search(anchor, text, flags) is not None

# Before applying edits, verify anchors against the CURRENT file text:
for edit in edits:
    if edit.get("op") == "anchor_insert":
        if not anchor_exists(edit["anchor"], current_text, edit.get("ignore_case", False)):
            if not edit.get("allow_noop", True):
                print(f"Anchor not found and allow_noop is False: {edit['anchor']}")

Type guard

import re

def will_anchor_match(edit: dict, text: str) -> bool:
    if edit.get("op") != "anchor_insert":
        return True
    flags = re.MULTILINE | (re.IGNORECASE if edit.get("ignore_case") else 0)
    return re.search(edit.get("anchor", ""), text, flags) is not None

Prevention

When it happens

Trigger: An edit with op=anchor_insert, allow_noop=false, and an anchor string that does not appear in the file (e.g., the file changed since the anchor was computed); the anchor has leading/trailing whitespace that doesn't match; ignore_case is False and the case differs; the anchor was computed against a different version of the file.

Common situations: An AI computed an anchor from a stale file read (file was edited by another process or a prior edit in the same batch shifted content); the anchor has subtle whitespace differences; case sensitivity mismatch; the anchor targets a code region that was already removed.

Related errors


AI-assisted analysis of CoplayDev/unity-mcp@c21bf496bc (2026-08-13). Data as JSON: /api/errors/55d320a75fa2b138. Report an issue: GitHub.