frida-ios-mcp
Allows interaction with TikTok app on iOS devices via Frida, enabling automated UI operations such as opening search, typing text, tapping, swiping, and taking screenshots for testing and exploration.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@frida-ios-mcpopen TikTok and take a screenshot"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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_closeIf 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 | 17.x (this repo pins |
iOS | same major as host npm |
USB | MVP: native USB only (no wecha TCP yet) |
Python (media only) | 3.x + |
Mismatch between host frida and phone frida-server → inject / session_open fails.
Related MCP server: ya-frida-mcp
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 |
RootHide | Recommended for TikTok-like apps (reduces inject fingerprints). Do not rely on |
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-serverVerify from the PC:
pnpm cli call device_list
# or
npx frida-ps -UOnly 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 repoUSB cable + trusted computer
For Photos import: Python with
pymobiledevice3, pointed to byFRIDA_MCP_PYTHON
4. TikTok / touch rules
Approach | Result |
| Touch reliable |
| Unreliable (blocked by default; |
Immediate Accessibility | 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 |
| Forced to spawn + warning |
Escape hatch |
|
kill old pid → device.spawn(bundleId) suspended → attach(pid) → inject agent → [netEnable?] → resumeDual parallel: App + SpringBoard (+ Photos side channel)
Channel | Session field | Lock | How to open |
App (TikTok) |
|
|
|
SpringBoard |
|
|
|
Photos album |
|
|
|
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 |
|
| spawn/attach/inject total timeout |
|
| soft timeout for script unload/detach (won't pin the lock) |
|
| max wait to acquire appLock/sbLock |
|
|
|
| unset (on) | Default |
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 pymobiledevice3Cursor / 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_clearVideo 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 |
| Upload + ensure Photos + PhotoKit import (+ sqlite verify); image or video |
| Split steps for retry |
| Untrashed assets; optional |
| 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 allowsInstall & build
cd D:\Project\tk\frida-mcp
pnpm install
pnpm buildProduct surface
Surface | Role |
MCP ( | Interactive AI/human probe (main) |
CLI ( | Scripts / CI / one-shot |
Core ( | 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 closeOpen-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 devDaemon + thin MCP (NSSM) — shared session with CLI
Daemon holds Frida session on
127.0.0.1:18765stdio 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.jsGrok 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 = trueOptional 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 |
| Frida devices (default USB-only; |
| Apps + pid. Default |
| spawn | attach + inject |
| lifecycle + stuck-lock recovery ( |
| blind sleep vs poll until text; TikTok use |
| agent liveness |
| simplified |
| texts refs are generation-scoped ( |
| lockdown pixel screenshot (pymobiledevice3); visual assist — not for tap refs |
| act — swipe prefer |
| Recommended probe loop + tool tiers ( |
| Humanized per-char typing into focused field ( |
| Preferred: tap → focus → humanized typing |
| focus / clear / step-gap pause |
| double-tap like at ref/x,y |
| TikTok OTP fill ( |
| coordinate setText (not humanized; debug) |
| mid-screen modal (blocked on TikTok; debug) |
| whitelisted agent RPC ( |
| device processes (pid/name) |
| SpringBoard system alerts |
| in-process NSURLSession + TTNet/Cronet capture (TLS plaintext after app decrypt) |
| 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 |
|
| Field already focused; per-char |
|
| tap → wait firstResponder → |
|
| 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 |
| Read Inbox + Message Requests/notification messages (username/content/conversation ID) |
| Reply via real chat composer; succeeds only after exact text re-read |
| Advanced IM diagnostics / compatibility actions |
| Self works list (in-app signed TTNet) |
|
|
NSSM (only after device tools work)
# Admin
cd D:\Project\tk\frida-mcp
pnpm build
.\scripts\install-nssm-service.ps1
nssm start FridaMcpDaemonUninstall: .\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), includespingOverride:
FRIDA_AGENT_ENTRY=path/to/agent_main.jsCompile: Frida Compiler,
projectRoot= repo root (frida-objc-bridgeinnode_modules)
License
Private / internal use.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityDmaintenanceAn MCP-compliant server that enables AI systems to interact with mobile and desktop applications through Frida's dynamic instrumentation capabilities, allowing for process management, device control, JavaScript execution, and script injection.405MIT
- AlicenseAqualityCmaintenanceA comprehensive MCP server for Frida dynamic instrumentation, enabling AI agents to manage devices, processes, scripts, memory, and ADB operations.3923MIT
- AlicenseCqualityCmaintenanceA Frida MCP server for authorized dynamic analysis and application security research, exposing device/session management, script injection, process and memory inspection, and platform-specific workflows.1002MIT
- AlicenseAqualityCmaintenanceMCP server that wraps the Frida dynamic instrumentation toolkit, allowing users to attach to processes, hook functions, enumerate modules and exports, and manage scripts through natural language.101MIT
Related MCP Connectors
MCP server for Appcircle mobile CI/CD platform.
Personal assistant MCP server with search, execute, packages, jobs, secrets, and integrations.
An MCP server that let you interact with Cycloid.io Internal Development Portal and Platform
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/YPYT1/frida-ios-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server