ida-pro-mcp-xjoker 1.0.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.
Files changed (45) hide show
  1. ida_pro_mcp/__init__.py +0 -0
  2. ida_pro_mcp/__main__.py +6 -0
  3. ida_pro_mcp/ida_mcp/__init__.py +68 -0
  4. ida_pro_mcp/ida_mcp/api_analysis.py +1296 -0
  5. ida_pro_mcp/ida_mcp/api_core.py +337 -0
  6. ida_pro_mcp/ida_mcp/api_debug.py +617 -0
  7. ida_pro_mcp/ida_mcp/api_memory.py +304 -0
  8. ida_pro_mcp/ida_mcp/api_modify.py +406 -0
  9. ida_pro_mcp/ida_mcp/api_python.py +179 -0
  10. ida_pro_mcp/ida_mcp/api_resources.py +295 -0
  11. ida_pro_mcp/ida_mcp/api_stack.py +167 -0
  12. ida_pro_mcp/ida_mcp/api_types.py +480 -0
  13. ida_pro_mcp/ida_mcp/auth.py +166 -0
  14. ida_pro_mcp/ida_mcp/cache.py +232 -0
  15. ida_pro_mcp/ida_mcp/config.py +228 -0
  16. ida_pro_mcp/ida_mcp/framework.py +547 -0
  17. ida_pro_mcp/ida_mcp/http.py +859 -0
  18. ida_pro_mcp/ida_mcp/port_utils.py +104 -0
  19. ida_pro_mcp/ida_mcp/rpc.py +187 -0
  20. ida_pro_mcp/ida_mcp/server_manager.py +339 -0
  21. ida_pro_mcp/ida_mcp/sync.py +233 -0
  22. ida_pro_mcp/ida_mcp/tests/__init__.py +14 -0
  23. ida_pro_mcp/ida_mcp/tests/test_api_analysis.py +336 -0
  24. ida_pro_mcp/ida_mcp/tests/test_api_core.py +237 -0
  25. ida_pro_mcp/ida_mcp/tests/test_api_memory.py +207 -0
  26. ida_pro_mcp/ida_mcp/tests/test_api_modify.py +123 -0
  27. ida_pro_mcp/ida_mcp/tests/test_api_resources.py +199 -0
  28. ida_pro_mcp/ida_mcp/tests/test_api_stack.py +77 -0
  29. ida_pro_mcp/ida_mcp/tests/test_api_types.py +249 -0
  30. ida_pro_mcp/ida_mcp/ui.py +357 -0
  31. ida_pro_mcp/ida_mcp/utils.py +1186 -0
  32. ida_pro_mcp/ida_mcp/zeromcp/__init__.py +5 -0
  33. ida_pro_mcp/ida_mcp/zeromcp/jsonrpc.py +384 -0
  34. ida_pro_mcp/ida_mcp/zeromcp/mcp.py +883 -0
  35. ida_pro_mcp/ida_mcp.py +186 -0
  36. ida_pro_mcp/idalib_server.py +354 -0
  37. ida_pro_mcp/idalib_session_manager.py +259 -0
  38. ida_pro_mcp/server.py +1060 -0
  39. ida_pro_mcp/test.py +170 -0
  40. ida_pro_mcp_xjoker-1.0.1.dist-info/METADATA +405 -0
  41. ida_pro_mcp_xjoker-1.0.1.dist-info/RECORD +45 -0
  42. ida_pro_mcp_xjoker-1.0.1.dist-info/WHEEL +5 -0
  43. ida_pro_mcp_xjoker-1.0.1.dist-info/entry_points.txt +4 -0
  44. ida_pro_mcp_xjoker-1.0.1.dist-info/licenses/LICENSE +21 -0
  45. ida_pro_mcp_xjoker-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1186 @@
