aboutsummaryrefslogtreecommitdiffstats
path: root/server/patch_server.py
blob: b9899305f15aaf9e5df6d227a66e58d5565a4216 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#!/usr/bin/env python3
"""Inject idatui's extra ida-pro-mcp tools into the installed server package.

ida-pro-mcp exposes no delete-type tool, which idatui's struct editor needs for
the 'D' in CRUD. Rather than vendor/fork the server, we keep the tool source here
and append it (idempotently) to the installed ``api_types.py``. That module is
imported by every worker (``python -m ida_pro_mcp.idalib_server``), so the tool
registers itself via ``@tool`` on the shared ``MCP_SERVER`` — no server code is
forked, and re-running this (spawn.sh does, on every start) re-applies it after a
reinstall/upgrade of ida-pro-mcp.

Run with the *same* interpreter the server uses (the idalib-mcp entry point's
``/usr/bin/python``), so it patches the file the workers actually import.
Safe to run repeatedly; a no-op once applied.
"""
from __future__ import annotations

import importlib.util
import pathlib
import sys

BEGIN = "# >>> idatui-ext: begin (auto-injected by server/patch_server.py) >>>"
END = "# <<< idatui-ext: end <<<"

# Appended to ida_pro_mcp/ida_mcp/api_types.py, which already imports
# ``Annotated``, ``tool``, ``idasync`` and ``ida_typeinf`` at module scope.
SNIPPET = f'''

{BEGIN}
@tool
@idasync
def del_type(
    name: Annotated[str, "Local type name to delete (struct/union/enum/typedef)"],
) -> dict:
    """Delete a named local type from the local type library."""
    til = ida_typeinf.get_idati()
    ok = ida_typeinf.del_named_type(til, name, ida_typeinf.NTF_TYPE)
    if not ok:
        return {{"name": name, "error": f"Type '{{name}}' not found or could not be deleted"}}
    return {{"name": name, "deleted": True}}
{END}
'''


def api_types_path() -> pathlib.Path | None:
    """Locate ida_pro_mcp/ida_mcp/api_types.py without importing it (importing the
    submodule would pull in IDA, which isn't available outside a worker)."""
    spec = importlib.util.find_spec("ida_pro_mcp")  # top-level pkg is IDA-free
    if spec is None or not spec.submodule_search_locations:
        return None
    p = pathlib.Path(spec.submodule_search_locations[0]) / "ida_mcp" / "api_types.py"
    return p if p.exists() else None


def main() -> int:
    path = api_types_path()
    if path is None:
        print("idatui: ida_pro_mcp not found; skipping del_type injection", file=sys.stderr)
        return 0
    text = path.read_text()
    if BEGIN in text:
        return 0  # already applied
    try:
        path.write_text(text.rstrip() + "\n" + SNIPPET)
    except OSError as e:
        print(f"idatui: could not patch {path}: {e}", file=sys.stderr)
        return 1
    print(f"idatui: injected del_type into {path}", file=sys.stderr)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())