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/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()