pyghidra-mcp-cli 0.1.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.
- pyghidra_mcp_cli/__init__.py +8 -0
- pyghidra_mcp_cli/client.py +373 -0
- pyghidra_mcp_cli/commands/__init__.py +0 -0
- pyghidra_mcp_cli/commands/callgraph.py +102 -0
- pyghidra_mcp_cli/commands/decompile.py +44 -0
- pyghidra_mcp_cli/commands/delete.py +43 -0
- pyghidra_mcp_cli/commands/import_cmd.py +32 -0
- pyghidra_mcp_cli/commands/list.py +109 -0
- pyghidra_mcp_cli/commands/metadata.py +43 -0
- pyghidra_mcp_cli/commands/read.py +45 -0
- pyghidra_mcp_cli/commands/search.py +155 -0
- pyghidra_mcp_cli/commands/xref.py +44 -0
- pyghidra_mcp_cli/main.py +158 -0
- pyghidra_mcp_cli/utils.py +216 -0
- pyghidra_mcp_cli-0.1.0.dist-info/METADATA +534 -0
- pyghidra_mcp_cli-0.1.0.dist-info/RECORD +18 -0
- pyghidra_mcp_cli-0.1.0.dist-info/WHEEL +4 -0
- pyghidra_mcp_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""MCP Client wrapper for pyghidra-mcp server.
|
|
2
|
+
|
|
3
|
+
This module provides a simplified async client interface to the pyghidra-mcp server
|
|
4
|
+
via HTTP transport only.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
client = PyGhidraMcpClient(host="localhost", port=8000)
|
|
8
|
+
await client.connect()
|
|
9
|
+
result = await client.list_project_binaries()
|
|
10
|
+
await client.close()
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from mcp import ClientSession
|
|
18
|
+
|
|
19
|
+
from .utils import get_server_start_message
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClientError(Exception):
|
|
23
|
+
"""Custom exception for client errors."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ServerNotRunningError(ClientError):
|
|
29
|
+
"""Raised when the pyghidra-mcp server is not running."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BinaryNotFoundError(ClientError):
|
|
35
|
+
"""Raised when a binary is not found in the project."""
|
|
36
|
+
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PyGhidraMcpClient:
|
|
41
|
+
"""
|
|
42
|
+
Wrapper around MCP ClientSession for pyghidra-mcp server.
|
|
43
|
+
|
|
44
|
+
Handles connection management and provides a simpler interface
|
|
45
|
+
for common operations via HTTP transport.
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
async with PyGhidraMcpClient(host="localhost", port=8000) as client:
|
|
49
|
+
result = await client.list_project_binaries()
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
host: str = "127.0.0.1",
|
|
55
|
+
port: int = 8000,
|
|
56
|
+
):
|
|
57
|
+
"""
|
|
58
|
+
Initialize the MCP client.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
host: Server host
|
|
62
|
+
port: Server port
|
|
63
|
+
"""
|
|
64
|
+
self.host = host
|
|
65
|
+
self.port = port
|
|
66
|
+
self._session: ClientSession | None = None
|
|
67
|
+
self._session_cm = None
|
|
68
|
+
self._transport_cm = None
|
|
69
|
+
self._connected = False
|
|
70
|
+
|
|
71
|
+
async def __aenter__(self):
|
|
72
|
+
"""Async context manager entry - establishes connection to server."""
|
|
73
|
+
await self._connect_internal()
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
77
|
+
"""Async context manager exit - closes connection and cleanup resources."""
|
|
78
|
+
await self._close_internal()
|
|
79
|
+
# Return None to propagate exceptions normally
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
async def _connect_internal(self) -> None:
|
|
83
|
+
"""Internal connection logic - establish connection to the pyghidra-mcp server."""
|
|
84
|
+
from mcp.client.session import ClientSession
|
|
85
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
86
|
+
|
|
87
|
+
url = f"http://{self.host}:{self.port}/mcp"
|
|
88
|
+
|
|
89
|
+
transport_gen = streamablehttp_client(url)
|
|
90
|
+
try:
|
|
91
|
+
read, write, _ = await asyncio.wait_for(transport_gen.__aenter__(), timeout=5.0)
|
|
92
|
+
except asyncio.TimeoutError:
|
|
93
|
+
try:
|
|
94
|
+
await transport_gen.__aexit__(None, None, None)
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
raise ServerNotRunningError(
|
|
98
|
+
f"Cannot connect to pyghidra-mcp server at {url}\n\n{get_server_start_message()}"
|
|
99
|
+
) from None
|
|
100
|
+
except (ConnectionError, OSError) as e:
|
|
101
|
+
try:
|
|
102
|
+
await transport_gen.__aexit__(None, None, None)
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
raise ServerNotRunningError(
|
|
106
|
+
f"Cannot connect to pyghidra-mcp server at {url}\n\n{get_server_start_message()}"
|
|
107
|
+
) from e
|
|
108
|
+
except Exception as e:
|
|
109
|
+
try:
|
|
110
|
+
await transport_gen.__aexit__(None, None, None)
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
error_msg = str(e)
|
|
114
|
+
if any(x in error_msg for x in ["ConnectError", "connection", "ConnectionRefused"]):
|
|
115
|
+
raise ServerNotRunningError(
|
|
116
|
+
f"Cannot connect to pyghidra-mcp server at {url}\n\n"
|
|
117
|
+
f"{get_server_start_message()}"
|
|
118
|
+
) from e
|
|
119
|
+
raise ServerNotRunningError(
|
|
120
|
+
f"Cannot connect to pyghidra-mcp server at {url}: {e}\n\n"
|
|
121
|
+
f"{get_server_start_message()}"
|
|
122
|
+
) from e
|
|
123
|
+
|
|
124
|
+
self._transport_cm = transport_gen
|
|
125
|
+
self._session_cm = ClientSession(read, write)
|
|
126
|
+
self._session = await self._session_cm.__aenter__()
|
|
127
|
+
await self._session.initialize()
|
|
128
|
+
self._connected = True
|
|
129
|
+
|
|
130
|
+
async def _close_internal(self) -> None:
|
|
131
|
+
"""Internal cleanup logic - close the connection and cleanup resources."""
|
|
132
|
+
if self._session_cm and self._connected:
|
|
133
|
+
self._connected = False
|
|
134
|
+
try:
|
|
135
|
+
await self._session_cm.__aexit__(None, None, None)
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
self._session_cm = None
|
|
139
|
+
self._session = None
|
|
140
|
+
|
|
141
|
+
if self._transport_cm:
|
|
142
|
+
try:
|
|
143
|
+
await self._transport_cm.__aexit__(None, None, None)
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
self._transport_cm = None
|
|
147
|
+
|
|
148
|
+
def _extract_result(self, result) -> dict[str, Any]:
|
|
149
|
+
"""Extract data from MCP result, handling structuredContent and errors."""
|
|
150
|
+
result_dict = result.model_dump()
|
|
151
|
+
|
|
152
|
+
if result_dict.get("isError"):
|
|
153
|
+
content = result_dict.get("content", [])
|
|
154
|
+
if content and len(content) > 0:
|
|
155
|
+
error_text = content[0].get("text", "Unknown error")
|
|
156
|
+
raise ClientError(error_text)
|
|
157
|
+
raise ClientError("Unknown error occurred")
|
|
158
|
+
|
|
159
|
+
if "structuredContent" in result_dict:
|
|
160
|
+
structured = result_dict["structuredContent"]
|
|
161
|
+
if structured is None:
|
|
162
|
+
# Check if there's valid data in content[0].text (for metadata responses)
|
|
163
|
+
content = result_dict.get("content", [])
|
|
164
|
+
if content and len(content) > 0 and content[0].get("text"):
|
|
165
|
+
try:
|
|
166
|
+
return json.loads(content[0]["text"])
|
|
167
|
+
except (json.JSONDecodeError, KeyError):
|
|
168
|
+
pass
|
|
169
|
+
raise BinaryNotFoundError(
|
|
170
|
+
"Binary not found. "
|
|
171
|
+
"Run 'pyghidra-mcp-cli list binaries' to see available binaries."
|
|
172
|
+
)
|
|
173
|
+
return structured
|
|
174
|
+
|
|
175
|
+
if result_dict is None or (isinstance(result_dict, dict) and not result_dict):
|
|
176
|
+
raise BinaryNotFoundError(
|
|
177
|
+
"Binary not found. Run 'pyghidra-mcp-cli list binaries' to see available binaries."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return result_dict
|
|
181
|
+
|
|
182
|
+
async def list_project_binaries(self) -> dict[str, Any]:
|
|
183
|
+
"""List all binaries in the project."""
|
|
184
|
+
if not self._connected:
|
|
185
|
+
raise ClientError("Not connected")
|
|
186
|
+
|
|
187
|
+
result = await self._session.call_tool("list_project_binaries", {})
|
|
188
|
+
return self._extract_result(result)
|
|
189
|
+
|
|
190
|
+
async def decompile_function(
|
|
191
|
+
self, binary_name: str, function_name_or_address: str
|
|
192
|
+
) -> dict[str, Any]:
|
|
193
|
+
"""Decompile a function."""
|
|
194
|
+
if not self._connected:
|
|
195
|
+
raise ClientError("Not connected")
|
|
196
|
+
|
|
197
|
+
result = await self._session.call_tool(
|
|
198
|
+
"decompile_function",
|
|
199
|
+
{"binary_name": binary_name, "name_or_address": function_name_or_address},
|
|
200
|
+
)
|
|
201
|
+
return self._extract_result(result)
|
|
202
|
+
|
|
203
|
+
async def search_symbols(
|
|
204
|
+
self, binary_name: str, query: str, offset: int = 0, limit: int = 25
|
|
205
|
+
) -> dict[str, Any]:
|
|
206
|
+
"""Search for symbols by name."""
|
|
207
|
+
if not self._connected:
|
|
208
|
+
raise ClientError("Not connected")
|
|
209
|
+
|
|
210
|
+
result = await self._session.call_tool(
|
|
211
|
+
"search_symbols_by_name",
|
|
212
|
+
{
|
|
213
|
+
"binary_name": binary_name,
|
|
214
|
+
"query": query,
|
|
215
|
+
"offset": offset,
|
|
216
|
+
"limit": limit,
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
return self._extract_result(result)
|
|
220
|
+
|
|
221
|
+
async def search_code(
|
|
222
|
+
self,
|
|
223
|
+
binary_name: str,
|
|
224
|
+
query: str,
|
|
225
|
+
limit: int = 5,
|
|
226
|
+
offset: int = 0,
|
|
227
|
+
search_mode: str = "semantic",
|
|
228
|
+
include_full_code: bool = True,
|
|
229
|
+
preview_length: int = 500,
|
|
230
|
+
similarity_threshold: float = 0.0,
|
|
231
|
+
) -> dict[str, Any]:
|
|
232
|
+
"""Search code by query."""
|
|
233
|
+
if not self._connected:
|
|
234
|
+
raise ClientError("Not connected")
|
|
235
|
+
|
|
236
|
+
result = await self._session.call_tool(
|
|
237
|
+
"search_code",
|
|
238
|
+
{
|
|
239
|
+
"binary_name": binary_name,
|
|
240
|
+
"query": query,
|
|
241
|
+
"limit": limit,
|
|
242
|
+
"offset": offset,
|
|
243
|
+
"search_mode": search_mode,
|
|
244
|
+
"include_full_code": include_full_code,
|
|
245
|
+
"preview_length": preview_length,
|
|
246
|
+
"similarity_threshold": similarity_threshold,
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
return self._extract_result(result)
|
|
250
|
+
|
|
251
|
+
async def search_strings(
|
|
252
|
+
self, binary_name: str, query: str, limit: int = 100
|
|
253
|
+
) -> dict[str, Any]:
|
|
254
|
+
"""Search for strings in the binary."""
|
|
255
|
+
if not self._connected:
|
|
256
|
+
raise ClientError("Not connected")
|
|
257
|
+
|
|
258
|
+
result = await self._session.call_tool(
|
|
259
|
+
"search_strings",
|
|
260
|
+
{"binary_name": binary_name, "query": query, "limit": limit},
|
|
261
|
+
)
|
|
262
|
+
return self._extract_result(result)
|
|
263
|
+
|
|
264
|
+
async def list_imports(
|
|
265
|
+
self, binary_name: str, query: str = ".*", offset: int = 0, limit: int = 25
|
|
266
|
+
) -> dict[str, Any]:
|
|
267
|
+
"""List imports in the binary."""
|
|
268
|
+
if not self._connected:
|
|
269
|
+
raise ClientError("Not connected")
|
|
270
|
+
|
|
271
|
+
result = await self._session.call_tool(
|
|
272
|
+
"list_imports",
|
|
273
|
+
{"binary_name": binary_name, "query": query, "offset": offset, "limit": limit},
|
|
274
|
+
)
|
|
275
|
+
return self._extract_result(result)
|
|
276
|
+
|
|
277
|
+
async def list_exports(
|
|
278
|
+
self, binary_name: str, query: str = ".*", offset: int = 0, limit: int = 25
|
|
279
|
+
) -> dict[str, Any]:
|
|
280
|
+
"""List exports in the binary."""
|
|
281
|
+
if not self._connected:
|
|
282
|
+
raise ClientError("Not connected")
|
|
283
|
+
|
|
284
|
+
result = await self._session.call_tool(
|
|
285
|
+
"list_exports",
|
|
286
|
+
{"binary_name": binary_name, "query": query, "offset": offset, "limit": limit},
|
|
287
|
+
)
|
|
288
|
+
return self._extract_result(result)
|
|
289
|
+
|
|
290
|
+
async def list_cross_references(self, binary_name: str, name_or_address: str) -> dict[str, Any]:
|
|
291
|
+
"""List cross-references to a symbol or address."""
|
|
292
|
+
if not self._connected:
|
|
293
|
+
raise ClientError("Not connected")
|
|
294
|
+
|
|
295
|
+
result = await self._session.call_tool(
|
|
296
|
+
"list_cross_references",
|
|
297
|
+
{"binary_name": binary_name, "name_or_address": name_or_address},
|
|
298
|
+
)
|
|
299
|
+
return self._extract_result(result)
|
|
300
|
+
|
|
301
|
+
async def read_bytes(self, binary_name: str, address: str, size: int = 32) -> dict[str, Any]:
|
|
302
|
+
"""Read bytes from memory."""
|
|
303
|
+
if not self._connected:
|
|
304
|
+
raise ClientError("Not connected")
|
|
305
|
+
|
|
306
|
+
result = await self._session.call_tool(
|
|
307
|
+
"read_bytes",
|
|
308
|
+
{"binary_name": binary_name, "address": address, "size": size},
|
|
309
|
+
)
|
|
310
|
+
return self._extract_result(result)
|
|
311
|
+
|
|
312
|
+
async def gen_callgraph(
|
|
313
|
+
self,
|
|
314
|
+
binary_name: str,
|
|
315
|
+
function_name: str,
|
|
316
|
+
direction: str = "calling",
|
|
317
|
+
display_type: str = "flow",
|
|
318
|
+
condense_threshold: int = 50,
|
|
319
|
+
top_layers: int = 3,
|
|
320
|
+
bottom_layers: int = 3,
|
|
321
|
+
max_run_time: int = 120,
|
|
322
|
+
) -> dict[str, Any]:
|
|
323
|
+
"""Generate a call graph for a function."""
|
|
324
|
+
if not self._connected:
|
|
325
|
+
raise ClientError("Not connected")
|
|
326
|
+
|
|
327
|
+
result = await self._session.call_tool(
|
|
328
|
+
"gen_callgraph",
|
|
329
|
+
{
|
|
330
|
+
"binary_name": binary_name,
|
|
331
|
+
"function_name": function_name,
|
|
332
|
+
"direction": direction,
|
|
333
|
+
"display_type": display_type,
|
|
334
|
+
"condense_threshold": condense_threshold,
|
|
335
|
+
"top_layers": top_layers,
|
|
336
|
+
"bottom_layers": bottom_layers,
|
|
337
|
+
"max_run_time": max_run_time,
|
|
338
|
+
},
|
|
339
|
+
)
|
|
340
|
+
return self._extract_result(result)
|
|
341
|
+
|
|
342
|
+
async def import_binary(self, binary_path: str) -> dict[str, Any]:
|
|
343
|
+
"""Import a binary into the project."""
|
|
344
|
+
if not self._connected:
|
|
345
|
+
raise ClientError("Not connected")
|
|
346
|
+
|
|
347
|
+
result = await self._session.call_tool(
|
|
348
|
+
"import_binary",
|
|
349
|
+
{"binary_path": binary_path},
|
|
350
|
+
)
|
|
351
|
+
return self._extract_result(result)
|
|
352
|
+
|
|
353
|
+
async def delete_binary(self, binary_name: str) -> dict[str, Any]:
|
|
354
|
+
"""Delete a binary from the project."""
|
|
355
|
+
if not self._connected:
|
|
356
|
+
raise ClientError("Not connected")
|
|
357
|
+
|
|
358
|
+
result = await self._session.call_tool(
|
|
359
|
+
"delete_project_binary",
|
|
360
|
+
{"binary_name": binary_name},
|
|
361
|
+
)
|
|
362
|
+
return self._extract_result(result)
|
|
363
|
+
|
|
364
|
+
async def list_project_binary_metadata(self, binary_name: str) -> dict[str, Any]:
|
|
365
|
+
"""Get metadata for a binary."""
|
|
366
|
+
if not self._connected:
|
|
367
|
+
raise ClientError("Not connected")
|
|
368
|
+
|
|
369
|
+
result = await self._session.call_tool(
|
|
370
|
+
"list_project_binary_metadata",
|
|
371
|
+
{"binary_name": binary_name},
|
|
372
|
+
)
|
|
373
|
+
return self._extract_result(result)
|
|
File without changes
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Call graph commands for pyghidra-mcp CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..client import PyGhidraMcpClient
|
|
8
|
+
from ..utils import format_output, handle_command_error
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def binary_option(func):
|
|
12
|
+
"""Common --binary option for commands that target a specific binary."""
|
|
13
|
+
return click.option(
|
|
14
|
+
"-b",
|
|
15
|
+
"--binary",
|
|
16
|
+
"binary_name",
|
|
17
|
+
required=True,
|
|
18
|
+
help="Binary name in the project (use 'list binaries' to see available binaries).",
|
|
19
|
+
)(func)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.command()
|
|
23
|
+
@binary_option
|
|
24
|
+
@click.argument("function_name")
|
|
25
|
+
@click.option(
|
|
26
|
+
"-d",
|
|
27
|
+
"--direction",
|
|
28
|
+
type=click.Choice(["calling", "called"], case_sensitive=False),
|
|
29
|
+
default="calling",
|
|
30
|
+
help="Direction of the call graph.",
|
|
31
|
+
)
|
|
32
|
+
@click.option(
|
|
33
|
+
"-t",
|
|
34
|
+
"--type",
|
|
35
|
+
"display_type",
|
|
36
|
+
type=click.Choice(["flow", "flow_ends"], case_sensitive=False),
|
|
37
|
+
default="flow",
|
|
38
|
+
help="Display type of the graph.",
|
|
39
|
+
)
|
|
40
|
+
@click.option(
|
|
41
|
+
"--condense-threshold",
|
|
42
|
+
type=int,
|
|
43
|
+
default=50,
|
|
44
|
+
help="Maximum number of edges before graph condensation (default: 50).",
|
|
45
|
+
)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--top-layers",
|
|
48
|
+
type=int,
|
|
49
|
+
default=3,
|
|
50
|
+
help="Number of top layers to show in condensed graph (default: 3).",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"--bottom-layers",
|
|
54
|
+
type=int,
|
|
55
|
+
default=3,
|
|
56
|
+
help="Number of bottom layers to show in condensed graph (default: 3).",
|
|
57
|
+
)
|
|
58
|
+
@click.option(
|
|
59
|
+
"--max-run-time",
|
|
60
|
+
type=int,
|
|
61
|
+
default=120,
|
|
62
|
+
help="Maximum run time in seconds (default: 120).",
|
|
63
|
+
)
|
|
64
|
+
@click.pass_context
|
|
65
|
+
def callgraph(
|
|
66
|
+
ctx: click.Context,
|
|
67
|
+
binary_name: str,
|
|
68
|
+
function_name: str,
|
|
69
|
+
direction: str,
|
|
70
|
+
display_type: str,
|
|
71
|
+
condense_threshold: int,
|
|
72
|
+
top_layers: int,
|
|
73
|
+
bottom_layers: int,
|
|
74
|
+
max_run_time: int,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Generate a call graph for a function in a binary."""
|
|
77
|
+
|
|
78
|
+
async def run():
|
|
79
|
+
client = PyGhidraMcpClient(
|
|
80
|
+
host=ctx.obj["HOST"],
|
|
81
|
+
port=ctx.obj["PORT"],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async with client:
|
|
85
|
+
result = await client.gen_callgraph(
|
|
86
|
+
binary_name,
|
|
87
|
+
function_name,
|
|
88
|
+
direction=direction,
|
|
89
|
+
display_type=display_type,
|
|
90
|
+
condense_threshold=condense_threshold,
|
|
91
|
+
top_layers=top_layers,
|
|
92
|
+
bottom_layers=bottom_layers,
|
|
93
|
+
max_run_time=max_run_time,
|
|
94
|
+
)
|
|
95
|
+
format_output(result, ctx.obj["OUTPUT_FORMAT"], ctx.obj["VERBOSE"])
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
from ..utils import run_async
|
|
99
|
+
|
|
100
|
+
run_async(run())
|
|
101
|
+
except (asyncio.exceptions.CancelledError, Exception) as e:
|
|
102
|
+
handle_command_error(e, ctx)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Decompile commands for pyghidra-mcp CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..client import PyGhidraMcpClient
|
|
8
|
+
from ..utils import format_output, handle_command_error
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def binary_option(func):
|
|
12
|
+
"""Common --binary option for commands that target a specific binary."""
|
|
13
|
+
return click.option(
|
|
14
|
+
"-b",
|
|
15
|
+
"--binary",
|
|
16
|
+
"binary_name",
|
|
17
|
+
required=True,
|
|
18
|
+
help="Binary name in the project (use 'list binaries' to see available binaries).",
|
|
19
|
+
)(func)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.command()
|
|
23
|
+
@binary_option
|
|
24
|
+
@click.argument("function_name_or_address")
|
|
25
|
+
@click.pass_context
|
|
26
|
+
def decompile(ctx: click.Context, binary_name: str, function_name_or_address: str) -> None:
|
|
27
|
+
"""Decompile a function in a binary."""
|
|
28
|
+
|
|
29
|
+
client = PyGhidraMcpClient(
|
|
30
|
+
host=ctx.obj["HOST"],
|
|
31
|
+
port=ctx.obj["PORT"],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
async def run():
|
|
35
|
+
async with client:
|
|
36
|
+
result = await client.decompile_function(binary_name, function_name_or_address)
|
|
37
|
+
format_output(result, ctx.obj["OUTPUT_FORMAT"], ctx.obj["VERBOSE"])
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
from ..utils import run_async
|
|
41
|
+
|
|
42
|
+
run_async(run())
|
|
43
|
+
except (asyncio.exceptions.CancelledError, Exception) as e:
|
|
44
|
+
handle_command_error(e, ctx)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Delete binary commands for pyghidra-mcp CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..client import PyGhidraMcpClient
|
|
8
|
+
from ..utils import format_output, handle_command_error
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def binary_option(func):
|
|
12
|
+
"""Common --binary option for commands that target a specific binary."""
|
|
13
|
+
return click.option(
|
|
14
|
+
"-b",
|
|
15
|
+
"--binary",
|
|
16
|
+
"binary_name",
|
|
17
|
+
required=True,
|
|
18
|
+
help="Binary name in the project (use 'list binaries' to see available binaries).",
|
|
19
|
+
)(func)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.command()
|
|
23
|
+
@binary_option
|
|
24
|
+
@click.pass_context
|
|
25
|
+
def delete(ctx: click.Context, binary_name: str) -> None:
|
|
26
|
+
"""Delete a binary from the project."""
|
|
27
|
+
|
|
28
|
+
client = PyGhidraMcpClient(
|
|
29
|
+
host=ctx.obj["HOST"],
|
|
30
|
+
port=ctx.obj["PORT"],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
async def run():
|
|
34
|
+
async with client:
|
|
35
|
+
result = await client.delete_binary(binary_name)
|
|
36
|
+
format_output(result, ctx.obj["OUTPUT_FORMAT"], ctx.obj["VERBOSE"])
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from ..utils import run_async
|
|
40
|
+
|
|
41
|
+
run_async(run())
|
|
42
|
+
except (asyncio.exceptions.CancelledError, Exception) as e:
|
|
43
|
+
handle_command_error(e, ctx)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Import command for pyghidra-mcp CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..client import PyGhidraMcpClient
|
|
8
|
+
from ..utils import format_output, handle_command_error
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.argument("binary_path")
|
|
13
|
+
@click.pass_context
|
|
14
|
+
def import_cmd(ctx: click.Context, binary_path: str) -> None:
|
|
15
|
+
"""Import a binary into the project."""
|
|
16
|
+
|
|
17
|
+
client = PyGhidraMcpClient(
|
|
18
|
+
host=ctx.obj["HOST"],
|
|
19
|
+
port=ctx.obj["PORT"],
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
async def run():
|
|
23
|
+
async with client:
|
|
24
|
+
result = await client.import_binary(binary_path)
|
|
25
|
+
format_output(result, ctx.obj["OUTPUT_FORMAT"], ctx.obj["VERBOSE"])
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from ..utils import run_async
|
|
29
|
+
|
|
30
|
+
run_async(run())
|
|
31
|
+
except (asyncio.exceptions.CancelledError, Exception) as e:
|
|
32
|
+
handle_command_error(e, ctx)
|