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
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""IDA MCP Configuration UI
|
|
2
|
+
|
|
3
|
+
Provides native IDA Pro dialogs for configuring MCP server instances.
|
|
4
|
+
Uses idaapi.Form for cross-platform compatibility.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import idaapi
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Optional, TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from .config import (
|
|
12
|
+
ServerInstanceConfig,
|
|
13
|
+
McpConfig,
|
|
14
|
+
get_config,
|
|
15
|
+
save_config,
|
|
16
|
+
reload_config,
|
|
17
|
+
)
|
|
18
|
+
from .server_manager import get_server_manager, ServerManager
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ServerConfigForm(idaapi.Form):
|
|
27
|
+
"""Form for adding/editing a server instance configuration."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: Optional[ServerInstanceConfig] = None):
|
|
30
|
+
self.config = config
|
|
31
|
+
is_new = config is None
|
|
32
|
+
|
|
33
|
+
# Default values
|
|
34
|
+
instance_id = "" if is_new else config.instance_id
|
|
35
|
+
host = "127.0.0.1" if is_new else config.host
|
|
36
|
+
port = 13337 if is_new else config.port
|
|
37
|
+
auth_enabled = False if is_new else config.auth_enabled
|
|
38
|
+
api_key = "" if is_new else (config.api_key or "")
|
|
39
|
+
auto_start = False if is_new else config.auto_start
|
|
40
|
+
|
|
41
|
+
form_template = r"""STARTITEM 0
|
|
42
|
+
BUTTON YES* Save
|
|
43
|
+
BUTTON CANCEL Cancel
|
|
44
|
+
{title}
|
|
45
|
+
|
|
46
|
+
<Instance ID:{iInstanceId}>
|
|
47
|
+
<Host:{iHost}>
|
|
48
|
+
<Port:{iPort}>
|
|
49
|
+
|
|
50
|
+
<Enable Authentication:{cAuthEnabled}>{cAuthGroup}>
|
|
51
|
+
<API Key:{iApiKey}>
|
|
52
|
+
|
|
53
|
+
<Auto-start on plugin load:{cAutoStart}>{cAutoGroup}>
|
|
54
|
+
"""
|
|
55
|
+
title = "Add Server" if is_new else f"Edit Server: {instance_id}"
|
|
56
|
+
|
|
57
|
+
idaapi.Form.__init__(
|
|
58
|
+
self,
|
|
59
|
+
form_template.format(title=title),
|
|
60
|
+
{
|
|
61
|
+
"iInstanceId": idaapi.Form.StringInput(value=instance_id),
|
|
62
|
+
"iHost": idaapi.Form.StringInput(value=host),
|
|
63
|
+
"iPort": idaapi.Form.NumericInput(value=port, tp=idaapi.Form.FT_DEC),
|
|
64
|
+
"cAuthGroup": idaapi.Form.ChkGroupControl(("cAuthEnabled",)),
|
|
65
|
+
"iApiKey": idaapi.Form.StringInput(value=api_key),
|
|
66
|
+
"cAutoGroup": idaapi.Form.ChkGroupControl(("cAutoStart",)),
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
self._is_new = is_new
|
|
71
|
+
|
|
72
|
+
def get_config(self) -> Optional[ServerInstanceConfig]:
|
|
73
|
+
"""Get the configured ServerInstanceConfig after form execution."""
|
|
74
|
+
instance_id = self.iInstanceId.value.strip()
|
|
75
|
+
if not instance_id:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
return ServerInstanceConfig(
|
|
79
|
+
instance_id=instance_id,
|
|
80
|
+
host=self.iHost.value.strip() or "127.0.0.1",
|
|
81
|
+
port=self.iPort.value or 13337,
|
|
82
|
+
enabled=True,
|
|
83
|
+
auth_enabled=bool(self.cAuthGroup.value & 1),
|
|
84
|
+
api_key=self.iApiKey.value.strip() or None,
|
|
85
|
+
auto_start=bool(self.cAutoGroup.value & 1),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ServerListChooser(idaapi.Choose):
|
|
90
|
+
"""Chooser widget for displaying and selecting server instances."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, manager: ServerManager, title: str = "MCP Servers"):
|
|
93
|
+
columns = [
|
|
94
|
+
["ID", 15],
|
|
95
|
+
["Address", 25],
|
|
96
|
+
["Status", 12],
|
|
97
|
+
["Auth", 8],
|
|
98
|
+
]
|
|
99
|
+
idaapi.Choose.__init__(
|
|
100
|
+
self,
|
|
101
|
+
title,
|
|
102
|
+
columns,
|
|
103
|
+
flags=idaapi.Choose.CH_MODAL | idaapi.Choose.CH_CAN_DEL,
|
|
104
|
+
)
|
|
105
|
+
self.manager = manager
|
|
106
|
+
self._items: list[tuple[str, str, str, str]] = []
|
|
107
|
+
self._refresh()
|
|
108
|
+
|
|
109
|
+
def _refresh(self) -> None:
|
|
110
|
+
"""Refresh the list of servers."""
|
|
111
|
+
self._items = []
|
|
112
|
+
for instance_id, status in self.manager.get_status().items():
|
|
113
|
+
self._items.append((
|
|
114
|
+
instance_id,
|
|
115
|
+
status["address"],
|
|
116
|
+
status["status"],
|
|
117
|
+
"Yes" if status["auth_enabled"] else "No",
|
|
118
|
+
))
|
|
119
|
+
|
|
120
|
+
def OnGetSize(self) -> int:
|
|
121
|
+
return len(self._items)
|
|
122
|
+
|
|
123
|
+
def OnGetLine(self, n: int) -> list[str]:
|
|
124
|
+
if 0 <= n < len(self._items):
|
|
125
|
+
return list(self._items[n])
|
|
126
|
+
return ["", "", "", ""]
|
|
127
|
+
|
|
128
|
+
def OnDeleteLine(self, n: int) -> int:
|
|
129
|
+
"""Handle delete action."""
|
|
130
|
+
if 0 <= n < len(self._items):
|
|
131
|
+
instance_id = self._items[n][0]
|
|
132
|
+
if self.manager.remove_server(instance_id):
|
|
133
|
+
self._refresh()
|
|
134
|
+
return n
|
|
135
|
+
return -1
|
|
136
|
+
|
|
137
|
+
def OnRefresh(self, n: int) -> int:
|
|
138
|
+
self._refresh()
|
|
139
|
+
return n
|
|
140
|
+
|
|
141
|
+
def get_selected_id(self, n: int) -> Optional[str]:
|
|
142
|
+
"""Get the instance ID at the given index."""
|
|
143
|
+
if 0 <= n < len(self._items):
|
|
144
|
+
return self._items[n][0]
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class McpConfigDialog(idaapi.Form):
|
|
149
|
+
"""Main configuration dialog for MCP servers."""
|
|
150
|
+
|
|
151
|
+
def __init__(self):
|
|
152
|
+
form_template = r"""STARTITEM 0
|
|
153
|
+
BUTTON YES* Close
|
|
154
|
+
MCP Server Configuration
|
|
155
|
+
|
|
156
|
+
{FormChangeCb}
|
|
157
|
+
<Server List:{cServerList}>
|
|
158
|
+
|
|
159
|
+
<Add Server:{iAddBtn}> <Start:{iStartBtn}> <Stop:{iStopBtn}> <Edit:{iEditBtn}>
|
|
160
|
+
|
|
161
|
+
<Tool Timeout (sec):{iTimeout}>
|
|
162
|
+
<Debug Mode:{cDebug}>{cDebugGroup}>
|
|
163
|
+
|
|
164
|
+
<Save Config:{iSaveBtn}> <Reload Config:{iReloadBtn}>
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
self.manager = get_server_manager()
|
|
168
|
+
self.config = get_config()
|
|
169
|
+
|
|
170
|
+
# Create embedded chooser
|
|
171
|
+
self.server_chooser = ServerListChooser(self.manager)
|
|
172
|
+
|
|
173
|
+
idaapi.Form.__init__(
|
|
174
|
+
self,
|
|
175
|
+
form_template,
|
|
176
|
+
{
|
|
177
|
+
"FormChangeCb": idaapi.Form.FormChangeCb(self._on_form_change),
|
|
178
|
+
"cServerList": idaapi.Form.EmbeddedChooserControl(self.server_chooser),
|
|
179
|
+
"iAddBtn": idaapi.Form.ButtonInput(self._on_add_click),
|
|
180
|
+
"iStartBtn": idaapi.Form.ButtonInput(self._on_start_click),
|
|
181
|
+
"iStopBtn": idaapi.Form.ButtonInput(self._on_stop_click),
|
|
182
|
+
"iEditBtn": idaapi.Form.ButtonInput(self._on_edit_click),
|
|
183
|
+
"iTimeout": idaapi.Form.NumericInput(
|
|
184
|
+
value=int(self.config.tool_timeout_sec),
|
|
185
|
+
tp=idaapi.Form.FT_DEC,
|
|
186
|
+
),
|
|
187
|
+
"cDebugGroup": idaapi.Form.ChkGroupControl(("cDebug",)),
|
|
188
|
+
"iSaveBtn": idaapi.Form.ButtonInput(self._on_save_click),
|
|
189
|
+
"iReloadBtn": idaapi.Form.ButtonInput(self._on_reload_click),
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def _on_form_change(self, fid: int) -> int:
|
|
194
|
+
return 1
|
|
195
|
+
|
|
196
|
+
def _get_selected_instance_id(self) -> Optional[str]:
|
|
197
|
+
"""Get the currently selected server instance ID."""
|
|
198
|
+
sel = self.GetControlValue(self.cServerList)
|
|
199
|
+
if sel is None or sel < 0:
|
|
200
|
+
return None
|
|
201
|
+
return self.server_chooser.get_selected_id(sel)
|
|
202
|
+
|
|
203
|
+
def _on_add_click(self, code: int) -> int:
|
|
204
|
+
"""Handle Add Server button click."""
|
|
205
|
+
form = ServerConfigForm()
|
|
206
|
+
form.Compile()
|
|
207
|
+
if form.Execute() == 1:
|
|
208
|
+
new_config = form.get_config()
|
|
209
|
+
if new_config:
|
|
210
|
+
try:
|
|
211
|
+
self.manager.add_server(new_config)
|
|
212
|
+
self.config.add_server(new_config)
|
|
213
|
+
self.RefreshField(self.cServerList)
|
|
214
|
+
print(f"[MCP] Added server: {new_config.instance_id}")
|
|
215
|
+
except ValueError as e:
|
|
216
|
+
print(f"[MCP] Error: {e}")
|
|
217
|
+
form.Free()
|
|
218
|
+
return 1
|
|
219
|
+
|
|
220
|
+
def _on_start_click(self, code: int) -> int:
|
|
221
|
+
"""Handle Start button click."""
|
|
222
|
+
instance_id = self._get_selected_instance_id()
|
|
223
|
+
if instance_id:
|
|
224
|
+
if self.manager.start_server(instance_id):
|
|
225
|
+
self.RefreshField(self.cServerList)
|
|
226
|
+
else:
|
|
227
|
+
print("[MCP] No server selected")
|
|
228
|
+
return 1
|
|
229
|
+
|
|
230
|
+
def _on_stop_click(self, code: int) -> int:
|
|
231
|
+
"""Handle Stop button click."""
|
|
232
|
+
instance_id = self._get_selected_instance_id()
|
|
233
|
+
if instance_id:
|
|
234
|
+
if self.manager.stop_server(instance_id):
|
|
235
|
+
self.RefreshField(self.cServerList)
|
|
236
|
+
else:
|
|
237
|
+
print("[MCP] No server selected")
|
|
238
|
+
return 1
|
|
239
|
+
|
|
240
|
+
def _on_edit_click(self, code: int) -> int:
|
|
241
|
+
"""Handle Edit button click."""
|
|
242
|
+
instance_id = self._get_selected_instance_id()
|
|
243
|
+
if not instance_id:
|
|
244
|
+
print("[MCP] No server selected")
|
|
245
|
+
return 1
|
|
246
|
+
|
|
247
|
+
instance = self.manager.get_instance(instance_id)
|
|
248
|
+
if not instance:
|
|
249
|
+
return 1
|
|
250
|
+
|
|
251
|
+
form = ServerConfigForm(instance.config)
|
|
252
|
+
form.Compile()
|
|
253
|
+
if form.Execute() == 1:
|
|
254
|
+
new_config = form.get_config()
|
|
255
|
+
if new_config:
|
|
256
|
+
# Update config
|
|
257
|
+
was_running = instance.is_running
|
|
258
|
+
if was_running:
|
|
259
|
+
self.manager.stop_server(instance_id)
|
|
260
|
+
|
|
261
|
+
# Update instance config
|
|
262
|
+
instance.config = new_config
|
|
263
|
+
|
|
264
|
+
# Restart if was running
|
|
265
|
+
if was_running:
|
|
266
|
+
self.manager.start_server(new_config.instance_id)
|
|
267
|
+
|
|
268
|
+
self.RefreshField(self.cServerList)
|
|
269
|
+
print(f"[MCP] Updated server: {new_config.instance_id}")
|
|
270
|
+
form.Free()
|
|
271
|
+
return 1
|
|
272
|
+
|
|
273
|
+
def _on_save_click(self, code: int) -> int:
|
|
274
|
+
"""Handle Save Config button click."""
|
|
275
|
+
# Update config from form values
|
|
276
|
+
self.config.tool_timeout_sec = float(self.iTimeout.value or 15)
|
|
277
|
+
self.config.debug = bool(self.cDebugGroup.value & 1)
|
|
278
|
+
|
|
279
|
+
# Sync server list
|
|
280
|
+
self.config.servers = [
|
|
281
|
+
inst.config for inst in self.manager._instances.values()
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
if save_config(self.config):
|
|
285
|
+
print("[MCP] Configuration saved")
|
|
286
|
+
else:
|
|
287
|
+
print("[MCP] Failed to save configuration")
|
|
288
|
+
return 1
|
|
289
|
+
|
|
290
|
+
def _on_reload_click(self, code: int) -> int:
|
|
291
|
+
"""Handle Reload Config button click."""
|
|
292
|
+
self.config = reload_config()
|
|
293
|
+
self.manager.load_from_config(self.config)
|
|
294
|
+
self.RefreshField(self.cServerList)
|
|
295
|
+
self.SetControlValue(self.iTimeout, int(self.config.tool_timeout_sec))
|
|
296
|
+
print("[MCP] Configuration reloaded")
|
|
297
|
+
return 1
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def show_config_dialog() -> None:
|
|
301
|
+
"""Show the MCP configuration dialog."""
|
|
302
|
+
dialog = McpConfigDialog()
|
|
303
|
+
dialog.Compile()
|
|
304
|
+
dialog.Execute()
|
|
305
|
+
dialog.Free()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class McpConfigAction(idaapi.action_handler_t):
|
|
309
|
+
"""Action handler for showing the config dialog."""
|
|
310
|
+
|
|
311
|
+
ACTION_ID = "ida_mcp:config"
|
|
312
|
+
ACTION_NAME = "MCP Configuration"
|
|
313
|
+
ACTION_HOTKEY = "Ctrl+Shift+M"
|
|
314
|
+
|
|
315
|
+
def __init__(self):
|
|
316
|
+
idaapi.action_handler_t.__init__(self)
|
|
317
|
+
|
|
318
|
+
def activate(self, ctx) -> int:
|
|
319
|
+
show_config_dialog()
|
|
320
|
+
return 1
|
|
321
|
+
|
|
322
|
+
def update(self, ctx) -> int:
|
|
323
|
+
return idaapi.AST_ENABLE_ALWAYS
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def register_actions() -> bool:
|
|
327
|
+
"""Register UI actions with IDA."""
|
|
328
|
+
action_desc = idaapi.action_desc_t(
|
|
329
|
+
McpConfigAction.ACTION_ID,
|
|
330
|
+
McpConfigAction.ACTION_NAME,
|
|
331
|
+
McpConfigAction(),
|
|
332
|
+
McpConfigAction.ACTION_HOTKEY,
|
|
333
|
+
"Configure MCP server instances",
|
|
334
|
+
-1,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if not idaapi.register_action(action_desc):
|
|
338
|
+
logger.warning("Failed to register MCP config action")
|
|
339
|
+
return False
|
|
340
|
+
|
|
341
|
+
return True
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def unregister_actions() -> None:
|
|
345
|
+
"""Unregister UI actions from IDA."""
|
|
346
|
+
idaapi.unregister_action(McpConfigAction.ACTION_ID)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
__all__ = [
|
|
350
|
+
"ServerConfigForm",
|
|
351
|
+
"ServerListChooser",
|
|
352
|
+
"McpConfigDialog",
|
|
353
|
+
"show_config_dialog",
|
|
354
|
+
"McpConfigAction",
|
|
355
|
+
"register_actions",
|
|
356
|
+
"unregister_actions",
|
|
357
|
+
]
|