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
ida_pro_mcp/ida_mcp.py ADDED
@@ -0,0 +1,186 @@
1
+ """IDA Pro MCP Plugin Loader
2
+
3
+ This file serves as the entry point for IDA Pro's plugin system.
4
+ It loads the actual implementation from the ida_mcp package.
5
+
6
+ Features:
7
+ - Web-based configuration at http://host:port/config.html
8
+ - Bilingual interface (English/中文)
9
+ - Server restart on config change
10
+ """
11
+
12
+ import sys
13
+ import idaapi
14
+ from typing import TYPE_CHECKING
15
+
16
+ if TYPE_CHECKING:
17
+ from . import ida_mcp
18
+
19
+
20
+ def unload_package(package_name: str):
21
+ """Remove every module that belongs to the package from sys.modules."""
22
+ to_remove = [
23
+ mod_name
24
+ for mod_name in sys.modules
25
+ if mod_name == package_name or mod_name.startswith(package_name + ".")
26
+ ]
27
+ for mod_name in to_remove:
28
+ del sys.modules[mod_name]
29
+
30
+
31
+ class MCP(idaapi.plugin_t):
32
+ flags = idaapi.PLUGIN_KEEP
33
+ comment = "MCP Server for LLM-assisted reverse engineering"
34
+ help = "Start/Stop MCP Server for AI assistants like Claude"
35
+ wanted_name = "MCP Server" # Menu name: Edit -> Plugins -> MCP Server
36
+ wanted_hotkey = "" # No hotkey
37
+
38
+ def init(self):
39
+ print("[MCP] Plugin loaded, use Edit -> Plugins -> MCP Server to toggle")
40
+ self.mcp = None
41
+ self._current_host = None
42
+ self._current_port = None
43
+
44
+ # Auto-start server on IDA launch
45
+ self._auto_start()
46
+
47
+ return idaapi.PLUGIN_KEEP
48
+
49
+ def _auto_start(self):
50
+ """Auto-start server when IDA loads a database."""
51
+ # Use timer to delay startup until IDA is fully initialized
52
+ def delayed_start():
53
+ if self.mcp is None:
54
+ self._start_server()
55
+ return -1 # Don't repeat
56
+
57
+ # Delay 1 second to ensure IDA is ready
58
+ idaapi.register_timer(1000, delayed_start)
59
+
60
+ def _start_server(self):
61
+ """Start the MCP server."""
62
+ if self.mcp:
63
+ return # Already running
64
+
65
+ # HACK: ensure fresh load of ida_mcp package
66
+ unload_package("ida_mcp")
67
+ if TYPE_CHECKING:
68
+ from .ida_mcp import MCP_SERVER, IdaMcpHttpRequestHandler, init_caches, set_server_restart_callback, get_server_config
69
+ from .ida_mcp.port_utils import try_serve_with_port_retry, format_port_exhausted_message
70
+ from .ida_mcp.rpc import set_download_base_url
71
+ else:
72
+ from ida_mcp import MCP_SERVER, IdaMcpHttpRequestHandler, init_caches, set_server_restart_callback, get_server_config
73
+ from ida_mcp.port_utils import try_serve_with_port_retry, format_port_exhausted_message
74
+ from ida_mcp.rpc import set_download_base_url
75
+
76
+ try:
77
+ init_caches()
78
+ except Exception as e:
79
+ print(f"[MCP] Cache init failed: {e}")
80
+
81
+ # Set restart callback for web config
82
+ set_server_restart_callback(self._restart_server)
83
+
84
+ # Get server config from IDA database
85
+ server_config = get_server_config()
86
+ host = server_config.get("host", "127.0.0.1")
87
+ port = server_config.get("port", 13337)
88
+
89
+ try:
90
+ actual_port, failed_ports = try_serve_with_port_retry(
91
+ MCP_SERVER, host, port, request_handler=IdaMcpHttpRequestHandler
92
+ )
93
+ if failed_ports:
94
+ print(f"[MCP] Port {port} was in use, auto-selected port {actual_port}")
95
+ self._current_host = host
96
+ self._current_port = actual_port
97
+ self.mcp = MCP_SERVER
98
+ set_download_base_url(f"http://{host}:{actual_port}")
99
+ print(f"[MCP] Server started on http://{host}:{actual_port}")
100
+ print(f" Config: http://{host}:{actual_port}/config.html")
101
+ except OSError as e:
102
+ if e.errno in (48, 98, 10048):
103
+ print(format_port_exhausted_message(host, port, list(range(port, port + 10))))
104
+ else:
105
+ print(f"[MCP] Error starting server: {e}")
106
+
107
+ def _restart_server(self, new_host: str, new_port: int):
108
+ """Callback to restart the server with new configuration.
109
+
110
+ This is called from a background thread, so we need to use
111
+ execute_sync to run the actual restart on IDA's main thread.
112
+ """
113
+ def do_restart():
114
+ print(f"[MCP] Restarting server on {new_host}:{new_port}...")
115
+
116
+ # Stop current server
117
+ if self.mcp:
118
+ try:
119
+ self.mcp.stop()
120
+ except Exception as e:
121
+ print(f"[MCP] Error stopping server: {e}")
122
+ self.mcp = None
123
+
124
+ # Reload package and start new server
125
+ unload_package("ida_mcp")
126
+
127
+ if TYPE_CHECKING:
128
+ from .ida_mcp import MCP_SERVER, IdaMcpHttpRequestHandler, init_caches, set_server_restart_callback
129
+ from .ida_mcp.port_utils import try_serve_with_port_retry, format_port_exhausted_message
130
+ from .ida_mcp.rpc import set_download_base_url
131
+ else:
132
+ from ida_mcp import MCP_SERVER, IdaMcpHttpRequestHandler, init_caches, set_server_restart_callback
133
+ from ida_mcp.port_utils import try_serve_with_port_retry, format_port_exhausted_message
134
+ from ida_mcp.rpc import set_download_base_url
135
+
136
+ try:
137
+ init_caches()
138
+ except Exception as e:
139
+ print(f"[MCP] Cache init failed: {e}")
140
+
141
+ # Set restart callback
142
+ set_server_restart_callback(self._restart_server)
143
+
144
+ try:
145
+ actual_port, failed_ports = try_serve_with_port_retry(
146
+ MCP_SERVER, new_host, new_port, request_handler=IdaMcpHttpRequestHandler
147
+ )
148
+ if failed_ports:
149
+ print(f"[MCP] Port {new_port} was in use, auto-selected port {actual_port}")
150
+ self._current_host = new_host
151
+ self._current_port = actual_port
152
+ self.mcp = MCP_SERVER
153
+ set_download_base_url(f"http://{new_host}:{actual_port}")
154
+ print(f"[MCP] Server restarted on http://{new_host}:{actual_port}")
155
+ print(f" Config: http://{new_host}:{actual_port}/config.html")
156
+ except OSError as e:
157
+ if e.errno in (48, 98, 10048):
158
+ print(format_port_exhausted_message(new_host, new_port, list(range(new_port, new_port + 10))))
159
+ else:
160
+ print(f"[MCP] Error starting server: {e}")
161
+
162
+ return 1 # Required for execute_sync callback
163
+
164
+ # Execute on IDA's main thread
165
+ idaapi.execute_sync(do_restart, idaapi.MFF_WRITE)
166
+
167
+ def run(self, arg):
168
+ """Toggle server on/off via menu."""
169
+ if self.mcp:
170
+ self.mcp.stop()
171
+ self.mcp = None
172
+ print("[MCP] Server stopped")
173
+ else:
174
+ self._start_server()
175
+
176
+ def term(self):
177
+ if self.mcp:
178
+ self.mcp.stop()
179
+
180
+
181
+ def PLUGIN_ENTRY():
182
+ return MCP()
183
+
184
+
185
+ # IDA plugin flags
186
+ PLUGIN_FLAGS = idaapi.PLUGIN_HIDE | idaapi.PLUGIN_FIX
@@ -0,0 +1,354 @@
1
+ import sys
2
+ import signal
3
+ import logging
4
+ import argparse
5
+ from pathlib import Path
6
+
7
+ # idapro must go first to initialize idalib
8
+ import idapro
9
+
10
+ from ida_pro_mcp.ida_mcp import MCP_SERVER
11
+
12
+ """IDALib-specific MCP tools for managing multiple binary sessions
13
+ """
14
+ from typing import Annotated, Optional
15
+ from ida_pro_mcp.ida_mcp.rpc import tool
16
+ from ida_pro_mcp.idalib_session_manager import get_session_manager
17
+
18
+
19
+ @tool
20
+ def idalib_open(
21
+ input_path: Annotated[str, "Path to the binary file to analyze"],
22
+ run_auto_analysis: Annotated[bool, "Run automatic analysis on the binary"] = True,
23
+ session_id: Annotated[
24
+ Optional[str], "Custom session ID (auto-generated if not provided)"
25
+ ] = None,
26
+ ) -> dict:
27
+ """Open a binary file and create a new IDA session (idalib mode only)
28
+
29
+ Opens a binary file for analysis and creates a new session. The binary will be
30
+ analyzed in IDA's headless mode. If the file is already open, returns the existing
31
+ session ID.
32
+
33
+ Args:
34
+ input_path: Path to the binary file to analyze
35
+ run_auto_analysis: Whether to run IDA's automatic analysis (default: True)
36
+ session_id: Optional custom session ID (default: auto-generated)
37
+
38
+ Returns:
39
+ Dictionary with session information:
40
+ - session_id: Unique identifier for this session
41
+ - input_path: Path to the binary file
42
+ - filename: Name of the binary file
43
+ - created_at: Session creation timestamp
44
+ - is_analyzing: Whether analysis is currently running
45
+
46
+ Example:
47
+ ```json
48
+ {
49
+ "session_id": "a3f4c8b2",
50
+ "input_path": "/path/to/binary.exe",
51
+ "filename": "binary.exe",
52
+ "created_at": "2025-12-25T10:30:00",
53
+ "is_analyzing": false
54
+ }
55
+ ```
56
+ """
57
+
58
+ try:
59
+ manager = get_session_manager()
60
+ session_id_result = manager.open_binary(
61
+ Path(input_path), run_auto_analysis=run_auto_analysis, session_id=session_id
62
+ )
63
+
64
+ session = manager.get_session(session_id_result)
65
+ if session is None:
66
+ return {
67
+ "error": f"Failed to retrieve session after opening: {session_id_result}"
68
+ }
69
+
70
+ return {
71
+ "success": True,
72
+ "session": session.to_dict(),
73
+ "message": f"Binary opened successfully: {session.input_path.name}",
74
+ }
75
+ except FileNotFoundError as e:
76
+ return {"error": str(e)}
77
+ except RuntimeError as e:
78
+ return {"error": f"Failed to open binary: {e}"}
79
+ except Exception as e:
80
+ return {"error": f"Unexpected error: {e}"}
81
+
82
+
83
+ @tool
84
+ def idalib_close(session_id: Annotated[str, "Session ID to close"]) -> dict:
85
+ """Close an IDA session and its associated database (idalib mode only)
86
+
87
+ Closes the specified session and releases all associated resources. If this is
88
+ the currently active session, the database will be closed.
89
+
90
+ Args:
91
+ session_id: Unique identifier of the session to close
92
+
93
+ Returns:
94
+ Dictionary with operation result:
95
+ - success: Whether the operation succeeded
96
+ - message: Descriptive message
97
+
98
+ Example:
99
+ ```json
100
+ {
101
+ "success": true,
102
+ "message": "Session closed: a3f4c8b2"
103
+ }
104
+ ```
105
+ """
106
+
107
+ try:
108
+ manager = get_session_manager()
109
+
110
+ if manager.close_session(session_id):
111
+ return {"success": True, "message": f"Session closed: {session_id}"}
112
+ else:
113
+ return {"success": False, "error": f"Session not found: {session_id}"}
114
+ except Exception as e:
115
+ return {"error": f"Failed to close session: {e}"}
116
+
117
+
118
+ @tool
119
+ def idalib_switch(session_id: Annotated[str, "Session ID to switch to"]) -> dict:
120
+ """Switch to a different IDA session (idalib mode only)
121
+
122
+ Switches the active session to the specified session. This closes the current
123
+ database and opens the target session's database. All subsequent MCP tool calls
124
+ will operate on the switched session.
125
+
126
+ Args:
127
+ session_id: Unique identifier of the session to switch to
128
+
129
+ Returns:
130
+ Dictionary with session information after switching:
131
+ - success: Whether the switch succeeded
132
+ - session: Current session details
133
+ - message: Descriptive message
134
+
135
+ Example:
136
+ ```json
137
+ {
138
+ "success": true,
139
+ "session": {
140
+ "session_id": "a3f4c8b2",
141
+ "filename": "binary.exe",
142
+ "is_current": true
143
+ },
144
+ "message": "Switched to session: a3f4c8b2"
145
+ }
146
+ ```
147
+ """
148
+
149
+ try:
150
+ manager = get_session_manager()
151
+
152
+ if manager.switch_session(session_id):
153
+ session = manager.get_current_session()
154
+ if session is None:
155
+ return {"error": "Failed to retrieve current session after switching"}
156
+
157
+ return {
158
+ "success": True,
159
+ "session": session.to_dict(),
160
+ "message": f"Switched to session: {session_id} ({session.input_path.name})",
161
+ }
162
+ except ValueError as e:
163
+ return {"error": str(e)}
164
+ except RuntimeError as e:
165
+ return {"error": f"Failed to switch session: {e}"}
166
+ except Exception as e:
167
+ return {"error": f"Unexpected error: {e}"}
168
+
169
+
170
+ @tool
171
+ def idalib_list() -> dict:
172
+ """List all open IDA sessions (idalib mode only)
173
+
174
+ Returns a list of all currently open sessions with their metadata. The current
175
+ active session is marked with is_current=true.
176
+
177
+ Returns:
178
+ Dictionary with sessions list:
179
+ - sessions: List of session dictionaries
180
+ - count: Total number of sessions
181
+ - current_session_id: ID of the currently active session
182
+
183
+ Example:
184
+ ```json
185
+ {
186
+ "sessions": [
187
+ {
188
+ "session_id": "a3f4c8b2",
189
+ "filename": "binary1.exe",
190
+ "input_path": "/path/to/binary1.exe",
191
+ "created_at": "2025-12-25T10:30:00",
192
+ "last_accessed": "2025-12-25T10:35:00",
193
+ "is_current": true,
194
+ "is_analyzing": false
195
+ },
196
+ {
197
+ "session_id": "b7e2d9f1",
198
+ "filename": "binary2.dll",
199
+ "input_path": "/path/to/binary2.dll",
200
+ "created_at": "2025-12-25T10:31:00",
201
+ "last_accessed": "2025-12-25T10:31:00",
202
+ "is_current": false,
203
+ "is_analyzing": false
204
+ }
205
+ ],
206
+ "count": 2,
207
+ "current_session_id": "a3f4c8b2"
208
+ }
209
+ ```
210
+ """
211
+
212
+ try:
213
+ manager = get_session_manager()
214
+ sessions = manager.list_sessions()
215
+ current_session = manager.get_current_session()
216
+
217
+ return {
218
+ "sessions": sessions,
219
+ "count": len(sessions),
220
+ "current_session_id": current_session.session_id
221
+ if current_session
222
+ else None,
223
+ }
224
+ except Exception as e:
225
+ return {"error": f"Failed to list sessions: {e}"}
226
+
227
+
228
+ @tool
229
+ def idalib_current() -> dict:
230
+ """Get information about the current active IDA session (idalib mode only)
231
+
232
+ Returns detailed information about the currently active session, or an error
233
+ if no session is active.
234
+
235
+ Returns:
236
+ Dictionary with current session information:
237
+ - session_id: Unique identifier
238
+ - filename: Binary file name
239
+ - input_path: Full path to the binary
240
+ - created_at: Session creation timestamp
241
+ - last_accessed: Last access timestamp
242
+ - is_analyzing: Whether analysis is running
243
+ - metadata: Additional session metadata
244
+
245
+ Example:
246
+ ```json
247
+ {
248
+ "session_id": "a3f4c8b2",
249
+ "filename": "binary.exe",
250
+ "input_path": "/path/to/binary.exe",
251
+ "created_at": "2025-12-25T10:30:00",
252
+ "last_accessed": "2025-12-25T10:35:00",
253
+ "is_analyzing": false,
254
+ "metadata": {}
255
+ }
256
+ ```
257
+ """
258
+
259
+ try:
260
+ manager = get_session_manager()
261
+ session = manager.get_current_session()
262
+
263
+ if session is None:
264
+ return {
265
+ "error": "No active session. Use idalib_open() to open a binary first."
266
+ }
267
+
268
+ return session.to_dict()
269
+ except Exception as e:
270
+ return {"error": f"Failed to get current session: {e}"}
271
+
272
+
273
+ logger = logging.getLogger(__name__)
274
+
275
+
276
+ def main():
277
+ parser = argparse.ArgumentParser(description="MCP server for IDA Pro via idalib")
278
+ parser.add_argument(
279
+ "--verbose", "-v", action="store_true", help="Show debug messages"
280
+ )
281
+ parser.add_argument(
282
+ "--host",
283
+ type=str,
284
+ default="127.0.0.1",
285
+ help="Host to listen on, default: 127.0.0.1",
286
+ )
287
+ parser.add_argument(
288
+ "--port", type=int, default=8745, help="Port to listen on, default: 8745"
289
+ )
290
+ parser.add_argument(
291
+ "--unsafe", action="store_true", help="Enable unsafe functions (DANGEROUS)"
292
+ )
293
+ parser.add_argument(
294
+ "input_path",
295
+ type=Path,
296
+ nargs="?", # Make input_path optional
297
+ help="Path to the input file to analyze (optional, can be loaded dynamically via MCP tools).",
298
+ )
299
+ args = parser.parse_args()
300
+
301
+ if args.verbose:
302
+ log_level = logging.DEBUG
303
+ idapro.enable_console_messages(True)
304
+ else:
305
+ log_level = logging.INFO
306
+ idapro.enable_console_messages(False)
307
+
308
+ logging.basicConfig(level=log_level)
309
+
310
+ # reset logging levels that might be initialized in idapythonrc.py
311
+ # which is evaluated during import of idalib.
312
+ logging.getLogger().setLevel(log_level)
313
+
314
+ # Initialize session manager for dynamic binary loading
315
+ from ida_pro_mcp.idalib_session_manager import get_session_manager
316
+
317
+ session_manager = get_session_manager()
318
+
319
+ # Open initial binary if provided
320
+ if args.input_path is not None:
321
+ if not args.input_path.exists():
322
+ raise FileNotFoundError(f"Input file not found: {args.input_path}")
323
+
324
+ logger.info("opening initial database: %s", args.input_path)
325
+ session_id = session_manager.open_binary(
326
+ args.input_path, run_auto_analysis=True
327
+ )
328
+ logger.info(f"Initial session created: {session_id}")
329
+ else:
330
+ logger.info(
331
+ "No initial binary specified. Use idalib_open() to load binaries dynamically."
332
+ )
333
+
334
+ # Setup signal handlers to ensure IDA database is properly closed on shutdown.
335
+ # When a signal arrives, our handlers execute first, allowing us to close the
336
+ # IDA database cleanly before the process terminates.
337
+ def cleanup_and_exit(signum, frame):
338
+ logger.info("Shutting down...")
339
+ logger.info("Closing all IDA sessions...")
340
+ session_manager.close_all_sessions()
341
+ logger.info("All sessions closed.")
342
+ sys.exit(0)
343
+
344
+ signal.signal(signal.SIGINT, cleanup_and_exit)
345
+ signal.signal(signal.SIGTERM, cleanup_and_exit)
346
+
347
+ # NOTE: npx -y @modelcontextprotocol/inspector for debugging
348
+ # TODO: with background=True the main thread (this one) does not fake any
349
+ # work from @idasync, so we deadlock.
350
+ MCP_SERVER.serve(host=args.host, port=args.port, background=False)
351
+
352
+
353
+ if __name__ == "__main__":
354
+ main()