MCP driver — drive MedusaNexus from your AI assistant
mnexus mcp-serve exposes the running Nexus instance as a
Model Context Protocol server so
Claude Desktop, Cursor, Zed, and any other MCP-aware client can read
projects, walk findings, read the decompiled source itself, and fire
Firebase probes through tool calls — without you copy-pasting curl
lines into the chat.
The wire format is plain JSON-RPC 2.0 over stdio. There’s no mcp
Python dep — the protocol is small and we’d rather pin nothing than
inherit somebody else’s release cadence.
TL;DR
Two terminals:
# T1 — the API the MCP driver talks to.
mnexus serve
# T2 — confirm the server speaks MCP. The initialize handshake should
# echo back the protocol revision.
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | mnexus mcp-serveThen wire it into your assistant (see “Claude Desktop” below) and ask:
“List every CRITICAL finding on PRJ-355151DF and propose a remediation order.”
The assistant calls list_findings → get_finding → reads the
remediation field on each → drafts the plan against your real data.
Exposed tools
Read tools — inspect what’s already in the workspace
| tool | purpose | underlying route |
|---|---|---|
list_projects | Every Project in the workspace with risk + counts. | GET /v1/projects |
get_project | One project’s overview (risk, severity counts, surface). | GET /v1/projects/{id} |
list_findings | Findings on a project, optionally filtered by severity/category. | GET /v1/projects/{id}/findings |
get_finding | Full finding body — evidence + remediation. | GET /v1/findings/{fid} |
list_recipes | Built-in + Medusa/Stheno recipe catalogue. | GET /v1/recipes |
decode_android_flag | Mango flag decoder (Intent / Receiver / PendingIntent / Content). | POST /v1/mango/decode-flags |
manifest_diff | Surface delta against the most recent prior scan. | GET /v1/projects/{id}/manifest-diff |
findings_diff | Security delta against the most recent prior scan. | GET /v1/projects/{id}/findings-diff |
firebase_probe | Standalone RTDB / Firestore / Storage probe. | POST /v1/firebase/probe |
doctor | Engine health check — what’s installed, what’s missing. | GET /v1/doctor |
Write tools — let the assistant drive a full inspection
| tool | purpose | underlying route |
|---|---|---|
scan_apk | Upload an APK + run the full static pipeline (apktool + jadx + ghidra + mobsf + deeplink_audit + webview_audit + chain_correlator). Returns the new project_id. Blocking 30–60s. | POST /v1/apks/upload (multipart) |
run_pipeline | Execute a named pipeline against an existing project (stacks engines). | POST /v1/pipelines/{name}/run |
analyze_native_lib | Per-.so analyser — JNI exports + hardcoded URLs + crypto routines. | GET /v1/projects/{id}/native/analyze?lib=… |
plan_attack | Build the offline attack plan — PoCs + PROVABLE verdicts. Nothing fired. | POST /v1/projects/{id}/attack/plan |
execute_attack | Fire the adb PoC subset (dry-run unless execute=true) → CONFIRMED/DISPROVEN. | POST /v1/projects/{id}/attack/execute |
get_attack_plan | Read the stored attack plan + verdicts. | GET /v1/projects/{id}/attack |
Code-navigation tools — read the decompiled source, not just the report
The default scan walks DEX byte-strings; it writes no source tree. These tools materialise the real jadx/apktool output on disk and then let the assistant read its way through the code — the jadx-mcp-server workflow (zinja-coder), minus the running jadx GUI, because Nexus already owns the decompiled workspace.
| tool | purpose | underlying route |
|---|---|---|
decompile_project | Materialise the jadx (Java/Kotlin) or apktool (smali) tree on disk. Caches; heavy on the jadx side. Run once before reading code. | POST /v1/projects/{id}/decompile?engine= |
get_class_source | One class body by fully-qualified name; inner classes fold into their outer file. fmt=java/smali. | GET /v1/projects/{id}/source?fqcn=&fmt= |
search_classes | List / filter decompiled classes by fqcn substring. | GET /v1/projects/{id}/classes?q=&fmt= |
search_source | Grep the whole decompiled workspace for a string/regex → file:line hits with attribution. | GET /v1/projects/{id}/find?q= |
get_manifest | Decoded AndroidManifest.xml (or iOS Info.plist), fmt=xml/json. | GET /v1/projects/{id}/manifest?fmt= |
get_class_source / search_classes return 409 until
decompile_project has run for that engine — an honest “no source on
disk yet”, never a fabricated body. When jadx isn’t on PATH,
decompile_project returns 503 rather than a fake tree.
Mitigation is first-class: get_finding always returns the
remediation block, so any assistant-generated plan starts from
ground truth rather than fabricated advice.
The agentic inspection loop
The write tools unlock this end-to-end conversation:
You ▸ "Scan ~/Downloads/target.apk and explain any 1-click chains
it has, with code paths and fix per link."
Claude ▸ calls scan_apk(apk_path="~/Downloads/target.apk") → PRJ-A5B7C291
▸ calls run_pipeline(name="full-static-android", (waits)
project_id="PRJ-A5B7C291")
▸ calls list_findings(project_id="PRJ-A5B7C291",
severity="critical")
▸ for each chain finding:
get_finding(finding_id=…) → reads evidence + remediation
▸ calls decompile_project(project_id="PRJ-A5B7C291", engine="jadx")
▸ calls get_class_source(fqcn=…) on each link's class → reads the
real code path instead of guessing
▸ synthesises a written report citing each link's code path.This is the chain-detection workflow driven entirely by the assistant — you don’t touch a CLI.
Control plane — enable/disable + tool allowlist
The MCP driver is a stdio process the agent spawns, so Nexus can’t stop a
client from launching it — but it can decide what that process is allowed
to expose. The MCP panel (#/mcp in the web UI, or /mcp in the REPL) is
that control plane:
- Master switch — off = the driver exposes nothing and refuses every call.
- Per-tool allowlist — toggle any tool; grouped read / nav / write.
ALLOW ALLrestores the open default,BLOCK ALLlocks everything. - Live status — a connection dot that lights when a driver has pinged in
the last 30s, plus which client (
clientInfo.name) is talking. - Setup snippets — paste-ready config for Claude Desktop / Cursor / Zed with the API base pre-filled, one click to copy.
Enforcement lives in the driver: tools/list returns only permitted tools, and
tools/call refuses a disabled tool even if the client cached it —
-32601 tool '<name>' is disabled by MedusaNexus MCP policy. State persists to
<workspace>/mcp_config.json; an absent or corrupt file fails open (every
tool enabled), so the control plane can never silently brick a working
assistant.
From the REPL:
/mcp # status + connection dot + allowed count
/mcp tools # full catalogue with enabled state
/mcp disable # master switch off
/mcp block scan_apk run_pipeline # gate the write tools
/mcp allow all # back to open
/mcp setup cursor # print the client configEndpoints behind it: GET/PUT /v1/mcp/config, POST /v1/mcp/heartbeat,
GET /v1/mcp/setup/{agent}.
Claude Desktop wiring
Open ~/Library/Application Support/Claude/claude_desktop_config.json
and add the entry under mcpServers:
{
"mcpServers": {
"medusa-nexus": {
"command": "mnexus",
"args": ["mcp-serve"],
"env": {
"MNEXUS_API_BASE": "http://127.0.0.1:8765"
}
}
}
}Restart Claude Desktop. The hammer icon next to the prompt should now
list the medusa-nexus tools. The MNEXUS_API_BASE env var lets you
point the driver at a remote Nexus — anywhere urllib can reach.
Cursor / Zed
Both ship MCP support in the same shape; copy the JSON above into the
editor’s MCP config (~/.cursor/mcp.json, ~/.config/zed/mcp.json,
etc.) and reload.
Wire shape
The dispatcher handles four MCP methods:
| method | purpose |
|---|---|
initialize | Returns {protocolVersion, capabilities: {tools:{}}, serverInfo}. |
notifications/initialized | Notification (no id) → no response. |
tools/list | Lists every tool descriptor in TOOLS. |
tools/call | Invokes a handler, wraps the result in {content: [{type:"text", text:<json>}]}. |
Errors map to JSON-RPC codes:
| code | when |
|---|---|
-32601 | Unknown method. |
-32602 | Unknown tool name or missing required argument. |
-32603 | Handler raised — surfaced as RuntimeError: … so the assistant sees the failure instead of the session dying. |
-32700 | stdin line wasn’t valid JSON. |
Smoke test from the shell
# Initialize → tools/list → call list_projects → done.
( \
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' ; \
echo '{"jsonrpc":"2.0","method":"notifications/initialized"}' ; \
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' ; \
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_projects","arguments":{}}}' \
) | mnexus mcp-serve | jq .Each line on stdin → one JSON response on stdout (except for the
notifications/initialized notification, which is silent by spec).
Why no mcp Python package?
Three reasons:
- Lean venv. The whole driver is ~390 lines and depends only on
stdlib
urllib. Adding a dep for a 9-method protocol felt like buying a forklift to move a kettle. - No version pin treadmill. MCP is young; the SDK is younger. If the spec moves we patch one file.
- Talking to remote Nexus is identical. The driver hits the
FastAPI server over HTTP, so it works against
127.0.0.1orhttps://nexus.internal:9000with one env var.
Limits
- Write tools cover inspection (scan_apk, run_pipeline, analyze_native_lib)
but NOT mutation of live state. Specifically not exposed:
start_dynamic_session(Frida attach),patch_apk/patch_ipa(byte-patch + re-sign),decrypt_ios(bagbak / frida-ios-dump). Those touch live devices and re-sign artifacts; we want an explicit human-in-the-loop confirmation flow before letting an assistant trigger them. Use the REPL or HTTP API directly for those. tools/calltext payloads are truncated at 60 000 chars to fit comfortably in the assistant’s context. Large project dumps still fit — the truncation only kicks in on pathological cases.- The server is stdio-only. No SSE transport, no HTTP transport. Two reasons: every MCP client we care about supports stdio, and stdio has no auth model to design.
- The allowlist is the closest thing to auth: since stdio has no
credentials, gating which tools a driver may call is how you keep a
curious assistant away from the write set. Toggle it in the MCP panel
(
#/mcp) or with/mcp block ….