frida-ios-mcp
MCP server for iOS Frida exploration enabling app control, UI interaction, and media import via spawn sessions on jailbroken devices.
README
frida-mcp
TypeScript stdio MCP for Frida iOS exploration — Playwright-style loop:
device_list → app_list → session_open → wait_until_texts (TikTok) / wait → screen_snapshot
→ tap(ref) / swipe / type_text → screen_snapshot → … → session_close
If session_open hangs or Cursor cancel leaves MCP half-dead: session_status → session_force_unlock → retry one open.
Independent of fleetcontrol (agent JS copied under agent/).
Requirements
| Component | Version |
|---|---|
| Node.js | ≥ 22 |
| pnpm | 9+ |
npm frida |
17.x (this repo pins ^17.16.2) |
iOS frida-server |
same major as host npm frida (e.g. both 17.x) |
| USB | MVP: native USB only (no wecha TCP yet) |
| Python (media only) | 3.x + pymobiledevice3 via FRIDA_MCP_PYTHON |
Mismatch between host frida and phone frida-server → inject / session_open fails.
Device prerequisites
This MCP targets jailbroken iPhones only. Without a jailbreak and a running frida-server, device_list may still show USB, but inject / touch / UI text collection will fail.
1. Supported jailbreak stacks
| Stack | Notes |
|---|---|
| Dopamine | Common; works well with RootHide. This repo’s default spawn-only path is built for this class of devices. |
| Waterfall / Serotonin-family | Same requirement: a matching frida-server must run on the phone (daemon or manual start). |
| RootHide | Recommended for TikTok-like apps (reduces inject fingerprints). Do not rely on attach to an already-running app process (_touchesEvent often stays null). |
Not supported: stock (non-jailbroken) devices, Developer Mode alone, pymobiledevice3 without Frida, or remote TCP Frida (USB-only MVP).
2. frida-server must be running on the phone
- Download
frida-serverfrom Frida releases with the same major as the host npmfridapackage (e.g. hostfrida@17.16.x→ devicefrida-server17.x). - Push it to the device and
chmod +x(paths vary by jailbreak; common:/var/jb/usr/sbin/frida-serveror/usr/sbin/frida-server). - Start it as root and keep it running, e.g.:
# On-device SSH / terminal (adjust path for your jailbreak)
sudo frida-server -D
# or foreground for debugging:
sudo frida-server
- Verify from the PC:
pnpm cli call device_list
# or
npx frida-ps -U
Only then start the MCP. If frida-server stops, later session_open calls will fail or hang.
3. Host machine
- Node.js ≥ 22;
pnpm install && pnpm buildin this repo - USB cable + trusted computer
- For Photos import: Python with
pymobiledevice3, pointed to byFRIDA_MCP_PYTHON
4. TikTok / touch rules
| Approach | Result |
|---|---|
session_open (default spawn: kill → inject while suspended → resume) |
Touch reliable |
attach to already-foreground TikTok |
Unreliable (blocked by default; FRIDA_MCP_ALLOW_ATTACH=1 escape hatch only) |
Immediate Accessibility dump_tree after launch |
Triggers anti-debug; this MCP uses safe text collection instead |
Search UI tip: prefer tool tiktok_open_search from For You (taps the top-right magnifier with retries). The top wide field is the text input ([input]). The narrow 搜尋 / 搜索 / Search label on the right is a submit button (tap after typing) — never smart_type_text it.
Spawn-only (this device stack)
RootHide / TikTok / many jailbreak setups cannot use reliable attach (touch _touchesEvent null, or process dies).
| Policy | Behavior |
|---|---|
| Default | Always mode=spawn |
mode=attach |
Forced to spawn + warning |
| Escape hatch | FRIDA_MCP_ALLOW_ATTACH=1 (not recommended) |
kill old pid → device.spawn(bundleId) suspended → attach(pid) → inject agent → [netEnable?] → resume
Dual parallel: App + SpringBoard (+ Photos side channel)
| Channel | Session field | Lock | How to open |
|---|---|---|---|
| App (TikTok) | live |
appLock |
session_open |
| SpringBoard | sbLive |
sbLock |
withSpringBoard:true / sb_ensure / first sb_* |
| Photos album | photosLive |
photosLock |
photos_ensure / photos_import_file |
Stuck open / half-dead MCP: Cursor cancel does not abort server-side Frida. If device_list works but session_open / ping hang forever, check session_status (appLockBusy, appLockWaiters), then call session_force_unlock or restart the MCP process. CLI can still open apps because it is a different Node process with its own locks.
| Env | Default | Meaning |
|---|---|---|
FRIDA_MCP_OPEN_TIMEOUT_MS |
60000 |
spawn/attach/inject total timeout |
FRIDA_MCP_CLOSE_TIMEOUT_MS |
8000 |
soft timeout for script unload/detach (won't pin the lock) |
FRIDA_MCP_LOCK_WAIT_MS |
90000 |
max wait to acquire appLock/sbLock |
FRIDA_MCP_TOOLS |
all |
core = hide net/photos/dual extras from MCP tool list |
FRIDA_MCP_ALLOW_DEBUG_TOOLS |
unset (on) | Default all registers debug tools. Set 0/false/off to hide rpc_call / dump_modal / set_text_at_point only |
session_open also has a hold timeout (~open+close+5s): if Frida close/spawn hangs but the event loop is alive, the lock is released with APP_LOCK_HOLD_TIMEOUT instead of pinning forever. On hold/open timeout the server best-effort kills inFlightPid / last app pid and sets orphanFridaOpPossible. Soft-close timeout on the app channel also kills that pid (SpringBoard is never killed).
Stuck / orphan recovery: session_status (look for orphanFridaOpPossible / inFlightPid) → session_force_unlock (kills orphan pid, clears flag) → one session_open. Do not immediately re-open while orphan is set.
- Held in parallel — App + SB Frida scripts; Photos is a temporary third channel.
- RPCs concurrent — separate locks;
dual_ping/Promise.all([app…, sb…])run together. - Not multi-app business — one app + SpringBoard; Photos is import/clear only.
- photos_ never closes TikTok* — spawn Photos may briefly steal foreground.
Media import (PhotoKit, no fleetcontrol HTTP)
Requires Python 3 with pymobiledevice3 on the interpreter MCP actually runs (AFC).
MCP does not pip install for you. Missing deps → stage: afc within ~5s (preflight), not a 120s hang.
Pin the interpreter (recommended on Windows):
# CLI
set FRIDA_MCP_PYTHON=C:\Users\You\AppData\Local\Programs\Python\Python312\python.exe
"%FRIDA_MCP_PYTHON%" -m pip install pymobiledevice3
Cursor / Claude MCP config example:
{
"mcpServers": {
"frida-ios": {
"command": "node",
"args": ["D:/Project/tk/frida-mcp/dist/index.js"],
"env": {
"FRIDA_MCP_PYTHON": "C:\\Users\\You\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"
}
}
}
}
# image or small mp4 (prefer no other session_open during video import)
pnpm cli call photos_import_file --localPath D:\path\to\clip.mp4 --mediaType video
pnpm cli call photos_list --mediaType video
pnpm cli call photos_clear
Video tip: concurrent App sessions (e.g. Preferences open via session_open) can delay sqlite verify → needsRetry:true with a valid localIdentifier. Close other sessions and photos_list / re-import; do not treat needsRetry as silent success.
| Tool | Role |
|---|---|
photos_import_file |
Upload + ensure Photos + PhotoKit import (+ sqlite verify); image or video |
media_upload / photos_ensure / photos_import |
Split steps for retry |
photos_list |
Untrashed assets; optional mediaType / idPrefix |
photos_clear |
Trash untrashed media (Recently Deleted), optional DCIM cleanup |
AFC helper: scripts/afc_tool.py (preflight / push / list-untrashed / rm-dcim). Host = Photos.app only. SQLite query aligns with fleetcontrol (ZKIND in (0,1,2) + extensions).
session_open { bundleId: TikTok, withSpringBoard: true }
dual_ping # both channels pong at once
# later, can issue app + sb work concurrently if client allows
Install & build
cd D:\Project\tk\frida-mcp
pnpm install
pnpm build
Product surface
| Surface | Role |
|---|---|
MCP (frida-mcp) |
Interactive AI/human probe (main) |
CLI (cli/frida-ios.mjs) |
Scripts / CI / one-shot |
Core (src/backend.ts + session) |
Shared API — not shared memory by default |
MCP vs CLI sessions (read this)
- Embedded MCP = its own Node process + own
sessionStore(Cursor/Grok default). - CLI = another process; cannot see the MCP session.
- To share one Frida session: run the daemon, then set
FRIDA_MCP_MODE=daemonon both MCP and CLI. - Without daemon, open/close in CLI is independent of Cursor.
- App acts are serialized in-process (AI parallel tap+swipe will queue, not race).
# CLI (after build) — separate process unless daemon
pnpm cli help
pnpm cli open --bundleId com.ss.iphone.ugc.Ame --withSpringBoard
pnpm cli call wait --ms 4000
pnpm cli snap --limit 20
pnpm cli call net_dump --summaryOnly
pnpm cli close
Open-source safety (net_dump defaults):
- Redact Authorization / Cookie /
*Token* - Drop
data:URLs (base64 images) - Fold binary / octet-stream body previews
summaryOnly: truefor host counts onlyredact:false/includeDataUrls/includeBinaryBodiesonly on trusted local machines — never paste raw dumps into issues/PRs.
Typing (real input path): Feed → tiktok_open_search (preferred; retries magnifier points) → smart_type_text on the wide [input] search bar → then tap the narrow 搜尋 / 搜索 / Search submit button to run the query.
Never smart_type_text on nav tabs or on the submit 搜尋 button itself.
Nav / composer chips (e.g. “What's on your mind”) are not fields → NOT_INPUT.
SB test alert: sb_alert_trigger → sb_alert_list (hasAlert) → single sb_alert_dismiss (post-settle cleared) or stacked sb_alert_dismiss({ all: true }); if needsRetry re-list / retry all.
Debug tools (rpc_call, dump_modal, set_text_at_point): registered by default in FRIDA_MCP_TOOLS=all (prefixed [debug]). Hide with FRIDA_MCP_ALLOW_DEBUG_TOOLS=0, or hide advanced+debug with FRIDA_MCP_TOOLS=core.
Modes
Embedded (default, recommended for Grok/Cursor trial)
Session lives inside the MCP process. No daemon, no env:
node dist/index.js
# or
pnpm dev
Daemon + thin MCP (NSSM) — shared session with CLI
- Daemon holds Frida session on
127.0.0.1:18765 - stdio MCP and CLI forward when
FRIDA_MCP_MODE=daemon(orFRIDA_MCP_DAEMON=1)
pnpm start:daemon
# other terminal / Cursor:
set FRIDA_MCP_MODE=daemon
node dist/index.js
Grok config (~/.grok/config.toml)
Why no args? MCP spawns command + optional args. We ship bin/frida-mcp.cmd
which already runs node dist/index.js, so config only needs the launcher path:
[mcp_servers.frida-ios]
command = "D:/Project/tk/frida-mcp/bin/frida-mcp.cmd"
enabled = true
Optional daemon mode (daemon must be running separately):
[mcp_servers.frida-ios]
command = "D:/Project/tk/frida-mcp/bin/frida-mcp.cmd"
enabled = true
[mcp_servers.frida-ios.env]
FRIDA_MCP_MODE = "daemon"
Cursor mcp.json
{
"mcpServers": {
"frida-ios": {
"command": "D:/Project/tk/frida-mcp/bin/frida-mcp.cmd"
}
}
}
TikTok red lines
- Never
dump_tree/find_view/find_buttons/dump_login_gate(MCP gates these). - Read UI only via
screen_snapshot→collectTextsWithFrames. - Prefer
session_openmode=spawnfor reliable touch. - After open,
wait3000–5000 ms before first snapshot. - Refs only valid for last snapshot — re-snapshot after UI changes.
Tools
| Tool | Purpose |
|---|---|
device_list |
Frida devices (default USB-only; usbOnly=false for all) |
app_list |
Apps + pid. Default userFacing=true filters Apple services; runningOnly / query supported |
session_open |
spawn | attach + inject |
session_status / session_respawn / session_close / session_force_unlock |
lifecycle + stuck-lock recovery (appLockBusy, openInFlight, refsValid) |
wait / wait_until_texts |
blind sleep vs poll until text; TikTok use preset:"tiktok_feed" (multi-locale) |
ping |
agent liveness |
screen_window |
simplified {width,height,x,y,cx,cy,className} |
screen_snapshot / screen_search |
texts refs are generation-scoped (g3t8); tree mode does not wipe texts refs |
screen_shot |
lockdown pixel screenshot (pymobiledevice3); visual assist — not for tap refs |
tap / swipe / press_home / wait |
act — swipe prefer durationMs (agent seconds; duration>10 = ms) |
probe_help |
Recommended probe loop + tool tiers (tools.core / advanced) |
type_text |
Humanized per-char typing into focused field (resnapshot default true) |
smart_type_text |
Preferred: tap → focus → humanized typing |
clear_text / first_responder / human_pause |
focus / clear / step-gap pause |
double_tap |
double-tap like at ref/x,y |
set_otp |
TikTok OTP fill (setOtpCode) |
set_text_at_point |
coordinate setText (not humanized; debug) |
dump_modal |
mid-screen modal (blocked on TikTok; debug) |
rpc_call |
whitelisted agent RPC ([debug]; on by default in all, hide with ALLOW_DEBUG_TOOLS=0) |
process_list |
device processes (pid/name) |
sb_alert_trigger / sb_alert_list / sb_alert_tap / sb_alert_dismiss / sb_close |
SpringBoard system alerts |
net_enable / net_disable / net_clear / net_status / net_dump |
in-process NSURLSession + TTNet/Cronet capture (TLS plaintext after app decrypt) |
tiktok_inbox / tiktok_reply / tiktok_im / tiktok_posts / tiktok_sign |
Inbox+MR read / composer reply / IM advanced / self posts / MetaSec sign I/O |
Refs expire after tap/swipe and across snapshot generations. Off-screen / zero-size nodes are marked and rejected on tap.
Humanized typing (inputText)
Agent: agent/text_input/comment.js (same approach as fleetcontrol).
| MCP tool | fleetcontrol counterpart | Behavior |
|---|---|---|
type_text |
TypeTextAction / HumanTypeInField |
Field already focused; per-char inputText |
smart_type_text |
SmartTypeTextAction |
tap → wait firstResponder → human_pause → type |
human_pause |
human_pause(min,max) |
Random gap between steps (not inter-key delay) |
- Default
perCharDelayMs=90; agent addsrandomDelay(base, jitter≈base)(jitter ≥ 30ms). - Insert fallbacks:
insertText→replaceRange→ innerinsertText→setText+ notify. - Nav tabs / chips like “Home” or “What's on your mind” are not inputs →
NOT_INPUT(session stays alive). - Prefer
smart_type_texton a real field ref; usefirst_responderif unsure (canInsertText). screen_snapshotdefaults:onScreenOnly=true,limit=40; optionalsearch/showDiff.tap/swipe/smart_type_textdefaultresnapshot=true(returnssnapshot).- Errors return
{ code, recovery[] }(e.g.SCRIPT_DESTROYED→ respawn). - Start probes with
probe_help; prefer first-class tools over[debug]rpc_call(hide debug withFRIDA_MCP_ALLOW_DEBUG_TOOLS=0).
Network capture (reverse-engineering)
# Best: capture launch traffic (hooks before resume)
session_open {
bundleId,
captureNet: true,
netOptions: { captureMode: "all", maxBody: 16384, captureResponse: true, signTrace: true }
}
→ use app (Inbox / Profile)
→ net_dump({ redact:false, dedupe:false, query:"imapi|inbox|/im/|profile/self|security-argus", includeBinaryBodies:true, limit:100 })
→ tiktok_sign({ action: "last" }) # recent sign headers (+ backtrace if signTrace)
captureMode:nsurl|ttnet|all(default).- Signing model (general capability): MCP does not reimplement offline
X-Gorgon/X-Argus. Instead it uses in-process TTNet (TTNetworkManagerJSON request) so the App's MetaSec signs, andnet_*/tiktok_signcapturex-security-argus/x-Tt-Token/x-metasec-*I/O (+ optionalsignTracebacktrace). - iOS sign headers (45.x):
x-security-argus,x-Tt-Token,x-metasec-*, ticket/device-guard — classicX-Gorgon/X-Argusnames are often absent on this build. - Responses:
captureResponse:trueuses TTNetonReadResponseData+setIsCompleted. Do not hookonURLFetchComplete(kills script). Many JSON/IM payloads may still arrive empty via this path (protobuf/other channels); binary/CDN bodies usually populate. Prefer ttnetRequest callback JSON for posts/list APIs. - DM / works URLs seen on device:
imapi-*.tiktokv.com/v1/message/get_by_user,…/v2/message/get_by_user_init,tiktok/v1/im/inbox_data/get,aweme/v1/user/profile/self, feed/post endpoints. - IM inbox/reply:
tiktok_inbox— refresh Inbox + Message Requests and returnusername, real textcontent,conversationId,peerUid.tiktok_reply/tiktok_im send_text— defaultdryRun:true. WithdryRun:false, opens the real chat composer (ChatInputTextView+ 傳送), and returnssent:trueonly after the exact text is re-read from liveAWEIMTextMessagemodels (blank bubbles / network-callback-only success are rejected).transport:"sdk"is disabled.tiktok_im action: messages— list recent messages with real text when available;open_chatacceptsconversationIdorpeerUid.
- Posts (
tiktok_posts): self post list via TTNet toaweme/v1/aweme/post/(overrideurl/userIdif needed). - Add Phone popup attribution (no dedicated tool):
session_openwithcaptureNet+ reproduce the popupnet_dump({ query: "passport/account|bind_phone|mobile", redact:false, dedupe:false })— avoid barephone(matchesiPhonedevice_type noise)tiktok_im({ action: "phone_status" })— on this build:AWEUserService.sharedService→currentLoginUser:havePhoneNumber/isPhoneBindedfalse + emptybindPhone⇒ account unbound (not just a cosmetic prompt)canShowThirdPartyPhoneBindingPopup/is3pBindPopupRequestShowdistinguish third-party bind UI vs mandatory add-phone
- Boot traffic often includes
passport/account/info/v2andpassport/token/beat/v2
- RE dump tip:
redact:false,dedupe:false,includeBinaryBodies:true.
Advanced TikTok tools (not core)
| Tool | Purpose |
|---|---|
tiktok_inbox |
Read Inbox + Message Requests/notification messages (username/content/conversation ID) |
tiktok_reply |
Reply via real chat composer; succeeds only after exact text re-read |
tiktok_im |
Advanced IM diagnostics / compatibility actions |
tiktok_posts |
Self works list (in-app signed TTNet) |
tiktok_sign |
last sign headers or enable_trace |
NSSM (only after device tools work)
# Admin
cd D:\Project\tk\frida-mcp
pnpm build
.\scripts\install-nssm-service.ps1
nssm start FridaMcpDaemon
Uninstall: .\scripts\uninstall-nssm-service.ps1
Logs: logs/daemon.stdout.log, logs/daemon.stderr.log
Typical probe
device_listapp_list→ find TikTok bundlesession_open{ "bundleId": "…", "mode": "spawn" }wait{ "ms": 4000 }ping→pongscreen_snapshot→ refstap{ "ref": "t3" }→screen_snapshotagain
Agent
- Source:
agent/agent_main.js(+ imports), includesping - Override:
FRIDA_AGENT_ENTRY=path/to/agent_main.js - Compile: Frida Compiler,
projectRoot= repo root (frida-objc-bridgeinnode_modules)
License
Private / internal use.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。