Forward ghidra()/ghidra_tools() to the shared read-only Ghidra MCP server
Every agent reaches GhidrAssistMCP through Harbor's own MCP server, so no per-host configuration is needed. Configured by ghidra_url in [reverse]; long decompiles are polled; also 'harbor re ghidra <tool> <json>'.
This commit is contained in:
1 parent
3f6df88f95
commit
7ed914ebdb
4 files changed
+186
-6
No files matched your search
@@ -57,3 +57,63 @@ class ReverseTools(unittest.TestCase):
|
||||
self.r.bytes_at("0x500000")
|
||||
with self.assertRaises(HarborError):
|
||||
Reverse({})
|
||||
|
||||
|
||||
class GhidraForwarder(unittest.TestCase):
|
||||
"""The ghidra() tool against a fake streamable-HTTP MCP server: a session, a polled async task, a refusal."""
|
||||
|
||||
def setUp(self):
|
||||
import http.server
|
||||
import threading
|
||||
calls = self.calls = []
|
||||
|
||||
class Fake(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
def do_POST(self):
|
||||
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
||||
calls.append((body["method"], body.get("params", {}).get("name"), self.headers.get("Mcp-Session-Id")))
|
||||
if "id" not in body:
|
||||
self.send_response(202)
|
||||
self.end_headers()
|
||||
return
|
||||
name = body.get("params", {}).get("name")
|
||||
if body["method"] == "initialize":
|
||||
result = {"protocolVersion": "2025-06-18"}
|
||||
elif body["method"] == "tools/list":
|
||||
result = {"tools": [{"name": "get_code", "description": "Code",
|
||||
"inputSchema": {"properties": {"function": {}}, "required": ["function"]}}]}
|
||||
elif name == "get_code":
|
||||
result = {"content": [{"type": "text", "text": "Task submitted for async execution.\n\n"
|
||||
"Task ID: 11111111-2222-3333-4444-555555555555\nStatus: RUNNING"}]}
|
||||
elif name == "get_task_status":
|
||||
result = {"content": [{"type": "text", "text": "[Context] Operating on: image.bin\n\nint F(void)"}]}
|
||||
else:
|
||||
result = {"isError": True, "content": [{"type": "text", "text": "Refused: read-only"}]}
|
||||
data = ("data: " + json.dumps({"jsonrpc": "2.0", "id": body["id"], "result": result}) + "\n\n").encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.send_header("Mcp-Session-Id", "s1")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
self.server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Fake)
|
||||
threading.Thread(target=self.server.serve_forever, daemon=True).start()
|
||||
from harbor.reverse import Ghidra
|
||||
self.g = Ghidra({"ghidra_url": f"http://127.0.0.1:{self.server.server_port}/mcp"})
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
|
||||
def test_tools_call_and_refusal(self):
|
||||
self.assertEqual(self.g.tools()[0]["required"], ["function"])
|
||||
self.assertEqual(self.g.call("get_code", {"function": "F"}), "int F(void)")
|
||||
self.assertIn(("tools/call", "get_task_status", "s1"), self.calls)
|
||||
with self.assertRaises(HarborError):
|
||||
self.g.call("rename_symbol", {})
|
||||
with self.assertRaises(HarborError):
|
||||
from harbor.reverse import Ghidra
|
||||
Ghidra({"ghidra_url": "http://127.0.0.1:9/mcp"}).tools()
|
||||
Reference in new issue
Block a user