superghidra-mcp-cli 0.5.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 +596 -0
- pyghidra_mcp_cli/commands/__init__.py +0 -0
- pyghidra_mcp_cli/commands/callgraph.py +102 -0
- pyghidra_mcp_cli/commands/decompile.py +60 -0
- pyghidra_mcp_cli/commands/delete.py +43 -0
- pyghidra_mcp_cli/commands/fork.py +87 -0
- pyghidra_mcp_cli/commands/goto.py +44 -0
- pyghidra_mcp_cli/commands/import_cmd.py +32 -0
- pyghidra_mcp_cli/commands/list.py +132 -0
- pyghidra_mcp_cli/commands/metadata.py +43 -0
- pyghidra_mcp_cli/commands/open_cmd.py +47 -0
- pyghidra_mcp_cli/commands/read.py +45 -0
- pyghidra_mcp_cli/commands/rename.py +85 -0
- pyghidra_mcp_cli/commands/save.py +31 -0
- pyghidra_mcp_cli/commands/search.py +162 -0
- pyghidra_mcp_cli/commands/set_cmd.py +153 -0
- pyghidra_mcp_cli/commands/xref.py +44 -0
- pyghidra_mcp_cli/main.py +149 -0
- pyghidra_mcp_cli/utils.py +216 -0
- superghidra_mcp_cli-0.5.0.dist-info/METADATA +629 -0
- superghidra_mcp_cli-0.5.0.dist-info/RECORD +24 -0
- superghidra_mcp_cli-0.5.0.dist-info/WHEEL +4 -0
- superghidra_mcp_cli-0.5.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,596 @@
|
|
|
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 streamable_http_client
|
|
86
|
+
|
|
87
|
+
url = f"http://{self.host}:{self.port}/mcp"
|
|
88
|
+
|
|
89
|
+
transport_gen = streamable_http_client(url)
|
|
90
|
+
try:
|
|
91
|
+
streams = await asyncio.wait_for(transport_gen.__aenter__(), timeout=5.0)
|
|
92
|
+
read, write = streams
|
|
93
|
+
except asyncio.TimeoutError:
|
|
94
|
+
try:
|
|
95
|
+
await transport_gen.__aexit__(None, None, None)
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
raise ServerNotRunningError(
|
|
99
|
+
f"Cannot connect to pyghidra-mcp server at {url}\n\n{get_server_start_message()}"
|
|
100
|
+
) from None
|
|
101
|
+
except (ConnectionError, OSError) as e:
|
|
102
|
+
try:
|
|
103
|
+
await transport_gen.__aexit__(None, None, None)
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
raise ServerNotRunningError(
|
|
107
|
+
f"Cannot connect to pyghidra-mcp server at {url}\n\n{get_server_start_message()}"
|
|
108
|
+
) from e
|
|
109
|
+
except Exception as e:
|
|
110
|
+
try:
|
|
111
|
+
await transport_gen.__aexit__(None, None, None)
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
error_msg = str(e)
|
|
115
|
+
if any(x in error_msg for x in ["ConnectError", "connection", "ConnectionRefused"]):
|
|
116
|
+
raise ServerNotRunningError(
|
|
117
|
+
f"Cannot connect to pyghidra-mcp server at {url}\n\n"
|
|
118
|
+
f"{get_server_start_message()}"
|
|
119
|
+
) from e
|
|
120
|
+
raise ServerNotRunningError(
|
|
121
|
+
f"Cannot connect to pyghidra-mcp server at {url}: {e}\n\n"
|
|
122
|
+
f"{get_server_start_message()}"
|
|
123
|
+
) from e
|
|
124
|
+
|
|
125
|
+
self._transport_cm = transport_gen
|
|
126
|
+
self._session_cm = ClientSession(read, write)
|
|
127
|
+
self._session = await self._session_cm.__aenter__()
|
|
128
|
+
await self._session.initialize()
|
|
129
|
+
self._connected = True
|
|
130
|
+
|
|
131
|
+
async def _close_internal(self) -> None:
|
|
132
|
+
"""Internal cleanup logic - close the connection and cleanup resources."""
|
|
133
|
+
if self._session_cm and self._connected:
|
|
134
|
+
self._connected = False
|
|
135
|
+
try:
|
|
136
|
+
await self._session_cm.__aexit__(None, None, None)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
self._session_cm = None
|
|
140
|
+
self._session = None
|
|
141
|
+
|
|
142
|
+
if self._transport_cm:
|
|
143
|
+
try:
|
|
144
|
+
await self._transport_cm.__aexit__(None, None, None)
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
self._transport_cm = None
|
|
148
|
+
|
|
149
|
+
def _extract_result(self, result) -> dict[str, Any]:
|
|
150
|
+
"""Extract data from MCP result, handling structuredContent and errors."""
|
|
151
|
+
result_dict = result.model_dump(by_alias=True)
|
|
152
|
+
|
|
153
|
+
if result_dict.get("isError"):
|
|
154
|
+
content = result_dict.get("content", [])
|
|
155
|
+
if content and len(content) > 0:
|
|
156
|
+
error_text = content[0].get("text", "Unknown error")
|
|
157
|
+
raise ClientError(error_text)
|
|
158
|
+
raise ClientError("Unknown error occurred")
|
|
159
|
+
|
|
160
|
+
if "structuredContent" in result_dict:
|
|
161
|
+
structured = result_dict["structuredContent"]
|
|
162
|
+
if structured is None:
|
|
163
|
+
# Check if there's valid data in content[0].text (for metadata responses)
|
|
164
|
+
content = result_dict.get("content", [])
|
|
165
|
+
if content and len(content) > 0 and content[0].get("text"):
|
|
166
|
+
try:
|
|
167
|
+
return json.loads(content[0]["text"])
|
|
168
|
+
except (json.JSONDecodeError, KeyError):
|
|
169
|
+
pass
|
|
170
|
+
raise BinaryNotFoundError(
|
|
171
|
+
"Binary not found. "
|
|
172
|
+
"Run 'pyghidra-mcp-cli list binaries' to see available binaries."
|
|
173
|
+
)
|
|
174
|
+
return structured
|
|
175
|
+
|
|
176
|
+
if result_dict is None or (isinstance(result_dict, dict) and not result_dict):
|
|
177
|
+
raise BinaryNotFoundError(
|
|
178
|
+
"Binary not found. Run 'pyghidra-mcp-cli list binaries' to see available binaries."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return result_dict
|
|
182
|
+
|
|
183
|
+
async def list_project_binaries(self) -> dict[str, Any]:
|
|
184
|
+
"""List all binaries in the project."""
|
|
185
|
+
if not self._connected:
|
|
186
|
+
raise ClientError("Not connected")
|
|
187
|
+
|
|
188
|
+
result = await self._session.call_tool("list_project_binaries", {})
|
|
189
|
+
return self._extract_result(result)
|
|
190
|
+
|
|
191
|
+
async def list_open_programs(self) -> dict[str, Any]:
|
|
192
|
+
"""List programs open in the Ghidra GUI."""
|
|
193
|
+
if not self._connected:
|
|
194
|
+
raise ClientError("Not connected")
|
|
195
|
+
|
|
196
|
+
result = await self._session.call_tool("list_open_programs", {})
|
|
197
|
+
return self._extract_result(result)
|
|
198
|
+
|
|
199
|
+
async def open_program_in_gui(
|
|
200
|
+
self, binary_name: str, new_window: bool = True
|
|
201
|
+
) -> dict[str, Any]:
|
|
202
|
+
"""Open a binary in the Ghidra GUI."""
|
|
203
|
+
if not self._connected:
|
|
204
|
+
raise ClientError("Not connected")
|
|
205
|
+
|
|
206
|
+
result = await self._session.call_tool(
|
|
207
|
+
"open_program_in_gui",
|
|
208
|
+
{"binary_name": binary_name, "new_window": new_window},
|
|
209
|
+
)
|
|
210
|
+
return self._extract_result(result)
|
|
211
|
+
|
|
212
|
+
async def set_current_program(self, binary_name: str) -> dict[str, Any]:
|
|
213
|
+
"""Set the current Ghidra GUI program."""
|
|
214
|
+
if not self._connected:
|
|
215
|
+
raise ClientError("Not connected")
|
|
216
|
+
|
|
217
|
+
result = await self._session.call_tool(
|
|
218
|
+
"set_current_program",
|
|
219
|
+
{"binary_name": binary_name},
|
|
220
|
+
)
|
|
221
|
+
return self._extract_result(result)
|
|
222
|
+
|
|
223
|
+
async def goto(self, binary_name: str, target: str, target_type: str) -> dict[str, Any]:
|
|
224
|
+
"""Navigate the Ghidra GUI."""
|
|
225
|
+
if not self._connected:
|
|
226
|
+
raise ClientError("Not connected")
|
|
227
|
+
|
|
228
|
+
result = await self._session.call_tool(
|
|
229
|
+
"goto",
|
|
230
|
+
{"binary_name": binary_name, "target": target, "target_type": target_type},
|
|
231
|
+
)
|
|
232
|
+
return self._extract_result(result)
|
|
233
|
+
|
|
234
|
+
async def decompile_function(
|
|
235
|
+
self,
|
|
236
|
+
binary_name: str,
|
|
237
|
+
function_name_or_address: str,
|
|
238
|
+
include_callees: bool = False,
|
|
239
|
+
include_strings: bool = False,
|
|
240
|
+
include_xrefs: bool = False,
|
|
241
|
+
) -> dict[str, Any]:
|
|
242
|
+
"""Decompile a function."""
|
|
243
|
+
if not self._connected:
|
|
244
|
+
raise ClientError("Not connected")
|
|
245
|
+
|
|
246
|
+
args: dict[str, Any] = {
|
|
247
|
+
"binary_name": binary_name,
|
|
248
|
+
"name_or_address": function_name_or_address,
|
|
249
|
+
}
|
|
250
|
+
if include_callees:
|
|
251
|
+
args["include_callees"] = True
|
|
252
|
+
if include_strings:
|
|
253
|
+
args["include_strings"] = True
|
|
254
|
+
if include_xrefs:
|
|
255
|
+
args["include_xrefs"] = True
|
|
256
|
+
|
|
257
|
+
result = await self._session.call_tool("decompile_function", args)
|
|
258
|
+
extracted = self._extract_result(result)
|
|
259
|
+
# Server returns list[DecompiledFunction]; unwrap single-item batch
|
|
260
|
+
if "result" in extracted and isinstance(extracted["result"], list):
|
|
261
|
+
return extracted["result"][0]
|
|
262
|
+
return extracted
|
|
263
|
+
|
|
264
|
+
async def search_symbols(
|
|
265
|
+
self,
|
|
266
|
+
binary_name: str,
|
|
267
|
+
query: str,
|
|
268
|
+
functions_only: bool = False,
|
|
269
|
+
offset: int = 0,
|
|
270
|
+
limit: int = 25,
|
|
271
|
+
) -> dict[str, Any]:
|
|
272
|
+
"""Search for symbols by name."""
|
|
273
|
+
if not self._connected:
|
|
274
|
+
raise ClientError("Not connected")
|
|
275
|
+
|
|
276
|
+
args: dict[str, Any] = {
|
|
277
|
+
"binary_name": binary_name,
|
|
278
|
+
"query": query,
|
|
279
|
+
"offset": offset,
|
|
280
|
+
"limit": limit,
|
|
281
|
+
}
|
|
282
|
+
if functions_only:
|
|
283
|
+
args["functions_only"] = True
|
|
284
|
+
|
|
285
|
+
result = await self._session.call_tool("search_symbols_by_name", args)
|
|
286
|
+
return self._extract_result(result)
|
|
287
|
+
|
|
288
|
+
async def search_code(
|
|
289
|
+
self,
|
|
290
|
+
binary_name: str,
|
|
291
|
+
query: str,
|
|
292
|
+
limit: int = 5,
|
|
293
|
+
offset: int = 0,
|
|
294
|
+
search_mode: str = "semantic",
|
|
295
|
+
include_full_code: bool = True,
|
|
296
|
+
preview_length: int = 500,
|
|
297
|
+
similarity_threshold: float = 0.0,
|
|
298
|
+
) -> dict[str, Any]:
|
|
299
|
+
"""Search code by query."""
|
|
300
|
+
if not self._connected:
|
|
301
|
+
raise ClientError("Not connected")
|
|
302
|
+
|
|
303
|
+
result = await self._session.call_tool(
|
|
304
|
+
"search_code",
|
|
305
|
+
{
|
|
306
|
+
"binary_name": binary_name,
|
|
307
|
+
"query": query,
|
|
308
|
+
"limit": limit,
|
|
309
|
+
"offset": offset,
|
|
310
|
+
"search_mode": search_mode,
|
|
311
|
+
"include_full_code": include_full_code,
|
|
312
|
+
"preview_length": preview_length,
|
|
313
|
+
"similarity_threshold": similarity_threshold,
|
|
314
|
+
},
|
|
315
|
+
)
|
|
316
|
+
return self._extract_result(result)
|
|
317
|
+
|
|
318
|
+
async def search_strings(
|
|
319
|
+
self, binary_name: str, query: str, limit: int = 100
|
|
320
|
+
) -> dict[str, Any]:
|
|
321
|
+
"""Search for strings in the binary."""
|
|
322
|
+
if not self._connected:
|
|
323
|
+
raise ClientError("Not connected")
|
|
324
|
+
|
|
325
|
+
result = await self._session.call_tool(
|
|
326
|
+
"search_strings",
|
|
327
|
+
{"binary_name": binary_name, "query": query, "limit": limit},
|
|
328
|
+
)
|
|
329
|
+
return self._extract_result(result)
|
|
330
|
+
|
|
331
|
+
async def list_imports(
|
|
332
|
+
self, binary_name: str, query: str = ".*", offset: int = 0, limit: int = 25
|
|
333
|
+
) -> dict[str, Any]:
|
|
334
|
+
"""List imports in the binary."""
|
|
335
|
+
if not self._connected:
|
|
336
|
+
raise ClientError("Not connected")
|
|
337
|
+
|
|
338
|
+
result = await self._session.call_tool(
|
|
339
|
+
"list_imports",
|
|
340
|
+
{"binary_name": binary_name, "query": query, "offset": offset, "limit": limit},
|
|
341
|
+
)
|
|
342
|
+
return self._extract_result(result)
|
|
343
|
+
|
|
344
|
+
async def list_exports(
|
|
345
|
+
self, binary_name: str, query: str = ".*", offset: int = 0, limit: int = 25
|
|
346
|
+
) -> dict[str, Any]:
|
|
347
|
+
"""List exports in the binary."""
|
|
348
|
+
if not self._connected:
|
|
349
|
+
raise ClientError("Not connected")
|
|
350
|
+
|
|
351
|
+
result = await self._session.call_tool(
|
|
352
|
+
"list_exports",
|
|
353
|
+
{"binary_name": binary_name, "query": query, "offset": offset, "limit": limit},
|
|
354
|
+
)
|
|
355
|
+
return self._extract_result(result)
|
|
356
|
+
|
|
357
|
+
async def list_xrefs(self, binary_name: str, name_or_address: str) -> dict[str, Any]:
|
|
358
|
+
"""List cross-references to a symbol or address."""
|
|
359
|
+
if not self._connected:
|
|
360
|
+
raise ClientError("Not connected")
|
|
361
|
+
|
|
362
|
+
result = await self._session.call_tool(
|
|
363
|
+
"list_xrefs",
|
|
364
|
+
{"binary_name": binary_name, "name_or_address": name_or_address},
|
|
365
|
+
)
|
|
366
|
+
extracted = self._extract_result(result)
|
|
367
|
+
# Server returns list[CrossReferenceInfos]; unwrap single-item batch
|
|
368
|
+
if "result" in extracted and isinstance(extracted["result"], list):
|
|
369
|
+
return extracted["result"][0]
|
|
370
|
+
return extracted
|
|
371
|
+
|
|
372
|
+
async def read_bytes(self, binary_name: str, address: str, size: int = 32) -> dict[str, Any]:
|
|
373
|
+
"""Read bytes from memory."""
|
|
374
|
+
if not self._connected:
|
|
375
|
+
raise ClientError("Not connected")
|
|
376
|
+
|
|
377
|
+
result = await self._session.call_tool(
|
|
378
|
+
"read_bytes",
|
|
379
|
+
{"binary_name": binary_name, "address": address, "size": size},
|
|
380
|
+
)
|
|
381
|
+
return self._extract_result(result)
|
|
382
|
+
|
|
383
|
+
async def gen_callgraph(
|
|
384
|
+
self,
|
|
385
|
+
binary_name: str,
|
|
386
|
+
function_name: str,
|
|
387
|
+
direction: str = "calling",
|
|
388
|
+
display_type: str = "flow",
|
|
389
|
+
condense_threshold: int = 50,
|
|
390
|
+
top_layers: int = 3,
|
|
391
|
+
bottom_layers: int = 3,
|
|
392
|
+
max_run_time: int = 120,
|
|
393
|
+
) -> dict[str, Any]:
|
|
394
|
+
"""Generate a call graph for a function."""
|
|
395
|
+
if not self._connected:
|
|
396
|
+
raise ClientError("Not connected")
|
|
397
|
+
|
|
398
|
+
result = await self._session.call_tool(
|
|
399
|
+
"gen_callgraph",
|
|
400
|
+
{
|
|
401
|
+
"binary_name": binary_name,
|
|
402
|
+
"function_name": function_name,
|
|
403
|
+
"direction": direction,
|
|
404
|
+
"display_type": display_type,
|
|
405
|
+
"condense_threshold": condense_threshold,
|
|
406
|
+
"top_layers": top_layers,
|
|
407
|
+
"bottom_layers": bottom_layers,
|
|
408
|
+
"max_run_time": max_run_time,
|
|
409
|
+
},
|
|
410
|
+
)
|
|
411
|
+
return self._extract_result(result)
|
|
412
|
+
|
|
413
|
+
async def import_binary(self, binary_path: str) -> dict[str, Any]:
|
|
414
|
+
"""Import a binary into the project."""
|
|
415
|
+
if not self._connected:
|
|
416
|
+
raise ClientError("Not connected")
|
|
417
|
+
|
|
418
|
+
result = await self._session.call_tool(
|
|
419
|
+
"import_binary",
|
|
420
|
+
{"binary_path": binary_path},
|
|
421
|
+
)
|
|
422
|
+
return self._extract_result(result)
|
|
423
|
+
|
|
424
|
+
async def save(self) -> dict[str, Any]:
|
|
425
|
+
"""Save all programs."""
|
|
426
|
+
if not self._connected:
|
|
427
|
+
raise ClientError("Not connected")
|
|
428
|
+
|
|
429
|
+
result = await self._session.call_tool(
|
|
430
|
+
"save",
|
|
431
|
+
)
|
|
432
|
+
return self._extract_result(result)
|
|
433
|
+
|
|
434
|
+
async def delete_binary(self, binary_name: str) -> dict[str, Any]:
|
|
435
|
+
"""Delete a binary from the project."""
|
|
436
|
+
if not self._connected:
|
|
437
|
+
raise ClientError("Not connected")
|
|
438
|
+
|
|
439
|
+
result = await self._session.call_tool(
|
|
440
|
+
"delete_project_binary",
|
|
441
|
+
{"binary_name": binary_name},
|
|
442
|
+
)
|
|
443
|
+
return self._extract_result(result)
|
|
444
|
+
|
|
445
|
+
async def list_forks(self) -> dict[str, Any]:
|
|
446
|
+
"""List forks of the shared project and this session's role."""
|
|
447
|
+
if not self._connected:
|
|
448
|
+
raise ClientError("Not connected")
|
|
449
|
+
|
|
450
|
+
result = await self._session.call_tool("list_forks", {})
|
|
451
|
+
return self._extract_result(result)
|
|
452
|
+
|
|
453
|
+
async def merge_fork(
|
|
454
|
+
self, fork_id: str, dry_run: bool = False, on_conflict: str = "skip", auto_discard: bool = False
|
|
455
|
+
) -> dict[str, Any]:
|
|
456
|
+
"""Replay a fork's journaled edits into the primary project."""
|
|
457
|
+
if not self._connected:
|
|
458
|
+
raise ClientError("Not connected")
|
|
459
|
+
|
|
460
|
+
result = await self._session.call_tool(
|
|
461
|
+
"merge_fork",
|
|
462
|
+
{"fork_id": fork_id, "dry_run": dry_run, "on_conflict": on_conflict, "auto_discard": auto_discard},
|
|
463
|
+
)
|
|
464
|
+
return self._extract_result(result)
|
|
465
|
+
|
|
466
|
+
async def prune_forks(
|
|
467
|
+
self, dry_run: bool = False, max_age_hours: float = 168.0, include_unmerged: bool = False
|
|
468
|
+
) -> dict[str, Any]:
|
|
469
|
+
"""Prune dead and merged forks to reclaim disk storage."""
|
|
470
|
+
if not self._connected:
|
|
471
|
+
raise ClientError("Not connected")
|
|
472
|
+
|
|
473
|
+
result = await self._session.call_tool(
|
|
474
|
+
"prune_forks",
|
|
475
|
+
{"dry_run": dry_run, "max_age_hours": max_age_hours, "include_unmerged": include_unmerged},
|
|
476
|
+
)
|
|
477
|
+
return self._extract_result(result)
|
|
478
|
+
|
|
479
|
+
async def discard_fork(self, fork_id: str) -> dict[str, Any]:
|
|
480
|
+
"""Delete a stopped fork and its journal."""
|
|
481
|
+
if not self._connected:
|
|
482
|
+
raise ClientError("Not connected")
|
|
483
|
+
|
|
484
|
+
result = await self._session.call_tool("discard_fork", {"fork_id": fork_id})
|
|
485
|
+
return self._extract_result(result)
|
|
486
|
+
|
|
487
|
+
async def list_project_binary_metadata(self, binary_name: str) -> dict[str, Any]:
|
|
488
|
+
"""Get metadata for a binary."""
|
|
489
|
+
if not self._connected:
|
|
490
|
+
raise ClientError("Not connected")
|
|
491
|
+
|
|
492
|
+
result = await self._session.call_tool(
|
|
493
|
+
"list_project_binary_metadata",
|
|
494
|
+
{"binary_name": binary_name},
|
|
495
|
+
)
|
|
496
|
+
return self._extract_result(result)
|
|
497
|
+
|
|
498
|
+
async def rename_function(
|
|
499
|
+
self, binary_name: str, function_name_or_address: str, new_name: str
|
|
500
|
+
) -> dict[str, Any]:
|
|
501
|
+
"""Rename a function."""
|
|
502
|
+
if not self._connected:
|
|
503
|
+
raise ClientError("Not connected")
|
|
504
|
+
|
|
505
|
+
result = await self._session.call_tool(
|
|
506
|
+
"rename_function",
|
|
507
|
+
{
|
|
508
|
+
"binary_name": binary_name,
|
|
509
|
+
"name_or_address": function_name_or_address,
|
|
510
|
+
"new_name": new_name,
|
|
511
|
+
},
|
|
512
|
+
)
|
|
513
|
+
return self._extract_result(result)
|
|
514
|
+
|
|
515
|
+
async def rename_variable(
|
|
516
|
+
self,
|
|
517
|
+
binary_name: str,
|
|
518
|
+
function_name_or_address: str,
|
|
519
|
+
variable_name: str,
|
|
520
|
+
new_name: str,
|
|
521
|
+
) -> dict[str, Any]:
|
|
522
|
+
"""Rename a parameter or local variable."""
|
|
523
|
+
if not self._connected:
|
|
524
|
+
raise ClientError("Not connected")
|
|
525
|
+
|
|
526
|
+
result = await self._session.call_tool(
|
|
527
|
+
"rename_variable",
|
|
528
|
+
{
|
|
529
|
+
"binary_name": binary_name,
|
|
530
|
+
"function_name_or_address": function_name_or_address,
|
|
531
|
+
"variable_name": variable_name,
|
|
532
|
+
"new_name": new_name,
|
|
533
|
+
},
|
|
534
|
+
)
|
|
535
|
+
return self._extract_result(result)
|
|
536
|
+
|
|
537
|
+
async def set_variable_type(
|
|
538
|
+
self,
|
|
539
|
+
binary_name: str,
|
|
540
|
+
function_name_or_address: str,
|
|
541
|
+
variable_name: str,
|
|
542
|
+
type_name: str,
|
|
543
|
+
) -> dict[str, Any]:
|
|
544
|
+
"""Set a parameter or local variable type."""
|
|
545
|
+
if not self._connected:
|
|
546
|
+
raise ClientError("Not connected")
|
|
547
|
+
|
|
548
|
+
result = await self._session.call_tool(
|
|
549
|
+
"set_variable_type",
|
|
550
|
+
{
|
|
551
|
+
"binary_name": binary_name,
|
|
552
|
+
"function_name_or_address": function_name_or_address,
|
|
553
|
+
"variable_name": variable_name,
|
|
554
|
+
"type_name": type_name,
|
|
555
|
+
},
|
|
556
|
+
)
|
|
557
|
+
return self._extract_result(result)
|
|
558
|
+
|
|
559
|
+
async def set_function_prototype(
|
|
560
|
+
self, binary_name: str, function_name_or_address: str, prototype: str
|
|
561
|
+
) -> dict[str, Any]:
|
|
562
|
+
"""Set a function prototype."""
|
|
563
|
+
if not self._connected:
|
|
564
|
+
raise ClientError("Not connected")
|
|
565
|
+
|
|
566
|
+
result = await self._session.call_tool(
|
|
567
|
+
"set_function_prototype",
|
|
568
|
+
{
|
|
569
|
+
"binary_name": binary_name,
|
|
570
|
+
"function_name_or_address": function_name_or_address,
|
|
571
|
+
"prototype": prototype,
|
|
572
|
+
},
|
|
573
|
+
)
|
|
574
|
+
return self._extract_result(result)
|
|
575
|
+
|
|
576
|
+
async def set_comment(
|
|
577
|
+
self,
|
|
578
|
+
binary_name: str,
|
|
579
|
+
target: str,
|
|
580
|
+
comment: str,
|
|
581
|
+
comment_type: str = "decompiler",
|
|
582
|
+
) -> dict[str, Any]:
|
|
583
|
+
"""Set a decompiler or listing comment."""
|
|
584
|
+
if not self._connected:
|
|
585
|
+
raise ClientError("Not connected")
|
|
586
|
+
|
|
587
|
+
result = await self._session.call_tool(
|
|
588
|
+
"set_comment",
|
|
589
|
+
{
|
|
590
|
+
"binary_name": binary_name,
|
|
591
|
+
"target": target,
|
|
592
|
+
"comment": comment,
|
|
593
|
+
"comment_type": comment_type,
|
|
594
|
+
},
|
|
595
|
+
)
|
|
596
|
+
return self._extract_result(result)
|
|
File without changes
|