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/__init__.py +2 -0
- ida_code/_search_utils.py +33 -0
- ida_code/comments.py +191 -0
- ida_code/config.py +9 -0
- ida_code/doc_search.py +255 -0
- ida_code/example_search.py +570 -0
- ida_code/executor.py +145 -0
- ida_code/guidelines.py +370 -0
- ida_code/macho.py +67 -0
- ida_code/prompts.py +176 -0
- ida_code/server.py +1011 -0
- ida_code/session.py +293 -0
- ida_code/snapshots.py +110 -0
- ida_code/structures.py +227 -0
- ida_code/undo.py +102 -0
- ida_code/variables.py +206 -0
- ida_code-0.2.1.dist-info/METADATA +167 -0
- ida_code-0.2.1.dist-info/RECORD +21 -0
- ida_code-0.2.1.dist-info/WHEEL +4 -0
- ida_code-0.2.1.dist-info/entry_points.txt +2 -0
- ida_code-0.2.1.dist-info/licenses/LICENSE +21 -0
ida_code/server.py
ADDED
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import hmac
|
|
3
|
+
import logging
|
|
4
|
+
import secrets
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from fastmcp import FastMCP
|
|
10
|
+
from fastmcp.exceptions import ToolError
|
|
11
|
+
|
|
12
|
+
from ida_code import guidelines as _guidelines
|
|
13
|
+
from ida_code import macho as _macho
|
|
14
|
+
from ida_code import session
|
|
15
|
+
from ida_code.config import LOG_LEVEL, MCP_AUTH_TOKEN
|
|
16
|
+
from ida_code.executor import execute as _execute
|
|
17
|
+
from ida_code.doc_search import search as _search_docs
|
|
18
|
+
from ida_code.example_search import search as _search_examples
|
|
19
|
+
from ida_code import comments as _comments
|
|
20
|
+
from ida_code import snapshots as _snapshots
|
|
21
|
+
from ida_code import structures as _structures
|
|
22
|
+
from ida_code import undo as _undo
|
|
23
|
+
from ida_code import variables as _variables
|
|
24
|
+
from ida_code import prompts as _prompts
|
|
25
|
+
|
|
26
|
+
CommentType = Literal["regular", "repeatable", "function", "anterior", "posterior"]
|
|
27
|
+
CommentTypeOrAll = Literal["regular", "repeatable", "function", "anterior", "posterior", ""]
|
|
28
|
+
ExampleCategory = Literal["ui", "disassembler", "decompiler", "debugger", "types", "misc", ""]
|
|
29
|
+
ExampleLevel = Literal["beginner", "intermediate", "advanced", ""]
|
|
30
|
+
|
|
31
|
+
mcp = FastMCP(
|
|
32
|
+
"ida-code",
|
|
33
|
+
instructions=(
|
|
34
|
+
"IDA Pro reverse engineering server. Open binaries, decompile functions, "
|
|
35
|
+
"annotate code, and run IDAPython scripts.\n\n"
|
|
36
|
+
"Typical workflow: open_database → list_functions → decompile → "
|
|
37
|
+
"annotate (rename_function, set_comment, set_variable) → iterate.\n\n"
|
|
38
|
+
"Only one database can be open at a time. Most tools require an open database."
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@mcp.tool
|
|
44
|
+
def list_architectures(path: str) -> list[str]:
|
|
45
|
+
"""List architecture slices in a fat (universal) Mach-O binary.
|
|
46
|
+
|
|
47
|
+
Returns e.g. ["x86_64", "arm64e"]. Returns an empty list if the file
|
|
48
|
+
is not a fat Mach-O. No database needs to be open.
|
|
49
|
+
|
|
50
|
+
Use this to discover available slices before calling open_database
|
|
51
|
+
with the *arch* parameter.
|
|
52
|
+
"""
|
|
53
|
+
return _macho.list_architectures(path)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@mcp.tool
|
|
57
|
+
def open_database(
|
|
58
|
+
path: str,
|
|
59
|
+
auto_analysis: bool = True,
|
|
60
|
+
overwrite: bool = False,
|
|
61
|
+
timeout: int = 0,
|
|
62
|
+
arch: str | None = None,
|
|
63
|
+
) -> dict:
|
|
64
|
+
"""Open a binary or IDA database via idalib.
|
|
65
|
+
|
|
66
|
+
Returns summary info (architecture, segments, entry points, function count).
|
|
67
|
+
If a database is already open, it is closed first.
|
|
68
|
+
|
|
69
|
+
Set overwrite=True to delete any existing .i64/.idb database and force
|
|
70
|
+
a fresh analysis from the original binary.
|
|
71
|
+
|
|
72
|
+
*timeout* limits auto-analysis wait time in seconds (default 0 = unlimited).
|
|
73
|
+
When the timeout expires the database stays open with partial analysis
|
|
74
|
+
and a warning is appended to the summary.
|
|
75
|
+
|
|
76
|
+
*arch* selects a specific architecture slice from a fat (universal) Mach-O
|
|
77
|
+
binary (e.g. "arm64e", "x86_64"). Use list_architectures to discover
|
|
78
|
+
available slices. Ignored for non-fat binaries.
|
|
79
|
+
"""
|
|
80
|
+
return session.open(path, auto_analysis, overwrite, timeout=timeout, arch=arch)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@mcp.tool
|
|
84
|
+
def get_database_info() -> dict:
|
|
85
|
+
"""Return summary info about the current database.
|
|
86
|
+
|
|
87
|
+
Returns processor type, bitness, segments, entry points, and function count
|
|
88
|
+
without opening or closing anything. If no database is open, says so.
|
|
89
|
+
"""
|
|
90
|
+
return session.info()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@mcp.tool
|
|
94
|
+
def close_database() -> dict:
|
|
95
|
+
"""Close the current database and free resources. Requires an open database.
|
|
96
|
+
|
|
97
|
+
The executor namespace is cleared. No database will be open after this call.
|
|
98
|
+
"""
|
|
99
|
+
session.require_open()
|
|
100
|
+
session.close()
|
|
101
|
+
return {"status": "closed"}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@mcp.tool
|
|
105
|
+
def execute(code: str, timeout: int = 30) -> dict:
|
|
106
|
+
"""Execute IDAPython code and return captured output. Requires an open database.
|
|
107
|
+
|
|
108
|
+
The execution namespace persists across calls — variables and functions defined
|
|
109
|
+
in one call are available in subsequent calls. Pre-imported modules:
|
|
110
|
+
``ida_funcs``, ``ida_bytes``, ``ida_name``, ``ida_segment``, ``ida_auto``,
|
|
111
|
+
``ida_idaapi``, ``ida_nalt``, ``ida_xref``, ``ida_ua``, ``ida_entry``,
|
|
112
|
+
``ida_lines``, ``ida_typeinf``, ``ida_hexrays``, ``idautils``, ``idc``.
|
|
113
|
+
|
|
114
|
+
Python tracebacks are returned as normal output for debugging.
|
|
115
|
+
|
|
116
|
+
*timeout* sets the maximum wall-clock seconds (default 30, 0 = unlimited).
|
|
117
|
+
|
|
118
|
+
Returns: ``{"output", "truncated"}``
|
|
119
|
+
"""
|
|
120
|
+
session.require_open()
|
|
121
|
+
text = _execute(code, timeout=timeout)
|
|
122
|
+
return {"output": text, "truncated": len(text) >= 50000}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@mcp.tool
|
|
126
|
+
def execute_file(path: str, args: str | None = None, timeout: int = 30) -> dict:
|
|
127
|
+
"""Execute an IDAPython script file and return captured output. Requires an open database.
|
|
128
|
+
|
|
129
|
+
Reads the file at `path` and executes it. Optionally, `args` provides
|
|
130
|
+
inline code that runs after the file in the same namespace — useful for
|
|
131
|
+
calling functions defined in the script or inspecting results.
|
|
132
|
+
|
|
133
|
+
The execution namespace persists across calls, same as `execute`.
|
|
134
|
+
|
|
135
|
+
*timeout* sets the maximum wall-clock seconds (default 30, 0 = unlimited).
|
|
136
|
+
|
|
137
|
+
Returns: ``{"output", "truncated"}``
|
|
138
|
+
"""
|
|
139
|
+
session.require_open()
|
|
140
|
+
|
|
141
|
+
p = Path(path)
|
|
142
|
+
if not p.is_file():
|
|
143
|
+
raise ToolError(f"File not found: {path}")
|
|
144
|
+
try:
|
|
145
|
+
code = p.read_text(errors="replace")
|
|
146
|
+
except OSError as e:
|
|
147
|
+
raise ToolError(f"Could not read file: {e}")
|
|
148
|
+
|
|
149
|
+
if args:
|
|
150
|
+
code = code + "\n" + args
|
|
151
|
+
|
|
152
|
+
text = _execute(code, timeout=timeout)
|
|
153
|
+
return {"output": text, "truncated": len(text) >= 50000}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _resolve_address(identifier: str) -> int:
|
|
157
|
+
"""Resolve a name or numeric string to an address.
|
|
158
|
+
|
|
159
|
+
Tries hex (with or without ``0x``), then decimal, then IDA name lookup.
|
|
160
|
+
Returns ``ida_idaapi.BADADDR`` on failure.
|
|
161
|
+
"""
|
|
162
|
+
import ida_idaapi
|
|
163
|
+
import ida_name
|
|
164
|
+
|
|
165
|
+
ea = ida_idaapi.BADADDR
|
|
166
|
+
s = identifier.strip()
|
|
167
|
+
|
|
168
|
+
# Try as hex address first (with or without 0x prefix).
|
|
169
|
+
try:
|
|
170
|
+
ea = int(s, 16)
|
|
171
|
+
except ValueError:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
# Try as decimal address.
|
|
175
|
+
if ea == ida_idaapi.BADADDR:
|
|
176
|
+
try:
|
|
177
|
+
ea = int(s, 10)
|
|
178
|
+
except ValueError:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
# Try as a name.
|
|
182
|
+
if ea == ida_idaapi.BADADDR:
|
|
183
|
+
ea = ida_name.get_name_ea(ida_idaapi.BADADDR, s)
|
|
184
|
+
|
|
185
|
+
return ea
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@mcp.tool
|
|
189
|
+
def decompile(function: str, max_length: int = 10000, offset: int = 0) -> dict:
|
|
190
|
+
"""Decompile a function and return pseudocode. Requires an open database.
|
|
191
|
+
|
|
192
|
+
*function* can be a name (e.g. "main", "_objc_msgSend") or a hex address
|
|
193
|
+
(e.g. "0x3f08", "3f08"). The address must fall within a recognized function.
|
|
194
|
+
|
|
195
|
+
Requires the Hex-Rays decompiler.
|
|
196
|
+
|
|
197
|
+
*max_length* caps the pseudocode returned (default 10000 chars).
|
|
198
|
+
*offset* starts from this character position (for paging).
|
|
199
|
+
If ``truncated`` is true, call again with ``offset=<offset + max_length>``
|
|
200
|
+
to get the next page.
|
|
201
|
+
"""
|
|
202
|
+
session.require_open()
|
|
203
|
+
|
|
204
|
+
import ida_funcs
|
|
205
|
+
import ida_hexrays
|
|
206
|
+
import ida_idaapi
|
|
207
|
+
|
|
208
|
+
ea = _resolve_address(function)
|
|
209
|
+
if ea == ida_idaapi.BADADDR:
|
|
210
|
+
raise ToolError(f"Could not resolve '{function}' to an address.")
|
|
211
|
+
|
|
212
|
+
# Ensure ea is within a function.
|
|
213
|
+
pfn = ida_funcs.get_func(ea)
|
|
214
|
+
if pfn is None:
|
|
215
|
+
raise ToolError(f"Address {ea:#x} is not within a recognized function.")
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
cfunc = ida_hexrays.decompile(pfn.start_ea)
|
|
219
|
+
except ida_hexrays.DecompilationFailure as e:
|
|
220
|
+
raise ToolError(f"Decompilation failed: {e}")
|
|
221
|
+
except Exception as e:
|
|
222
|
+
raise ToolError(f"{type(e).__name__}: {e}")
|
|
223
|
+
|
|
224
|
+
if cfunc is None:
|
|
225
|
+
raise ToolError("Decompilation returned no result.")
|
|
226
|
+
|
|
227
|
+
func_name = ida_funcs.get_func_name(pfn.start_ea) or f"sub_{pfn.start_ea:x}"
|
|
228
|
+
pseudocode = str(cfunc)
|
|
229
|
+
total_length = len(pseudocode)
|
|
230
|
+
chunk = pseudocode[offset:offset + max_length]
|
|
231
|
+
return {
|
|
232
|
+
"name": func_name,
|
|
233
|
+
"address": f"{pfn.start_ea:#x}",
|
|
234
|
+
"size": f"{pfn.end_ea - pfn.start_ea:#x}",
|
|
235
|
+
"pseudocode": chunk,
|
|
236
|
+
"offset": offset,
|
|
237
|
+
"total_length": total_length,
|
|
238
|
+
"truncated": offset + max_length < total_length,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@mcp.tool
|
|
243
|
+
def get_disassembly(start: str, length: int = 0x100) -> dict:
|
|
244
|
+
"""Get disassembly for an address range. Requires an open database.
|
|
245
|
+
|
|
246
|
+
*start* can be a name (e.g. "main") or address (hex "0x3f08" / "3f08",
|
|
247
|
+
decimal "16136"). *length* is the number of bytes from start to disassemble
|
|
248
|
+
(default 256, capped at 64 KB).
|
|
249
|
+
|
|
250
|
+
Returns: ``{"start", "end", "count", "instructions": [{"address", "disasm"}]}``
|
|
251
|
+
"""
|
|
252
|
+
session.require_open()
|
|
253
|
+
|
|
254
|
+
import ida_idaapi
|
|
255
|
+
import idc
|
|
256
|
+
import idautils
|
|
257
|
+
|
|
258
|
+
ea = _resolve_address(start)
|
|
259
|
+
if ea == ida_idaapi.BADADDR:
|
|
260
|
+
raise ToolError(f"Could not resolve '{start}' to an address.")
|
|
261
|
+
|
|
262
|
+
length = max(1, min(length, 0x10000))
|
|
263
|
+
end_ea = ea + length
|
|
264
|
+
|
|
265
|
+
instructions: list[dict] = []
|
|
266
|
+
for head in idautils.Heads(ea, end_ea):
|
|
267
|
+
instructions.append({"address": f"{head:#x}", "disasm": idc.GetDisasm(head)})
|
|
268
|
+
|
|
269
|
+
if not instructions:
|
|
270
|
+
raise ToolError(f"No instructions found in range {ea:#x}\u2013{end_ea:#x}.")
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
"start": f"{ea:#x}",
|
|
274
|
+
"end": f"{end_ea:#x}",
|
|
275
|
+
"count": len(instructions),
|
|
276
|
+
"instructions": instructions,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@mcp.tool
|
|
281
|
+
def list_functions(offset: int = 0, limit: int = 50, name_filter: str = "") -> dict:
|
|
282
|
+
"""List functions in the database with pagination. Requires an open database.
|
|
283
|
+
|
|
284
|
+
Returns address, size, and name for each function.
|
|
285
|
+
|
|
286
|
+
*offset* skips the first N functions (for pagination).
|
|
287
|
+
*limit* caps the number of functions returned (default 50, max 1000).
|
|
288
|
+
*name_filter* if non-empty, only includes functions whose name contains
|
|
289
|
+
this substring (case-insensitive).
|
|
290
|
+
|
|
291
|
+
If more results exist, increase *offset* by *limit* to get the next page.
|
|
292
|
+
|
|
293
|
+
Returns: ``{"functions": [{"address", "size", "name"}], "total", "showing", "offset", "name_filter"}``
|
|
294
|
+
"""
|
|
295
|
+
session.require_open()
|
|
296
|
+
|
|
297
|
+
import ida_funcs
|
|
298
|
+
import idautils
|
|
299
|
+
|
|
300
|
+
limit = min(limit, 1000)
|
|
301
|
+
all_funcs = list(idautils.Functions())
|
|
302
|
+
total = len(all_funcs)
|
|
303
|
+
|
|
304
|
+
functions: list[dict] = []
|
|
305
|
+
skipped = 0
|
|
306
|
+
for ea in all_funcs:
|
|
307
|
+
name = ida_funcs.get_func_name(ea) or f"sub_{ea:x}"
|
|
308
|
+
if name_filter and name_filter.lower() not in name.lower():
|
|
309
|
+
continue
|
|
310
|
+
if skipped < offset:
|
|
311
|
+
skipped += 1
|
|
312
|
+
continue
|
|
313
|
+
pfn = ida_funcs.get_func(ea)
|
|
314
|
+
size = pfn.end_ea - pfn.start_ea if pfn else 0
|
|
315
|
+
functions.append({"address": f"{ea:#x}", "size": f"{size:#x}", "name": name})
|
|
316
|
+
if len(functions) >= limit:
|
|
317
|
+
break
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
"functions": functions,
|
|
321
|
+
"total": total,
|
|
322
|
+
"showing": len(functions),
|
|
323
|
+
"offset": offset,
|
|
324
|
+
"name_filter": name_filter,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@mcp.tool
|
|
329
|
+
def search_docs(
|
|
330
|
+
query: str,
|
|
331
|
+
max_results: int = 5,
|
|
332
|
+
max_snippet_length: int = 150,
|
|
333
|
+
include_examples: bool = True,
|
|
334
|
+
) -> dict:
|
|
335
|
+
"""Look up IDA API functions, constants, and usage. No database needs to be open.
|
|
336
|
+
|
|
337
|
+
Use this to find the right API for a task, check function signatures,
|
|
338
|
+
or understand parameter meanings.
|
|
339
|
+
|
|
340
|
+
Searches two corpora:
|
|
341
|
+
- IDA HTML documentation (developer guide, user guide, etc.)
|
|
342
|
+
- IDAPython API source files (ida_*.py function signatures and docstrings)
|
|
343
|
+
|
|
344
|
+
Uses word-boundary matching: "set" matches "set_name" but not "reset".
|
|
345
|
+
|
|
346
|
+
When *include_examples* is True (default), also returns up to 2 matching
|
|
347
|
+
example scripts in the ``related_examples`` key.
|
|
348
|
+
|
|
349
|
+
*max_snippet_length* caps each snippet (default 150 chars).
|
|
350
|
+
"""
|
|
351
|
+
return _search_docs(query, max_results, max_snippet_length, include_examples)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@mcp.tool
|
|
355
|
+
def search_examples(
|
|
356
|
+
query: str,
|
|
357
|
+
max_results: int = 5,
|
|
358
|
+
max_snippet_lines: int = 10,
|
|
359
|
+
category: ExampleCategory = "",
|
|
360
|
+
level: ExampleLevel = "",
|
|
361
|
+
) -> dict:
|
|
362
|
+
"""Find working IDAPython code examples for common tasks. No database needs to be open.
|
|
363
|
+
|
|
364
|
+
Use this to find code patterns (e.g. "list strings", "decompile",
|
|
365
|
+
"enumerate imports") or see how specific IDA APIs are used in practice.
|
|
366
|
+
|
|
367
|
+
Searches example titles, descriptions, keywords, APIs used, and source code.
|
|
368
|
+
For API signatures and documentation, use `search_docs` instead.
|
|
369
|
+
|
|
370
|
+
*max_snippet_lines* caps source snippets (default 10 lines).
|
|
371
|
+
"""
|
|
372
|
+
return _search_examples(query, max_results, category, level, max_snippet_lines)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@mcp.tool
|
|
376
|
+
def list_snapshots() -> dict:
|
|
377
|
+
"""List all database snapshots. Requires an open database.
|
|
378
|
+
|
|
379
|
+
Returns snapshot IDs, descriptions, and filenames for the current database.
|
|
380
|
+
|
|
381
|
+
Returns: ``{"snapshots": [{"id", "desc", "filename"}], "count"}``
|
|
382
|
+
"""
|
|
383
|
+
return _snapshots.list_snapshots()
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@mcp.tool
|
|
387
|
+
def create_snapshot(desc: str = "") -> dict:
|
|
388
|
+
"""Create a database snapshot to checkpoint the current state. Requires an open database.
|
|
389
|
+
|
|
390
|
+
Snapshots let you save the database state before making destructive changes
|
|
391
|
+
(renaming, patching, type changes) and roll back if needed.
|
|
392
|
+
|
|
393
|
+
*desc* is an optional short description (max 128 chars).
|
|
394
|
+
|
|
395
|
+
Returns: ``{"id", "desc", "filename"}``
|
|
396
|
+
"""
|
|
397
|
+
return _snapshots.create_snapshot(desc)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@mcp.tool
|
|
401
|
+
def restore_snapshot(snapshot_id: str) -> dict:
|
|
402
|
+
"""Restore the database to a previous snapshot. Requires an open database.
|
|
403
|
+
|
|
404
|
+
*snapshot_id* is the snapshot ID from list_snapshots or create_snapshot.
|
|
405
|
+
The executor namespace is reset after restore since the database state changed.
|
|
406
|
+
"""
|
|
407
|
+
return _snapshots.restore_snapshot(snapshot_id)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@mcp.tool
|
|
411
|
+
def delete_snapshot(snapshot_id: str) -> dict:
|
|
412
|
+
"""Delete a database snapshot by removing its file from disk. Requires an open database.
|
|
413
|
+
|
|
414
|
+
*snapshot_id* is the snapshot ID from list_snapshots or create_snapshot.
|
|
415
|
+
"""
|
|
416
|
+
return _snapshots.remove_snapshot(snapshot_id)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@mcp.tool
|
|
420
|
+
def get_undo_status() -> dict:
|
|
421
|
+
"""Check what undo/redo actions are available. Requires an open database.
|
|
422
|
+
|
|
423
|
+
Returns whether undo and redo are possible, along with labels describing
|
|
424
|
+
the next undo/redo actions. IDA only exposes the *next* action in each
|
|
425
|
+
direction, not the full history stack.
|
|
426
|
+
|
|
427
|
+
Returns: ``{"can_undo", "undo_action", "can_redo", "redo_action"}``
|
|
428
|
+
"""
|
|
429
|
+
return _undo.get_status()
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@mcp.tool
|
|
433
|
+
def perform_undo(steps: int = 1) -> dict:
|
|
434
|
+
"""Undo the last database action(s). Requires an open database.
|
|
435
|
+
|
|
436
|
+
*steps* is how many undo steps to perform (default 1). If fewer steps
|
|
437
|
+
are available than requested, performs as many as possible (partial success).
|
|
438
|
+
|
|
439
|
+
The executor namespace is reset after undo since the database state changed.
|
|
440
|
+
|
|
441
|
+
Returns: ``{"status", "steps_requested", "steps_performed", "actions", "next_undo", "next_redo"}``
|
|
442
|
+
"""
|
|
443
|
+
return _undo.perform_undo(steps)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@mcp.tool
|
|
447
|
+
def perform_redo(steps: int = 1) -> dict:
|
|
448
|
+
"""Redo the last undone database action(s). Requires an open database.
|
|
449
|
+
|
|
450
|
+
*steps* is how many redo steps to perform (default 1). If fewer steps
|
|
451
|
+
are available than requested, performs as many as possible (partial success).
|
|
452
|
+
|
|
453
|
+
The executor namespace is reset after redo since the database state changed.
|
|
454
|
+
|
|
455
|
+
Returns: ``{"status", "steps_requested", "steps_performed", "actions", "next_undo", "next_redo"}``
|
|
456
|
+
"""
|
|
457
|
+
return _undo.perform_redo(steps)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@mcp.tool
|
|
461
|
+
def list_structures(offset: int = 0, limit: int = 50, name_filter: str = "") -> dict:
|
|
462
|
+
"""List structures (structs/unions) in the database with pagination. Requires an open database.
|
|
463
|
+
|
|
464
|
+
Returns name, size, alignment, and member count for each structure.
|
|
465
|
+
|
|
466
|
+
*offset* skips the first N matching structures (for pagination).
|
|
467
|
+
*limit* caps the number returned (default 50, max 1000).
|
|
468
|
+
*name_filter* if non-empty, only includes structures whose name contains
|
|
469
|
+
this substring (case-insensitive).
|
|
470
|
+
|
|
471
|
+
Returns: ``{"structures": [{"name", "size", "alignment", "member_count"}], "total", "showing", "offset", "name_filter"}``
|
|
472
|
+
"""
|
|
473
|
+
return _structures.list_structures(offset, min(limit, 1000), name_filter)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@mcp.tool
|
|
477
|
+
def get_structure(name: str) -> dict:
|
|
478
|
+
"""Get detailed info about a structure (struct/union) by name. Requires an open database.
|
|
479
|
+
|
|
480
|
+
Returns name, size, alignment, is_union flag, member count, and the full
|
|
481
|
+
C definition with ``/* offset */`` comments on each field.
|
|
482
|
+
|
|
483
|
+
Returns: ``{"name", "size", "is_union", "alignment", "member_count", "definition"}``
|
|
484
|
+
"""
|
|
485
|
+
return _structures.get_structure(name)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@mcp.tool
|
|
489
|
+
def create_structure(definition: str) -> dict:
|
|
490
|
+
"""Create a new structure from a C definition string. Requires an open database.
|
|
491
|
+
|
|
492
|
+
*definition* is a valid C struct or union definition, e.g.:
|
|
493
|
+
``struct foo { int x; char *y; };``
|
|
494
|
+
|
|
495
|
+
Fails if a structure with the same name already exists.
|
|
496
|
+
Returns the newly created structure details.
|
|
497
|
+
"""
|
|
498
|
+
return _structures.create_structure(definition)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@mcp.tool
|
|
502
|
+
def edit_structure(definition: str) -> dict:
|
|
503
|
+
"""Edit an existing structure by replacing its C definition. Requires an open database.
|
|
504
|
+
|
|
505
|
+
*definition* is a valid C struct or union definition with the same name
|
|
506
|
+
as an existing structure. The old definition is fully replaced.
|
|
507
|
+
|
|
508
|
+
Fails if the structure does not exist.
|
|
509
|
+
Returns the updated structure details.
|
|
510
|
+
"""
|
|
511
|
+
return _structures.edit_structure(definition)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
@mcp.tool
|
|
515
|
+
def delete_structure(name: str) -> dict:
|
|
516
|
+
"""Delete a structure (struct/union) by name. Requires an open database.
|
|
517
|
+
|
|
518
|
+
Removes the named type from the database type library.
|
|
519
|
+
Fails if the structure does not exist.
|
|
520
|
+
"""
|
|
521
|
+
return _structures.delete_structure(name)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
@mcp.tool
|
|
525
|
+
def get_variable(name: str, scope: str | None = None) -> dict:
|
|
526
|
+
"""Get info about a variable by name. Requires an open database.
|
|
527
|
+
|
|
528
|
+
If *scope* is provided, looks up a **local** (decompiler) variable
|
|
529
|
+
within that function. *scope* can be a function name (e.g. "main") or hex
|
|
530
|
+
address (e.g. "0x3f08").
|
|
531
|
+
|
|
532
|
+
If *scope* is omitted, resolves *name* as a **global** variable
|
|
533
|
+
(symbol name or address).
|
|
534
|
+
|
|
535
|
+
Local variables require Hex-Rays.
|
|
536
|
+
|
|
537
|
+
Returns (local): ``{"name", "type", "width", "is_arg", "function", "scope": "local"}``
|
|
538
|
+
Returns (global): ``{"name", "type", "address", "scope": "global"}``
|
|
539
|
+
"""
|
|
540
|
+
session.require_open()
|
|
541
|
+
|
|
542
|
+
import ida_idaapi
|
|
543
|
+
|
|
544
|
+
if scope is not None:
|
|
545
|
+
func_ea = _resolve_address(scope)
|
|
546
|
+
if func_ea == ida_idaapi.BADADDR:
|
|
547
|
+
raise ToolError(f"Could not resolve function '{scope}' to an address.")
|
|
548
|
+
return _variables.get_local_variable(func_ea, name)
|
|
549
|
+
else:
|
|
550
|
+
ea = _resolve_address(name)
|
|
551
|
+
if ea == ida_idaapi.BADADDR:
|
|
552
|
+
raise ToolError(f"Could not resolve '{name}' to an address.")
|
|
553
|
+
return _variables.get_global_variable(ea)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@mcp.tool
|
|
557
|
+
def set_variable(
|
|
558
|
+
name: str,
|
|
559
|
+
scope: str | None = None,
|
|
560
|
+
new_name: str | None = None,
|
|
561
|
+
new_type: str | None = None,
|
|
562
|
+
) -> dict:
|
|
563
|
+
"""Rename and/or retype a variable. Requires an open database.
|
|
564
|
+
|
|
565
|
+
If *scope* is provided, modifies a **local** (decompiler) variable
|
|
566
|
+
within that function. *scope* can be a function name or hex address.
|
|
567
|
+
|
|
568
|
+
If *scope* is omitted, modifies a **global** variable.
|
|
569
|
+
*name* is resolved as a symbol name or address.
|
|
570
|
+
|
|
571
|
+
At least one of *new_name* or *new_type* must be provided.
|
|
572
|
+
Local variables require Hex-Rays.
|
|
573
|
+
"""
|
|
574
|
+
session.require_open()
|
|
575
|
+
|
|
576
|
+
if new_name is None and new_type is None:
|
|
577
|
+
raise ToolError("At least one of new_name or new_type must be provided.")
|
|
578
|
+
|
|
579
|
+
import ida_idaapi
|
|
580
|
+
|
|
581
|
+
if scope is not None:
|
|
582
|
+
func_ea = _resolve_address(scope)
|
|
583
|
+
if func_ea == ida_idaapi.BADADDR:
|
|
584
|
+
raise ToolError(f"Could not resolve function '{scope}' to an address.")
|
|
585
|
+
return _variables.set_local_variable(func_ea, name, new_name, new_type)
|
|
586
|
+
else:
|
|
587
|
+
ea = _resolve_address(name)
|
|
588
|
+
if ea == ida_idaapi.BADADDR:
|
|
589
|
+
raise ToolError(f"Could not resolve '{name}' to an address.")
|
|
590
|
+
return _variables.set_global_variable(ea, new_name, new_type)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
@mcp.tool
|
|
594
|
+
def get_comment(address: str, comment_type: CommentTypeOrAll = "") -> dict:
|
|
595
|
+
"""Get comment(s) at an address. Requires an open database.
|
|
596
|
+
|
|
597
|
+
*address* can be a name (e.g. "main") or hex address (e.g. "0x3f08").
|
|
598
|
+
|
|
599
|
+
*comment_type* selects which comment to read, or empty string (default)
|
|
600
|
+
to return all non-empty comment types at once.
|
|
601
|
+
|
|
602
|
+
- **regular** — inline comment on a disassembly line
|
|
603
|
+
- **repeatable** — inline comment that propagates to cross-references
|
|
604
|
+
- **function** — comment on the function header (ea must be in a function)
|
|
605
|
+
- **anterior** — multi-line block before the address
|
|
606
|
+
- **posterior** — multi-line block after the address
|
|
607
|
+
"""
|
|
608
|
+
session.require_open()
|
|
609
|
+
|
|
610
|
+
import ida_idaapi
|
|
611
|
+
|
|
612
|
+
ea = _resolve_address(address)
|
|
613
|
+
if ea == ida_idaapi.BADADDR:
|
|
614
|
+
raise ToolError(f"Could not resolve '{address}' to an address.")
|
|
615
|
+
return _comments.get_comment(ea, comment_type)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@mcp.tool
|
|
619
|
+
def set_comment(address: str, comment: str, comment_type: CommentType = "regular") -> dict:
|
|
620
|
+
"""Set a comment at an address. Requires an open database.
|
|
621
|
+
|
|
622
|
+
*address* can be a name or hex address.
|
|
623
|
+
*comment* is the comment text (use ``\\n`` for multi-line anterior/posterior).
|
|
624
|
+
|
|
625
|
+
Returns: ``{"address", "comment_type", "comment", "status": "updated"}``
|
|
626
|
+
"""
|
|
627
|
+
session.require_open()
|
|
628
|
+
|
|
629
|
+
import ida_idaapi
|
|
630
|
+
|
|
631
|
+
ea = _resolve_address(address)
|
|
632
|
+
if ea == ida_idaapi.BADADDR:
|
|
633
|
+
raise ToolError(f"Could not resolve '{address}' to an address.")
|
|
634
|
+
return _comments.set_comment(ea, comment, comment_type)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
@mcp.tool
|
|
638
|
+
def delete_comment(address: str, comment_type: CommentType = "regular") -> dict:
|
|
639
|
+
"""Delete a comment at an address. Requires an open database.
|
|
640
|
+
|
|
641
|
+
*address* can be a name or hex address.
|
|
642
|
+
|
|
643
|
+
Returns: ``{"address", "comment_type", "status": "deleted"}``
|
|
644
|
+
"""
|
|
645
|
+
session.require_open()
|
|
646
|
+
|
|
647
|
+
import ida_idaapi
|
|
648
|
+
|
|
649
|
+
ea = _resolve_address(address)
|
|
650
|
+
if ea == ida_idaapi.BADADDR:
|
|
651
|
+
raise ToolError(f"Could not resolve '{address}' to an address.")
|
|
652
|
+
return _comments.delete_comment(ea, comment_type)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
@mcp.tool
|
|
656
|
+
def rename_function(function: str, new_name: str) -> dict:
|
|
657
|
+
"""Rename a function. Requires an open database.
|
|
658
|
+
|
|
659
|
+
*function* can be a name (e.g. "sub_3f08") or hex address (e.g. "0x3f08").
|
|
660
|
+
*new_name* is the new function name.
|
|
661
|
+
|
|
662
|
+
Returns: ``{"address", "old_name", "new_name", "status": "renamed"}``
|
|
663
|
+
"""
|
|
664
|
+
session.require_open()
|
|
665
|
+
|
|
666
|
+
import ida_funcs
|
|
667
|
+
import ida_idaapi
|
|
668
|
+
import ida_name
|
|
669
|
+
|
|
670
|
+
ea = _resolve_address(function)
|
|
671
|
+
if ea == ida_idaapi.BADADDR:
|
|
672
|
+
raise ToolError(f"Could not resolve '{function}' to an address.")
|
|
673
|
+
|
|
674
|
+
pfn = ida_funcs.get_func(ea)
|
|
675
|
+
if pfn is None:
|
|
676
|
+
raise ToolError(f"Address {ea:#x} is not within a recognized function.")
|
|
677
|
+
|
|
678
|
+
old_name = ida_funcs.get_func_name(pfn.start_ea) or f"sub_{pfn.start_ea:x}"
|
|
679
|
+
ok = ida_name.set_name(pfn.start_ea, new_name, ida_name.SN_NOCHECK)
|
|
680
|
+
if not ok:
|
|
681
|
+
raise ToolError(f"Failed to rename function at {pfn.start_ea:#x} to '{new_name}'.")
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
"address": f"{pfn.start_ea:#x}",
|
|
685
|
+
"old_name": old_name,
|
|
686
|
+
"new_name": new_name,
|
|
687
|
+
"status": "renamed",
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
@mcp.tool
|
|
692
|
+
def retype_function(function: str, new_type: str) -> dict:
|
|
693
|
+
"""Change a function's type signature. Requires an open database.
|
|
694
|
+
|
|
695
|
+
*function* can be a name (e.g. "main") or hex address (e.g. "0x3f08").
|
|
696
|
+
*new_type* is a C function type string (e.g. "int __fastcall(int argc, char **argv)").
|
|
697
|
+
|
|
698
|
+
Returns: ``{"address", "name", "old_type", "new_type", "status": "retyped"}``
|
|
699
|
+
"""
|
|
700
|
+
session.require_open()
|
|
701
|
+
|
|
702
|
+
import ida_funcs
|
|
703
|
+
import ida_idaapi
|
|
704
|
+
import ida_typeinf
|
|
705
|
+
import idc
|
|
706
|
+
|
|
707
|
+
ea = _resolve_address(function)
|
|
708
|
+
if ea == ida_idaapi.BADADDR:
|
|
709
|
+
raise ToolError(f"Could not resolve '{function}' to an address.")
|
|
710
|
+
|
|
711
|
+
pfn = ida_funcs.get_func(ea)
|
|
712
|
+
if pfn is None:
|
|
713
|
+
raise ToolError(f"Address {ea:#x} is not within a recognized function.")
|
|
714
|
+
|
|
715
|
+
func_name = ida_funcs.get_func_name(pfn.start_ea) or f"sub_{pfn.start_ea:x}"
|
|
716
|
+
|
|
717
|
+
# Get old type.
|
|
718
|
+
old_type = idc.get_type(pfn.start_ea) or ""
|
|
719
|
+
|
|
720
|
+
# Apply new type.
|
|
721
|
+
ok = idc.SetType(pfn.start_ea, new_type)
|
|
722
|
+
if not ok:
|
|
723
|
+
raise ToolError(
|
|
724
|
+
f"Failed to apply type '{new_type}' to function '{func_name}' at {pfn.start_ea:#x}. "
|
|
725
|
+
"Check C syntax."
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
return {
|
|
729
|
+
"address": f"{pfn.start_ea:#x}",
|
|
730
|
+
"name": func_name,
|
|
731
|
+
"old_type": old_type,
|
|
732
|
+
"new_type": new_type,
|
|
733
|
+
"status": "retyped",
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _xref_type_name(xref_type: int) -> str:
|
|
738
|
+
"""Convert an IDA xref type constant to a human-readable name."""
|
|
739
|
+
import ida_xref
|
|
740
|
+
|
|
741
|
+
_NAMES = {
|
|
742
|
+
ida_xref.fl_CF: "call_far",
|
|
743
|
+
ida_xref.fl_CN: "call_near",
|
|
744
|
+
ida_xref.fl_JF: "jump_far",
|
|
745
|
+
ida_xref.fl_JN: "jump_near",
|
|
746
|
+
ida_xref.fl_F: "ordinary_flow",
|
|
747
|
+
ida_xref.dr_O: "data_offset",
|
|
748
|
+
ida_xref.dr_W: "data_write",
|
|
749
|
+
ida_xref.dr_R: "data_read",
|
|
750
|
+
ida_xref.dr_T: "data_text",
|
|
751
|
+
ida_xref.dr_I: "data_info",
|
|
752
|
+
}
|
|
753
|
+
return _NAMES.get(xref_type, f"unknown_{xref_type}")
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
@mcp.tool
|
|
757
|
+
def get_xrefs_to(address: str, max_results: int = 100) -> dict:
|
|
758
|
+
"""Get cross-references to an address (who references this?). Requires an open database.
|
|
759
|
+
|
|
760
|
+
*address* can be a name (e.g. "main") or hex address (e.g. "0x3f08").
|
|
761
|
+
*max_results* caps the number of xrefs returned (default 100).
|
|
762
|
+
|
|
763
|
+
Returns: ``{"address", "xrefs": [{"from", "type"}], "count", "truncated"}``
|
|
764
|
+
"""
|
|
765
|
+
session.require_open()
|
|
766
|
+
|
|
767
|
+
import ida_idaapi
|
|
768
|
+
import idautils
|
|
769
|
+
|
|
770
|
+
ea = _resolve_address(address)
|
|
771
|
+
if ea == ida_idaapi.BADADDR:
|
|
772
|
+
raise ToolError(f"Could not resolve '{address}' to an address.")
|
|
773
|
+
|
|
774
|
+
xrefs = []
|
|
775
|
+
truncated = False
|
|
776
|
+
for xref in idautils.XrefsTo(ea):
|
|
777
|
+
if len(xrefs) >= max_results:
|
|
778
|
+
truncated = True
|
|
779
|
+
break
|
|
780
|
+
xrefs.append({"from": f"{xref.frm:#x}", "type": _xref_type_name(xref.type)})
|
|
781
|
+
|
|
782
|
+
return {
|
|
783
|
+
"address": f"{ea:#x}",
|
|
784
|
+
"xrefs": xrefs,
|
|
785
|
+
"count": len(xrefs),
|
|
786
|
+
"truncated": truncated,
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
@mcp.tool
|
|
791
|
+
def get_xrefs_from(address: str, max_results: int = 100) -> dict:
|
|
792
|
+
"""Get cross-references from an address (what does this reference?). Requires an open database.
|
|
793
|
+
|
|
794
|
+
*address* can be a name (e.g. "main") or hex address (e.g. "0x3f08").
|
|
795
|
+
*max_results* caps the number of xrefs returned (default 100).
|
|
796
|
+
|
|
797
|
+
Returns: ``{"address", "xrefs": [{"to", "type"}], "count", "truncated"}``
|
|
798
|
+
"""
|
|
799
|
+
session.require_open()
|
|
800
|
+
|
|
801
|
+
import ida_idaapi
|
|
802
|
+
import idautils
|
|
803
|
+
|
|
804
|
+
ea = _resolve_address(address)
|
|
805
|
+
if ea == ida_idaapi.BADADDR:
|
|
806
|
+
raise ToolError(f"Could not resolve '{address}' to an address.")
|
|
807
|
+
|
|
808
|
+
xrefs = []
|
|
809
|
+
truncated = False
|
|
810
|
+
for xref in idautils.XrefsFrom(ea):
|
|
811
|
+
if len(xrefs) >= max_results:
|
|
812
|
+
truncated = True
|
|
813
|
+
break
|
|
814
|
+
xrefs.append({"to": f"{xref.to:#x}", "type": _xref_type_name(xref.type)})
|
|
815
|
+
|
|
816
|
+
return {
|
|
817
|
+
"address": f"{ea:#x}",
|
|
818
|
+
"xrefs": xrefs,
|
|
819
|
+
"count": len(xrefs),
|
|
820
|
+
"truncated": truncated,
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
@mcp.tool
|
|
825
|
+
def get_strings(min_length: int = 5, max_results: int = 200, name_filter: str = "") -> dict:
|
|
826
|
+
"""List strings found in the database. Requires an open database.
|
|
827
|
+
|
|
828
|
+
*min_length* minimum string length to include (default 5).
|
|
829
|
+
*max_results* caps the number of strings returned (default 200).
|
|
830
|
+
*name_filter* if non-empty, only includes strings containing this
|
|
831
|
+
substring (case-insensitive).
|
|
832
|
+
|
|
833
|
+
Returns: ``{"strings": [{"address", "value", "length", "type"}], "count", "truncated"}``
|
|
834
|
+
"""
|
|
835
|
+
session.require_open()
|
|
836
|
+
|
|
837
|
+
import ida_nalt
|
|
838
|
+
import idautils
|
|
839
|
+
|
|
840
|
+
_TYPE_NAMES = {0: "C", 1: "C_16", 2: "C_32", 3: "PASCAL", 4: "PASCAL_16", 5: "LEN2", 6: "LEN4", 7: "LEN2_16"}
|
|
841
|
+
|
|
842
|
+
sc = idautils.Strings()
|
|
843
|
+
sc.setup(
|
|
844
|
+
strtypes=[ida_nalt.STRTYPE_C, ida_nalt.STRTYPE_C_16],
|
|
845
|
+
minlen=min_length,
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
strings = []
|
|
849
|
+
truncated = False
|
|
850
|
+
for s in sc:
|
|
851
|
+
value = str(s)
|
|
852
|
+
if name_filter and name_filter.lower() not in value.lower():
|
|
853
|
+
continue
|
|
854
|
+
if len(strings) >= max_results:
|
|
855
|
+
truncated = True
|
|
856
|
+
break
|
|
857
|
+
strings.append({
|
|
858
|
+
"address": f"{s.ea:#x}",
|
|
859
|
+
"value": value,
|
|
860
|
+
"length": s.length,
|
|
861
|
+
"type": _TYPE_NAMES.get(s.strtype, f"type_{s.strtype}"),
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
return {
|
|
865
|
+
"strings": strings,
|
|
866
|
+
"count": len(strings),
|
|
867
|
+
"truncated": truncated,
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
@mcp.tool
|
|
872
|
+
def get_imports() -> dict:
|
|
873
|
+
"""List all imported functions grouped by module. Requires an open database.
|
|
874
|
+
|
|
875
|
+
Returns: ``{"modules": [{"name", "imports": [{"address", "name", "ordinal"}]}], "total_imports"}``
|
|
876
|
+
"""
|
|
877
|
+
session.require_open()
|
|
878
|
+
|
|
879
|
+
import ida_nalt
|
|
880
|
+
|
|
881
|
+
modules = []
|
|
882
|
+
total_imports = 0
|
|
883
|
+
|
|
884
|
+
for i in range(ida_nalt.get_import_module_qty()):
|
|
885
|
+
mod_name = ida_nalt.get_import_module_name(i) or f"module_{i}"
|
|
886
|
+
imports = []
|
|
887
|
+
|
|
888
|
+
def _cb(ea, name, ordinal):
|
|
889
|
+
imports.append({
|
|
890
|
+
"address": f"{ea:#x}",
|
|
891
|
+
"name": name or "",
|
|
892
|
+
"ordinal": ordinal,
|
|
893
|
+
})
|
|
894
|
+
return True # Continue enumeration.
|
|
895
|
+
|
|
896
|
+
ida_nalt.enum_import_names(i, _cb)
|
|
897
|
+
total_imports += len(imports)
|
|
898
|
+
modules.append({"name": mod_name, "imports": imports})
|
|
899
|
+
|
|
900
|
+
return {"modules": modules, "total_imports": total_imports}
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
@mcp.tool
|
|
904
|
+
def get_exports() -> dict:
|
|
905
|
+
"""List all exported functions/symbols. Requires an open database.
|
|
906
|
+
|
|
907
|
+
Returns: ``{"exports": [{"address", "name", "ordinal"}], "count"}``
|
|
908
|
+
"""
|
|
909
|
+
session.require_open()
|
|
910
|
+
|
|
911
|
+
import idautils
|
|
912
|
+
|
|
913
|
+
exports = []
|
|
914
|
+
for ordinal, ea, name in idautils.Entries():
|
|
915
|
+
exports.append({
|
|
916
|
+
"address": f"{ea:#x}",
|
|
917
|
+
"name": name or "",
|
|
918
|
+
"ordinal": ordinal,
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
return {"exports": exports, "count": len(exports)}
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
@mcp.resource("guidelines://standalone_script")
|
|
925
|
+
def standalone_script_guidelines() -> str:
|
|
926
|
+
"""Architecture and boilerplate for standalone idalib scripts."""
|
|
927
|
+
return _guidelines.get("standalone_script")
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
@mcp.resource("guidelines://plugin")
|
|
931
|
+
def plugin_guidelines() -> str:
|
|
932
|
+
"""Architecture and boilerplate for IDA plugins (idaapi.plugin_t)."""
|
|
933
|
+
return _guidelines.get("plugin")
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
@mcp.resource("guidelines://idapython_script")
|
|
937
|
+
def idapython_script_guidelines() -> str:
|
|
938
|
+
"""Architecture and boilerplate for IDAPython scripts run inside IDA GUI."""
|
|
939
|
+
return _guidelines.get("idapython_script")
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
@mcp.prompt
|
|
943
|
+
def reverse_engineer() -> str:
|
|
944
|
+
"""Comprehensive workflow for reverse engineering a binary with ida-code.
|
|
945
|
+
|
|
946
|
+
Covers reconnaissance, triage, deep analysis, annotation, and iteration
|
|
947
|
+
using the full set of MCP tools.
|
|
948
|
+
"""
|
|
949
|
+
return _prompts.reverse_engineer()
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
@mcp.prompt
|
|
953
|
+
def create_script(target: str, description: str | None = None) -> str:
|
|
954
|
+
"""Coding guidelines and best practices for writing IDAPython scripts.
|
|
955
|
+
|
|
956
|
+
*target* is one of: ``standalone_script``, ``plugin``, ``idapython_script``.
|
|
957
|
+
*description* is an optional description of what the script should do.
|
|
958
|
+
"""
|
|
959
|
+
return _prompts.create_script(target, description)
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def _parse_host_port(value: str) -> tuple[str, int]:
|
|
963
|
+
"""Parse a ``host:port`` string, defaulting to ``127.0.0.1:8080``."""
|
|
964
|
+
if not value:
|
|
965
|
+
return "127.0.0.1", 8080
|
|
966
|
+
# Split on the *last* colon so IPv6 addresses work if quoted.
|
|
967
|
+
if ":" in value:
|
|
968
|
+
host, _, port_str = value.rpartition(":")
|
|
969
|
+
host = host or "127.0.0.1"
|
|
970
|
+
return host, int(port_str)
|
|
971
|
+
# Bare hostname / IP with no port.
|
|
972
|
+
return value, 8080
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def main():
|
|
976
|
+
logging.basicConfig(
|
|
977
|
+
level=getattr(logging, LOG_LEVEL, logging.WARNING),
|
|
978
|
+
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
979
|
+
stream=sys.stderr,
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
parser = argparse.ArgumentParser(prog="ida-code", description="MCP server for IDAPython scripting via idalib")
|
|
983
|
+
group = parser.add_mutually_exclusive_group()
|
|
984
|
+
group.add_argument("--http", nargs="?", const="127.0.0.1:8080", metavar="HOST:PORT",
|
|
985
|
+
help="Run with streamable-http transport (default: 127.0.0.1:8080)")
|
|
986
|
+
group.add_argument("--sse", nargs="?", const="127.0.0.1:8080", metavar="HOST:PORT",
|
|
987
|
+
help="Run with SSE transport (default: 127.0.0.1:8080)")
|
|
988
|
+
args = parser.parse_args()
|
|
989
|
+
|
|
990
|
+
if args.http or args.sse:
|
|
991
|
+
transport = "streamable-http" if args.http else "sse"
|
|
992
|
+
host, port = _parse_host_port(args.http or args.sse)
|
|
993
|
+
|
|
994
|
+
auth_token = MCP_AUTH_TOKEN
|
|
995
|
+
if not auth_token:
|
|
996
|
+
auth_token = secrets.token_urlsafe(32)
|
|
997
|
+
print(f"Generated auth token: {auth_token}", file=sys.stderr)
|
|
998
|
+
|
|
999
|
+
from fastmcp.server.auth import DebugTokenVerifier
|
|
1000
|
+
|
|
1001
|
+
mcp.auth = DebugTokenVerifier(
|
|
1002
|
+
validate=lambda token: hmac.compare_digest(token, auth_token),
|
|
1003
|
+
client_id="ida-code-client",
|
|
1004
|
+
)
|
|
1005
|
+
mcp.run(transport=transport, host=host, port=port)
|
|
1006
|
+
else:
|
|
1007
|
+
mcp.run(transport="stdio")
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
if __name__ == "__main__":
|
|
1011
|
+
main()
|