mcpyida 0.6.0__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.
- mcpyida/__init__.py +0 -0
- mcpyida/_version.py +24 -0
- mcpyida/custom_types_312.py +11 -0
- mcpyida/custom_types_p312.py +5 -0
- mcpyida/headless.py +197 -0
- mcpyida/ida_helpers.py +687 -0
- mcpyida/ida_plugin/mcpyida_proxy.py +9 -0
- mcpyida/installer.py +32 -0
- mcpyida/mcp2openapi.py +197 -0
- mcpyida/mcpserver.py +405 -0
- mcpyida/mcpyida.py +327 -0
- mcpyida/models.py +365 -0
- mcpyida/py.typed +0 -0
- mcpyida/rpc_callbacks.py +336 -0
- mcpyida/rpc_types.py +66 -0
- mcpyida/server.py +1945 -0
- mcpyida/tools/__init__.py +14 -0
- mcpyida/tools/analysis.py +544 -0
- mcpyida/tools/cfg.py +470 -0
- mcpyida/tools/core.py +890 -0
- mcpyida/tools/modify.py +600 -0
- mcpyida/tools/scripting.py +292 -0
- mcpyida/tools/search.py +323 -0
- mcpyida/tools/search_utils.py +102 -0
- mcpyida/tools/types.py +738 -0
- mcpyida/util.py +200 -0
- mcpyida-0.6.0.dist-info/METADATA +212 -0
- mcpyida-0.6.0.dist-info/RECORD +31 -0
- mcpyida-0.6.0.dist-info/WHEEL +4 -0
- mcpyida-0.6.0.dist-info/entry_points.txt +3 -0
- mcpyida-0.6.0.dist-info/licenses/LICENSE +202 -0
mcpyida/__init__.py
ADDED
|
File without changes
|
mcpyida/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.6.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 6, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
mcpyida/headless.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""MCPyIDA headless MCP server.
|
|
2
|
+
|
|
3
|
+
Launch IDA headless via idalib, open a binary, run auto-analysis,
|
|
4
|
+
and start the MCP server. Blocks until interrupted.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
mcpyida-headless --binary /path/to/elf [--host 127.0.0.1] [--port 6150]
|
|
8
|
+
python -m mcpyida.headless --binary /path/to/elf
|
|
9
|
+
|
|
10
|
+
Prints JSON readiness signal to stdout when the server is ready:
|
|
11
|
+
{"status": "ready", "host": "127.0.0.1", "port": 6150, "binary": "/path/to/elf"}
|
|
12
|
+
|
|
13
|
+
This is the contract that test harnesses and wingman CLI rely on.
|
|
14
|
+
|
|
15
|
+
Requires idapro pip package (idalib).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import queue
|
|
24
|
+
import sys
|
|
25
|
+
import threading
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_on_main_thread(func, *args, **kwargs):
|
|
30
|
+
"""Submit *func* to the main thread and block until the result is ready.
|
|
31
|
+
|
|
32
|
+
Called from uvicorn worker threads via the sync @run_in_ida_main decorator.
|
|
33
|
+
Raises any exception that func raises.
|
|
34
|
+
"""
|
|
35
|
+
from mcpyida.mcpserver import get_ida_work_queue
|
|
36
|
+
|
|
37
|
+
result_event = threading.Event()
|
|
38
|
+
result_holder: list = [None, None] # [result, exception]
|
|
39
|
+
|
|
40
|
+
def work_item() -> None:
|
|
41
|
+
try:
|
|
42
|
+
result_holder[0] = func(*args, **kwargs)
|
|
43
|
+
except Exception as exc:
|
|
44
|
+
result_holder[1] = exc
|
|
45
|
+
finally:
|
|
46
|
+
result_event.set()
|
|
47
|
+
|
|
48
|
+
get_ida_work_queue().put(work_item)
|
|
49
|
+
result_event.wait() # Block until the main thread completes the call
|
|
50
|
+
|
|
51
|
+
if result_holder[1] is not None:
|
|
52
|
+
raise result_holder[1]
|
|
53
|
+
return result_holder[0]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main() -> None:
|
|
57
|
+
parser = argparse.ArgumentParser(
|
|
58
|
+
description='MCPyIDA headless MCP server',
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
'--binary',
|
|
62
|
+
required=True,
|
|
63
|
+
help='Path to binary file to analyze',
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
'--host',
|
|
67
|
+
default='127.0.0.1',
|
|
68
|
+
help='Host to bind MCP server (default: 127.0.0.1)',
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
'--port',
|
|
72
|
+
type=int,
|
|
73
|
+
default=6150,
|
|
74
|
+
help='Port for MCP server (default: 6150, 0 for auto-assign)',
|
|
75
|
+
)
|
|
76
|
+
args = parser.parse_args()
|
|
77
|
+
|
|
78
|
+
binary_path = Path(args.binary).resolve()
|
|
79
|
+
if not binary_path.exists():
|
|
80
|
+
print(f'Error: binary not found: {binary_path}', file=sys.stderr)
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
# Check prerequisites before expensive imports.
|
|
84
|
+
#
|
|
85
|
+
# Headless mode is built on idalib, which was introduced in IDA Pro 9.0.
|
|
86
|
+
# The `idapro` module does NOT exist in IDA 7.x/8.x — on older IDA, use the
|
|
87
|
+
# GUI plugin (`mcpyida_install`) instead of headless mode.
|
|
88
|
+
try:
|
|
89
|
+
import idapro
|
|
90
|
+
except ImportError as e:
|
|
91
|
+
error_msg = str(e)
|
|
92
|
+
if 'libidalib' in error_msg or 'py-activate-idalib' in error_msg:
|
|
93
|
+
# idapro is importable but idalib has not been activated yet.
|
|
94
|
+
print(
|
|
95
|
+
'Error: idalib is not configured.\n'
|
|
96
|
+
'\n'
|
|
97
|
+
'Headless mode requires IDA Pro 9.0 or later (idalib). The idapro\n'
|
|
98
|
+
'module is present but idalib has not been activated in this\n'
|
|
99
|
+
'environment. Activate it by running the script shipped with IDA,\n'
|
|
100
|
+
'in the SAME virtual environment used to run mcpyida-headless:\n'
|
|
101
|
+
' python /path/to/ida-pro-9/idalib/python/py-activate-idalib.py\n'
|
|
102
|
+
'\n'
|
|
103
|
+
'Full setup:\n'
|
|
104
|
+
' 1. Install IDA Pro 9.0+ (https://hex-rays.com). idalib does not\n'
|
|
105
|
+
' exist in IDA 7.x/8.x — use the GUI plugin there instead.\n'
|
|
106
|
+
' 2. Run py-activate-idalib.py (writes ~/.idapro/ida-config.json)\n'
|
|
107
|
+
' in the same venv as mcpyida-headless.\n'
|
|
108
|
+
' 3. pip install mcpyida\n'
|
|
109
|
+
' 4. mcpyida-headless --binary /path/to/elf',
|
|
110
|
+
file=sys.stderr,
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
# The idapro module itself is missing — typically IDA older than 9.0,
|
|
114
|
+
# or idalib not installed/activated in this environment.
|
|
115
|
+
print(
|
|
116
|
+
f'Error: could not import idapro (idalib): {e}\n'
|
|
117
|
+
'\n'
|
|
118
|
+
'Headless mode requires IDA Pro 9.0 or later. idalib and its\n'
|
|
119
|
+
'idapro module were introduced in IDA 9.0 and do NOT exist in\n'
|
|
120
|
+
'IDA 7.x or 8.x. On older IDA, use the GUI plugin (mcpyida_install)\n'
|
|
121
|
+
'instead of headless mode.\n'
|
|
122
|
+
'\n'
|
|
123
|
+
'If you do have IDA Pro 9.0+, activate idalib in this environment:\n'
|
|
124
|
+
' python /path/to/ida-pro-9/idalib/python/py-activate-idalib.py\n'
|
|
125
|
+
'\n'
|
|
126
|
+
'See docs/installation.md for full setup.',
|
|
127
|
+
file=sys.stderr,
|
|
128
|
+
)
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
|
|
131
|
+
# Signal headless mode before importing mcpserver so that is_headless()
|
|
132
|
+
# returns True immediately, even if idalib doesn't set the batch flag.
|
|
133
|
+
os.environ['MCPYIDA_HEADLESS'] = '1'
|
|
134
|
+
|
|
135
|
+
# idalib must be imported first — initializes the IDA environment
|
|
136
|
+
print('Starting IDA headless (idalib)...', file=sys.stderr)
|
|
137
|
+
try:
|
|
138
|
+
idapro.open_database(str(binary_path), run_auto_analysis=True)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
print(f'Error: Failed to open binary: {e}', file=sys.stderr)
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
import ida_auto
|
|
144
|
+
|
|
145
|
+
print('Waiting for auto-analysis to complete...', file=sys.stderr)
|
|
146
|
+
ida_auto.auto_wait()
|
|
147
|
+
print('Analysis complete.', file=sys.stderr)
|
|
148
|
+
|
|
149
|
+
print(f'Starting MCP server on {args.host}:{args.port}...', file=sys.stderr)
|
|
150
|
+
from mcpyida.mcpserver import McpServer, set_headless_dispatcher, get_ida_work_queue
|
|
151
|
+
|
|
152
|
+
# Register the work-queue dispatcher so that run_in_ida_main routes IDA
|
|
153
|
+
# API calls through the main-thread queue instead of calling directly from
|
|
154
|
+
# the uvicorn thread (which IDA 9 rejects with RuntimeError).
|
|
155
|
+
set_headless_dispatcher(run_on_main_thread)
|
|
156
|
+
|
|
157
|
+
# Use McpServer for lifecycle management (threading, sockets, port assignment).
|
|
158
|
+
# McpServer.start() calls server.create_mcp_app() internally.
|
|
159
|
+
server = McpServer()
|
|
160
|
+
server.start(args.host, args.port)
|
|
161
|
+
|
|
162
|
+
actual_port = server.port
|
|
163
|
+
if actual_port is None or actual_port <= 0:
|
|
164
|
+
print(f'Error: server port not assigned (got {actual_port!r})', file=sys.stderr)
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
|
|
167
|
+
status = {
|
|
168
|
+
'status': 'ready',
|
|
169
|
+
'host': args.host,
|
|
170
|
+
'port': actual_port,
|
|
171
|
+
'binary': str(binary_path),
|
|
172
|
+
}
|
|
173
|
+
print(json.dumps(status), flush=True)
|
|
174
|
+
|
|
175
|
+
# Pump the IDA main-thread work queue.
|
|
176
|
+
#
|
|
177
|
+
# Both async tool handlers (via run_on_ida_main_async) and sync tool
|
|
178
|
+
# handlers (via run_in_ida_main / run_on_main_thread) submit work items
|
|
179
|
+
# here. We execute them on the main thread and return results via
|
|
180
|
+
# asyncio Future callbacks or threading.Event signals respectively.
|
|
181
|
+
_work_queue = get_ida_work_queue()
|
|
182
|
+
try:
|
|
183
|
+
while True:
|
|
184
|
+
try:
|
|
185
|
+
work_item = _work_queue.get(timeout=0.1)
|
|
186
|
+
work_item()
|
|
187
|
+
except queue.Empty:
|
|
188
|
+
pass
|
|
189
|
+
except KeyboardInterrupt:
|
|
190
|
+
print('Shutting down...', file=sys.stderr)
|
|
191
|
+
server.stop()
|
|
192
|
+
finally:
|
|
193
|
+
idapro.close_database()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == '__main__':
|
|
197
|
+
main()
|