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 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
@@ -0,0 +1,11 @@
1
+ from typing import List, Mapping
2
+
3
+ type JsonValueTypes = (
4
+ str
5
+ | int
6
+ | float
7
+ | bool
8
+ | None
9
+ | List[JsonValueTypes]
10
+ | Mapping[str, JsonValueTypes]
11
+ )
@@ -0,0 +1,5 @@
1
+ from typing import TypeAlias, Union, List, Any, Mapping
2
+
3
+ JsonValueTypes: TypeAlias = Union[
4
+ str, int, float, bool, None, List[Any], Mapping[str, Any]
5
+ ]
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()