Skip to main content
Glama
frida-mobile-mcp

frida-mobile-mcp

by monkeywave
README.md
# frida-mobile-mcp

[![npm](https://img.shields.io/npm/v/frida-mobile-mcp.svg)](https://www.npmjs.com/package/frida-mobile-mcp)
[![skills.sh](https://skills.sh/b/monkeywave/frida-mobile-mcp)](https://skills.sh/monkeywave/frida-mobile-mcp/mobile-dynamic-analysis)
[![CI](https://github.com/monkeywave/frida-mobile-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/monkeywave/frida-mobile-mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Node >=22.12](https://img.shields.io/badge/node-%3E%3D22.12-brightgreen.svg)](package.json)

An MCP server that gives AI agents dynamic instrumentation of Android and iOS apps through Frida 17, and runs next to REA, ReVa (Ghidra) and IDA MCP servers. It exposes 71 tools: high-level ones that manage devices, sessions and scripts for you (explore an app, hook a Java/ObjC/native method, trace calls, map reachability coverage and autonomously drive the UI toward it, find secrets in memory and turn them into reusable friTap patterns, cast a wide net over crypto, bypass SSL pinning, capture TLS keys and plaintext with friTap, run prebuilt analysis scripts, validate custom scripts) and low-level primitives (spawn/attach, module and symbol enumeration, address resolution, control-flow analysis, memory dumps). Addresses use the same `module+0xoffset` notation as Ghidra/IDA so results move between static and dynamic tools without conversion, and every result carries an evidence envelope (`evidence_id`, `limitations`, `coverage`). Optional UI automation goes through [mobile-mcp](https://github.com/mobile-next/mobile-mcp).

## Quick start

```bash
npx frida-mobile-mcp                      # stdio MCP server (default `serve` command)
frida-mobile-mcp doctor                   # check Node, frida-node, adb, devices, frida-server, bridge bundles
frida-mobile-mcp setup --client cursor    # write the server into a client config (claude-code, claude-desktop, cursor, vscode, windsurf, codex)
frida-mobile-mcp gadget --platform android --print   # generate a frida-gadget config.json + APK/IPA repackaging guidance
```

The `gadget` command generates a [frida-gadget](https://frida.re/docs/gadget/) `config.json` (`--interaction listen` or `--interaction script`) and prints the APK/IPA repackaging steps. Embedding the gadget is the only way to instrument native detection that runs in `.init_array` / `JNI_OnLoad` **before** an attach-time script can load, and it removes the frida-server `:27042` port as a fingerprint — see `frida_help({ topic: "evasion" })`.

Claude Code:

```bash
claude mcp add frida -- npx -y frida-mobile-mcp
```

Claude Desktop (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "frida": {
      "command": "npx",
      "args": ["-y", "frida-mobile-mcp"],
      "env": { "FRIDA_DEVICE_ID": "emulator-5554" }
    }
  }
}
```

Per-client instructions, HTTP mode and remote frida-server setup are in [docs/clients.md](docs/clients.md). A Claude Desktop extension manifest (`manifest.json`, MCPB) and an MCP registry entry (`server.json`) ship in the repo.

## Requirements

- Node.js >= 22.12 (CI runs 22 and 24 on Linux, macOS and Windows). `npm install` runs the `frida` package's install script, which downloads the native frida-node prebuild.
- A device or emulator running **frida-server 17.x**. This package depends on frida-node `^17.26.2`; frida-server on the device should be 17.26.x (same major is required, same minor is recommended; `realm: 'emulated'` needs >= 17.26). `frida-mobile-mcp doctor` prints both versions and flags mismatches.
- Android: download the matching `frida-server-17.x.y-android-<arch>.xz` from <https://github.com/frida/frida/releases>, then

  ```bash
  xz -d frida-server-17.26.2-android-arm64.xz
  adb push frida-server-17.26.2-android-arm64 /data/local/tmp/frida-server
  adb shell "chmod 755 /data/local/tmp/frida-server"
  adb shell "su -c '/data/local/tmp/frida-server &'"
  ```

- iOS: a jailbroken device with the matching Frida package from the Frida Cydia/Sileo repository. Non-jailbroken devices need the app repackaged with the Frida Gadget; the server then attaches to the gadget-enabled process like any other.
- Optional: `pip install friTap` for `capture_tls` (otherwise the pinned agent is downloaded on first use), and network access for `npx -y @mobilenext/mobile-mcp@latest` on first `mobile_action`.

## What is new for Frida 17

- **Bridges are bundled.** Frida 17 no longer ships the Java/ObjC bridges in the GumJS runtime. `npm run build:bridges` bundles `frida-java-bridge` and `frida-objc-bridge` into `dist/bridges/{java,objc}.js`; the server injects the right one by platform (Java on Android, ObjC on iOS/macOS) ahead of every script.
- **Legacy scripts still work.** `execute_script` runs classic (non-module) scripts through `Script.evaluate`, so a `Java.perform(...)` script written for Frida 16 runs unchanged and keeps its own line numbers in errors. ES modules (`import Java from "frida-java-bridge"`, JS or TS) are compiled on the host with the Frida Compiler; frida-compile bundles (starting with the `📦` marker) are loaded as-is.
- **Removed APIs are linted.** `validate_script` flags the static `Module.findExportByName`/`findBaseAddress`/`enumerateExports`, `Memory.readX`/`writeX` and `*Sync` enumerators that throw on Frida 17 and suggests the Module-instance / `NativePointer` replacements. Generated scripts get `findExport(lib, name)` and `hookNative(lib, name, callbacks)` helpers from the agent preamble, and template options as `__FRIDA_MCP__.options`.
- `analyze_native_function` uses `Process.findFunctionRange` and the Frida 17.12+ `ControlFlowGraph` (with a linear-disassembly fallback reported in `limitations`).

## Tool reference

### Tier 1: high-level tools

| Tool | Description |
|------|-------------|
| `get_status` | Connected devices, active sessions (platform, script count, last activity), hooks, traces and recent detaches/crashes. Run first and after a session is lost. |
| `frida_help` | Topic help: overview, hooking, tracing, memory, scripts, native, tls, mobile, examples, advanced, evasion, script_catalog, script_api, recipes, interop. |
| `detect_app_technologies` | Fingerprint libraries, frameworks and security mechanisms; returns recommended script targets. |
| `explore_app` | Spawn an app suspended, instrument, resume; returns pid, platform, modules and a filtered class list. |
| `get_app_info` | Identify the installed artifact: version, SDK levels, debuggable, installer and every APK (base + splits) with on-device sha256/size; `pull_to` pulls and hash-verifies them for a static tool such as REA `inspect_android_package`. iOS: Frida app parameters only. |
| `search_classes_and_methods` | Regex search over classes (and methods with `include_methods`) in a running app. |
| `describe_class` | Full shape of one loaded class: every method overload with parameter/return types and a hook-ready `signature`, constructors, fields, superclass/interfaces (ObjC: selectors with type encodings). Feed a signature to `hook_method` `evidence: {class, signature}`. |
| `discover_crypto` | Find where an app does crypto (or any API family) without assuming `javax.crypto`: ranked candidate hook targets across Java/ObjC classes and native symbols. |
| `find_secrets_in_memory` | Content-aware memory scan for the *structure* of secrets: RC4 S-box (`byte[]` or Java `int[256]`), AES key schedule (aeskeyfind — encryption **and** decryption, mode-independent), ChaCha20/Salsa20 keys (sigma constant), high-entropy 16/24/32-byte keys, PEM/DER private keys, JWTs. Finds keys without reaching the code that uses them. |
| `generate_memory_pattern` | Turn a located secret into a reusable **anchor pattern** (stable byte signature near the key + offset): re-locate it with `scan_memory`, or hand it to friTap via `capture_tls` (`patterns`/`offsets`). |
| `monitor_crypto` | Crypto-oracle wide net: discover and hook **all** likely crypto sinks at once (app-owned classes + `javax.crypto`/`Mac`/`MessageDigest`, iOS CommonCrypto, native OpenSSL/BoringSSL) and dump every key, input and output as hex. |
| `trace_coverage` | Reachability map: arm cheap persistent entry-hooks over a class/package/module scope and track **covered vs cold** targets across `trigger_and_wait` drives, so you know what code you have not yet exercised. |
| `drive_coverage` | Autonomously drive an instrumented session's UI toward uncovered code — taps on-screen elements breadth-first and stops the instant a crypto/secret signal fires (or coverage stalls / `max_steps`). Pair with `monitor_crypto` / `trace_coverage`. |
| `watch_loads` | Watch for late-loaded `.so` libraries and Java classes, plus anti-Frida probes; emits `watch_event` messages as the app runs. |
| `trigger_and_wait` | Perform a UI action and block until the resulting hook/message output arrives, so you learn which hook a tap fired (or that none did). |
| `hook_method` | Hook Java (`com.example.Class.method`), ObjC (`-[Class sel:]`) or native targets (`libssl.so!SSL_read`, `libfoo.so+0x1234`, `0x7f...`, `!open`) with auto session management; backtraces as `module+0xoff (symbol)`. |
| `trace_method` | frida-trace style tracing of glob patterns (`com.example.net.*`, `-[NSURL*]`, `libssl.so!SSL_*`) for a duration. |
| `execute_script` | Run a custom script: classic JS (bridges injected), ES module JS/TS (compiled with the Frida Compiler) or a frida-compile bundle. `timeout_ms` bounds the initial run. Requires `allowCustomScripts`. |
| `validate_script` | Syntax, type and Frida 17 removed-API check without a device. Use before `execute_script`. |
| `run_prebuilt_script` | Run one of the 9 prebuilt scripts below; call without `script_name` to list them. |
| `bypass_ssl_pinning` | One-click pinning bypass (Android: TrustManager, OkHttp CertificatePinner, Conscrypt; iOS: SecTrust, ATS, BoringSSL). |
| `capture_tls` | friTap: TLS session keys (NSS key log) and optional decrypted plaintext / synthetic pcap for OpenSSL/BoringSSL, Conscrypt, NSS, GnuTLS, wolfSSL, mbedTLS, Secure Transport and more. Accepts friTap `patterns`/`offsets` (e.g. from `generate_memory_pattern` or BoringSecretHunter) for stripped libraries. Options: `protocols` (`tls`/`rc4`/`auto`/`all`), `pairip_safe`, `no_loader_hook`, `probe`, `attribute_sdk` (per-connection `fritap_caller_stack`), `default_fd`, `library_scan`, `experimental`; stats include `per_library`. friTap v2.6.5, agent ABI 2 and 3. Use the `tls_traffic_audit` prompt for a guided flow; `fritap mcp` can analyse the resulting key log/pcap (see docs/interop.md). |
| `read_memory` | Read up to 4 MB at any address notation; returns hex plus absolute address and `module+offset`. |
| `write_memory` | Write hex bytes to memory. Disabled unless `memoryWriteEnabled`. |
| `scan_memory` | Byte-pattern scan (`48 89 5c 24 ?? 57`), optionally limited to a module. |
| `get_messages` | Buffered hook invocations, trace hits, `send()` output and errors; `since` paginates; the cursor (`last_index`) points after the last returned message, with `truncated` and `remaining`. |
| `get_evidence` | Re-read a persisted tool result (params, status, structured result) by `evidence_id`. |
| `export_evidence_bundle` | Export persisted results as a JSON or Markdown bundle, inline or to a file. |
| `stop_instrumentation` | Remove all hooks, scripts and traces on a target and detach (newest script first; eternalized scripts are skipped). Returns `fritap_captures[]` with final friTap stats. |
| `unload_script` | Unload a single script (hook, trace or custom) from a session while keeping it attached. Fails with `UNLOAD_ORDER_UNSAFE` if newer Java-bridge scripts are loaded; `cascade: true` unloads them first (`cascaded_scripts`). |
| `mobile_action` | Gateway to mobile-mcp UI automation (screenshot, elements, tap, swipe, type, launch app, list apps). |
| `search_recipes` | Search the purpose-indexed library of ready-to-run classic-JS recipes (custom crypto-key capture, native/JNI hooks, classloader/intent/WebView, iOS Keychain); returns ranked recipes with source to adapt and run via `execute_script`. |
| `save_script` | Save a script under `~/.config/frida-mobile-mcp/scripts` for reuse. Requires `allowCustomScripts`. |
| `list_saved_scripts` | List scripts saved with `save_script`. |
| `run_saved_script` | Run a saved script by name against a target. Requires `allowCustomScripts`. |
| `export_script` | Export a script as a standalone classic `.js` that runs under the plain `frida -U -f <pkg> -l file.js` CLI (preamble + source; bridges provided by the frida-tools CLI, not bundled). |

### Tier 2: advanced primitives

| Tool | Description |
|------|-------------|
| `list_devices` | Local, USB and remote Frida devices (frida-ls-devices). |
| `select_device` | Select the active device by ID, type or remote host. |
| `get_device_info` | OS version, architecture and system parameters. |
| `list_processes` | Running processes (frida-ps). |
| `list_applications` | Installed and running applications. |
| `get_frontmost_application` | Foreground application. |
| `spawn_process` | Spawn suspended for early instrumentation; resume with `resume_process`. |
| `attach_process` | Attach to a running process by PID or name. |
| `resume_process` | Resume a suspended process. |
| `kill_process` | Kill a process. |
| `detach_session` | Detach a session and unload its scripts. |
| `list_sessions` | Sessions managed by this server. |
| `spawn_gating` | Enable / disable / list device-wide spawn gating — hold newly spawned (incl. child/isolated) processes suspended for early instrumentation; release with `resume_process`. |
| `child_gating` | Enable / disable child gating on a session so forked/exec'd child processes are held for instrumentation. |
| `eternalize_script` | Pin a loaded script in the target so it survives detach (fire-and-forget: no more messages/unload) — leave a bypass/monitor running while you free the session. |
| `enumerate_modules` | Loaded modules with name, base, end, size, path; regex `filter`. |
| `enumerate_exports` | Exports of a module, paginated. |
| `enumerate_imports` | Imports of a module with the providing module, paginated. |
| `enumerate_symbols` | Symbols including non-exported/debug symbols; regex filter and paging inside the agent (Linux, Android, iOS, macOS). |
| `resolve_address` | Translate `0x7f...` to `module+0xoffset` (+ nearest symbol) and `module+0xoff` / `module!sym` to an absolute address. |
| `analyze_native_function` | Function range, basic blocks and edges, first instructions; addresses as absolute and `module+0xoffset`. |
| `dump_module_memory` | Dump a module base-relative to a host file (under cwd, the temp dir or `~/.config/frida-mobile-mcp/dumps`) for raw import into Ghidra/IDA. |
| `list_threads` | Threads with id, name, state, current pc as `module+0xoffset`; optional 8-frame backtraces. |
| `hook_function` | Low-level `Interceptor` hook on a `session_id` with optional custom `on_enter`/`on_leave` bodies (custom bodies need `allowCustomScripts`). |
| `unhook_function` | Remove a hook by ID. |
| `frida_server_status` | Android (adb): report frida-server PIDs, device CPU ABI, matching frida release arch, and the GitHub `.xz` asset URL for the installed frida-node version. |
| `frida_server_install` | Android (adb): push a local (decompressed) frida-server binary to the device and `chmod 755` it. |
| `frida_server_start` | Android (adb): start frida-server daemonized (optionally via `su`) with a custom listen address/port and binary name — a non-default port/name evades `:27042`/`frida-server` scans. Enforces `allowedDevices`. |
| `frida_server_stop` | Android (adb): signal the matching frida-server process to stop (`pkill`, optionally via `su`). |
| `adb_shell` | Android (adb): run an arbitrary device shell command. **Disabled by default** — requires `allowAdbShell` (config) or `FRIDA_MCP_ALLOW_ADB_SHELL=1`. |
| `adb_logcat` | Android (adb): dump the last N logcat lines (one-shot), optional `tag:priority` filters, or clear the buffer. |
| `adb_pull` / `adb_push` | Android (adb): move files between host and device. |
| `adb_forward` | Android (adb): add / remove / list port forwards (reach a frida-server on a non-default device port). |
| `inject_library` | Inject a native library (.so/.dylib) from the host into a process via Frida's injector, independent of the JS engine — the parity path for a prebuilt gadget/agent or custom .so. **Disabled by default** — needs `allowNativeInjection` (config) or `FRIDA_MCP_ALLOW_NATIVE_INJECTION=1` (a capability separate from `allowCustomScripts`) and an architecture-matched library. |

### mobile_action gateway

`mobile_action({ action, params })` forwards to a lazily spawned mobile-mcp process (`npx -y @mobilenext/mobile-mcp@latest` by default, configurable via `mobileMcp`, disabled with `--no-mobile-mcp`). Call it without `action` to list the available actions. All Frida tools work without it; when it cannot start the error is `MOBILE_MCP_UNAVAILABLE`.

The server also exposes `frida://` resources (version, devices, processes, apps, sessions, messages, script catalog, persisted evidence via `frida://evidence` and `frida://evidence/{id}`) and five workflow prompts (`explore_app`, `security_audit`, `trace_functionality`, `write_frida_script`, `hunt_secrets`).

## Prebuilt scripts

| Script | Purpose |
|--------|---------|
| `ssl_pinning_bypass` | Bypass certificate pinning (TrustManager, OkHttp, Conscrypt, WebView; SecTrust, ATS, BoringSSL, AFNetworking) |
| `root_jailbreak_bypass` | Bypass root/jailbreak detection (Java, SystemProperties, native fopen/access/stat, canOpenURL, sysctl) |
| `class_enumeration` | List loaded Java/ObjC classes with a regex filter, optionally with methods |
| `method_hook` | Generic method hook (Java/ObjC methods or a native `target`) with argument and return value logging |
| `crypto_monitor` | javax.crypto, CommonCrypto and OpenSSL calls |
| `network_inspector` | Sockets, DNS, UDP and TLS SSL_read/SSL_write (plaintext bytes with `log_data`) |
| `keychain_prefs` | iOS Keychain and Android SharedPreferences access |
| `filesystem_monitor` | File I/O and SQLite |
| `anti_frida_evasion` | Observe (`mode=report`, default) or neutralize (`mode=spoof`, needs `memoryWriteEnabled`) the native primitives RASP uses to detect Frida: `/proc/self/{maps,status,task}` reads, `strstr` "frida" greps, thread-name / `dlopen` probes, and iOS dyld image scans |

## Address notation and evidence envelope

Every tool that takes or returns a native address uses one vocabulary:

| Form | Meaning | Example |
|------|---------|---------|
| `module!symbol` | export or ELF/Mach-O symbol | `libssl.so!SSL_write` |
| `module+0xOFFSET` | offset from the module base (Ghidra/IDA RVA); decimal `libfoo.so+4660` also accepted | `libfoo.so+0x1a2b0` |
| `0xADDRESS` | absolute runtime address | `0x7f3a1c2b1234` |
| `symbol` / `!symbol` | global export searched across all modules | `!open` |

Results prefer `module+0xoffset` so a static tool can decompile at image base + offset; absolute addresses appear only outside known modules and come with a `limitations` entry.

Every successful call returns `result`, `evidence_id` (UUID, also written to the audit log), `limitations[]`, `coverage` (`complete` | `unknown`), `session_context` and `suggested_next`. Errors carry `code`, `category`, `retryable`, `remediation` and `recovery_actions`. Every result, errors included, is also persisted to the evidence store and retrievable by its `evidence_id` via `get_evidence`, `export_evidence_bundle` and the `frida://evidence/{id}` resource (`frida://evidence` lists the latest 100). Field semantics and the error-code table are in [docs/interop.md](docs/interop.md); the agent workflow around them is in [skills/mobile-dynamic-analysis/SKILL.md](skills/mobile-dynamic-analysis/SKILL.md).

## Working with REA, ReVa and IDA

frida-mobile-mcp is built to run next to ReVa (Ghidra), ida-pro-mcp (IDA Pro) and REA: the static side answers what the code says, this server answers what it does at runtime. A static tool finds `libfoo.so+0x1234`; `hook_method` or `analyze_native_function` take that string as-is; backtraces and `resolve_address` give offsets back in the same form for decompilation. `enumerate_modules` returns `base`/`end` for rebasing a Ghidra/IDA project, `dump_module_memory` writes a base-relative image of an unpacked library for raw import, and `capture_tls` shows the bytes a statically found wrapper actually puts on the wire. A three-server client configuration and a full static -> dynamic -> static walkthrough are in [docs/interop.md](docs/interop.md).

[REA](https://github.com/morluto/rea) covers static Android analysis (`inspect_android_package`, `inspect_android_method`), ADB housekeeping and a generic Frida lane for desktop binaries; this server covers the mobile runtime (Java/ObjC bridges, prebuilt scripts, TLS keys, memory, UI driving). Register both in the same client, start with `get_app_info` so both servers analyse the same APK hash, and keep the combined catalog small with `FRIDA_MCP_TOOL_PROFILE=core` and `FRIDA_MCP_TOOLS_EXCLUDE=adb_*,frida_server_*`. Both servers return an evidence envelope with citable `evidence_id`s and an `export_evidence_bundle` tool, so one finding ledger can cite both. Routing rules, install lines, the field and error-code mapping and an APK -> static -> hook -> report walkthrough are in [docs/rea.md](docs/rea.md).

## HTTP transport

```bash
frida-mobile-mcp --transport http --port 3000                 # loopback (127.0.0.1), token optional
FRIDA_MCP_HTTP_TOKEN=<token> frida-mobile-mcp --transport http --host 0.0.0.0 --port 3000
```

Streamable HTTP endpoint at `/mcp` with one MCP session per client (`Mcp-Session-Id`); `DELETE /mcp` with that header ends the session. The server binds to loopback by default; a Bearer token (`--http-token` or `FRIDA_MCP_HTTP_TOKEN`) is mandatory when `--host` is not a loopback address and the server refuses to start without one. `Host` and `Origin` headers are validated against the bound host (DNS-rebinding protection). There is no TLS; use a reverse proxy or SSH tunnel across networks. `--port 0` picks a free port; the bound URL is printed as a JSON line on stderr. Full curl examples: [docs/clients.md](docs/clients.md#http-mode).

## Library usage

```ts
import { McpServer } from '@modelcontextprotocol/server';
import { createServer, registerFridaTools, createSessionServer } from 'frida-mobile-mcp';

const server = createServer({ allowCustomScripts: false });      // ready-made McpServer with every tool
// or add the tools to your own server:
const mine = new McpServer({ name: 'my-agent', version: '1.0.0' });
const deviceManager = registerFridaTools(mine, { resources: true, prompts: false });
// or one server per HTTP session:
const perSession = createSessionServer();
```

Also exported: `scriptRegistry`, `DeviceManager`, `composeAgentSource`/`createAgentScript`, `findRemovedApis`, `loadBridgeSource`, `startHttpServer`, `createStdioTransport`, `loadConfig`, `ensureState` and the types (`dist/lib.d.ts`).

## Configuration

Config file `~/.config/frida-mobile-mcp/config.json` (merged over defaults, then CLI flags, then environment):

```json
{
  "allowCustomScripts": false,
  "memoryWriteEnabled": false,
  "allowNativeInjection": false,
  "allowedDevices": [],
  "maxSessions": 4,
  "sessionTimeoutMinutes": 30,
  "maxMessageBuffer": 1000,
  "auditLogPath": "~/.config/frida-mobile-mcp/audit.jsonl",
  "evidence": { "enabled": true, "path": "~/.config/frida-mobile-mcp/evidence", "maxMb": 200, "maxEntries": 5000, "maxEntryKb": 2048 },
  "mobileMcp": { "enabled": true, "command": "npx", "args": ["-y", "@mobilenext/mobile-mcp@latest"] },
  "rateLimits": { "scriptsPerMinute": 10, "memoryReadsPerMinute": 60, "sessionsPerMinute": 5 }
}
```

| Variable | Default | Description |
|----------|---------|-------------|
| `FRIDA_DEVICE_ID` | first USB device | Device to use; same as `--device` |
| `FRIDA_MCP_ALLOW_CUSTOM_SCRIPTS` | unset | `1`/`true` enables `execute_script` and custom `hook_function` bodies; same as `--allow-custom-scripts` |
| `FRIDA_MCP_MEMORY_WRITE` | unset | `1`/`true` enables `write_memory`; same as `--allow-memory-write` |
| `FRIDA_MCP_ALLOW_NATIVE_INJECTION` | unset | `1`/`true` enables `inject_library` (native .so/.dylib injection); a capability separate from custom scripts |
| `FRIDA_MCP_ALLOW_ADB_SHELL` | unset | `1`/`true` enables `adb_shell` (arbitrary device shell commands) |
| `FRIDA_MCP_DEBUG` | unset | `1` enables debug logging on stderr; same as `--debug` |
| `FRIDA_MCP_REMOTE` | unset | `host[:port]` of a frida-server added as a remote device at startup (port 27042); same as `--remote` |
| `FRIDA_MCP_REMOTE_TOKEN` | unset | Auth token of that frida-server (`frida-server --token`); same as `--remote-token` |
| `FRIDA_MCP_REMOTE_CERT` | unset | Path to the PEM certificate of a TLS frida-server (`frida-server --certificate`); same as `--remote-cert` |
| `FRIDA_MCP_HTTP_TOKEN` | unset | Bearer token for the HTTP transport; same as `--http-token` |
| `FRIDA_MCP_SCHEMA_PROFILE` | `full` | `compact` shortens tool schemas for small context windows |
| `FRIDA_MCP_TOOL_PROFILE` | `full` | `core` registers only the core tool set (smaller `tools/list`, e.g. next to REA); also `--tool-profile`. With `FRIDA_MCP_SCHEMA_PROFILE=compact` the catalog drops to 17% of full; see [docs/rea.md](docs/rea.md#4-context-budget) |
| `FRIDA_MCP_TOOLS_EXCLUDE` | (none) | Comma-separated tool-name globs never registered, e.g. `adb_*,frida_server_*` when REA's ADB tools handle the device; also `--exclude-tools`. `get_status` reports the count as `hidden_tools` |
| `FRIDA_MCP_MAX_RESPONSE_CHARS` | `25000` | Truncation limit for a single tool response |
| `FRIDA_MCP_RUNTIME` | Frida default | `qjs` or `v8` script runtime for injected agents |
| `FRIDA_MCP_SNAPSHOTS` | unset | `1` pre-warms a per-session V8 snapshot of the bridge bundle (only with `FRIDA_MCP_RUNTIME=v8`); see [docs/clients.md](docs/clients.md#script-snapshots-snapshots--frida_mcp_snapshots) |
| `FRIDA_MCP_EVIDENCE` | `1` | `0` stops persisting tool results to the evidence store; same as config `evidence.enabled` |
| `FRIDA_MCP_EVIDENCE_PATH` | `~/.config/frida-mobile-mcp/evidence` | Evidence store directory (`YYYY-MM-DD/<evidence_id>.json` plus `index.jsonl`, mode 0600) |
| `FRIDA_MCP_EVIDENCE_MAX_MB` | `200` | Size cap of the evidence store; the oldest records are pruned beyond it |
| `FRIDA_MCP_BRIDGES_DIR` | `dist/bridges` | Directory with the bundled `java.js`/`objc.js` bridges |
| `FRIDA_MCP_COMPILER_PROJECT_DIR` | `$TMPDIR/frida-mobile-mcp/compiler-project` | Project dir the Frida Compiler uses for ES module scripts |
| `FRITAP_AGENT_PATH` | unset | Path to a `fritap_agent.js` bundle for `capture_tls` |
| `FRITAP_VERSION` | `v2.6.5` | friTap tag to download when no local agent is found |
| `FRITAP_AGENT_SHA256` | pinned for `v2.6.5` | Bare sha256 of the downloaded friTap agent for the selected `FRITAP_VERSION` tag; a mismatch refuses the bundle |

## Security guardrails

- `execute_script` and custom `hook_function` handler bodies are refused with `CUSTOM_SCRIPTS_DISABLED` unless `allowCustomScripts` is on; prebuilt scripts and typed hook tools never need it.
- `write_memory` is refused with `MEMORY_WRITE_DISABLED` unless `memoryWriteEnabled` is on. Prebuilt scripts that write target memory (e.g. `anti_frida_evasion` with `mode=spoof`) and `watch_loads` with `spoof=true` are gated the same way.
- `inject_library` is refused with `NATIVE_INJECTION_DISABLED` unless `allowNativeInjection` is on — a capability kept separate from `allowCustomScripts` so native code injection can be withheld even when JS agents are permitted.
- `allowedDevices` restricts which device IDs may be used (empty = any).
- Rate limits per minute for script loads, memory reads/scans and attach/spawn (`RATE_LIMIT_EXCEEDED`, retryable); `maxSessions` and `sessionTimeoutMinutes` cap concurrent and idle sessions.
- Every tool call is appended to the JSONL audit log at `auditLogPath` with its `evidence_id`.
- Every tool result (including memory dumps' metadata, read memory and found secrets) is persisted under the evidence path (`evidence.path`, default `~/.config/frida-mobile-mcp/evidence`) with mode 0600; opt out with `FRIDA_MCP_EVIDENCE=0`.
- `dump_module_memory` only writes under the current directory, the OS temp directory or `~/.config/frida-mobile-mcp/dumps`.
- HTTP transport: loopback bind by default, token required off-loopback, Host/Origin validation.

See [SECURITY.md](SECURITY.md) for reporting vulnerabilities.

## Troubleshooting

- **Version mismatch** (`unable to connect to remote frida-server`, protocol/"unexpected message" errors, timeouts while `adb devices` works): frida-server on the device must be 17.x matching frida-node. Run `frida-mobile-mcp doctor`, then push the matching release (see Requirements).
- **"Timeout was reached" / app killed after spawn**: Android kills a spawned app that stays suspended for more than ~10 s (Frida >= 17.6 cannot disable the launch timeout). `explore_app` resumes before class enumeration for this reason; with `spawn_process`, call `resume_process` promptly. If spawning is slow, launch the app first and attach (`spawn: false` / `attach_process`).
- **No Java messages right after hooking**: Java hooks install inside `Java.perform`, which runs once the ART VM is up. Poll `get_messages` again after a few seconds. "Java is not available" means a native-only process; use `hook_function`, `enumerate_exports` or `analyze_native_function`.
- **`Frida java bridge bundle not found`**: run `npm run build:bridges` (part of `npm run build` and `pretest`) or point `FRIDA_MCP_BRIDGES_DIR` at a directory with `java.js`/`objc.js`.
- **`npm install` fails downloading the frida prebuild**: the `frida` package fetches its native binding in an install script; make sure install scripts are allowed (`npm ci --foreground-scripts` shows the download) and GitHub releases are reachable.
- **Two devices attached**: set `FRIDA_DEVICE_ID`/`--device`, pass `device` to the tool, or `select_device`; with `allowedDevices` set an unlisted device yields `DEVICE_NOT_FOUND`.
- **`SESSION_LOST` / "script destroyed"**: the app crashed or was detached; `get_status` shows `recent_detaches`. Re-running the hook tool creates a fresh session; frequent crashes right after hooking point to anti-Frida checks (`root_jailbreak_bypass`, `capture_tls({ anti_root: true })`).
- **`FRITAP_AGENT_UNAVAILABLE`**: `pip install friTap`, set `FRITAP_AGENT_PATH`, or allow the download (`allow_download: true`).

## Development

```bash
npm ci --foreground-scripts   # installs deps and the frida prebuild
npm run build                 # build:bridges (rollup bundles of the Java/ObjC bridges) + tsc
npm run lint                  # tsc --noEmit
npm test                      # vitest (pretest rebuilds the bridges); device suites are skipped
npm run dev                   # tsc --watch
```

Device end-to-end tests opt in through environment variables:

```bash
FRIDA_MCP_E2E_DEVICE=emulator-5556 FRIDA_MCP_E2E_TARGET=com.example.rc4demo npx vitest run test/e2e
FRIDA_MCP_E2E_DEVICE=emulator-5556 npx vitest run test/fritap/capture-tls.e2e.test.ts
```

The older `FRIDA_E2E=1`, `FRIDA_DEVICE_ID` and `FRIDA_E2E_TARGET` names still work for the `capture_tls` test.

Commits follow Conventional Commits (`feat:`, `fix:`, `docs:`, ...); release-please opens the release PR, maintains `CHANGELOG.md` and the `publish` job in `release.yml` publishes to npm via OIDC trusted publishing. See [CONTRIBUTING.md](CONTRIBUTING.md) and [AGENTS.md](AGENTS.md).

## License

[MIT](LICENSE)