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/server.py
ADDED
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import argparse
|
|
6
|
+
import http.client
|
|
7
|
+
import tempfile
|
|
8
|
+
import traceback
|
|
9
|
+
import tomllib
|
|
10
|
+
import tomli_w
|
|
11
|
+
import threading
|
|
12
|
+
from queue import Queue, Empty, Full
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
from urllib.parse import urlparse
|
|
16
|
+
import glob
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ida_pro_mcp.ida_mcp.zeromcp import McpServer
|
|
20
|
+
from ida_pro_mcp.ida_mcp.zeromcp.jsonrpc import JsonRpcResponse, JsonRpcRequest
|
|
21
|
+
else:
|
|
22
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "ida_mcp"))
|
|
23
|
+
from zeromcp import McpServer
|
|
24
|
+
from zeromcp.jsonrpc import JsonRpcResponse, JsonRpcRequest
|
|
25
|
+
|
|
26
|
+
sys.path.pop(0) # Clean up
|
|
27
|
+
|
|
28
|
+
IDA_HOST = "127.0.0.1"
|
|
29
|
+
IDA_PORT = 13337
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ============================================================================
|
|
33
|
+
# HTTP Connection Pool
|
|
34
|
+
# ============================================================================
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ConnectionPool:
|
|
38
|
+
"""HTTP connection pool for reusing TCP connections to IDA Pro.
|
|
39
|
+
|
|
40
|
+
Reduces TCP handshake overhead by maintaining a pool of persistent connections.
|
|
41
|
+
Thread-safe implementation with automatic connection recycling.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, host: str, port: int, max_size: int = 10, timeout: float = 30.0):
|
|
45
|
+
self.host = host
|
|
46
|
+
self.port = port
|
|
47
|
+
self.max_size = max_size
|
|
48
|
+
self.timeout = timeout
|
|
49
|
+
self._pool: Queue = Queue(maxsize=max_size)
|
|
50
|
+
self._lock = threading.Lock()
|
|
51
|
+
self._created = 0
|
|
52
|
+
|
|
53
|
+
def _create_connection(self) -> http.client.HTTPConnection:
|
|
54
|
+
"""Create a new HTTP connection."""
|
|
55
|
+
return http.client.HTTPConnection(self.host, self.port, timeout=self.timeout)
|
|
56
|
+
|
|
57
|
+
@contextmanager
|
|
58
|
+
def get_connection(self):
|
|
59
|
+
"""Get a connection from the pool (context manager).
|
|
60
|
+
|
|
61
|
+
Yields a connection from the pool or creates a new one if pool is empty.
|
|
62
|
+
Returns connection to pool on success, discards on error.
|
|
63
|
+
"""
|
|
64
|
+
conn = None
|
|
65
|
+
reused = False
|
|
66
|
+
|
|
67
|
+
# Try to get from pool first (non-blocking)
|
|
68
|
+
try:
|
|
69
|
+
conn = self._pool.get_nowait()
|
|
70
|
+
reused = True
|
|
71
|
+
except Empty:
|
|
72
|
+
# Pool empty, try to create new connection
|
|
73
|
+
with self._lock:
|
|
74
|
+
if self._created < self.max_size:
|
|
75
|
+
conn = self._create_connection()
|
|
76
|
+
self._created += 1
|
|
77
|
+
|
|
78
|
+
# If still no connection, wait for one from pool
|
|
79
|
+
if conn is None:
|
|
80
|
+
try:
|
|
81
|
+
conn = self._pool.get(timeout=self.timeout)
|
|
82
|
+
reused = True
|
|
83
|
+
except Empty:
|
|
84
|
+
# Timeout waiting for connection, create one anyway
|
|
85
|
+
conn = self._create_connection()
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# Verify connection is still alive if reused
|
|
89
|
+
if reused:
|
|
90
|
+
try:
|
|
91
|
+
# Check if connection is still valid
|
|
92
|
+
conn.sock # Access sock to verify connection state
|
|
93
|
+
except Exception:
|
|
94
|
+
# Connection broken, create new one
|
|
95
|
+
try:
|
|
96
|
+
conn.close()
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
conn = self._create_connection()
|
|
100
|
+
|
|
101
|
+
yield conn
|
|
102
|
+
|
|
103
|
+
# Return connection to pool on success
|
|
104
|
+
try:
|
|
105
|
+
self._pool.put_nowait(conn)
|
|
106
|
+
except Full:
|
|
107
|
+
# Pool full, close this connection
|
|
108
|
+
try:
|
|
109
|
+
conn.close()
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
except Exception:
|
|
113
|
+
# Error occurred, close and discard connection
|
|
114
|
+
try:
|
|
115
|
+
conn.close()
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
with self._lock:
|
|
119
|
+
if self._created > 0:
|
|
120
|
+
self._created -= 1
|
|
121
|
+
raise
|
|
122
|
+
|
|
123
|
+
def close_all(self):
|
|
124
|
+
"""Close all connections in the pool."""
|
|
125
|
+
while True:
|
|
126
|
+
try:
|
|
127
|
+
conn = self._pool.get_nowait()
|
|
128
|
+
try:
|
|
129
|
+
conn.close()
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
except Empty:
|
|
133
|
+
break
|
|
134
|
+
with self._lock:
|
|
135
|
+
self._created = 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
_connection_pool: ConnectionPool | None = None
|
|
139
|
+
_pool_lock = threading.Lock()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _get_connection_pool() -> ConnectionPool:
|
|
143
|
+
"""Get or create the global connection pool (thread-safe singleton)."""
|
|
144
|
+
global _connection_pool
|
|
145
|
+
if _connection_pool is None:
|
|
146
|
+
with _pool_lock:
|
|
147
|
+
if _connection_pool is None:
|
|
148
|
+
_connection_pool = ConnectionPool(IDA_HOST, IDA_PORT)
|
|
149
|
+
return _connection_pool
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _reset_connection_pool():
|
|
153
|
+
"""Reset the connection pool (called when IDA host/port changes)."""
|
|
154
|
+
global _connection_pool
|
|
155
|
+
with _pool_lock:
|
|
156
|
+
if _connection_pool is not None:
|
|
157
|
+
_connection_pool.close_all()
|
|
158
|
+
_connection_pool = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
mcp = McpServer("ida-pro-mcp")
|
|
162
|
+
dispatch_original = mcp.registry.dispatch
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def dispatch_proxy(request: dict | str | bytes | bytearray) -> JsonRpcResponse | None:
|
|
166
|
+
"""Dispatch JSON-RPC requests to the MCP server registry.
|
|
167
|
+
|
|
168
|
+
Uses connection pooling for improved performance.
|
|
169
|
+
"""
|
|
170
|
+
if not isinstance(request, dict):
|
|
171
|
+
request_obj: JsonRpcRequest = json.loads(request)
|
|
172
|
+
else:
|
|
173
|
+
request_obj: JsonRpcRequest = request # type: ignore
|
|
174
|
+
|
|
175
|
+
if request_obj["method"] == "initialize":
|
|
176
|
+
return dispatch_original(request)
|
|
177
|
+
elif request_obj["method"].startswith("notifications/"):
|
|
178
|
+
return dispatch_original(request)
|
|
179
|
+
|
|
180
|
+
pool = _get_connection_pool()
|
|
181
|
+
try:
|
|
182
|
+
if isinstance(request, dict):
|
|
183
|
+
request_bytes = json.dumps(request).encode("utf-8")
|
|
184
|
+
elif isinstance(request, str):
|
|
185
|
+
request_bytes = request.encode("utf-8")
|
|
186
|
+
else:
|
|
187
|
+
request_bytes = request
|
|
188
|
+
|
|
189
|
+
with pool.get_connection() as conn:
|
|
190
|
+
conn.request(
|
|
191
|
+
"POST", "/mcp", request_bytes, {"Content-Type": "application/json"}
|
|
192
|
+
)
|
|
193
|
+
response = conn.getresponse()
|
|
194
|
+
data = response.read().decode()
|
|
195
|
+
return json.loads(data)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
full_info = traceback.format_exc()
|
|
198
|
+
id = request_obj.get("id")
|
|
199
|
+
if id is None:
|
|
200
|
+
return None # Notification, no response needed
|
|
201
|
+
|
|
202
|
+
return JsonRpcResponse(
|
|
203
|
+
{
|
|
204
|
+
"jsonrpc": "2.0",
|
|
205
|
+
"error": {
|
|
206
|
+
"code": -32000,
|
|
207
|
+
"message": f"Failed to connect to IDA Pro! Did you run Edit -> Plugins -> MCP Server to start the server?\n{full_info}",
|
|
208
|
+
"data": str(e),
|
|
209
|
+
},
|
|
210
|
+
"id": id,
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
mcp.registry.dispatch = dispatch_proxy
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
219
|
+
IDA_PLUGIN_PKG = os.path.join(SCRIPT_DIR, "ida_mcp")
|
|
220
|
+
IDA_PLUGIN_LOADER = os.path.join(SCRIPT_DIR, "ida_mcp.py")
|
|
221
|
+
|
|
222
|
+
# NOTE: This is in the global scope on purpose
|
|
223
|
+
if not os.path.exists(IDA_PLUGIN_PKG):
|
|
224
|
+
raise RuntimeError(
|
|
225
|
+
f"IDA plugin package not found at {IDA_PLUGIN_PKG} (did you move it?)"
|
|
226
|
+
)
|
|
227
|
+
if not os.path.exists(IDA_PLUGIN_LOADER):
|
|
228
|
+
raise RuntimeError(
|
|
229
|
+
f"IDA plugin loader not found at {IDA_PLUGIN_LOADER} (did you move it?)"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_python_executable():
|
|
234
|
+
"""Get the path to the Python executable"""
|
|
235
|
+
venv = os.environ.get("VIRTUAL_ENV")
|
|
236
|
+
if venv:
|
|
237
|
+
if sys.platform == "win32":
|
|
238
|
+
python = os.path.join(venv, "Scripts", "python.exe")
|
|
239
|
+
else:
|
|
240
|
+
python = os.path.join(venv, "bin", "python3")
|
|
241
|
+
if os.path.exists(python):
|
|
242
|
+
return python
|
|
243
|
+
|
|
244
|
+
for path in sys.path:
|
|
245
|
+
if sys.platform == "win32":
|
|
246
|
+
path = path.replace("/", "\\")
|
|
247
|
+
|
|
248
|
+
split = path.split(os.sep)
|
|
249
|
+
if split[-1].endswith(".zip"):
|
|
250
|
+
path = os.path.dirname(path)
|
|
251
|
+
if sys.platform == "win32":
|
|
252
|
+
python_executable = os.path.join(path, "python.exe")
|
|
253
|
+
else:
|
|
254
|
+
python_executable = os.path.join(path, "..", "bin", "python3")
|
|
255
|
+
python_executable = os.path.abspath(python_executable)
|
|
256
|
+
|
|
257
|
+
if os.path.exists(python_executable):
|
|
258
|
+
return python_executable
|
|
259
|
+
return sys.executable
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def copy_python_env(env: dict[str, str]):
|
|
263
|
+
# Reference: https://docs.python.org/3/using/cmdline.html#environment-variables
|
|
264
|
+
python_vars = [
|
|
265
|
+
"PYTHONHOME",
|
|
266
|
+
"PYTHONPATH",
|
|
267
|
+
"PYTHONSAFEPATH",
|
|
268
|
+
"PYTHONPLATLIBDIR",
|
|
269
|
+
"PYTHONPYCACHEPREFIX",
|
|
270
|
+
"PYTHONNOUSERSITE",
|
|
271
|
+
"PYTHONUSERBASE",
|
|
272
|
+
]
|
|
273
|
+
# MCP servers are run without inheriting the environment, so we need to forward
|
|
274
|
+
# the environment variables that affect Python's dependency resolution by hand.
|
|
275
|
+
# Issue: https://github.com/xjoker/ida-pro-mcp/issues/111
|
|
276
|
+
result = False
|
|
277
|
+
for var in python_vars:
|
|
278
|
+
value = os.environ.get(var)
|
|
279
|
+
if value:
|
|
280
|
+
result = True
|
|
281
|
+
env[var] = value
|
|
282
|
+
return result
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def generate_mcp_config(*, stdio: bool):
|
|
286
|
+
if stdio:
|
|
287
|
+
mcp_config = {
|
|
288
|
+
"command": get_python_executable(),
|
|
289
|
+
"args": [
|
|
290
|
+
__file__,
|
|
291
|
+
"--ida-rpc",
|
|
292
|
+
f"http://{IDA_HOST}:{IDA_PORT}",
|
|
293
|
+
],
|
|
294
|
+
}
|
|
295
|
+
env = {}
|
|
296
|
+
if copy_python_env(env):
|
|
297
|
+
print("[WARNING] Custom Python environment variables detected")
|
|
298
|
+
mcp_config["env"] = env
|
|
299
|
+
return mcp_config
|
|
300
|
+
else:
|
|
301
|
+
return {"type": "http", "url": f"http://{IDA_HOST}:{IDA_PORT}/mcp"}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def print_mcp_config():
|
|
305
|
+
print("[HTTP MCP CONFIGURATION]")
|
|
306
|
+
print(
|
|
307
|
+
json.dumps(
|
|
308
|
+
{"mcpServers": {mcp.name: generate_mcp_config(stdio=False)}}, indent=2
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
print("\n[STDIO MCP CONFIGURATION]")
|
|
312
|
+
print(
|
|
313
|
+
json.dumps(
|
|
314
|
+
{"mcpServers": {mcp.name: generate_mcp_config(stdio=True)}}, indent=2
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def install_mcp_servers(*, stdio: bool = False, uninstall=False, quiet=False):
|
|
320
|
+
# Map client names to their JSON key paths for clients that don't use "mcpServers"
|
|
321
|
+
# Format: client_name -> (top_level_key, nested_key)
|
|
322
|
+
# None means use default "mcpServers" at top level
|
|
323
|
+
special_json_structures = {
|
|
324
|
+
"VS Code": ("mcp", "servers"),
|
|
325
|
+
"VS Code Insiders": ("mcp", "servers"),
|
|
326
|
+
"Visual Studio 2022": (None, "servers"), # servers at top level
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if sys.platform == "win32":
|
|
330
|
+
configs = {
|
|
331
|
+
"Cline": (
|
|
332
|
+
os.path.join(
|
|
333
|
+
os.getenv("APPDATA", ""),
|
|
334
|
+
"Code",
|
|
335
|
+
"User",
|
|
336
|
+
"globalStorage",
|
|
337
|
+
"saoudrizwan.claude-dev",
|
|
338
|
+
"settings",
|
|
339
|
+
),
|
|
340
|
+
"cline_mcp_settings.json",
|
|
341
|
+
),
|
|
342
|
+
"Roo Code": (
|
|
343
|
+
os.path.join(
|
|
344
|
+
os.getenv("APPDATA", ""),
|
|
345
|
+
"Code",
|
|
346
|
+
"User",
|
|
347
|
+
"globalStorage",
|
|
348
|
+
"rooveterinaryinc.roo-cline",
|
|
349
|
+
"settings",
|
|
350
|
+
),
|
|
351
|
+
"mcp_settings.json",
|
|
352
|
+
),
|
|
353
|
+
"Kilo Code": (
|
|
354
|
+
os.path.join(
|
|
355
|
+
os.getenv("APPDATA", ""),
|
|
356
|
+
"Code",
|
|
357
|
+
"User",
|
|
358
|
+
"globalStorage",
|
|
359
|
+
"kilocode.kilo-code",
|
|
360
|
+
"settings",
|
|
361
|
+
),
|
|
362
|
+
"mcp_settings.json",
|
|
363
|
+
),
|
|
364
|
+
"Claude": (
|
|
365
|
+
os.path.join(os.getenv("APPDATA", ""), "Claude"),
|
|
366
|
+
"claude_desktop_config.json",
|
|
367
|
+
),
|
|
368
|
+
"Cursor": (os.path.join(os.path.expanduser("~"), ".cursor"), "mcp.json"),
|
|
369
|
+
"Windsurf": (
|
|
370
|
+
os.path.join(os.path.expanduser("~"), ".codeium", "windsurf"),
|
|
371
|
+
"mcp_config.json",
|
|
372
|
+
),
|
|
373
|
+
"Claude Code": (os.path.join(os.path.expanduser("~")), ".claude.json"),
|
|
374
|
+
"LM Studio": (
|
|
375
|
+
os.path.join(os.path.expanduser("~"), ".lmstudio"),
|
|
376
|
+
"mcp.json",
|
|
377
|
+
),
|
|
378
|
+
"Codex": (os.path.join(os.path.expanduser("~"), ".codex"), "config.toml"),
|
|
379
|
+
"Zed": (
|
|
380
|
+
os.path.join(os.getenv("APPDATA", ""), "Zed"),
|
|
381
|
+
"settings.json",
|
|
382
|
+
),
|
|
383
|
+
"Gemini CLI": (
|
|
384
|
+
os.path.join(os.path.expanduser("~"), ".gemini"),
|
|
385
|
+
"settings.json",
|
|
386
|
+
),
|
|
387
|
+
"Qwen Coder": (
|
|
388
|
+
os.path.join(os.path.expanduser("~"), ".qwen"),
|
|
389
|
+
"settings.json",
|
|
390
|
+
),
|
|
391
|
+
"Copilot CLI": (
|
|
392
|
+
os.path.join(os.path.expanduser("~"), ".copilot"),
|
|
393
|
+
"mcp-config.json",
|
|
394
|
+
),
|
|
395
|
+
"Crush": (
|
|
396
|
+
os.path.join(os.path.expanduser("~")),
|
|
397
|
+
"crush.json",
|
|
398
|
+
),
|
|
399
|
+
"Augment Code": (
|
|
400
|
+
os.path.join(
|
|
401
|
+
os.getenv("APPDATA", ""),
|
|
402
|
+
"Code",
|
|
403
|
+
"User",
|
|
404
|
+
),
|
|
405
|
+
"settings.json",
|
|
406
|
+
),
|
|
407
|
+
"Qodo Gen": (
|
|
408
|
+
os.path.join(
|
|
409
|
+
os.getenv("APPDATA", ""),
|
|
410
|
+
"Code",
|
|
411
|
+
"User",
|
|
412
|
+
),
|
|
413
|
+
"settings.json",
|
|
414
|
+
),
|
|
415
|
+
"Antigravity IDE": (
|
|
416
|
+
os.path.join(os.path.expanduser("~"), ".gemini", "antigravity"),
|
|
417
|
+
"mcp_config.json",
|
|
418
|
+
),
|
|
419
|
+
"Warp": (
|
|
420
|
+
os.path.join(os.path.expanduser("~"), ".warp"),
|
|
421
|
+
"mcp_config.json",
|
|
422
|
+
),
|
|
423
|
+
"Amazon Q": (
|
|
424
|
+
os.path.join(os.path.expanduser("~"), ".aws", "amazonq"),
|
|
425
|
+
"mcp_config.json",
|
|
426
|
+
),
|
|
427
|
+
"Opencode": (
|
|
428
|
+
os.path.join(os.path.expanduser("~"), ".opencode"),
|
|
429
|
+
"mcp_config.json",
|
|
430
|
+
),
|
|
431
|
+
"Kiro": (
|
|
432
|
+
os.path.join(os.path.expanduser("~"), ".kiro"),
|
|
433
|
+
"mcp_config.json",
|
|
434
|
+
),
|
|
435
|
+
"Trae": (
|
|
436
|
+
os.path.join(os.path.expanduser("~"), ".trae"),
|
|
437
|
+
"mcp_config.json",
|
|
438
|
+
),
|
|
439
|
+
"VS Code": (
|
|
440
|
+
os.path.join(
|
|
441
|
+
os.getenv("APPDATA", ""),
|
|
442
|
+
"Code",
|
|
443
|
+
"User",
|
|
444
|
+
),
|
|
445
|
+
"settings.json",
|
|
446
|
+
),
|
|
447
|
+
"VS Code Insiders": (
|
|
448
|
+
os.path.join(
|
|
449
|
+
os.getenv("APPDATA", ""),
|
|
450
|
+
"Code - Insiders",
|
|
451
|
+
"User",
|
|
452
|
+
),
|
|
453
|
+
"settings.json",
|
|
454
|
+
),
|
|
455
|
+
}
|
|
456
|
+
elif sys.platform == "darwin":
|
|
457
|
+
configs = {
|
|
458
|
+
"Cline": (
|
|
459
|
+
os.path.join(
|
|
460
|
+
os.path.expanduser("~"),
|
|
461
|
+
"Library",
|
|
462
|
+
"Application Support",
|
|
463
|
+
"Code",
|
|
464
|
+
"User",
|
|
465
|
+
"globalStorage",
|
|
466
|
+
"saoudrizwan.claude-dev",
|
|
467
|
+
"settings",
|
|
468
|
+
),
|
|
469
|
+
"cline_mcp_settings.json",
|
|
470
|
+
),
|
|
471
|
+
"Roo Code": (
|
|
472
|
+
os.path.join(
|
|
473
|
+
os.path.expanduser("~"),
|
|
474
|
+
"Library",
|
|
475
|
+
"Application Support",
|
|
476
|
+
"Code",
|
|
477
|
+
"User",
|
|
478
|
+
"globalStorage",
|
|
479
|
+
"rooveterinaryinc.roo-cline",
|
|
480
|
+
"settings",
|
|
481
|
+
),
|
|
482
|
+
"mcp_settings.json",
|
|
483
|
+
),
|
|
484
|
+
"Kilo Code": (
|
|
485
|
+
os.path.join(
|
|
486
|
+
os.path.expanduser("~"),
|
|
487
|
+
"Library",
|
|
488
|
+
"Application Support",
|
|
489
|
+
"Code",
|
|
490
|
+
"User",
|
|
491
|
+
"globalStorage",
|
|
492
|
+
"kilocode.kilo-code",
|
|
493
|
+
"settings",
|
|
494
|
+
),
|
|
495
|
+
"mcp_settings.json",
|
|
496
|
+
),
|
|
497
|
+
"Claude": (
|
|
498
|
+
os.path.join(
|
|
499
|
+
os.path.expanduser("~"), "Library", "Application Support", "Claude"
|
|
500
|
+
),
|
|
501
|
+
"claude_desktop_config.json",
|
|
502
|
+
),
|
|
503
|
+
"Cursor": (os.path.join(os.path.expanduser("~"), ".cursor"), "mcp.json"),
|
|
504
|
+
"Windsurf": (
|
|
505
|
+
os.path.join(os.path.expanduser("~"), ".codeium", "windsurf"),
|
|
506
|
+
"mcp_config.json",
|
|
507
|
+
),
|
|
508
|
+
"Claude Code": (os.path.join(os.path.expanduser("~")), ".claude.json"),
|
|
509
|
+
"LM Studio": (
|
|
510
|
+
os.path.join(os.path.expanduser("~"), ".lmstudio"),
|
|
511
|
+
"mcp.json",
|
|
512
|
+
),
|
|
513
|
+
"Codex": (os.path.join(os.path.expanduser("~"), ".codex"), "config.toml"),
|
|
514
|
+
"Antigravity IDE": (
|
|
515
|
+
os.path.join(os.path.expanduser("~"), ".gemini", "antigravity"),
|
|
516
|
+
"mcp_config.json",
|
|
517
|
+
),
|
|
518
|
+
"Zed": (
|
|
519
|
+
os.path.join(
|
|
520
|
+
os.path.expanduser("~"), "Library", "Application Support", "Zed"
|
|
521
|
+
),
|
|
522
|
+
"settings.json",
|
|
523
|
+
),
|
|
524
|
+
"Gemini CLI": (
|
|
525
|
+
os.path.join(os.path.expanduser("~"), ".gemini"),
|
|
526
|
+
"settings.json",
|
|
527
|
+
),
|
|
528
|
+
"Qwen Coder": (
|
|
529
|
+
os.path.join(os.path.expanduser("~"), ".qwen"),
|
|
530
|
+
"settings.json",
|
|
531
|
+
),
|
|
532
|
+
"Copilot CLI": (
|
|
533
|
+
os.path.join(os.path.expanduser("~"), ".copilot"),
|
|
534
|
+
"mcp-config.json",
|
|
535
|
+
),
|
|
536
|
+
"Crush": (
|
|
537
|
+
os.path.join(os.path.expanduser("~")),
|
|
538
|
+
"crush.json",
|
|
539
|
+
),
|
|
540
|
+
"Augment Code": (
|
|
541
|
+
os.path.join(
|
|
542
|
+
os.path.expanduser("~"),
|
|
543
|
+
"Library",
|
|
544
|
+
"Application Support",
|
|
545
|
+
"Code",
|
|
546
|
+
"User",
|
|
547
|
+
),
|
|
548
|
+
"settings.json",
|
|
549
|
+
),
|
|
550
|
+
"Qodo Gen": (
|
|
551
|
+
os.path.join(
|
|
552
|
+
os.path.expanduser("~"),
|
|
553
|
+
"Library",
|
|
554
|
+
"Application Support",
|
|
555
|
+
"Code",
|
|
556
|
+
"User",
|
|
557
|
+
),
|
|
558
|
+
"settings.json",
|
|
559
|
+
),
|
|
560
|
+
"BoltAI": (
|
|
561
|
+
os.path.join(
|
|
562
|
+
os.path.expanduser("~"),
|
|
563
|
+
"Library",
|
|
564
|
+
"Application Support",
|
|
565
|
+
"BoltAI",
|
|
566
|
+
),
|
|
567
|
+
"config.json",
|
|
568
|
+
),
|
|
569
|
+
"Perplexity": (
|
|
570
|
+
os.path.join(
|
|
571
|
+
os.path.expanduser("~"),
|
|
572
|
+
"Library",
|
|
573
|
+
"Application Support",
|
|
574
|
+
"Perplexity",
|
|
575
|
+
),
|
|
576
|
+
"mcp_config.json",
|
|
577
|
+
),
|
|
578
|
+
"Warp": (
|
|
579
|
+
os.path.join(os.path.expanduser("~"), ".warp"),
|
|
580
|
+
"mcp_config.json",
|
|
581
|
+
),
|
|
582
|
+
"Amazon Q": (
|
|
583
|
+
os.path.join(os.path.expanduser("~"), ".aws", "amazonq"),
|
|
584
|
+
"mcp_config.json",
|
|
585
|
+
),
|
|
586
|
+
"Opencode": (
|
|
587
|
+
os.path.join(os.path.expanduser("~"), ".opencode"),
|
|
588
|
+
"mcp_config.json",
|
|
589
|
+
),
|
|
590
|
+
"Kiro": (
|
|
591
|
+
os.path.join(os.path.expanduser("~"), ".kiro"),
|
|
592
|
+
"mcp_config.json",
|
|
593
|
+
),
|
|
594
|
+
"Trae": (
|
|
595
|
+
os.path.join(os.path.expanduser("~"), ".trae"),
|
|
596
|
+
"mcp_config.json",
|
|
597
|
+
),
|
|
598
|
+
"VS Code": (
|
|
599
|
+
os.path.join(
|
|
600
|
+
os.path.expanduser("~"),
|
|
601
|
+
"Library",
|
|
602
|
+
"Application Support",
|
|
603
|
+
"Code",
|
|
604
|
+
"User",
|
|
605
|
+
),
|
|
606
|
+
"settings.json",
|
|
607
|
+
),
|
|
608
|
+
"VS Code Insiders": (
|
|
609
|
+
os.path.join(
|
|
610
|
+
os.path.expanduser("~"),
|
|
611
|
+
"Library",
|
|
612
|
+
"Application Support",
|
|
613
|
+
"Code - Insiders",
|
|
614
|
+
"User",
|
|
615
|
+
),
|
|
616
|
+
"settings.json",
|
|
617
|
+
),
|
|
618
|
+
}
|
|
619
|
+
elif sys.platform == "linux":
|
|
620
|
+
configs = {
|
|
621
|
+
"Cline": (
|
|
622
|
+
os.path.join(
|
|
623
|
+
os.path.expanduser("~"),
|
|
624
|
+
".config",
|
|
625
|
+
"Code",
|
|
626
|
+
"User",
|
|
627
|
+
"globalStorage",
|
|
628
|
+
"saoudrizwan.claude-dev",
|
|
629
|
+
"settings",
|
|
630
|
+
),
|
|
631
|
+
"cline_mcp_settings.json",
|
|
632
|
+
),
|
|
633
|
+
"Roo Code": (
|
|
634
|
+
os.path.join(
|
|
635
|
+
os.path.expanduser("~"),
|
|
636
|
+
".config",
|
|
637
|
+
"Code",
|
|
638
|
+
"User",
|
|
639
|
+
"globalStorage",
|
|
640
|
+
"rooveterinaryinc.roo-cline",
|
|
641
|
+
"settings",
|
|
642
|
+
),
|
|
643
|
+
"mcp_settings.json",
|
|
644
|
+
),
|
|
645
|
+
"Kilo Code": (
|
|
646
|
+
os.path.join(
|
|
647
|
+
os.path.expanduser("~"),
|
|
648
|
+
".config",
|
|
649
|
+
"Code",
|
|
650
|
+
"User",
|
|
651
|
+
"globalStorage",
|
|
652
|
+
"kilocode.kilo-code",
|
|
653
|
+
"settings",
|
|
654
|
+
),
|
|
655
|
+
"mcp_settings.json",
|
|
656
|
+
),
|
|
657
|
+
# Claude not supported on Linux
|
|
658
|
+
"Cursor": (os.path.join(os.path.expanduser("~"), ".cursor"), "mcp.json"),
|
|
659
|
+
"Windsurf": (
|
|
660
|
+
os.path.join(os.path.expanduser("~"), ".codeium", "windsurf"),
|
|
661
|
+
"mcp_config.json",
|
|
662
|
+
),
|
|
663
|
+
"Claude Code": (os.path.join(os.path.expanduser("~")), ".claude.json"),
|
|
664
|
+
"LM Studio": (
|
|
665
|
+
os.path.join(os.path.expanduser("~"), ".lmstudio"),
|
|
666
|
+
"mcp.json",
|
|
667
|
+
),
|
|
668
|
+
"Codex": (os.path.join(os.path.expanduser("~"), ".codex"), "config.toml"),
|
|
669
|
+
"Antigravity IDE": (
|
|
670
|
+
os.path.join(os.path.expanduser("~"), ".gemini", "antigravity"),
|
|
671
|
+
"mcp_config.json",
|
|
672
|
+
),
|
|
673
|
+
"Zed": (
|
|
674
|
+
os.path.join(os.path.expanduser("~"), ".config", "zed"),
|
|
675
|
+
"settings.json",
|
|
676
|
+
),
|
|
677
|
+
"Gemini CLI": (
|
|
678
|
+
os.path.join(os.path.expanduser("~"), ".gemini"),
|
|
679
|
+
"settings.json",
|
|
680
|
+
),
|
|
681
|
+
"Qwen Coder": (
|
|
682
|
+
os.path.join(os.path.expanduser("~"), ".qwen"),
|
|
683
|
+
"settings.json",
|
|
684
|
+
),
|
|
685
|
+
"Copilot CLI": (
|
|
686
|
+
os.path.join(os.path.expanduser("~"), ".copilot"),
|
|
687
|
+
"mcp-config.json",
|
|
688
|
+
),
|
|
689
|
+
"Crush": (
|
|
690
|
+
os.path.join(os.path.expanduser("~")),
|
|
691
|
+
"crush.json",
|
|
692
|
+
),
|
|
693
|
+
"Augment Code": (
|
|
694
|
+
os.path.join(
|
|
695
|
+
os.path.expanduser("~"),
|
|
696
|
+
".config",
|
|
697
|
+
"Code",
|
|
698
|
+
"User",
|
|
699
|
+
),
|
|
700
|
+
"settings.json",
|
|
701
|
+
),
|
|
702
|
+
"Qodo Gen": (
|
|
703
|
+
os.path.join(
|
|
704
|
+
os.path.expanduser("~"),
|
|
705
|
+
".config",
|
|
706
|
+
"Code",
|
|
707
|
+
"User",
|
|
708
|
+
),
|
|
709
|
+
"settings.json",
|
|
710
|
+
),
|
|
711
|
+
"Warp": (
|
|
712
|
+
os.path.join(os.path.expanduser("~"), ".warp"),
|
|
713
|
+
"mcp_config.json",
|
|
714
|
+
),
|
|
715
|
+
"Amazon Q": (
|
|
716
|
+
os.path.join(os.path.expanduser("~"), ".aws", "amazonq"),
|
|
717
|
+
"mcp_config.json",
|
|
718
|
+
),
|
|
719
|
+
"Opencode": (
|
|
720
|
+
os.path.join(os.path.expanduser("~"), ".opencode"),
|
|
721
|
+
"mcp_config.json",
|
|
722
|
+
),
|
|
723
|
+
"Kiro": (
|
|
724
|
+
os.path.join(os.path.expanduser("~"), ".kiro"),
|
|
725
|
+
"mcp_config.json",
|
|
726
|
+
),
|
|
727
|
+
"Trae": (
|
|
728
|
+
os.path.join(os.path.expanduser("~"), ".trae"),
|
|
729
|
+
"mcp_config.json",
|
|
730
|
+
),
|
|
731
|
+
"VS Code": (
|
|
732
|
+
os.path.join(
|
|
733
|
+
os.path.expanduser("~"),
|
|
734
|
+
".config",
|
|
735
|
+
"Code",
|
|
736
|
+
"User",
|
|
737
|
+
),
|
|
738
|
+
"settings.json",
|
|
739
|
+
),
|
|
740
|
+
"VS Code Insiders": (
|
|
741
|
+
os.path.join(
|
|
742
|
+
os.path.expanduser("~"),
|
|
743
|
+
".config",
|
|
744
|
+
"Code - Insiders",
|
|
745
|
+
"User",
|
|
746
|
+
),
|
|
747
|
+
"settings.json",
|
|
748
|
+
),
|
|
749
|
+
}
|
|
750
|
+
else:
|
|
751
|
+
print(f"Unsupported platform: {sys.platform}")
|
|
752
|
+
return
|
|
753
|
+
|
|
754
|
+
installed = 0
|
|
755
|
+
for name, (config_dir, config_file) in configs.items():
|
|
756
|
+
config_path = os.path.join(config_dir, config_file)
|
|
757
|
+
is_toml = config_file.endswith(".toml")
|
|
758
|
+
|
|
759
|
+
if not os.path.exists(config_dir):
|
|
760
|
+
action = "uninstall" if uninstall else "installation"
|
|
761
|
+
if not quiet:
|
|
762
|
+
print(f"Skipping {name} {action}\n Config: {config_path} (not found)")
|
|
763
|
+
continue
|
|
764
|
+
|
|
765
|
+
# Read existing config
|
|
766
|
+
if not os.path.exists(config_path):
|
|
767
|
+
config = {}
|
|
768
|
+
else:
|
|
769
|
+
with open(
|
|
770
|
+
config_path,
|
|
771
|
+
"rb" if is_toml else "r",
|
|
772
|
+
encoding=None if is_toml else "utf-8",
|
|
773
|
+
) as f:
|
|
774
|
+
if is_toml:
|
|
775
|
+
data = f.read()
|
|
776
|
+
if len(data) == 0:
|
|
777
|
+
config = {}
|
|
778
|
+
else:
|
|
779
|
+
try:
|
|
780
|
+
config = tomllib.loads(data.decode("utf-8"))
|
|
781
|
+
except tomllib.TOMLDecodeError:
|
|
782
|
+
if not quiet:
|
|
783
|
+
print(
|
|
784
|
+
f"Skipping {name} uninstall\n Config: {config_path} (invalid TOML)"
|
|
785
|
+
)
|
|
786
|
+
continue
|
|
787
|
+
else:
|
|
788
|
+
data = f.read().strip()
|
|
789
|
+
if len(data) == 0:
|
|
790
|
+
config = {}
|
|
791
|
+
else:
|
|
792
|
+
try:
|
|
793
|
+
config = json.loads(data)
|
|
794
|
+
except json.decoder.JSONDecodeError:
|
|
795
|
+
if not quiet:
|
|
796
|
+
print(
|
|
797
|
+
f"Skipping {name} uninstall\n Config: {config_path} (invalid JSON)"
|
|
798
|
+
)
|
|
799
|
+
continue
|
|
800
|
+
|
|
801
|
+
# Handle TOML vs JSON structure
|
|
802
|
+
if is_toml:
|
|
803
|
+
if "mcp_servers" not in config:
|
|
804
|
+
config["mcp_servers"] = {}
|
|
805
|
+
mcp_servers = config["mcp_servers"]
|
|
806
|
+
else:
|
|
807
|
+
# Check if this client uses a special JSON structure
|
|
808
|
+
if name in special_json_structures:
|
|
809
|
+
top_key, nested_key = special_json_structures[name]
|
|
810
|
+
if top_key is None:
|
|
811
|
+
# servers at top level (e.g., Visual Studio 2022)
|
|
812
|
+
if nested_key not in config:
|
|
813
|
+
config[nested_key] = {}
|
|
814
|
+
mcp_servers = config[nested_key]
|
|
815
|
+
else:
|
|
816
|
+
# nested structure (e.g., VS Code uses mcp.servers)
|
|
817
|
+
if top_key not in config:
|
|
818
|
+
config[top_key] = {}
|
|
819
|
+
if nested_key not in config[top_key]:
|
|
820
|
+
config[top_key][nested_key] = {}
|
|
821
|
+
mcp_servers = config[top_key][nested_key]
|
|
822
|
+
else:
|
|
823
|
+
# Default: mcpServers at top level
|
|
824
|
+
if "mcpServers" not in config:
|
|
825
|
+
config["mcpServers"] = {}
|
|
826
|
+
mcp_servers = config["mcpServers"]
|
|
827
|
+
|
|
828
|
+
# Migrate old name
|
|
829
|
+
old_name = "github.com/xjoker/ida-pro-mcp"
|
|
830
|
+
if old_name in mcp_servers:
|
|
831
|
+
mcp_servers[mcp.name] = mcp_servers[old_name]
|
|
832
|
+
del mcp_servers[old_name]
|
|
833
|
+
|
|
834
|
+
if uninstall:
|
|
835
|
+
if mcp.name not in mcp_servers:
|
|
836
|
+
if not quiet:
|
|
837
|
+
print(
|
|
838
|
+
f"Skipping {name} uninstall\n Config: {config_path} (not installed)"
|
|
839
|
+
)
|
|
840
|
+
continue
|
|
841
|
+
del mcp_servers[mcp.name]
|
|
842
|
+
else:
|
|
843
|
+
mcp_servers[mcp.name] = generate_mcp_config(stdio=stdio)
|
|
844
|
+
|
|
845
|
+
# Atomic write: temp file + rename
|
|
846
|
+
suffix = ".toml" if is_toml else ".json"
|
|
847
|
+
fd, temp_path = tempfile.mkstemp(
|
|
848
|
+
dir=config_dir, prefix=".tmp_", suffix=suffix, text=True
|
|
849
|
+
)
|
|
850
|
+
try:
|
|
851
|
+
with os.fdopen(
|
|
852
|
+
fd, "wb" if is_toml else "w", encoding=None if is_toml else "utf-8"
|
|
853
|
+
) as f:
|
|
854
|
+
if is_toml:
|
|
855
|
+
f.write(tomli_w.dumps(config).encode("utf-8"))
|
|
856
|
+
else:
|
|
857
|
+
json.dump(config, f, indent=2)
|
|
858
|
+
os.replace(temp_path, config_path)
|
|
859
|
+
except Exception:
|
|
860
|
+
os.unlink(temp_path)
|
|
861
|
+
raise
|
|
862
|
+
|
|
863
|
+
if not quiet:
|
|
864
|
+
action = "Uninstalled" if uninstall else "Installed"
|
|
865
|
+
print(
|
|
866
|
+
f"{action} {name} MCP server (restart required)\n Config: {config_path}"
|
|
867
|
+
)
|
|
868
|
+
installed += 1
|
|
869
|
+
if not uninstall and installed == 0:
|
|
870
|
+
print(
|
|
871
|
+
"No MCP servers installed. For unsupported MCP clients, use the following config:\n"
|
|
872
|
+
)
|
|
873
|
+
print_mcp_config()
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def install_ida_plugin(
|
|
877
|
+
*, uninstall: bool = False, quiet: bool = False, allow_ida_free: bool = False
|
|
878
|
+
):
|
|
879
|
+
if sys.platform == "win32":
|
|
880
|
+
ida_folder = os.path.join(os.environ["APPDATA"], "Hex-Rays", "IDA Pro")
|
|
881
|
+
else:
|
|
882
|
+
ida_folder = os.path.join(os.path.expanduser("~"), ".idapro")
|
|
883
|
+
if not allow_ida_free:
|
|
884
|
+
free_licenses = glob.glob(os.path.join(ida_folder, "idafree_*.hexlic"))
|
|
885
|
+
if len(free_licenses) > 0:
|
|
886
|
+
print(
|
|
887
|
+
"IDA Free does not support plugins and cannot be used. Purchase and install IDA Pro instead."
|
|
888
|
+
)
|
|
889
|
+
sys.exit(1)
|
|
890
|
+
ida_plugin_folder = os.path.join(ida_folder, "plugins")
|
|
891
|
+
|
|
892
|
+
# Install both the loader file and package directory
|
|
893
|
+
loader_source = IDA_PLUGIN_LOADER
|
|
894
|
+
loader_destination = os.path.join(ida_plugin_folder, "ida_mcp.py")
|
|
895
|
+
|
|
896
|
+
pkg_source = IDA_PLUGIN_PKG
|
|
897
|
+
pkg_destination = os.path.join(ida_plugin_folder, "ida_mcp")
|
|
898
|
+
|
|
899
|
+
# Clean up old plugin if it exists
|
|
900
|
+
old_plugin = os.path.join(ida_plugin_folder, "mcp-plugin.py")
|
|
901
|
+
|
|
902
|
+
if uninstall:
|
|
903
|
+
# Remove loader
|
|
904
|
+
if os.path.lexists(loader_destination):
|
|
905
|
+
os.remove(loader_destination)
|
|
906
|
+
if not quiet:
|
|
907
|
+
print(f"Uninstalled IDA plugin loader\n Path: {loader_destination}")
|
|
908
|
+
|
|
909
|
+
# Remove package
|
|
910
|
+
if os.path.exists(pkg_destination):
|
|
911
|
+
if os.path.isdir(pkg_destination) and not os.path.islink(pkg_destination):
|
|
912
|
+
shutil.rmtree(pkg_destination)
|
|
913
|
+
else:
|
|
914
|
+
os.remove(pkg_destination)
|
|
915
|
+
if not quiet:
|
|
916
|
+
print(f"Uninstalled IDA plugin package\n Path: {pkg_destination}")
|
|
917
|
+
|
|
918
|
+
# Remove old plugin if it exists
|
|
919
|
+
if os.path.lexists(old_plugin):
|
|
920
|
+
os.remove(old_plugin)
|
|
921
|
+
if not quiet:
|
|
922
|
+
print(f"Removed old plugin\n Path: {old_plugin}")
|
|
923
|
+
else:
|
|
924
|
+
# Create IDA plugins folder
|
|
925
|
+
if not os.path.exists(ida_plugin_folder):
|
|
926
|
+
os.makedirs(ida_plugin_folder)
|
|
927
|
+
|
|
928
|
+
# Remove old plugin if it exists
|
|
929
|
+
if os.path.lexists(old_plugin):
|
|
930
|
+
os.remove(old_plugin)
|
|
931
|
+
if not quiet:
|
|
932
|
+
print(f"Removed old plugin file\n Path: {old_plugin}")
|
|
933
|
+
|
|
934
|
+
installed_items = []
|
|
935
|
+
|
|
936
|
+
# Install loader file
|
|
937
|
+
loader_realpath = (
|
|
938
|
+
os.path.realpath(loader_destination)
|
|
939
|
+
if os.path.lexists(loader_destination)
|
|
940
|
+
else None
|
|
941
|
+
)
|
|
942
|
+
if loader_realpath != loader_source:
|
|
943
|
+
if os.path.lexists(loader_destination):
|
|
944
|
+
os.remove(loader_destination)
|
|
945
|
+
|
|
946
|
+
try:
|
|
947
|
+
os.symlink(loader_source, loader_destination)
|
|
948
|
+
installed_items.append(f"loader: {loader_destination}")
|
|
949
|
+
except OSError:
|
|
950
|
+
shutil.copy(loader_source, loader_destination)
|
|
951
|
+
installed_items.append(f"loader: {loader_destination}")
|
|
952
|
+
|
|
953
|
+
# Install package directory
|
|
954
|
+
pkg_realpath = (
|
|
955
|
+
os.path.realpath(pkg_destination)
|
|
956
|
+
if os.path.lexists(pkg_destination)
|
|
957
|
+
else None
|
|
958
|
+
)
|
|
959
|
+
if pkg_realpath != pkg_source:
|
|
960
|
+
if os.path.lexists(pkg_destination):
|
|
961
|
+
if os.path.isdir(pkg_destination) and not os.path.islink(
|
|
962
|
+
pkg_destination
|
|
963
|
+
):
|
|
964
|
+
shutil.rmtree(pkg_destination)
|
|
965
|
+
else:
|
|
966
|
+
os.remove(pkg_destination)
|
|
967
|
+
|
|
968
|
+
try:
|
|
969
|
+
os.symlink(pkg_source, pkg_destination)
|
|
970
|
+
installed_items.append(f"package: {pkg_destination}")
|
|
971
|
+
except OSError:
|
|
972
|
+
shutil.copytree(pkg_source, pkg_destination)
|
|
973
|
+
installed_items.append(f"package: {pkg_destination}")
|
|
974
|
+
|
|
975
|
+
if not quiet:
|
|
976
|
+
if installed_items:
|
|
977
|
+
print("Installed IDA Pro plugin (IDA restart required)")
|
|
978
|
+
for item in installed_items:
|
|
979
|
+
print(f" {item}")
|
|
980
|
+
else:
|
|
981
|
+
print("Skipping IDA plugin installation (already up to date)")
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def main():
|
|
985
|
+
global IDA_HOST, IDA_PORT
|
|
986
|
+
parser = argparse.ArgumentParser(description="IDA Pro MCP Server")
|
|
987
|
+
parser.add_argument(
|
|
988
|
+
"--install", action="store_true", help="Install the MCP Server and IDA plugin"
|
|
989
|
+
)
|
|
990
|
+
parser.add_argument(
|
|
991
|
+
"--uninstall",
|
|
992
|
+
action="store_true",
|
|
993
|
+
help="Uninstall the MCP Server and IDA plugin",
|
|
994
|
+
)
|
|
995
|
+
parser.add_argument(
|
|
996
|
+
"--allow-ida-free",
|
|
997
|
+
action="store_true",
|
|
998
|
+
help="Allow installation despite IDA Free being installed",
|
|
999
|
+
)
|
|
1000
|
+
parser.add_argument(
|
|
1001
|
+
"--transport",
|
|
1002
|
+
type=str,
|
|
1003
|
+
default="stdio",
|
|
1004
|
+
help="MCP transport protocol to use (stdio or http://127.0.0.1:8744)",
|
|
1005
|
+
)
|
|
1006
|
+
parser.add_argument(
|
|
1007
|
+
"--ida-rpc",
|
|
1008
|
+
type=str,
|
|
1009
|
+
default=f"http://{IDA_HOST}:{IDA_PORT}",
|
|
1010
|
+
help=f"IDA RPC server to use (default: http://{IDA_HOST}:{IDA_PORT})",
|
|
1011
|
+
)
|
|
1012
|
+
parser.add_argument(
|
|
1013
|
+
"--config", action="store_true", help="Generate MCP config JSON"
|
|
1014
|
+
)
|
|
1015
|
+
args = parser.parse_args()
|
|
1016
|
+
|
|
1017
|
+
# Parse IDA RPC server argument
|
|
1018
|
+
ida_rpc = urlparse(args.ida_rpc)
|
|
1019
|
+
if ida_rpc.hostname is None or ida_rpc.port is None:
|
|
1020
|
+
raise Exception(f"Invalid IDA RPC server: {args.ida_rpc}")
|
|
1021
|
+
IDA_HOST = ida_rpc.hostname
|
|
1022
|
+
IDA_PORT = ida_rpc.port
|
|
1023
|
+
|
|
1024
|
+
# Reset connection pool if host/port changed
|
|
1025
|
+
_reset_connection_pool()
|
|
1026
|
+
|
|
1027
|
+
if args.install and args.uninstall:
|
|
1028
|
+
print("Cannot install and uninstall at the same time")
|
|
1029
|
+
return
|
|
1030
|
+
|
|
1031
|
+
if args.install:
|
|
1032
|
+
install_ida_plugin(allow_ida_free=args.allow_ida_free)
|
|
1033
|
+
install_mcp_servers(stdio=(args.transport == "stdio"))
|
|
1034
|
+
return
|
|
1035
|
+
|
|
1036
|
+
if args.uninstall:
|
|
1037
|
+
install_ida_plugin(uninstall=True, allow_ida_free=args.allow_ida_free)
|
|
1038
|
+
install_mcp_servers(uninstall=True)
|
|
1039
|
+
return
|
|
1040
|
+
|
|
1041
|
+
if args.config:
|
|
1042
|
+
print_mcp_config()
|
|
1043
|
+
return
|
|
1044
|
+
|
|
1045
|
+
try:
|
|
1046
|
+
if args.transport == "stdio":
|
|
1047
|
+
mcp.stdio()
|
|
1048
|
+
else:
|
|
1049
|
+
url = urlparse(args.transport)
|
|
1050
|
+
if url.hostname is None or url.port is None:
|
|
1051
|
+
raise Exception(f"Invalid transport URL: {args.transport}")
|
|
1052
|
+
# NOTE: npx -y @modelcontextprotocol/inspector for debugging
|
|
1053
|
+
mcp.serve(url.hostname, url.port)
|
|
1054
|
+
input("Server is running, press Enter or Ctrl+C to stop.")
|
|
1055
|
+
except (KeyboardInterrupt, EOFError):
|
|
1056
|
+
pass
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
if __name__ == "__main__":
|
|
1060
|
+
main()
|