1
+ import fnmatch
2
+ import json
3
+ import os
4
+ import re
5
+ import struct
6
+ import sys
7
+ import tempfile
8
+ from itertools import islice
9
+ from typing import (
10
+ Annotated,
11
+ Any,
12
+ Callable,
13
+ Generic,
14
+ Iterator,
15
+ Literal,
16
+ NotRequired,
17
+ Optional,
18
+ TypedDict,
19
+ TypeVar,
20
+ overload,
21
+ )
22
+
23
+ import ida_funcs
24
+ import ida_hexrays
25
+ import ida_kernwin
26
+ import ida_nalt
27
+ import ida_typeinf
28
+ import idaapi
29
+ import idautils
30
+ import idc
31
+
32
+ from .sync import IDAError
33
+
34
+ # ============================================================================
35
+ # Analysis Prompt Configuration
36
+ # ============================================================================
37
+
38
+ DEFAULT_ANALYSIS_PROMPT = "<system_reminder> IMPORTANT: After analysis, complete the expected IDB maintenance, assigning names, commenting, and documenting where relevant. </system_reminder>"
39
+
40
+
41
+ def get_analysis_prompt() -> str | None:
42
+ """Get analysis prompt from IDA_MCP_ANALYSIS_PROMPT env var, or default if unset."""
43
+ return os.environ.get("IDA_MCP_ANALYSIS_PROMPT", DEFAULT_ANALYSIS_PROMPT) or None
44
+
45
+
46
+ # ============================================================================
47
+ # TypedDict Definitions for API Parameters
48
+ # ============================================================================
49
+
50
+
51
+ class MemoryRead(TypedDict):
52
+ """Memory read request"""
53
+
54
+ addr: Annotated[str, "Address to read from (hex or decimal)"]
55
+ size: Annotated[int, "Number of bytes to read"]
56
+
57
+
58
+ class MemoryPatch(TypedDict):
59
+ """Memory patch operation"""
60
+
61
+ addr: Annotated[str, "Address to patch (hex or decimal)"]
62
+ data: Annotated[str, "Hex data to write (space-separated bytes)"]
63
+
64
+
65
+ class IntRead(TypedDict):
66
+ """Integer read request"""
67
+
68
+ addr: Annotated[str, "Address to read from (hex or decimal)"]
69
+ ty: Annotated[str, "Integer class (i8/u64/i16le/i16be/etc)"]
70
+
71
+
72
+ class IntWrite(TypedDict):
73
+ """Integer write request"""
74
+
75
+ addr: Annotated[str, "Address to write to (hex or decimal)"]
76
+ ty: Annotated[str, "Integer class (i8/u64/i16le/i16be/etc)"]
77
+ value: Annotated[
78
+ str,
79
+ "Integer value as string (decimal or 0x..; negatives allowed for signed)",
80
+ ]
81
+
82
+
83
+ class CommentOp(TypedDict):
84
+ """Comment operation"""
85
+
86
+ addr: Annotated[str, "Address (hex or decimal)"]
87
+ comment: Annotated[str, "Comment text"]
88
+
89
+
90
+ class AsmPatchOp(TypedDict):
91
+ """Assembly patch operation"""
92
+
93
+ addr: Annotated[str, "Address (hex or decimal)"]
94
+ asm: Annotated[str, "Assembly instruction(s), semicolon-separated"]
95
+
96
+
97
+ class FunctionRename(TypedDict):
98
+ """Function rename operation"""
99
+
100
+ addr: Annotated[str, "Function address (hex or decimal)"]
101
+ name: Annotated[str, "New function name"]
102
+
103
+
104
+ class GlobalRename(TypedDict):
105
+ """Global variable rename operation"""
106
+
107
+ old: Annotated[str, "Current variable name"]
108
+ new: Annotated[str, "New variable name"]
109
+
110
+
111
+ class LocalRename(TypedDict):
112
+ """Local variable rename operation"""
113
+
114
+ func_addr: Annotated[str, "Function address containing the local variable"]
115
+ old: Annotated[str, "Current variable name"]
116
+ new: Annotated[str, "New variable name"]
117
+
118
+
119
+ class StackRename(TypedDict):
120
+ """Stack variable rename operation"""
121
+
122
+ func_addr: Annotated[str, "Function address containing the stack variable"]
123
+ old: Annotated[str, "Current variable name"]
124
+ new: Annotated[str, "New variable name"]
125
+
126
+
127
+ class RenameBatch(TypedDict, total=False):
128
+ """Batch rename operations across all entity types"""
129
+
130
+ func: Annotated[
131
+ list[FunctionRename] | FunctionRename | None, "Function rename operations"
132
+ ]
133
+ data: Annotated[
134
+ list[GlobalRename] | GlobalRename | None,
135
+ "Global/data variable rename operations",
136
+ ]
137
+ local: Annotated[
138
+ list[LocalRename] | LocalRename | None, "Local variable rename operations"
139
+ ]
140
+ stack: Annotated[
141
+ list[StackRename] | StackRename | None, "Stack variable rename operations"
142
+ ]
143
+
144
+
145
+ class StructFieldQuery(TypedDict):
146
+ """Struct field query for xrefs"""
147
+
148
+ struct: Annotated[str, "Structure name"]
149
+ field: Annotated[str, "Field name"]
150
+
151
+
152
+ class ListQuery(TypedDict, total=False):
153
+ """Pagination query for listing operations"""
154
+
155
+ filter: Annotated[str, "Optional glob pattern to filter results"]
156
+ offset: Annotated[int, "Starting index (default: 0)"]
157
+ count: Annotated[int, "Maximum number of results (default: 50, 0 for all)"]
158
+
159
+
160
+ class BreakpointOp(TypedDict):
161
+ """Debugger breakpoint operation"""
162
+
163
+ addr: Annotated[str, "Breakpoint address (hex or decimal)"]
164
+ enabled: Annotated[bool, "Enable (true) or disable (false)"]
165
+
166
+
167
+ class InsnPattern(TypedDict, total=False):
168
+ """Instruction pattern for operand search"""
169
+
170
+ mnem: Annotated[str, "Instruction mnemonic to match"]
171
+ op0: Annotated[int, "Value to match in first operand"]
172
+ op1: Annotated[int, "Value to match in second operand"]
173
+ op2: Annotated[int, "Value to match in third operand"]
174
+ op_any: Annotated[int, "Value to match in any operand"]
175
+ func: Annotated[str, "Function address to scope the scan"]
176
+ segment: Annotated[str, "Segment name to scope the scan"]
177
+ start: Annotated[str, "Start address (hex/dec) to scope the scan"]
178
+ end: Annotated[str, "End address (hex/dec, exclusive) to scope the scan"]
179
+ max_scan_insns: Annotated[
180
+ int, "Max instructions to scan (default: 200000, max: 2000000)"
181
+ ]
182
+ allow_broad: Annotated[
183
+ bool,
184
+ "Allow scans without scope (default: false). Use with care on large binaries.",
185
+ ]
186
+
187
+
188
+ class NumberConversion(TypedDict, total=False):
189
+ """Number conversion request"""
190
+
191
+ text: Annotated[str, "Number string to convert"]
192
+ size: Annotated[int, "Byte size for conversion (omit for auto)"]
193
+
194
+
195
+ class StructRead(TypedDict, total=False):
196
+ """Structure read request
197
+
198
+ Address is required. Struct name is optional - if omitted, will attempt
199
+ to auto-detect from type information already applied at the address.
200
+ """
201
+
202
+ addr: Annotated[str, "Memory address (hex or decimal)"]
203
+ struct: Annotated[
204
+ NotRequired[str], "Structure name (optional, auto-detect if omitted)"
205
+ ]
206
+
207
+
208
+ class TypeEdit(TypedDict, total=False):
209
+ """Type application operation"""
210
+
211
+ addr: Annotated[str, "Memory address"]
212
+ name: Annotated[str, "Variable/function name"]
213
+ ty: Annotated[str, "Type name or declaration"]
214
+ kind: Annotated[str, "Type of entity (auto-detected if omitted)"]
215
+ signature: Annotated[str, "Function signature (for kind=function)"]
216
+ variable: Annotated[str, "Local variable name (for kind=local)"]
217
+
218
+
219
+ class StackVarDecl(TypedDict):
220
+ """Stack variable declaration"""
221
+
222
+ addr: Annotated[str, "Function address"]
223
+ offset: Annotated[str, "Stack offset"]
224
+ name: Annotated[str, "Variable name"]
225
+ ty: Annotated[str, "Type name"]
226
+
227
+
228
+ class StackVarDelete(TypedDict):
229
+ """Stack variable deletion"""
230
+
231
+ addr: Annotated[str, "Function address"]
232
+ name: Annotated[str, "Variable name"]
233
+
234
+
235
+ # ============================================================================
236
+ # TypedDict Definitions for Results
237
+ # ============================================================================
238
+
239
+
240
+ class Metadata(TypedDict):
241
+ path: str
242
+ module: str
243
+ base: str
244
+ size: str
245
+ md5: str
246
+ sha256: str
247
+ crc32: str
248
+ filesize: str
249
+
250
+
251
+ class Function(TypedDict):
252
+ addr: str
253
+ name: str
254
+ size: str
255
+
256
+
257
+ class ConvertedNumber(TypedDict):
258
+ decimal: str
259
+ hexadecimal: str
260
+ bytes: str
261
+ ascii: Optional[str]
262
+ binary: str
263
+
264
+
265
+ class Global(TypedDict):
266
+ addr: str
267
+ name: str
268
+
269
+
270
+ class Import(TypedDict):
271
+ addr: str
272
+ imported_name: str
273
+ module: str
274
+
275
+
276
+ class String(TypedDict):
277
+ addr: str
278
+ length: int
279
+ string: str
280
+
281
+
282
+ class Segment(TypedDict):
283
+ name: str
284
+ start: str
285
+ end: str
286
+ size: str
287
+ permissions: str
288
+
289
+
290
+ class DisassemblyLine(TypedDict):
291
+ segment: NotRequired[str]
292
+ addr: str
293
+ label: NotRequired[str]
294
+ instruction: str
295
+ comments: NotRequired[list[str]]
296
+
297
+
298
+ class Argument(TypedDict):
299
+ name: str
300
+ type: str
301
+
302
+
303
+ class StackFrameVariable(TypedDict):
304
+ name: str
305
+ offset: str
306
+ size: str
307
+ type: str
308
+
309
+
310
+ class DisassemblyFunction(TypedDict):
311
+ name: str
312
+ start_ea: str
313
+ return_type: NotRequired[str]
314
+ arguments: NotRequired[list[Argument]]
315
+ stack_frame: list[StackFrameVariable]
316
+ lines: list[DisassemblyLine]
317
+
318
+
319
+ class Xref(TypedDict):
320
+ addr: str
321
+ type: str
322
+ fn: Optional[Function]
323
+
324
+
325
+ class StructureMember(TypedDict):
326
+ name: str
327
+ offset: str
328
+ size: str
329
+ type: str
330
+
331
+
332
+ class StructureDefinition(TypedDict):
333
+ name: str
334
+ size: str
335
+ members: list[StructureMember]
336
+
337
+
338
+ class RegisterValue(TypedDict):
339
+ name: str
340
+ value: str
341
+
342
+
343
+ class ThreadRegisters(TypedDict):
344
+ thread_id: int
345
+ registers: list[RegisterValue]
346
+
347
+
348
+ class Breakpoint(TypedDict):
349
+ addr: str
350
+ enabled: bool
351
+ condition: Optional[str]
352
+
353
+
354
+ class FunctionAnalysis(TypedDict):
355
+ addr: str
356
+ name: Optional[str]
357
+ code: Optional[str]
358
+ asm: Optional[str]
359
+ xto: list[Xref]
360
+ xfrom: list[Xref]
361
+ callees: list[dict]
362
+ callers: list[Function]
363
+ strings: list[String]
364
+ constants: list[dict]
365
+ blocks: list[dict]
366
+ error: Optional[str]
367
+ prompt: Optional[str]
368
+
369
+
370
+ class PatternMatch(TypedDict):
371
+ pattern: str
372
+ matches: list[str]
373
+ count: int
374
+
375
+
376
+ class CodePattern(TypedDict):
377
+ mnemonic: str
378
+ operands: NotRequired[list[str]]
379
+
380
+
381
+ class BasicBlock(TypedDict):
382
+ start: str
383
+ end: str
384
+ size: int
385
+ type: int
386
+ successors: list[str]
387
+ predecessors: list[str]
388
+
389
+
390
+ T = TypeVar("T")
391
+
392
+
393
+ class Page(TypedDict, Generic[T]):
394
+ data: list[T]
395
+ next_offset: Optional[int]
396
+
397
+
398
+ # ============================================================================
399
+ # Helper Functions
400
+ # ============================================================================
401
+
402
+
403
+ def get_image_size() -> int:
404
+ try:
405
+ info = idaapi.get_inf_structure()
406
+ omin_ea = info.omin_ea
407
+ omax_ea = info.omax_ea
408
+ except AttributeError:
409
+ import ida_ida
410
+
411
+ omin_ea = ida_ida.inf_get_omin_ea()
412
+ omax_ea = ida_ida.inf_get_omax_ea()
413
+ image_size = omax_ea - omin_ea
414
+ header = idautils.peutils_t().header()
415
+ if header and header[:4] == b"PE\0\0":
416
+ image_size = struct.unpack("<I", header[0x50:0x54])[0]
417
+ return image_size
418
+
419
+
420
+ def parse_address(addr: str | int) -> int:
421
+ if isinstance(addr, int):
422
+ return addr
423
+ try:
424
+ return int(addr, 0)
425
+ except ValueError:
426
+ for ch in addr:
427
+ if ch not in "0123456789abcdefABCDEF":
428
+ raise IDAError(f"Failed to parse address: {addr}")
429
+ raise IDAError(f"Failed to parse address (missing 0x prefix): {addr}")
430
+
431
+
432
+ def normalize_list_input(value: list | str) -> list:
433
+ """Normalize input to list - accepts list or comma-separated string"""
434
+ if isinstance(value, list):
435
+ return value
436
+ if isinstance(value, str):
437
+ return [item.strip() for item in value.split(",") if item.strip()]
438
+ return [value]
439
+
440
+
441
+ def normalize_dict_list(
442
+ value: list[dict] | dict | str | list[str] | Any,
443
+ string_parser: Optional[Callable[[str], dict]] = None,
444
+ ) -> list[dict]:
445
+ """Normalize input to list[dict] with optional string parsing
446
+
447
+ Args:
448
+ value: Input value (dict, list[dict], str, list[str], or any)
449
+ string_parser: Optional function to convert string → dict
450
+ If None, strings → empty dict
451
+
452
+ Flow:
453
+ dict → [dict]
454
+ str → split by ',' → list[str] → map(string_parser) → list[dict]
455
+ list[str] → map(string_parser) → list[dict]
456
+ list[dict] → list[dict]
457
+ Any → [{}]
458
+ """
459
+ if isinstance(value, dict):
460
+ return [value]
461
+ elif isinstance(value, list):
462
+ if not value:
463
+ return [{}]
464
+ # Check if list[str] or list[dict]
465
+ if all(isinstance(item, dict) for item in value):
466
+ return value
467
+ elif all(isinstance(item, str) for item in value):
468
+ # list[str] → map with parser
469
+ if string_parser:
470
+ return [string_parser(s.strip()) for s in value if s.strip()]
471
+ return [{}]
472
+ else:
473
+ # Mixed types - filter dicts only
474
+ return [item for item in value if isinstance(item, dict)] or [{}]
475
+ elif isinstance(value, str):
476
+ # Try JSON parse first
477
+ try:
478
+ parsed = json.loads(value)
479
+ if isinstance(parsed, dict):
480
+ return [parsed]
481
+ elif isinstance(parsed, list):
482
+ return parsed
483
+ except (json.JSONDecodeError, ValueError):
484
+ pass
485
+
486
+ # Not JSON - split by comma and parse
487
+ parts = [s.strip() for s in value.split(",") if s.strip()]
488
+ if not parts:
489
+ return [{}]
490
+
491
+ if string_parser:
492
+ return [string_parser(part) for part in parts]
493
+ return [{}]
494
+ else:
495
+ # Any other type → empty dict
496
+ return [{}]
497
+
498
+
499
+ def looks_like_address(s: str) -> bool:
500
+ """Check if string looks like an address (0x prefix or all hex chars)"""
501
+ if s.startswith("0x") or s.startswith("0X"):
502
+ return True
503
+ # All hex chars and at least 4 chars → likely address
504
+ if len(s) >= 4 and all(c in "0123456789abcdefABCDEF" for c in s):
505
+ return True
506
+ return False
507
+
508
+
509
+ @overload
510
+ def get_function(addr: int, *, raise_error: Literal[True]) -> Function: ...
511
+
512
+
513
+ @overload
514
+ def get_function(addr: int) -> Function: ...
515
+
516
+
517
+ @overload
518
+ def get_function(addr: int, *, raise_error: Literal[False]) -> Optional[Function]: ...
519
+
520
+
521
+ def get_function(addr, *, raise_error=True):
522
+ fn = idaapi.get_func(addr)
523
+ if fn is None:
524
+ if raise_error:
525
+ raise IDAError(f"No function found at address {hex(addr)}")
526
+ return None
527
+
528
+ try:
529
+ name = fn.get_name()
530
+ except AttributeError:
531
+ name = ida_funcs.get_func_name(fn.start_ea)
532
+
533
+ return Function(addr=hex(addr), name=name, size=hex(fn.end_ea - fn.start_ea))
534
+
535
+
536
+ def get_prototype(fn: ida_funcs.func_t) -> Optional[str]:
537
+ try:
538
+ prototype: ida_typeinf.tinfo_t = fn.get_prototype()
539
+ if prototype is not None:
540
+ return str(prototype)
541
+ else:
542
+ return None
543
+ except AttributeError:
544
+ try:
545
+ return idc.get_type(fn.start_ea)
546
+ except Exception:
547
+ tif = ida_typeinf.tinfo_t()
548
+ if ida_nalt.get_tinfo(tif, fn.start_ea):
549
+ return str(tif)
550
+ return None
551
+ except Exception as e:
552
+ print(f"Error getting function prototype: {e}")
553
+ return None
554
+
555
+
556
+ DEMANGLED_TO_EA = {}
557
+
558
+
559
+ def create_demangled_to_ea_map():
560
+ for ea in idautils.Functions():
561
+ demangled = idaapi.demangle_name(idc.get_name(ea, 0), idaapi.MNG_NODEFINIT)
562
+ if demangled:
563
+ DEMANGLED_TO_EA[demangled] = ea
564
+
565
+
566
+ def get_type_by_name(type_name: str) -> ida_typeinf.tinfo_t:
567
+ # 8-bit integers
568
+ if type_name in ("int8", "__int8", "int8_t", "char", "signed char"):
569
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT8)
570
+ elif type_name in ("uint8", "__uint8", "uint8_t", "unsigned char", "byte", "BYTE"):
571
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT8)
572
+ # 16-bit integers
573
+ elif type_name in (
574
+ "int16",
575
+ "__int16",
576
+ "int16_t",
577
+ "short",
578
+ "short int",
579
+ "signed short",
580
+ "signed short int",
581
+ ):
582
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT16)
583
+ elif type_name in (
584
+ "uint16",
585
+ "__uint16",
586
+ "uint16_t",
587
+ "unsigned short",
588
+ "unsigned short int",
589
+ "word",
590
+ "WORD",
591
+ ):
592
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT16)
593
+ # 32-bit integers
594
+ elif type_name in (
595
+ "int32",
596
+ "__int32",
597
+ "int32_t",
598
+ "int",
599
+ "signed int",
600
+ "long",
601
+ "long int",
602
+ "signed long",
603
+ "signed long int",
604
+ ):
605
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT32)
606
+ elif type_name in (
607
+ "uint32",
608
+ "__uint32",
609
+ "uint32_t",
610
+ "unsigned int",
611
+ "unsigned long",
612
+ "unsigned long int",
613
+ "dword",
614
+ "DWORD",
615
+ ):
616
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT32)
617
+ # 64-bit integers
618
+ elif type_name in (
619
+ "int64",
620
+ "__int64",
621
+ "int64_t",
622
+ "long long",
623
+ "long long int",
624
+ "signed long long",
625
+ "signed long long int",
626
+ ):
627
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT64)
628
+ elif type_name in (
629
+ "uint64",
630
+ "__uint64",
631
+ "uint64_t",
632
+ "unsigned int64",
633
+ "unsigned long long",
634
+ "unsigned long long int",
635
+ "qword",
636
+ "QWORD",
637
+ ):
638
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT64)
639
+ # 128-bit integers
640
+ elif type_name in ("int128", "__int128", "int128_t", "__int128_t"):
641
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT128)
642
+ elif type_name in (
643
+ "uint128",
644
+ "__uint128",
645
+ "uint128_t",
646
+ "__uint128_t",
647
+ "unsigned int128",
648
+ ):
649
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT128)
650
+ # Floating point types
651
+ elif type_name in ("float",):
652
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_FLOAT)
653
+ elif type_name in ("double",):
654
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_DOUBLE)
655
+ elif type_name in ("long double", "ldouble"):
656
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_LDOUBLE)
657
+ # Boolean type
658
+ elif type_name in ("bool", "_Bool", "boolean"):
659
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_BOOL)
660
+ # Void type
661
+ elif type_name in ("void",):
662
+ return ida_typeinf.tinfo_t(ida_typeinf.BTF_VOID)
663
+ # Named types
664
+ tif = ida_typeinf.tinfo_t()
665
+ if tif.get_named_type(None, type_name, ida_typeinf.BTF_STRUCT):
666
+ return tif
667
+ if tif.get_named_type(None, type_name, ida_typeinf.BTF_TYPEDEF):
668
+ return tif
669
+ if tif.get_named_type(None, type_name, ida_typeinf.BTF_ENUM):
670
+ return tif
671
+ if tif.get_named_type(None, type_name, ida_typeinf.BTF_UNION):
672
+ return tif
673
+
674
+ # Try parsing as a type string (e.g., "int *")
675
+ tif = ida_typeinf.tinfo_t()
676
+ if ida_typeinf.parse_decl(tif, None, f"{type_name} x;", ida_typeinf.PT_SIL):
677
+ return tif
678
+
679
+ raise IDAError(f"Unable to retrieve {type_name} type info object")
680
+
681
+
682
+ def paginate(data: list[T], offset: int, count: int) -> Page[T]:
683
+ if count == 0:
684
+ count = len(data)
685
+ next_offset = offset + count
686
+ if next_offset >= len(data):
687
+ next_offset = None
688
+ return {
689
+ "data": data[offset : offset + count],
690
+ "next_offset": next_offset,
691
+ }
692
+
693
+
694
+ def lazy_paginate(
695
+ iterator: Iterator[T],
696
+ offset: int,
697
+ count: int,
698
+ filter_fn: Optional[Callable[[T], bool]] = None,
699
+ ) -> Page[T]:
700
+ """Lazy pagination that only iterates over needed elements.
701
+
702
+ Much more efficient for large datasets when only fetching first pages.
703
+ Complexity: O(offset + count) instead of O(N) for full dataset.
704
+
705
+ Args:
706
+ iterator: An iterator over the data source
707
+ offset: Number of items to skip
708
+ count: Maximum number of items to return (0 for all remaining)
709
+ filter_fn: Optional filter function to apply before pagination
710
+
711
+ Returns:
712
+ Page with data and next_offset (None if no more items)
713
+ """
714
+ # Apply filter if provided
715
+ if filter_fn is not None:
716
+ iterator = filter(filter_fn, iterator)
717
+
718
+ # Skip offset items
719
+ if offset > 0:
720
+ iterator = islice(iterator, offset, None)
721
+
722
+ # Handle count=0 (all remaining items)
723
+ if count == 0:
724
+ items = list(iterator)
725
+ return {
726
+ "data": items,
727
+ "next_offset": None,
728
+ }
729
+
730
+ # Fetch count + 1 items to check if there are more
731
+ items = list(islice(iterator, count + 1))
732
+
733
+ has_more = len(items) > count
734
+ if has_more:
735
+ items = items[:count]
736
+
737
+ return {
738
+ "data": items,
739
+ "next_offset": offset + count if has_more else None,
740
+ }
741
+
742
+
743
+ def pattern_filter(data: list[T], pattern: str, key: str) -> list[T]:
744
+ if not pattern:
745
+ return data
746
+
747
+ regex = None
748
+ use_glob = False
749
+
750
+ # Regex pattern: /pattern/flags
751
+ if pattern.startswith("/") and pattern.count("/") >= 2:
752
+ last_slash = pattern.rfind("/")
753
+ body = pattern[1:last_slash]
754
+ flag_str = pattern[last_slash + 1 :]
755
+
756
+ flags = 0
757
+ for ch in flag_str:
758
+ if ch == "i":
759
+ flags |= re.IGNORECASE
760
+ elif ch == "m":
761
+ flags |= re.MULTILINE
762
+ elif ch == "s":
763
+ flags |= re.DOTALL
764
+
765
+ try:
766
+ regex = re.compile(body, flags or re.IGNORECASE)
767
+ except re.error:
768
+ regex = None
769
+ # Glob pattern: contains * or ?
770
+ elif "*" in pattern or "?" in pattern:
771
+ use_glob = True
772
+
773
+ def get_value(item) -> str:
774
+ try:
775
+ v = item[key]
776
+ except Exception:
777
+ v = getattr(item, key, "")
778
+ return "" if v is None else str(v)
779
+
780
+ def matches(item) -> bool:
781
+ text = get_value(item)
782
+ if regex is not None:
783
+ return bool(regex.search(text))
784
+ if use_glob:
785
+ return fnmatch.fnmatch(text.lower(), pattern.lower())
786
+ return pattern.lower() in text.lower()
787
+
788
+ return [item for item in data if matches(item)]
789
+
790
+
791
+ def refresh_decompiler_widget():
792
+ widget = ida_kernwin.get_current_widget()
793
+ if widget is not None:
794
+ vu = ida_hexrays.get_widget_vdui(widget)
795
+ if vu is not None:
796
+ vu.refresh_ctext()
797
+
798
+
799
+ def refresh_decompiler_ctext(fn_addr: int):
800
+ error = ida_hexrays.hexrays_failure_t()
801
+ cfunc: ida_hexrays.cfunc_t = ida_hexrays.decompile_func(
802
+ fn_addr, error, ida_hexrays.DECOMP_WARNINGS
803
+ )
804
+ if cfunc:
805
+ cfunc.refresh_func_ctext()
806
+
807
+
808
+ class my_modifier_t(ida_hexrays.user_lvar_modifier_t):
809
+ def __init__(self, var_name: str, new_type: ida_typeinf.tinfo_t):
810
+ ida_hexrays.user_lvar_modifier_t.__init__(self)
811
+ self.var_name = var_name
812
+ self.new_type = new_type
813
+
814
+ def modify_lvars(self, lvinf):
815
+ for lvar_saved in lvinf.lvvec:
816
+ lvar_saved: ida_hexrays.lvar_saved_info_t
817
+ if lvar_saved.name == self.var_name:
818
+ lvar_saved.type = self.new_type
819
+ return True
820
+ return False
821
+
822
+
823
+ def parse_decls_ctypes(decls: str, hti_flags: int) -> tuple[int, list[str]]:
824
+ if sys.platform == "win32":
825
+ import ctypes
826
+
827
+ assert isinstance(decls, str), "decls must be a string"
828
+ assert isinstance(hti_flags, int), "hti_flags must be an int"
829
+ c_decls = decls.encode("utf-8")
830
+ c_til = None
831
+ ida_dll = ctypes.CDLL("ida")
832
+ ida_dll.parse_decls.argtypes = [
833
+ ctypes.c_void_p,
834
+ ctypes.c_char_p,
835
+ ctypes.c_void_p,
836
+ ctypes.c_int,
837
+ ]
838
+ ida_dll.parse_decls.restype = ctypes.c_int
839
+
840
+ messages: list[str] = []
841
+
842
+ @ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p)
843
+ def magic_printer(fmt: bytes, arg1: bytes):
844
+ if fmt.count(b"%") == 1 and b"%s" in fmt:
845
+ formatted = fmt.replace(b"%s", arg1)
846
+ messages.append(formatted.decode("utf-8"))
847
+ return len(formatted) + 1
848
+ else:
849
+ messages.append(f"unsupported magic_printer fmt: {repr(fmt)}")
850
+ return 0
851
+
852
+ errors = ida_dll.parse_decls(c_til, c_decls, magic_printer, hti_flags)
853
+ else:
854
+ errors = ida_typeinf.parse_decls(None, decls, False, hti_flags)
855
+ messages = []
856
+ return errors, messages
857
+
858
+
859
+ def get_stack_frame_variables_internal(
860
+ fn_addr: int, raise_error: bool
861
+ ) -> list[StackFrameVariable]:
862
+ from .sync import ida_major
863
+
864
+ if ida_major < 9:
865
+ return []
866
+
867
+ func = idaapi.get_func(fn_addr)
868
+ if not func:
869
+ if raise_error:
870
+ raise IDAError(f"No function found at address {fn_addr}")
871
+ return []
872
+
873
+ tif = ida_typeinf.tinfo_t()
874
+ if not tif.get_type_by_tid(func.frame) or not tif.is_udt():
875
+ return []
876
+
877
+ members: list[StackFrameVariable] = []
878
+ udt = ida_typeinf.udt_type_data_t()
879
+ tif.get_udt_details(udt)
880
+ for udm in udt:
881
+ if not udm.is_gap():
882
+ name = udm.name
883
+ offset = udm.offset // 8
884
+ size = udm.size // 8
885
+ type = str(udm.type)
886
+ members.append(
887
+ StackFrameVariable(
888
+ name=name, offset=hex(offset), size=hex(size), type=type
889
+ )
890
+ )
891
+ return members
892
+
893
+
894
+ def decompile_checked(addr: int):
895
+ """Decompile a function and raise IDAError on failure (uses cache)"""
896
+ if not ida_hexrays.init_hexrays_plugin():
897
+ raise IDAError("Hex-Rays decompiler is not available")
898
+ hf = ida_hexrays.hexrays_failure_t()
899
+ cfunc = ida_hexrays.decompile(addr, hf)
900
+ if not cfunc:
901
+ if hf.code == ida_hexrays.MERR_LICENSE:
902
+ raise IDAError(
903
+ "Decompiler license is not available. Use `disassemble_function` to get the assembly code instead."
904
+ )
905
+
906
+ message = f"Decompilation failed at {hex(addr)}"
907
+ if hf.str:
908
+ message += f": {hf.str}"
909
+ if hf.errea != idaapi.BADADDR:
910
+ message += f" (address: {hex(hf.errea)})"
911
+ raise IDAError(message)
912
+ return cfunc
913
+
914
+
915
+ def decompile_function_safe(ea: int) -> Optional[str]:
916
+ """Safely decompile a function, returning None on failure (uses cache)"""
917
+ import ida_lines
918
+ import ida_kernwin
919
+
920
+ try:
921
+ if not ida_hexrays.init_hexrays_plugin():
922
+ return None
923
+ cfunc = ida_hexrays.decompile(ea)
924
+ if not cfunc:
925
+ return None
926
+ sv = cfunc.get_pseudocode()
927
+ lines = []
928
+ for sl in sv:
929
+ sl: ida_kernwin.simpleline_t
930
+ item = ida_hexrays.ctree_item_t()
931
+ line_ea = None
932
+ if cfunc.get_line_item(sl.line, 0, False, None, item, None):
933
+ dstr: str | None = item.dstr()
934
+ if dstr:
935
+ ds = dstr.split(": ")
936
+ if len(ds) == 2:
937
+ try:
938
+ line_ea = int(ds[0], 16)
939
+ except ValueError:
940
+ pass
941
+ text = ida_lines.tag_remove(sl.line)
942
+ if line_ea is not None:
943
+ lines.append(f"{text} /*{line_ea:#x}*/")
944
+ else:
945
+ lines.append(text)
946
+ return "\n".join(lines)
947
+ except Exception:
948
+ return None
949
+
950
+
951
+ def get_assembly_lines(ea: int) -> str:
952
+ """Get assembly lines for a function in compact string format"""
953
+ func = idaapi.get_func(ea)
954
+ if not func:
955
+ return ""
956
+
957
+ func_name: str = ida_funcs.get_func_name(func.start_ea) or "<unnamed>"
958
+
959
+ # Get segment from first instruction
960
+ first_seg = idaapi.getseg(func.start_ea)
961
+ segment_name = idaapi.get_segm_name(first_seg) if first_seg else "UNKNOWN"
962
+
963
+ # Build compact string format
964
+ lines_str = f"{func_name} ({segment_name} @ {hex(func.start_ea)}):"
965
+
966
+ for item_ea in idautils.FuncItems(func.start_ea):
967
+ mnem = idc.print_insn_mnem(item_ea) or ""
968
+ ops = []
969
+ for n in range(8):
970
+ if idc.get_operand_type(item_ea, n) == idaapi.o_void:
971
+ break
972
+ ops.append(idc.print_operand(item_ea, n) or "")
973
+ instruction = f"{mnem} {', '.join(ops)}".rstrip()
974
+ lines_str += f"\n{item_ea:x} {instruction}"
975
+
976
+ return lines_str
977
+
978
+
979
+ def get_all_xrefs(ea: int) -> dict:
980
+ """Get all xrefs to and from an address"""
981
+ return {
982
+ "to": [
983
+ {"addr": hex(x.frm), "type": "code" if x.iscode else "data"}
984
+ for x in idautils.XrefsTo(ea, 0)
985
+ ],
986
+ "from": [
987
+ {"addr": hex(x.to), "type": "code" if x.iscode else "data"}
988
+ for x in idautils.XrefsFrom(ea, 0)
989
+ ],
990
+ }
991
+
992
+
993
+ def get_all_comments(ea: int) -> dict:
994
+ """Get all comments for an address"""
995
+ func = idaapi.get_func(ea)
996
+ if not func:
997
+ return {}
998
+
999
+ comments = {}
1000
+ for item_ea in idautils.FuncItems(func.start_ea):
1001
+ cmt = idaapi.get_cmt(item_ea, False)
1002
+ if cmt:
1003
+ comments[hex(item_ea)] = {"regular": cmt}
1004
+ cmt = idaapi.get_cmt(item_ea, True)
1005
+ if cmt:
1006
+ if hex(item_ea) not in comments:
1007
+ comments[hex(item_ea)] = {}
1008
+ comments[hex(item_ea)]["repeatable"] = cmt
1009
+ return comments
1010
+
1011
+
1012
+ def get_callees(addr: str) -> list[dict]:
1013
+ """Get callees for a single function address"""
1014
+ try:
1015
+ func_start = parse_address(addr)
1016
+ func = idaapi.get_func(func_start)
1017
+ if not func:
1018
+ return []
1019
+ func_end = idc.find_func_end(func_start)
1020
+ callees: list[dict[str, str]] = []
1021
+ current_ea = func_start
1022
+ while current_ea < func_end:
1023
+ insn = idaapi.insn_t()
1024
+ idaapi.decode_insn(insn, current_ea)
1025
+ if insn.itype in [idaapi.NN_call, idaapi.NN_callfi, idaapi.NN_callni]:
1026
+ target = idc.get_operand_value(current_ea, 0)
1027
+ target_type = idc.get_operand_type(current_ea, 0)
1028
+ if target_type in [idaapi.o_mem, idaapi.o_near, idaapi.o_far]:
1029
+ func_type = (
1030
+ "internal"
1031
+ if idaapi.get_func(target) is not None
1032
+ else "external"
1033
+ )
1034
+ func_name = idc.get_name(target)
1035
+ if func_name is not None:
1036
+ callees.append(
1037
+ {
1038
+ "addr": hex(target),
1039
+ "name": func_name,
1040
+ "type": func_type,
1041
+ }
1042
+ )
1043
+ current_ea = idc.next_head(current_ea, func_end)
1044
+
1045
+ unique_callee_tuples = {tuple(callee.items()) for callee in callees}
1046
+ unique_callees = [dict(callee) for callee in unique_callee_tuples]
1047
+ return unique_callees
1048
+ except Exception:
1049
+ return []
1050
+
1051
+
1052
+ def get_callers(addr: str, limit: int = 50) -> list[Function]:
1053
+ """Get callers for a single function address"""
1054
+ try:
1055
+ callers = {}
1056
+ iterations = 0
1057
+ max_iterations = limit * 100
1058
+ for caller_addr in idautils.CodeRefsTo(parse_address(addr), 0):
1059
+ iterations += 1
1060
+ if len(callers) >= limit or iterations >= max_iterations:
1061
+ break
1062
+ func = get_function(caller_addr, raise_error=False)
1063
+ if not func:
1064
+ continue
1065
+ insn = idaapi.insn_t()
1066
+ idaapi.decode_insn(insn, caller_addr)
1067
+ if insn.itype not in [
1068
+ idaapi.NN_call,
1069
+ idaapi.NN_callfi,
1070
+ idaapi.NN_callni,
1071
+ ]:
1072
+ continue
1073
+ callers[func["addr"]] = func
1074
+
1075
+ return list(callers.values())
1076
+ except Exception:
1077
+ return []
1078
+
1079
+
1080
+ def get_xrefs_from_internal(ea: int) -> list[Xref]:
1081
+ """Get all xrefs from an address"""
1082
+ xrefs = []
1083
+ for xref in idautils.XrefsFrom(ea, 0):
1084
+ xrefs.append(
1085
+ Xref(
1086
+ addr=hex(xref.to),
1087
+ type="code" if xref.iscode else "data",
1088
+ fn=get_function(xref.to, raise_error=False),
1089
+ )
1090
+ )
1091
+ return xrefs
1092
+
1093
+
1094
+ def extract_function_strings(ea: int) -> list[String]:
1095
+ """Extract string references from a function"""
1096
+ func = idaapi.get_func(ea)
1097
+ if not func:
1098
+ return []
1099
+
1100
+ strings = []
1101
+ for item_ea in idautils.FuncItems(func.start_ea):
1102
+ for xref in idautils.XrefsFrom(item_ea, 0):
1103
+ if not xref.iscode:
1104
+ # Check if target is a string
1105
+ str_type = ida_nalt.get_str_type(xref.to)
1106
+ if str_type != ida_nalt.STRTYPE_C:
1107
+ continue
1108
+ try:
1109
+ str_content = idc.get_strlit_contents(xref.to)
1110
+ if str_content:
1111
+ strings.append(
1112
+ String(
1113
+ addr=hex(xref.to),
1114
+ length=len(str_content),
1115
+ string=str_content.decode("utf-8", errors="replace"),
1116
+ )
1117
+ )
1118
+ except Exception:
1119
+ pass
1120
+ return strings
1121
+
1122
+
1123
+ def extract_function_constants(ea: int) -> list[dict]:
1124
+ """Extract immediate constants from a function"""
1125
+ func = idaapi.get_func(ea)
1126
+ if not func:
1127
+ return []
1128
+
1129
+ constants = []
1130
+ for item_ea in idautils.FuncItems(func.start_ea):
1131
+ insn = idaapi.insn_t()
1132
+ if idaapi.decode_insn(insn, item_ea) > 0:
1133
+ for op in insn.ops:
1134
+ if op.type == idaapi.o_imm:
1135
+ constants.append(
1136
+ {
1137
+ "addr": hex(item_ea),
1138
+ "value": hex(op.value),
1139
+ "decimal": op.value,
1140
+ }
1141
+ )
1142
+ return constants
1143
+
1144
+
1145
+ # ============================================================================
1146
+ # Large Output Handling
1147
+ # ============================================================================
1148
+
1149
+
1150
+ def handle_large_output(result: Any, line_threshold: int = 3000) -> Any:
1151
+ """
1152
+ Handle potentially large outputs by writing to temp file if needed.
1153
+
1154
+ Args:
1155
+ result: The result object to check
1156
+ line_threshold: Number of lines above which to write to file (default: 3000)
1157
+
1158
+ Returns:
1159
+ Either the original result or a dict with file path if written to file
1160
+ """
1161
+ try:
1162
+ serialized = json.dumps(result, indent=2)
1163
+ line_count = serialized.count("\n") + 1
1164
+
1165
+ if line_count > line_threshold:
1166
+ fd, temp_path = tempfile.mkstemp(
1167
+ suffix=".json", prefix="ida_mcp_", text=True
1168
+ )
1169
+ try:
1170
+ with os.fdopen(fd, "w") as f:
1171
+ f.write(serialized)
1172
+
1173
+ return {
1174
+ "type": "file_reference",
1175
+ "path": temp_path,
1176
+ "line_count": line_count,
1177
+ "message": f"Output too large ({line_count} lines), written to file",
1178
+ }
1179
+ except Exception:
1180
+ os.close(fd)
1181
+ raise
1182
+
1183
+ return result
1184
+
1185
+ except Exception:
1186
+ return result