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,480 @@
1
+ from typing import Annotated
2
+
3
+ import ida_typeinf
4
+ import ida_hexrays
5
+ import ida_nalt
6
+ import ida_bytes
7
+ import ida_frame
8
+ import ida_ida
9
+ import idaapi
10
+
11
+ from .rpc import tool
12
+ from .sync import idasync, ida_major
13
+ from .utils import (
14
+ normalize_list_input,
15
+ normalize_dict_list,
16
+ parse_address,
17
+ get_type_by_name,
18
+ parse_decls_ctypes,
19
+ my_modifier_t,
20
+ StructRead,
21
+ TypeEdit,
22
+ )
23
+
24
+
25
+ # ============================================================================
26
+ # Type Declaration
27
+ # ============================================================================
28
+
29
+
30
+ @tool
31
+ @idasync
32
+ def declare_type(
33
+ decls: Annotated[list[str] | str, "C type declarations"],
34
+ ) -> list[dict]:
35
+ """Declare types"""
36
+ decls = normalize_list_input(decls)
37
+ results = []
38
+
39
+ for decl in decls:
40
+ try:
41
+ flags = ida_typeinf.PT_SIL | ida_typeinf.PT_EMPTY | ida_typeinf.PT_TYP
42
+ errors, messages = parse_decls_ctypes(decl, flags)
43
+
44
+ pretty_messages = "\n".join(messages)
45
+ if errors > 0:
46
+ results.append(
47
+ {"decl": decl, "error": f"Failed to parse:\n{pretty_messages}"}
48
+ )
49
+ else:
50
+ results.append({"decl": decl, "ok": True})
51
+ except Exception as e:
52
+ results.append({"decl": decl, "error": str(e)})
53
+
54
+ return results
55
+
56
+
57
+ # ============================================================================
58
+ # Structure Operations
59
+ # ============================================================================
60
+
61
+
62
+ @tool
63
+ @idasync
64
+ def read_struct(queries: list[StructRead] | StructRead) -> list[dict]:
65
+ """Reads struct type definition and parses actual memory values at the
66
+ given address as instances of that struct type.
67
+
68
+ If struct name is not provided, attempts to auto-detect from address.
69
+ Auto-detection only works if IDA already has type information applied
70
+ at that address
71
+
72
+ Returns struct layout with actual memory values for each field.
73
+ """
74
+
75
+ queries = normalize_dict_list(queries)
76
+
77
+ results = []
78
+ for query in queries:
79
+ addr_str = query.get("addr", "")
80
+ struct_name = query.get("struct", "")
81
+
82
+ try:
83
+ # Parse address - this is required
84
+ if not addr_str:
85
+ results.append(
86
+ {
87
+ "addr": None,
88
+ "struct": struct_name,
89
+ "members": None,
90
+ "error": "Address is required for reading struct fields",
91
+ }
92
+ )
93
+ continue
94
+
95
+ # Try to parse as address, then try name resolution
96
+ try:
97
+ addr = parse_address(addr_str)
98
+ except Exception:
99
+ addr = idaapi.get_name_ea(idaapi.BADADDR, addr_str)
100
+ if addr == idaapi.BADADDR:
101
+ results.append(
102
+ {
103
+ "addr": addr_str,
104
+ "struct": struct_name,
105
+ "members": None,
106
+ "error": f"Failed to resolve address: {addr_str}",
107
+ }
108
+ )
109
+ continue
110
+
111
+ # Auto-detect struct type from address if not provided
112
+ if not struct_name:
113
+ tif_auto = ida_typeinf.tinfo_t()
114
+ if ida_nalt.get_tinfo(tif_auto, addr) and tif_auto.is_udt():
115
+ struct_name = tif_auto.get_type_name()
116
+
117
+ if not struct_name:
118
+ results.append(
119
+ {
120
+ "addr": addr_str,
121
+ "struct": None,
122
+ "members": None,
123
+ "error": "No struct specified and could not auto-detect from address",
124
+ }
125
+ )
126
+ continue
127
+
128
+ tif = ida_typeinf.tinfo_t()
129
+ if not tif.get_named_type(None, struct_name):
130
+ results.append(
131
+ {
132
+ "addr": addr_str,
133
+ "struct": struct_name,
134
+ "members": None,
135
+ "error": f"Struct '{struct_name}' not found",
136
+ }
137
+ )
138
+ continue
139
+
140
+ udt_data = ida_typeinf.udt_type_data_t()
141
+ if not tif.get_udt_details(udt_data):
142
+ results.append(
143
+ {
144
+ "addr": addr_str,
145
+ "struct": struct_name,
146
+ "members": None,
147
+ "error": "Failed to get struct details",
148
+ }
149
+ )
150
+ continue
151
+
152
+ members = []
153
+ for member in udt_data:
154
+ offset = member.begin() // 8
155
+ member_type = member.type._print()
156
+ member_name = member.name
157
+ member_size = member.type.get_size()
158
+
159
+ # Read memory value at member address
160
+ member_addr = addr + offset
161
+ try:
162
+ if member.type.is_ptr():
163
+ is_64bit = (
164
+ ida_ida.inf_is_64bit()
165
+ if ida_major >= 9
166
+ else idaapi.get_inf_structure().is_64bit()
167
+ )
168
+ if is_64bit:
169
+ value = idaapi.get_qword(member_addr)
170
+ value_str = f"0x{value:016X}"
171
+ else:
172
+ value = idaapi.get_dword(member_addr)
173
+ value_str = f"0x{value:08X}"
174
+ elif member_size == 1:
175
+ value = idaapi.get_byte(member_addr)
176
+ value_str = f"0x{value:02X} ({value})"
177
+ elif member_size == 2:
178
+ value = idaapi.get_word(member_addr)
179
+ value_str = f"0x{value:04X} ({value})"
180
+ elif member_size == 4:
181
+ value = idaapi.get_dword(member_addr)
182
+ value_str = f"0x{value:08X} ({value})"
183
+ elif member_size == 8:
184
+ value = idaapi.get_qword(member_addr)
185
+ value_str = f"0x{value:016X} ({value})"
186
+ else:
187
+ bytes_data = []
188
+ for i in range(min(member_size, 16)):
189
+ try:
190
+ bytes_data.append(
191
+ f"{idaapi.get_byte(member_addr + i):02X}"
192
+ )
193
+ except Exception:
194
+ break
195
+ value_str = f"[{' '.join(bytes_data)}{'...' if member_size > 16 else ''}]"
196
+ except Exception:
197
+ value_str = "<failed to read>"
198
+
199
+ member_info = {
200
+ "offset": f"0x{offset:08X}",
201
+ "type": member_type,
202
+ "name": member_name,
203
+ "size": member_size,
204
+ "value": value_str,
205
+ }
206
+
207
+ members.append(member_info)
208
+
209
+ results.append(
210
+ {"addr": addr_str, "struct": struct_name, "members": members}
211
+ )
212
+ except Exception as e:
213
+ results.append(
214
+ {
215
+ "addr": addr_str,
216
+ "struct": struct_name,
217
+ "members": None,
218
+ "error": str(e),
219
+ }
220
+ )
221
+
222
+ return results
223
+
224
+
225
+ @tool
226
+ @idasync
227
+ def search_structs(
228
+ filter: Annotated[
229
+ str, "Case-insensitive substring to search for in structure names"
230
+ ],
231
+ ) -> list[dict]:
232
+ """Search structs"""
233
+ results = []
234
+ limit = ida_typeinf.get_ordinal_limit()
235
+
236
+ for ordinal in range(1, limit):
237
+ tif = ida_typeinf.tinfo_t()
238
+ if tif.get_numbered_type(None, ordinal):
239
+ type_name: str = tif.get_type_name()
240
+ if type_name and filter.lower() in type_name.lower():
241
+ if tif.is_udt():
242
+ udt_data = ida_typeinf.udt_type_data_t()
243
+ cardinality = 0
244
+ if tif.get_udt_details(udt_data):
245
+ cardinality = udt_data.size()
246
+
247
+ results.append(
248
+ {
249
+ "name": type_name,
250
+ "size": tif.get_size(),
251
+ "cardinality": cardinality,
252
+ "is_union": (
253
+ udt_data.is_union
254
+ if tif.get_udt_details(udt_data)
255
+ else False
256
+ ),
257
+ "ordinal": ordinal,
258
+ }
259
+ )
260
+
261
+ return results
262
+
263
+
264
+ # ============================================================================
265
+ # Type Inference & Application
266
+ # ============================================================================
267
+
268
+
269
+ @tool
270
+ @idasync
271
+ def set_type(edits: list[TypeEdit] | TypeEdit) -> list[dict]:
272
+ """Apply types (function/global/local/stack)"""
273
+
274
+ def parse_addr_type(s: str) -> dict:
275
+ # Support "addr:typename" format (auto-detects kind)
276
+ if ":" in s:
277
+ parts = s.split(":", 1)
278
+ return {"addr": parts[0].strip(), "ty": parts[1].strip()}
279
+ # Just typename without address (invalid)
280
+ return {"ty": s.strip()}
281
+
282
+ edits = normalize_dict_list(edits, parse_addr_type)
283
+ results = []
284
+
285
+ for edit in edits:
286
+ try:
287
+ # Auto-detect kind if not provided
288
+ kind = edit.get("kind")
289
+ if not kind:
290
+ if "signature" in edit:
291
+ kind = "function"
292
+ elif "variable" in edit:
293
+ kind = "local"
294
+ elif "addr" in edit:
295
+ # Check if address points to a function
296
+ try:
297
+ addr = parse_address(edit["addr"])
298
+ func = idaapi.get_func(addr)
299
+ if func and "name" in edit and "ty" in edit:
300
+ kind = "stack"
301
+ else:
302
+ kind = "global"
303
+ except Exception:
304
+ kind = "global"
305
+ else:
306
+ kind = "global"
307
+
308
+ if kind == "function":
309
+ func = idaapi.get_func(parse_address(edit["addr"]))
310
+ if not func:
311
+ results.append({"edit": edit, "error": "Function not found"})
312
+ continue
313
+
314
+ tif = ida_typeinf.tinfo_t(edit["signature"], None, ida_typeinf.PT_SIL)
315
+ if not tif.is_func():
316
+ results.append({"edit": edit, "error": "Not a function type"})
317
+ continue
318
+
319
+ success = ida_typeinf.apply_tinfo(
320
+ func.start_ea, tif, ida_typeinf.PT_SIL
321
+ )
322
+ results.append(
323
+ {
324
+ "edit": edit,
325
+ "ok": success,
326
+ "error": None if success else "Failed to apply type",
327
+ }
328
+ )
329
+
330
+ elif kind == "global":
331
+ ea = idaapi.get_name_ea(idaapi.BADADDR, edit.get("name", ""))
332
+ if ea == idaapi.BADADDR:
333
+ ea = parse_address(edit["addr"])
334
+
335
+ tif = get_type_by_name(edit["ty"])
336
+ success = ida_typeinf.apply_tinfo(ea, tif, ida_typeinf.PT_SIL)
337
+ results.append(
338
+ {
339
+ "edit": edit,
340
+ "ok": success,
341
+ "error": None if success else "Failed to apply type",
342
+ }
343
+ )
344
+
345
+ elif kind == "local":
346
+ func = idaapi.get_func(parse_address(edit["addr"]))
347
+ if not func:
348
+ results.append({"edit": edit, "error": "Function not found"})
349
+ continue
350
+
351
+ new_tif = ida_typeinf.tinfo_t(edit["ty"], None, ida_typeinf.PT_SIL)
352
+ modifier = my_modifier_t(edit["variable"], new_tif)
353
+ success = ida_hexrays.modify_user_lvars(func.start_ea, modifier)
354
+ results.append(
355
+ {
356
+ "edit": edit,
357
+ "ok": success,
358
+ "error": None if success else "Failed to apply type",
359
+ }
360
+ )
361
+
362
+ elif kind == "stack":
363
+ func = idaapi.get_func(parse_address(edit["addr"]))
364
+ if not func:
365
+ results.append({"edit": edit, "error": "No function found"})
366
+ continue
367
+
368
+ frame_tif = ida_typeinf.tinfo_t()
369
+ if not ida_frame.get_func_frame(frame_tif, func):
370
+ results.append({"edit": edit, "error": "No frame"})
371
+ continue
372
+
373
+ idx, udm = frame_tif.get_udm(edit["name"])
374
+ if not udm:
375
+ results.append({"edit": edit, "error": f"{edit['name']} not found"})
376
+ continue
377
+
378
+ tid = frame_tif.get_udm_tid(idx)
379
+ udm = ida_typeinf.udm_t()
380
+ frame_tif.get_udm_by_tid(udm, tid)
381
+ offset = udm.offset // 8
382
+
383
+ tif = get_type_by_name(edit["ty"])
384
+ success = ida_frame.set_frame_member_type(func, offset, tif)
385
+ results.append(
386
+ {
387
+ "edit": edit,
388
+ "ok": success,
389
+ "error": None if success else "Failed to set type",
390
+ }
391
+ )
392
+
393
+ else:
394
+ results.append({"edit": edit, "error": f"Unknown kind: {kind}"})
395
+
396
+ except Exception as e:
397
+ results.append({"edit": edit, "error": str(e)})
398
+
399
+ return results
400
+
401
+
402
+ @tool
403
+ @idasync
404
+ def infer_types(
405
+ addrs: Annotated[list[str] | str, "Addresses to infer types for"],
406
+ ) -> list[dict]:
407
+ """Infer types"""
408
+ addrs = normalize_list_input(addrs)
409
+ results = []
410
+
411
+ for addr in addrs:
412
+ try:
413
+ ea = parse_address(addr)
414
+ tif = ida_typeinf.tinfo_t()
415
+
416
+ # Try Hex-Rays inference
417
+ if ida_hexrays.init_hexrays_plugin() and ida_hexrays.guess_tinfo(tif, ea):
418
+ results.append(
419
+ {
420
+ "addr": addr,
421
+ "inferred_type": str(tif),
422
+ "method": "hexrays",
423
+ "confidence": "high",
424
+ }
425
+ )
426
+ continue
427
+
428
+ # Try getting existing type info
429
+ if ida_nalt.get_tinfo(tif, ea):
430
+ results.append(
431
+ {
432
+ "addr": addr,
433
+ "inferred_type": str(tif),
434
+ "method": "existing",
435
+ "confidence": "high",
436
+ }
437
+ )
438
+ continue
439
+
440
+ # Try to guess from size
441
+ size = ida_bytes.get_item_size(ea)
442
+ if size > 0:
443
+ type_guess = {
444
+ 1: "uint8_t",
445
+ 2: "uint16_t",
446
+ 4: "uint32_t",
447
+ 8: "uint64_t",
448
+ }.get(size, f"uint8_t[{size}]")
449
+
450
+ results.append(
451
+ {
452
+ "addr": addr,
453
+ "inferred_type": type_guess,
454
+ "method": "size_based",
455
+ "confidence": "low",
456
+ }
457
+ )
458
+ continue
459
+
460
+ results.append(
461
+ {
462
+ "addr": addr,
463
+ "inferred_type": None,
464
+ "method": None,
465
+ "confidence": "none",
466
+ }
467
+ )
468
+
469
+ except Exception as e:
470
+ results.append(
471
+ {
472
+ "addr": addr,
473
+ "inferred_type": None,
474
+ "method": None,
475
+ "confidence": "none",
476
+ "error": str(e),
477
+ }
478
+ )
479
+
480
+ return results
@@ -0,0 +1,166 @@
1
+ """IDA MCP API Key Authentication
2
+
3
+ Provides authentication middleware for MCP server with support for:
4
+ - Bearer token authentication (Authorization: Bearer <key>)
5
+ - X-API-Key header authentication
6
+ - Timing-attack resistant comparison
7
+ """
8
+
9
+ import hmac
10
+ import logging
11
+ from typing import Optional, Callable
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Paths that don't require authentication
16
+ AUTH_EXEMPT_PATHS = frozenset({
17
+ "/health",
18
+ "/config.html",
19
+ })
20
+
21
+
22
+ def check_api_key(provided_key: Optional[str], expected_key: Optional[str]) -> bool:
23
+ """Compare API keys using constant-time comparison to prevent timing attacks.
24
+
25
+ Args:
26
+ provided_key: The key provided by the client
27
+ expected_key: The expected API key from configuration
28
+
29
+ Returns:
30
+ True if keys match, False otherwise
31
+ """
32
+ if not expected_key:
33
+ # No key configured = authentication disabled
34
+ return True
35
+
36
+ if not provided_key:
37
+ return False
38
+
39
+ # Use hmac.compare_digest for constant-time comparison
40
+ return hmac.compare_digest(provided_key.encode("utf-8"), expected_key.encode("utf-8"))
41
+
42
+
43
+ def extract_api_key_from_headers(headers: dict) -> Optional[str]:
44
+ """Extract API key from request headers.
45
+
46
+ Supports two formats:
47
+ - Authorization: Bearer <key>
48
+ - X-API-Key: <key>
49
+
50
+ Args:
51
+ headers: Dictionary of HTTP headers (case-insensitive keys)
52
+
53
+ Returns:
54
+ The extracted API key or None
55
+ """
56
+ # Try Authorization header first (Bearer token)
57
+ auth_header = headers.get("Authorization") or headers.get("authorization")
58
+ if auth_header:
59
+ parts = auth_header.split(" ", 1)
60
+ if len(parts) == 2 and parts[0].lower() == "bearer":
61
+ return parts[1].strip()
62
+
63
+ # Try X-API-Key header
64
+ api_key = headers.get("X-API-Key") or headers.get("x-api-key")
65
+ if api_key:
66
+ return api_key.strip()
67
+
68
+ return None
69
+
70
+
71
+ def is_path_exempt(path: str) -> bool:
72
+ """Check if a path is exempt from authentication.
73
+
74
+ Args:
75
+ path: The request path (e.g., "/health", "/mcp")
76
+
77
+ Returns:
78
+ True if the path doesn't require authentication
79
+ """
80
+ # Remove query string if present
81
+ if "?" in path:
82
+ path = path.split("?", 1)[0]
83
+
84
+ return path in AUTH_EXEMPT_PATHS
85
+
86
+
87
+ class AuthMiddleware:
88
+ """Authentication middleware for HTTP request handlers.
89
+
90
+ Usage:
91
+ auth = AuthMiddleware(api_key="secret")
92
+
93
+ # In request handler:
94
+ if not auth.authenticate(request):
95
+ return send_401_response()
96
+ """
97
+
98
+ def __init__(self, api_key: Optional[str] = None, enabled: bool = False):
99
+ """Initialize authentication middleware.
100
+
101
+ Args:
102
+ api_key: The expected API key (None = no authentication)
103
+ enabled: Whether authentication is enabled
104
+ """
105
+ self._api_key = api_key
106
+ self._enabled = enabled and api_key is not None
107
+
108
+ @property
109
+ def enabled(self) -> bool:
110
+ return self._enabled
111
+
112
+ def update_key(self, api_key: Optional[str], enabled: bool = True) -> None:
113
+ """Update the API key configuration.
114
+
115
+ Args:
116
+ api_key: New API key
117
+ enabled: Whether to enable authentication
118
+ """
119
+ self._api_key = api_key
120
+ self._enabled = enabled and api_key is not None
121
+
122
+ def authenticate(self, path: str, headers: dict) -> bool:
123
+ """Authenticate a request.
124
+
125
+ Args:
126
+ path: Request path
127
+ headers: Request headers dictionary
128
+
129
+ Returns:
130
+ True if authenticated, False if authentication failed
131
+ """
132
+ # Skip if authentication is disabled
133
+ if not self._enabled:
134
+ return True
135
+
136
+ # Check if path is exempt
137
+ if is_path_exempt(path):
138
+ return True
139
+
140
+ # Extract and verify API key
141
+ provided_key = extract_api_key_from_headers(headers)
142
+ return check_api_key(provided_key, self._api_key)
143
+
144
+
145
+ def create_auth_check(api_key: Optional[str], enabled: bool = False) -> Callable[[str, dict], bool]:
146
+ """Create a simple authentication check function.
147
+
148
+ Args:
149
+ api_key: The expected API key
150
+ enabled: Whether authentication is enabled
151
+
152
+ Returns:
153
+ A function that takes (path, headers) and returns True if authenticated
154
+ """
155
+ middleware = AuthMiddleware(api_key, enabled)
156
+ return middleware.authenticate
157
+
158
+
159
+ __all__ = [
160
+ "check_api_key",
161
+ "extract_api_key_from_headers",
162
+ "is_path_exempt",
163
+ "AuthMiddleware",
164
+ "create_auth_check",
165
+ "AUTH_EXEMPT_PATHS",
166
+ ]