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.
- ida_pro_mcp/__init__.py +0 -0
- ida_pro_mcp/__main__.py +6 -0
- ida_pro_mcp/ida_mcp/__init__.py +68 -0
- ida_pro_mcp/ida_mcp/api_analysis.py +1296 -0
- ida_pro_mcp/ida_mcp/api_core.py +337 -0
- ida_pro_mcp/ida_mcp/api_debug.py +617 -0
- ida_pro_mcp/ida_mcp/api_memory.py +304 -0
- ida_pro_mcp/ida_mcp/api_modify.py +406 -0
- ida_pro_mcp/ida_mcp/api_python.py +179 -0
- ida_pro_mcp/ida_mcp/api_resources.py +295 -0
- ida_pro_mcp/ida_mcp/api_stack.py +167 -0
- ida_pro_mcp/ida_mcp/api_types.py +480 -0
- ida_pro_mcp/ida_mcp/auth.py +166 -0
- ida_pro_mcp/ida_mcp/cache.py +232 -0
- ida_pro_mcp/ida_mcp/config.py +228 -0
- ida_pro_mcp/ida_mcp/framework.py +547 -0
- ida_pro_mcp/ida_mcp/http.py +859 -0
- ida_pro_mcp/ida_mcp/port_utils.py +104 -0
- ida_pro_mcp/ida_mcp/rpc.py +187 -0
- ida_pro_mcp/ida_mcp/server_manager.py +339 -0
- ida_pro_mcp/ida_mcp/sync.py +233 -0
- ida_pro_mcp/ida_mcp/tests/__init__.py +14 -0
- ida_pro_mcp/ida_mcp/tests/test_api_analysis.py +336 -0
- ida_pro_mcp/ida_mcp/tests/test_api_core.py +237 -0
- ida_pro_mcp/ida_mcp/tests/test_api_memory.py +207 -0
- ida_pro_mcp/ida_mcp/tests/test_api_modify.py +123 -0
- ida_pro_mcp/ida_mcp/tests/test_api_resources.py +199 -0
- ida_pro_mcp/ida_mcp/tests/test_api_stack.py +77 -0
- ida_pro_mcp/ida_mcp/tests/test_api_types.py +249 -0
- ida_pro_mcp/ida_mcp/ui.py +357 -0
- ida_pro_mcp/ida_mcp/utils.py +1186 -0
- ida_pro_mcp/ida_mcp/zeromcp/__init__.py +5 -0
- ida_pro_mcp/ida_mcp/zeromcp/jsonrpc.py +384 -0
- ida_pro_mcp/ida_mcp/zeromcp/mcp.py +883 -0
- ida_pro_mcp/ida_mcp.py +186 -0
- ida_pro_mcp/idalib_server.py +354 -0
- ida_pro_mcp/idalib_session_manager.py +259 -0
- ida_pro_mcp/server.py +1060 -0
- ida_pro_mcp/test.py +170 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/METADATA +405 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/RECORD +45 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/WHEEL +5 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/entry_points.txt +4 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/licenses/LICENSE +21 -0
- 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()
|