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())
|