ida-code 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ida_code/undo.py ADDED
@@ -0,0 +1,102 @@
1
+ import logging
2
+
3
+ from fastmcp.exceptions import ToolError
4
+
5
+ from ida_code import session
6
+
7
+ log = logging.getLogger(__name__)
8
+
9
+
10
+ def get_status() -> dict:
11
+ """Return current undo/redo availability and action labels."""
12
+ session.require_open()
13
+
14
+ import ida_undo
15
+
16
+ undo_label = ida_undo.get_undo_action_label() or ""
17
+ redo_label = ida_undo.get_redo_action_label() or ""
18
+
19
+ return {
20
+ "can_undo": bool(undo_label),
21
+ "undo_action": undo_label,
22
+ "can_redo": bool(redo_label),
23
+ "redo_action": redo_label,
24
+ }
25
+
26
+
27
+ def perform_undo(steps: int = 1) -> dict:
28
+ """Undo the last action(s). Returns details of what was undone."""
29
+ session.require_open()
30
+
31
+ if steps < 1:
32
+ raise ToolError("steps must be at least 1.")
33
+
34
+ import ida_undo
35
+
36
+ undo_label = ida_undo.get_undo_action_label() or ""
37
+ if not undo_label:
38
+ raise ToolError("Nothing to undo.")
39
+
40
+ actions: list[str] = []
41
+ for _ in range(steps):
42
+ label = ida_undo.get_undo_action_label() or ""
43
+ if not label:
44
+ break
45
+ ida_undo.perform_undo()
46
+ actions.append(label)
47
+
48
+ # Database state changed — reset executor namespace.
49
+ from ida_code.executor import reset
50
+ reset()
51
+
52
+ undo_next = ida_undo.get_undo_action_label() or ""
53
+ redo_next = ida_undo.get_redo_action_label() or ""
54
+
55
+ log.info("Undo %d/%d steps: %s", len(actions), steps, actions)
56
+ return {
57
+ "status": "undone",
58
+ "steps_requested": steps,
59
+ "steps_performed": len(actions),
60
+ "actions": actions,
61
+ "next_undo": undo_next,
62
+ "next_redo": redo_next,
63
+ }
64
+
65
+
66
+ def perform_redo(steps: int = 1) -> dict:
67
+ """Redo the last undone action(s). Returns details of what was redone."""
68
+ session.require_open()
69
+
70
+ if steps < 1:
71
+ raise ToolError("steps must be at least 1.")
72
+
73
+ import ida_undo
74
+
75
+ redo_label = ida_undo.get_redo_action_label() or ""
76
+ if not redo_label:
77
+ raise ToolError("Nothing to redo.")
78
+
79
+ actions: list[str] = []
80
+ for _ in range(steps):
81
+ label = ida_undo.get_redo_action_label() or ""
82
+ if not label:
83
+ break
84
+ ida_undo.perform_redo()
85
+ actions.append(label)
86
+
87
+ # Database state changed — reset executor namespace.
88
+ from ida_code.executor import reset
89
+ reset()
90
+
91
+ undo_next = ida_undo.get_undo_action_label() or ""
92
+ redo_next = ida_undo.get_redo_action_label() or ""
93
+
94
+ log.info("Redo %d/%d steps: %s", len(actions), steps, actions)
95
+ return {
96
+ "status": "redone",
97
+ "steps_requested": steps,
98
+ "steps_performed": len(actions),
99
+ "actions": actions,
100
+ "next_undo": undo_next,
101
+ "next_redo": redo_next,
102
+ }
ida_code/variables.py ADDED
@@ -0,0 +1,206 @@
1
+ """Variable inspection and modification (local/decompiler and global)."""
2
+
3
+ import logging
4
+
5
+ from fastmcp.exceptions import ToolError
6
+
7
+ from ida_code import session
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+
12
+ def _decompile_func(func_ea: int):
13
+ """Decompile the function at *func_ea*, returning a cfunc_t.
14
+
15
+ Raises ToolError if Hex-Rays is unavailable or decompilation fails.
16
+ """
17
+ try:
18
+ import ida_hexrays
19
+ except ImportError:
20
+ raise ToolError("Hex-Rays decompiler is not available.")
21
+
22
+ import ida_funcs
23
+
24
+ pfn = ida_funcs.get_func(func_ea)
25
+ if pfn is None:
26
+ raise ToolError(f"Address {func_ea:#x} is not within a recognized function.")
27
+
28
+ try:
29
+ cfunc = ida_hexrays.decompile(pfn.start_ea)
30
+ except ida_hexrays.DecompilationFailure as e:
31
+ raise ToolError(f"Decompilation failed: {e}")
32
+ except Exception as e:
33
+ raise ToolError(f"{type(e).__name__}: {e}")
34
+
35
+ if cfunc is None:
36
+ raise ToolError("Decompilation returned no result.")
37
+ return cfunc
38
+
39
+
40
+ def _find_lvar(cfunc, name: str):
41
+ """Find a local variable by name in *cfunc*.lvars.
42
+
43
+ Raises ToolError if not found, listing available names.
44
+ """
45
+ for lv in cfunc.lvars:
46
+ if lv.name == name:
47
+ return lv
48
+
49
+ available = [lv.name for lv in cfunc.lvars if lv.name]
50
+ raise ToolError(
51
+ f"Local variable '{name}' not found. "
52
+ f"Available: {available}"
53
+ )
54
+
55
+
56
+ def _lvar_to_dict(lv, func_ea: int) -> dict:
57
+ """Convert a local variable (lvar_t) to a result dict."""
58
+ import ida_funcs
59
+
60
+ func_name = ida_funcs.get_func_name(func_ea) or f"sub_{func_ea:x}"
61
+ return {
62
+ "name": lv.name,
63
+ "type": str(lv.type()),
64
+ "width": lv.width,
65
+ "is_arg": lv.is_arg_var,
66
+ "function": func_name,
67
+ "scope": "local",
68
+ }
69
+
70
+
71
+ def _global_to_dict(ea: int) -> dict:
72
+ """Convert a global address to a result dict."""
73
+ import ida_name
74
+ import ida_nalt
75
+ import ida_typeinf
76
+ import idc
77
+
78
+ name = ida_name.get_name(ea) or f"unk_{ea:x}"
79
+
80
+ # Try to get type info.
81
+ tif = ida_typeinf.tinfo_t()
82
+ if ida_nalt.get_tinfo(tif, ea):
83
+ type_str = str(tif)
84
+ else:
85
+ type_str = idc.get_type(ea) or ""
86
+
87
+ return {
88
+ "name": name,
89
+ "type": type_str,
90
+ "address": f"{ea:#x}",
91
+ "scope": "global",
92
+ }
93
+
94
+
95
+ def _parse_type(type_str: str):
96
+ """Parse a C type string into a tinfo_t.
97
+
98
+ Raises ToolError if parsing fails.
99
+ """
100
+ import ida_typeinf
101
+
102
+ tif = ida_typeinf.tinfo_t()
103
+ til = ida_typeinf.get_idati()
104
+ decl = f"{type_str} __dummy;"
105
+ if not ida_typeinf.parse_decl(tif, til, decl, ida_typeinf.PT_SIL):
106
+ raise ToolError(f"Failed to parse type: '{type_str}'")
107
+ return tif
108
+
109
+
110
+ def get_local_variable(func_ea: int, name: str) -> dict:
111
+ """Get info about a local (decompiler) variable."""
112
+ session.require_open()
113
+
114
+ cfunc = _decompile_func(func_ea)
115
+ lv = _find_lvar(cfunc, name)
116
+ return _lvar_to_dict(lv, func_ea)
117
+
118
+
119
+ def get_global_variable(ea: int) -> dict:
120
+ """Get info about a global variable/name at *ea*."""
121
+ session.require_open()
122
+ return _global_to_dict(ea)
123
+
124
+
125
+ def set_local_variable(
126
+ func_ea: int,
127
+ name: str,
128
+ new_name: str | None = None,
129
+ new_type: str | None = None,
130
+ ) -> dict:
131
+ """Rename and/or retype a local (decompiler) variable."""
132
+ session.require_open()
133
+
134
+ import ida_hexrays
135
+ import ida_funcs
136
+
137
+ pfn = ida_funcs.get_func(func_ea)
138
+ if pfn is None:
139
+ raise ToolError(f"Address {func_ea:#x} is not within a recognized function.")
140
+ start_ea = pfn.start_ea
141
+
142
+ # Verify the variable exists first.
143
+ cfunc = _decompile_func(func_ea)
144
+ _find_lvar(cfunc, name)
145
+
146
+ if new_name is not None:
147
+ ok = ida_hexrays.rename_lvar(start_ea, name, new_name)
148
+ if not ok:
149
+ raise ToolError(f"Failed to rename local variable '{name}' to '{new_name}'.")
150
+ log.info("Renamed local variable '%s' -> '%s' in func at %#x", name, new_name, start_ea)
151
+
152
+ if new_type is not None:
153
+ tif = _parse_type(new_type)
154
+ lsi = ida_hexrays.lvar_saved_info_t()
155
+ current_name = new_name if new_name is not None else name
156
+ lsi.name = current_name
157
+ lsi.type = tif
158
+ lsi.size = tif.get_size()
159
+ lsi.flags = ida_hexrays.LVINF_TYPE
160
+
161
+ # Find the lvar to get its location info.
162
+ cfunc2 = _decompile_func(func_ea)
163
+ lv = _find_lvar(cfunc2, current_name)
164
+ lsi.ll = lv.location
165
+
166
+ ok = ida_hexrays.modify_user_lvar_info(start_ea, ida_hexrays.MLI_TYPE, lsi)
167
+ if not ok:
168
+ raise ToolError(f"Failed to retype local variable '{current_name}' to '{new_type}'.")
169
+ log.info("Retyped local variable '%s' to '%s' in func at %#x", current_name, new_type, start_ea)
170
+
171
+ # Re-decompile and return updated info.
172
+ final_name = new_name if new_name is not None else name
173
+ cfunc_final = _decompile_func(func_ea)
174
+ lv_final = _find_lvar(cfunc_final, final_name)
175
+ result = _lvar_to_dict(lv_final, func_ea)
176
+ result["status"] = "modified"
177
+ return result
178
+
179
+
180
+ def set_global_variable(
181
+ ea: int,
182
+ new_name: str | None = None,
183
+ new_type: str | None = None,
184
+ ) -> dict:
185
+ """Rename and/or retype a global variable/name."""
186
+ session.require_open()
187
+
188
+ import ida_name
189
+ import ida_typeinf
190
+
191
+ if new_name is not None:
192
+ ok = ida_name.set_name(ea, new_name, ida_name.SN_CHECK)
193
+ if not ok:
194
+ raise ToolError(f"Failed to rename global at {ea:#x} to '{new_name}'.")
195
+ log.info("Renamed global at %#x -> '%s'", ea, new_name)
196
+
197
+ if new_type is not None:
198
+ tif = _parse_type(new_type)
199
+ ok = ida_typeinf.apply_tinfo(ea, tif, ida_typeinf.TINFO_DEFINITE)
200
+ if not ok:
201
+ raise ToolError(f"Failed to retype global at {ea:#x} to '{new_type}'.")
202
+ log.info("Retyped global at %#x to '%s'", ea, new_type)
203
+
204
+ result = _global_to_dict(ea)
205
+ result["status"] = "modified"
206
+ return result
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.4
2
+ Name: ida-code
3
+ Version: 0.2.1
4
+ Summary: MCP server for AI-assisted IDAPython scripting via idalib
5
+ Project-URL: Homepage, https://github.com/Dil4rd/ida-code
6
+ Project-URL: Repository, https://github.com/Dil4rd/ida-code
7
+ Project-URL: Issues, https://github.com/Dil4rd/ida-code/issues
8
+ Project-URL: Changelog, https://github.com/Dil4rd/ida-code/blob/main/CHANGELOG.md
9
+ Author: Dil4rd
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: ida,ida-pro,idalib,idapython,mcp,reverse-engineering
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Information Technology
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Security
21
+ Classifier: Topic :: Software Development :: Disassemblers
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: fastmcp<4,>=3.0
25
+ Requires-Dist: lief>=0.15
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # ida-code
31
+
32
+ MCP server that lets AI coding agents interact with IDA Pro. Open binaries, decompile, run IDAPython, search the API docs — all through tool calls.
33
+
34
+ Built on [idalib](https://docs.hex-rays.com/developer-guide/idalib) for headless in-process operation and [fastmcp](https://github.com/jlowin/fastmcp) for the MCP transport.
35
+
36
+ > **Requires** a licensed IDA Pro 9.2+ with idalib support. `ida-code` does not install or replace IDA Pro — it loads `idapro` from your existing install at startup.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ uv tool install ida-code
42
+ ```
43
+
44
+ This puts the `ida-code` CLI on your `PATH` (via `uv`'s tool dir) so MCP clients can launch it directly. Don't have uv? `pip install ida-code` works too.
45
+
46
+ Then point `IDA_INSTALL_DIR` at your IDA Pro install (the directory that contains `idalib/python/`):
47
+
48
+ | OS | Typical path |
49
+ |---|---|
50
+ | Linux | `/opt/ida-pro-9.3` |
51
+ | macOS | `/Applications/IDA Professional 9.3.app/Contents/MacOS` |
52
+ | Windows | `C:\Program Files\IDA Professional 9.3` |
53
+
54
+ ## Use with Claude Code
55
+
56
+ Add `ida-code` to your project's `.mcp.json`:
57
+
58
+ ```json
59
+ {
60
+ "mcpServers": {
61
+ "ida-code": {
62
+ "command": "ida-code",
63
+ "env": {
64
+ "IDA_INSTALL_DIR": "/opt/ida-pro-9.3"
65
+ }
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ Restart Claude Code; the server is picked up automatically. You can confirm it's wired up by asking Claude to open a binary — it should call `open_database` and report architecture, entry point, and load address.
72
+
73
+ For other MCP clients, run the server directly:
74
+
75
+ ```bash
76
+ IDA_INSTALL_DIR=/opt/ida-pro-9.3 ida-code # stdio transport
77
+ ```
78
+
79
+ ## Tools (35)
80
+
81
+ Full parameter docs live in each tool's docstring — surfaced automatically to MCP clients via `tools/list`.
82
+
83
+ | Domain | Tools |
84
+ |---|---|
85
+ | Database | `open_database`, `close_database`, `get_database_info`, `list_architectures` |
86
+ | Code execution | `execute`, `execute_file` |
87
+ | Navigation | `list_functions`, `decompile`, `get_disassembly`, `get_xrefs_to`, `get_xrefs_from` |
88
+ | Annotation | `rename_function`, `retype_function`, `get_comment`, `set_comment`, `delete_comment`, `get_variable`, `set_variable` |
89
+ | Structures | `list_structures`, `get_structure`, `create_structure`, `edit_structure`, `delete_structure` |
90
+ | Snapshots | `list_snapshots`, `create_snapshot`, `restore_snapshot`, `delete_snapshot` |
91
+ | Undo/redo | `get_undo_status`, `perform_undo`, `perform_redo` |
92
+ | Inventory | `get_strings`, `get_imports`, `get_exports` |
93
+ | Search | `search_docs`, `search_examples` |
94
+
95
+ ## Resources & prompts
96
+
97
+ | Type | URI / name | Purpose |
98
+ |---|---|---|
99
+ | Resource | `guidelines://standalone_script` | Boilerplate for standalone idalib scripts |
100
+ | Resource | `guidelines://plugin` | Boilerplate for IDA plugins (`plugin_t`) |
101
+ | Resource | `guidelines://idapython_script` | Boilerplate for IDAPython scripts run inside IDA GUI |
102
+ | Prompt | `reverse_engineer` | Five-phase RE workflow (recon, triage, analysis, annotation, iteration) |
103
+ | Prompt | `create_script` | Coding guidelines for a chosen target script type |
104
+
105
+ ## Transport modes
106
+
107
+ ```bash
108
+ ida-code # stdio (default)
109
+ ida-code --http # streamable-http on 127.0.0.1:8080
110
+ ida-code --http 0.0.0.0:9090 # custom host:port
111
+ ida-code --sse # SSE on 127.0.0.1:8080
112
+ ida-code --sse :9090 # SSE on 127.0.0.1:9090
113
+ ```
114
+
115
+ HTTP/SSE require bearer token auth. Set `MCP_AUTH_TOKEN` or let the server generate one (printed to stderr on startup).
116
+
117
+ ## Environment variables
118
+
119
+ | Variable | Default | Description |
120
+ |---|---|---|
121
+ | `IDA_INSTALL_DIR` | `/opt/ida-pro-9.3` | IDA Pro installation directory (must contain `idalib/python/`) |
122
+ | `LOG_LEVEL` | `WARNING` | Logging verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
123
+ | `MCP_AUTH_TOKEN` | (auto-generated) | Bearer token for HTTP/SSE transports |
124
+
125
+ Doc and example paths are derived from `IDA_INSTALL_DIR` (`docs/`, `python/`, `python/examples/`).
126
+
127
+ ## Install from source
128
+
129
+ ```bash
130
+ git clone https://github.com/Dil4rd/ida-code
131
+ cd ida-code
132
+ uv sync
133
+ uv run ida-code
134
+ ```
135
+
136
+ When wiring a source checkout into `.mcp.json`, use `uv` as the command:
137
+
138
+ ```json
139
+ {
140
+ "mcpServers": {
141
+ "ida-code": {
142
+ "command": "uv",
143
+ "args": ["run", "--directory", "/path/to/ida-code", "ida-code"],
144
+ "env": { "IDA_INSTALL_DIR": "/opt/ida-pro-9.3" }
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ > **Note:** the `fastmcp` dependency is the [community fastmcp](https://github.com/jlowin/fastmcp) package, not the official `mcp` SDK. Don't install `mcp` by mistake.
151
+
152
+ ## Development
153
+
154
+ ```bash
155
+ uv sync --extra dev
156
+ uv run pytest
157
+ ```
158
+
159
+ The test suite covers the executor, doc/example search, comments, snapshots, structures, undo, variables, and Mach-O parsing. Tests that need idalib are skipped if it's not available.
160
+
161
+ ## Credits
162
+
163
+ Thanks to [@p41l](https://github.com/p41l) for ideas and for testing the tool across different LLMs.
164
+
165
+ ## License
166
+
167
+ MIT — see `LICENSE`.
@@ -0,0 +1,21 @@
1
+ ida_code/__init__.py,sha256=zWUY4fR0EJQx_22ZBPqyIDLdAMmZTbMzLR8PdkHzLZE,125
2
+ ida_code/_search_utils.py,sha256=fCrEc-exNzcRMms-cZMFGezxHUkgXmndWg2MVSk__84,1103
3
+ ida_code/comments.py,sha256=EvNuRIagaLpksg2nJETw4GXclXSAALU-pz-rIgUVKY4,5604
4
+ ida_code/config.py,sha256=lJwGy8F9j7yAS6gQYLsWS39B-KExYGPungzN8AOxbwo,358
5
+ ida_code/doc_search.py,sha256=ZQijMTInq93JvcF9CZv16n3jNI-qpc7iAgH6-qiZFqY,7461
6
+ ida_code/example_search.py,sha256=DCDuVrU2suhfn3hL6GhXTwTvdtXihCNaZJsQ6a87Tt4,17375
7
+ ida_code/executor.py,sha256=Ed_j0tpg4nTENQhT2LOrNRClfgh22MUfTHGbMXQvytM,4327
8
+ ida_code/guidelines.py,sha256=lqWgoPRgMEjamO4ORXaN3oKFo4STKFiBgIog6HtyOSM,10008
9
+ ida_code/macho.py,sha256=n8Ju7kWqTVKwFZAleLOaZupcDsvEHorzJchjSvpiKX8,2077
10
+ ida_code/prompts.py,sha256=UI9Wpy5tXzW9pTl3-UQyZrd0Ry2EhuT9agknqva82v4,8226
11
+ ida_code/server.py,sha256=k_NDC-_bYHk9lR7egiAKOdN-JiqiashnU7xinx1gask,33048
12
+ ida_code/session.py,sha256=7abx4BlX-LF8rYDDorc8Dohy0NfMjkyxuAog8EdN-Zc,8592
13
+ ida_code/snapshots.py,sha256=OxiIXIjuGVXQI1gGLH1s2HDPNW4Qm7CREkZTWJWiXoE,2967
14
+ ida_code/structures.py,sha256=JM3QXUGfLMcEZxhYnL6_RD2E2PG4-3JX_eVI3BpswA4,6370
15
+ ida_code/undo.py,sha256=2wYFpfDqPpqfXjyh1zzYAbULnTPgvdt_FNk4SwFcKGE,2705
16
+ ida_code/variables.py,sha256=orj-3qx5eYhsKV2S4oNoUwclWar0YFKWV5lKS1oj8GY,5980
17
+ ida_code-0.2.1.dist-info/METADATA,sha256=YgFfhCRL2oY8xsn48g4dJvn_r8XA_sdXenF2gnnIUDY,6066
18
+ ida_code-0.2.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
19
+ ida_code-0.2.1.dist-info/entry_points.txt,sha256=PxudvHpujlv2Ww0De7UN78LLjsN5VFHqC13znUTdIqg,50
20
+ ida_code-0.2.1.dist-info/licenses/LICENSE,sha256=6q3O_h5USiWV6W1EycYFMGr1IOeirQ8n37KS022-s6c,1063
21
+ ida_code-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ida-code = ida_code.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dil4rd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.