Skip to Content
Alpha — full Android pipeline + iOS toolkit + live dynamic loop. API still shifting; pin to commits in CI.
IntegrationsMCP driver

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-serve

Then 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_findingsget_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

toolpurposeunderlying route
list_projectsEvery Project in the workspace with risk + counts.GET /v1/projects
get_projectOne project’s overview (risk, severity counts, surface).GET /v1/projects/{id}
list_findingsFindings on a project, optionally filtered by severity/category.GET /v1/projects/{id}/findings
get_findingFull finding body — evidence + remediation.GET /v1/findings/{fid}
list_recipesBuilt-in + Medusa/Stheno recipe catalogue.GET /v1/recipes
decode_android_flagMango flag decoder (Intent / Receiver / PendingIntent / Content).POST /v1/mango/decode-flags
manifest_diffSurface delta against the most recent prior scan.GET /v1/projects/{id}/manifest-diff
findings_diffSecurity delta against the most recent prior scan.GET /v1/projects/{id}/findings-diff
firebase_probeStandalone RTDB / Firestore / Storage probe.POST /v1/firebase/probe
doctorEngine health check — what’s installed, what’s missing.GET /v1/doctor

Write tools — let the assistant drive a full inspection

toolpurposeunderlying route
scan_apkUpload 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_pipelineExecute a named pipeline against an existing project (stacks engines).POST /v1/pipelines/{name}/run
analyze_native_libPer-.so analyser — JNI exports + hardcoded URLs + crypto routines.GET /v1/projects/{id}/native/analyze?lib=…
plan_attackBuild the offline attack plan — PoCs + PROVABLE verdicts. Nothing fired.POST /v1/projects/{id}/attack/plan
execute_attackFire the adb PoC subset (dry-run unless execute=true) → CONFIRMED/DISPROVEN.POST /v1/projects/{id}/attack/execute
get_attack_planRead 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.

toolpurposeunderlying route
decompile_projectMaterialise 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_sourceOne class body by fully-qualified name; inner classes fold into their outer file. fmt=java/smali.GET /v1/projects/{id}/source?fqcn=&fmt=
search_classesList / filter decompiled classes by fqcn substring.GET /v1/projects/{id}/classes?q=&fmt=
search_sourceGrep the whole decompiled workspace for a string/regex → file:line hits with attribution.GET /v1/projects/{id}/find?q=
get_manifestDecoded 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 ALL restores the open default, BLOCK ALL locks 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 config

Endpoints 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:

methodpurpose
initializeReturns {protocolVersion, capabilities: {tools:{}}, serverInfo}.
notifications/initializedNotification (no id) → no response.
tools/listLists every tool descriptor in TOOLS.
tools/callInvokes a handler, wraps the result in {content: [{type:"text", text:<json>}]}.

Errors map to JSON-RPC codes:

codewhen
-32601Unknown method.
-32602Unknown tool name or missing required argument.
-32603Handler raised — surfaced as RuntimeError: … so the assistant sees the failure instead of the session dying.
-32700stdin 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:

  1. 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.
  2. No version pin treadmill. MCP is young; the SDK is younger. If the spec moves we patch one file.
  3. Talking to remote Nexus is identical. The driver hits the FastAPI server over HTTP, so it works against 127.0.0.1 or https://nexus.internal:9000 with 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/call text 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 ….