{"record":{"id":"ea17c087f94758a6","repo":"CoplayDev/unity-mcp","slug":"unknown-edit-op-op-allowed-allowed-use-op","errorCode":null,"errorMessage":"unknown edit op: {op}; allowed: {allowed}. Use 'op' (aliases accepted: type/mode/operation).","messagePattern":"unknown edit op: (.+?); allowed: (.+?)\\. Use 'op' \\(aliases accepted: type/mode/operation\\)\\.","errorType":"validation","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"Server/src/services/tools/script_apply_edits.py","lineNumber":455,"sourceCode":"                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)\n        else:\n            allowed = \"anchor_insert, prepend, append, replace_range, regex_replace\"\n            raise RuntimeError(\n                f\"unknown edit op: {op}; allowed: {allowed}. Use 'op' (aliases accepted: type/mode/operation).\")\n    return text\n\n\ndef _find_best_anchor_match(pattern: str, text: str, flags: int, prefer_last: bool = True):\n    \"\"\"\n    Find the best anchor match using improved heuristics.\n\n    For patterns like \\\\s*}\\\\s*$ that are meant to find class-ending braces,\n    this function uses heuristics to choose the most semantically appropriate match:\n\n    1. If prefer_last=True, prefer the last match (common for class-end insertions)\n    2. Use indentation levels to distinguish class vs method braces\n    3. Consider context to avoid matches inside strings/comments\n\n    Args:\n        pattern: Regex pattern to search for\n        text: Text to search in  ","sourceCodeStart":437,"sourceCodeEnd":473,"githubUrl":"https://github.com/CoplayDev/unity-mcp/blob/c21bf496bca87d54e75bad048563c3adb1782081/Server/src/services/tools/script_apply_edits.py#L437-L473","documentation":"In script_apply_edits, after checking op is non-empty, the dispatch chain (prepend, append, anchor_insert, replace_range, regex_replace) is evaluated. If op matches none of these, the else branch fires listing the five allowed values. This means the caller provided a recognized-but-unsupported operation name, or a typo. Unlike error 176 (op missing), here op IS present but has an unrecognized value.","triggerScenarios":"A caller uses op='insert' (should be 'anchor_insert'); op='replace' (should be 'replace_range'); op='delete' (not supported); a typo like 'apend' or 'replce'; a caller assumes an op like 'overwrite' or 'line_insert' exists.","commonSituations":"An LLM invents a plausible op name not in the allowed set; a caller abbreviates an op name; documentation/version mismatch where an op was renamed; a caller confuses this tool's ops with another editor's vocabulary (e.g., LSP TextDocumentEdit operations).","solutions":["Use one of the exact allowed values: anchor_insert, prepend, append, replace_range, regex_replace.","Check for typos and abbreviations — the match is exact and case-insensitive.","If you need to delete text, use replace_range with text=\"\" covering the range, or regex_replace with an empty replacement."],"exampleFix":"// before\nedits=[{\"op\": \"insert\", \"anchor\": \"class Foo {\", \"text\": \"// new\\n\"}]\n// after\nedits=[{\"op\": \"anchor_insert\", \"anchor\": \"class Foo {\", \"position\": \"before\", \"text\": \"// new\\n\"}]","handlingStrategy":"validation","validationCode":"ALLOWED_OPS = {\"anchor_insert\", \"prepend\", \"append\", \"replace_range\", \"regex_replace\"}\nfor i, edit in enumerate(edits):\n    op = (edit.get(\"op\") or edit.get(\"operation\") or edit.get(\"type\") or edit.get(\"mode\") or \"\").strip().lower()\n    if op and op not in ALLOWED_OPS:\n        raise ValueError(f\"Edit {i} has unknown op '{op}'. Allowed: {sorted(ALLOWED_OPS)}\")","typeGuard":"ALLOWED_OPS = {\"anchor_insert\", \"prepend\", \"append\", \"replace_range\", \"regex_replace\"}\n\ndef edit_has_recognized_op(edit: dict) -> bool:\n    op = (edit.get(\"op\") or edit.get(\"operation\") or edit.get(\"type\") or edit.get(\"mode\") or \"\").strip().lower()\n    return op == \"\" or op in ALLOWED_OPS","tryCatchPattern":null,"preventionTips":["Use exact op names: anchor_insert, prepend, append, replace_range, regex_replace.","For deletion, use replace_range with text=\"\" or regex_replace with empty replacement.","Validate op names against the allowed set before applying edits."],"tags":["script-edit","edit-op","unknown-op","validation","mcp-tool"],"backgroundTag":null,"analyzedSha":"c21bf496bca87d54e75bad048563c3adb1782081","analyzedAt":"2026-08-13T17:36:56.095Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}