app_install
Installs an app on an iOS simulator. Accepts a path to a .app directory or a .zip archive.
Drives an iOS simulator, a real iPhone or an Android device from the assistant — taps by text or accessibility id, asserts what is on screen with evidence, and reads React Native logs, network and state.
LABSCO SUMMARY
Elements are addressed by text, accessibility id or DOM selector, which is why the canvas and Rive support matters — those are exactly the surfaces where a screenshot-based agent is reduced to guessing at pixels, and here they come back as objects with coordinates. The assertion design is the other half: the ladder tries the most trustworthy oracle available and the absence check fails closed rather than reporting success it could not verify, so a passing flow means something. Start with the health check and the device list; which tools work depends on what is installed on the host.
One endpoint over the whole mobile toolchain. Device and app lifecycle, gestures, view hierarchies, Maestro flows, WebView DOM and network, React Native debugging over CDP, and canvas or game-engine UIs — routed to whichever backend is installed on the host.
macOS with the Xcode command-line tools, since iOS simulator control goes through xcrun and simctl, and Node.js 22 or newer. The default native gesture and WebView backend ships as a dependency, so there is nothing extra to install for the common path. Everything else is optional and detected: idb with its companion becomes the preferred gesture backend when present, Maestro on the PATH is what runs declarative flows and is the gesture fallback, a running Metro bundler is needed for the React Native tools, and the Android SDK with adb is detection-only — absent toolchains return an informative result rather than an error. Real iPhones go through devicectl with an opt-in WebDriverAgent backend, and the game-engine tools need an AltTester-instrumented development build. Call the health check first; it tells you which of these the host actually has. MIT.
One command — npx -y podium-mcp
USE CASES
Boot the simulator, install and launch the build, then tap and type by text or accessibility id and take a screenshot when something looks wrong.
Run the visibility assertions and get a verdict with the evidence behind it, rather than a screenshot and an opinion.
Execute the ordered steps once, then export that sequence as a Maestro flow the QA team can run without you.
Read the Metro console logs, capture the network calls it made, and evaluate an expression to see the state it is actually in.
Record the screen while reproducing it, then pull the crash report and read the parsed header rather than describing the failure from memory.
List the canvas objects as elements, resolve an intent like close or settings to a ranked target with evidence, and tap it at real coordinates.
Read the full inventory of state machines, inputs and view-model properties, drive them in order, and drain the events fired back as confirmation.
Resolve a selector to coordinates, evaluate JavaScript in the page, and capture the HTTP traffic it makes as a HAR.
Counted once with only startup setup. Other versions, modes or optional credentials may expose a different list.
app_installInstalls an app on an iOS simulator. Accepts a path to a .app directory or a .zip archive.
app_launchLaunches an app on an iOS simulator by bundle ID.
app_listReturns the list of installed apps on a booted iOS simulator. Includes bundle ID, display name, and application type (User/System).
app_stateChecks whether an app is installed and/or running on an iOS simulator. installed: exact bundle-id match against the parsed simctl listapps output; running: matches the launchctl UIKitApplication:<bundleId> label on a token boundary (no prefix false positives).
app_terminateTerminates a running app on an iOS simulator by bundle ID.
app_uninstallUninstalls an app from an iOS simulator by bundle ID.
assert_not_visibleAsserts an element/text is ABSENT. FAILS CLOSED: if absence cannot be verified (e.g. a WebView whose DOM is unreadable — native a11y is blind to web content), returns an 'unverifiable' error rather than a false pass. Passes only when a capable oracle confirms absence.
assert_textAsserts the given text is visible on screen (by-text shorthand for assert_visible). Same oracle ladder + unverifiable handling.
assert_visibleAsserts an element/text is visible, via the oracle ladder (WebView-DOM > native a11y > Maestro). Passes only when a capable oracle confirms presence; if the surface is a WebView whose DOM can't be read (isInspectable=false), returns an 'unverifiable' error instead of a false pass. Provide text (any surface) or selector (WebView).
canvas_inspectLists canvas/WebGL objects (Pixi/Konva/Fabric/Phaser/Three/Babylon) as DOM-like elements with tap-ready coordinates — NO screenshot/vision. Optionally filter by selector (name/id/text/type/role). Requires an inspectable WKWebView (isInspectable=true; debug/staging) hosting a canvas/WebGL framework (Pixi/Konva/Fabric/Phaser/Three/Babylon) with its root reachable (commonly on window, or Pixi's __PIXI_APP__). NO screenshots/vision — fails closed otherwise.
canvas_resolveResolves a fuzzy intent (e.g. "close", "settings", "✕") to a ranked, EVIDENCED canvas target without tapping — the 'close brain'. Returns the best match, all candidates with reasons, and a fail-closed confidentEnough flag (false when two targets tie). NO vision. Requires an inspectable WKWebView (isInspectable=true; debug/staging) hosting a canvas/WebGL framework (Pixi/Konva/Fabric/Phaser/Three/Babylon) with its root reachable (commonly on window, or Pixi's __PIXI_APP__). NO screenshots/vision — fails closed otherwise.
canvas_tapResolves an intent to a single canvas target and TAPS it at absolute screen coordinates — the native-like 'close this' for canvas UIs. Fail-closed: if no confident, unambiguous match exists it does NOT tap and returns the candidates so you can disambiguate. NO vision. When to use: the UI is drawn on a <canvas> (Pixi/Konva/Fabric/Phaser/Three/Babylon game UIs). For native iOS UI use tap_on; for WebView DOM use webview_inspect then tap_on with the returned coordinates. Requires an inspectable WKWebView (isInspectable=true; debug/staging) hosting a canvas/WebGL framework (Pixi/Konva/Fabric/Phaser/Three/Babylon) with its root reachable (commonly on window, or Pixi's __PIXI_APP__). NO screenshots/vision — fails closed otherwise.
cheat_sheetReturns the bundled Maestro flow script cheat sheet (offline copy). Consult this before authoring unfamiliar Maestro commands, required args, nested properties, conditionals, or multi-screen flows.
crash_getReads a crash report by its id (filename from crash_list). For .ips files returns a parsed JSON header and the report body (first ~8000 chars, truncated flag set if longer). Pass the same udid used for crash_list to also resolve sim-container reports. Path-traversal-safe.
crash_listLists crash reports (.ips/.crash) from ~/Library/Logs/DiagnosticReports — plus the simulator's own container DiagnosticReports when udid is given — sorted newest first. Filter by processName (case-insensitive substring) and/or sinceHours.
device_bootBoots an iOS simulator by UDID. Waits up to 30 seconds for the boot command to complete. Idempotent: booting an already-booted device returns ok with alreadyBooted:true.
device_listReturns a merged inventory of available iOS simulators (udid, name, state, runtime) plus any adb-detected Android devices. If adb is absent, the android section reports availability: false instead of failing. NOTE: Android entries are detection-only — podium's automation tools (tap/inspect/etc.) currently target iOS simulators.
engine_callInvokes a C# component method on a game-engine object by reflection — the engine analog of firing a DOM event handler. Requires an AltTester-instrumented build (dev/staging) with the in-app server running — production App Store builds are not instrumented. Uses no screenshots/vision.
engine_inspectLists game-engine (Unity/GL) objects matching a selector, each with absolute screen coordinates for tapping — DOM-like addressing with NO vision. Requires an AltTester-instrumented build (dev/staging) with the in-app server running — production App Store builds are not instrumented. Uses no screenshots/vision.
engine_swipeSwipes between two screen coordinates inside a game-engine view. Requires an AltTester-instrumented build (dev/staging) with the in-app server running — production App Store builds are not instrumented. Uses no screenshots/vision.
engine_tapTaps a game-engine object resolved by selector (engine-reported screen coords, no vision). Requires an AltTester-instrumented build (dev/staging) with the in-app server running — production App Store builds are not instrumented. Uses no screenshots/vision.
export_flowExports a run_steps action sequence to a reusable Maestro flow (the engineer→QA bridge). Selector-based steps (tapText by id/text, key, swipe-by-direction, waitFor, assertVisible, screenshot) transpile cleanly. The lossy steps — coordinate tap/swipe, focused-field type, regex tapText — are emitted as commented '# TODO[unstable]' lines plus a warnings[] list, never as silently divergent YAML. Fix the TODOs to make the flow durable regression.
input_textTypes text into the currently-focused element via an ephemeral Maestro flow. Set submit:true to press Enter after typing. Note: Android does not support Unicode via inputText. WebView caveat: inputText injects at the native buffer level — React onChange/onChangeText never fires. For WebView forms use mobile-mcp mobile_type_keys (real keystroke simulation) instead.
inspect_screenReturns the current view hierarchy for a booted iOS simulator (podium's target platform). Uses idb's flat accessibility tree when idb is installed (fast), else maestro hierarchy. Defaults to compact:true — a flattened list of only the nodes that carry text / accessibility labels / resource-ids (dramatically smaller than the raw tree). Pass compact:false for the full nested hierarchy. LIMITATION: WebView (WKWebView/WebView) content is opaque — the hierarchy shows a single WebView node with no children. Web-rendered buttons, inputs, and labels are invisible to this tool. For WebView apps, identify elements visually via screenshot then calculate logical-point coordinates (screenshot pixels ÷ device scale factor, typically ÷3 on 3× Retina).
metro_appsLists React Native apps currently connected to a Metro bundler inspector. Returns CDP-style targets (id, title, webSocketDebuggerUrl). Returns a structured error if Metro is not running on the given port.
metro_logsReads console logs from a React Native app via the Metro CDP debugger. If webSocketDebuggerUrl is omitted, auto-discovers via metro_apps and uses the first connected app. Reports which app was chosen. Pass saveTo to also write the console timeline to a file for evidence (e.g. attach to a bug).
metro_networkCaptures network requests from a React Native app via the Metro CDP debugger (Network domain). If webSocketDebuggerUrl is omitted, auto-discovers via metro_apps and uses the first connected app. Pairs requestWillBeSent with responseReceived by requestId (method, url, status, headers, timing). format:'har' emits a valid HAR 1.2 log (HAR-lite — no response bodies yet) you can open in Chrome DevTools → Import HAR; pass saveTo to write the .har file. Sensitive headers (authorization/cookie/…) are REDACTED by default — set redact:false to keep them (don't commit unredacted HAR: it leaks tokens).
metro_stateReads app state from a React Native app by evaluating a JS expression in its runtime via the Metro CDP debugger (Runtime.evaluate, returnByValue). Default expression reads a globally-exposed Redux store; override expression to read any in-app value. The app must expose the value on a global the runtime can reach. Auto-discovers the ws via metro_apps when omitted.
notification_bar_clearAttempts to dismiss the React Native debug notification bar that sometimes appears at the bottom of the screen and intercepts taps. Taps the debug icons area at (50, 850) via the native backend (idb, else Maestro) and takes a before/after screenshot. NOTE: the (50,850) tap point is a device-specific heuristic, and 'cleared' is decided by a screenshot byte-size delta — a best-effort signal, not a guarantee (see tap_with_fallback caveat).
open_urlOpens a URL on an iOS simulator (deep-links, https:// etc.).
orientation_getReturns the current orientation of a booted iOS simulator. Queries the native backend (mobilecli) when available for an exact answer; otherwise derives it from the screenshot aspect ratio.
orientation_setSets the screen orientation on an iOS simulator via an ephemeral Maestro flow. bundleId is required (Maestro needs it for the appId flow header). Valid values: PORTRAIT, LANDSCAPE_LEFT, LANDSCAPE_RIGHT, UPSIDE_DOWN
podium_healthReturns health status of the podium-mcp server and toolchain availability. Scope: iOS (simulator + real device), Android (emulator + real via adb), game-engine (Unity/GL via AltTester), no-vision canvas/WebGL automation, and Rive interactive-animation control + dual-source (live + static) analysis. macOS + Xcode required; adb for Android, an instrumented build for engine tools, an inspectable WebView for Rive live control/analysis.
podium_token_reportQuantifies Podium's token savings: for an N-step flow, computes input tokens for Podium's no-vision structured flow vs a screenshot/vision loop, the savings ratio, and the fixed per-request tool-definition overhead. Heuristic estimates (~4 chars/token text, ~750 px/token image) — deterministic, no network.
press_keyPresses a hardware or system key via an ephemeral Maestro flow on the iOS simulator. Note: back/power/tab are Android key events and have no effect on iOS — they remain in the enum for a future Android backend. Valid keys: enter, home, lock, backspace, volume up, volume down, back, power, tab
record_startStarts a screen recording on a booted iOS simulator. The recording runs in a detached background process. Call record_stop to finalize and retrieve the file.
record_stopStops the active screen recording for an iOS simulator. Sends SIGINT to flush the video file, waits for the file size to stabilize, and returns the path and size.
rive_analyzeReports the COMPLETE element inventory of a Rive file — every artboard, animation, state machine, input, view-model property, text run, event — with per-type histograms, an explicit triggerable{smTriggers,vmTriggers,total} rollup, and each state machine's states. Read-only — never fires or sets anything. Two independent, mergeable sources: udid (a booted device with an inspectable WKWebView + a running Rive instance) gives LIVE values, the view-model tree, text runs, events, and RUNTIME-OBSERVED state-machine states; src (a .riv URL or local file path, read by @rive-app/canvas-advanced in Node — no device needed) gives the COMPLETE static artboard/animation/state-machine/input inventory, incl. nested artboards. Pass both to merge: static counts win for structure, live overlays values + observed states. Web/WebView Rive runtimes only (@rive-app/canvas / @rive-app/webgl) — native iOS/Android/RN/Flutter Rive has no JS bridge, out of scope.
rive_e2e_initScaffolds a coverage-gated Rive E2E suite into a target repo (the podium init rive-e2e capability). Runs rive_analyze (static src .riv and/or a live udid WebView), freezes rive.contract.json (the coverage denominator), and writes rive-e2e/: config, the generated smoke suite, a hand-flow skeleton, thin web/mobile runner entry points, fixtures, and .gitignore. Idempotent — re-init refreshes generated files and never clobbers hand-written config/flows. Needs at least one of src or udid for the contract.
rive_eventsDrains buffered Rive events (name/type/properties) fired by the state machine — the closed-loop VERIFY oracle after a rive_fire/rive_set. Poll shortly after an action; events fire on the next tick. Requires an inspectable WKWebView (isInspectable=true; debug/staging) hosting a Rive web runtime (@rive-app/canvas or @rive-app/webgl). For reliable capture, expose the instance as window.__rive = riveInstance (or an array). NO screenshots/vision — fails closed otherwise.
rive_fireFires a Rive TRIGGER — a state-machine trigger input OR a view-model trigger property — by name/path. A '/'-path (e.g. cta/ctaClick) targets a view-model trigger; a bare name tries the state-machine input first, then a view-model trigger. The runtime-correct way to 'click' a Rive control. Verify with rive_events (events fire on the next tick, so poll shortly after). Requires an inspectable WKWebView (isInspectable=true; debug/staging) hosting a Rive web runtime (@rive-app/canvas or @rive-app/webgl). For reliable capture, expose the instance as window.__rive = riveInstance (or an array). NO screenshots/vision — fails closed otherwise.
rive_inspectLists a Rive canvas's interactable surface — state-machine inputs (boolean/number/trigger), VIEW MODEL data-binding properties (modern Rive: number/boolean/string/trigger, nested by '/'-path e.g. meter/tierNum), text runs, artboards, and buffered events — NO screenshot/vision. The input names and view-model paths are the deterministic addresses used by rive_fire / rive_set. Requires an inspectable WKWebView (isInspectable=true; debug/staging) hosting a Rive web runtime (@rive-app/canvas or @rive-app/webgl). For reliable capture, expose the instance as window.__rive = riveInstance (or an array). NO screenshots/vision — fails closed otherwise.
rive_runDrives an ordered Rive flow in one call: a list of fire/set/setText steps against named state-machine inputs and text runs, executed deterministically (no inference), then Rive events are drained to verify. The Rive equivalent of run_intent. Requires an inspectable WKWebView (isInspectable=true; debug/staging) hosting a Rive web runtime (@rive-app/canvas or @rive-app/webgl). For reliable capture, expose the instance as window.__rive = riveInstance (or an array). NO screenshots/vision — fails closed otherwise.
rive_setSets a Rive BOOLEAN or NUMBER value — a state-machine input OR a view-model property — by name/path (toggles, sliders, tier numbers, progress). A '/'-path (e.g. meter/tierNum) targets a view-model property; a bare name tries the state-machine input first, then a view-model property. Deterministic — no coordinate tap. Requires an inspectable WKWebView (isInspectable=true; debug/staging) hosting a Rive web runtime (@rive-app/canvas or @rive-app/webgl). For reliable capture, expose the instance as window.__rive = riveInstance (or an array). NO screenshots/vision — fails closed otherwise.
rive_set_textSets a Rive TEXT RUN's string by name (requires a runtime with setTextRunValue). Requires an inspectable WKWebView (isInspectable=true; debug/staging) hosting a Rive web runtime (@rive-app/canvas or @rive-app/webgl). For reliable capture, expose the instance as window.__rive = riveInstance (or an array). NO screenshots/vision — fails closed otherwise.
run_flowExecute one or more Maestro flows on a device. Provide exactly one of: yaml (inline YAML string), files (array of flow file paths), or dir (directory path). includeTags and excludeTags are only applicable when using dir. When to use: run_flow gives the full Maestro vocabulary (assertions, conditionals, loops, retries); for a simple sequence of taps/types prefer run_steps, and for one gesture use the individual tools. TRUST BOUNDARY: Maestro flows can run arbitrary JS (evalScript) and local files (runScript/files/dir), so treat run_flow input as locally-executable code.
run_intentDrive a whole intent PLAN in ONE call — the closed-loop, model-agnostic autopilot. Each step is { do, target?, value?, index? }: the server resolves the fuzzy target against the live a11y tree (deterministic, evidenced, fail-closed on ambiguity), acts on the OS-frame center, verifies the effect, and self-recovers (retry / re-resolve / scroll / wait) — all WITHOUT returning to the model between steps. On genuine ambiguity or an unknown blocking modal it STOPS and returns a decidable escalation (status + expected-vs-observed + candidates); re-call the failing step with index to disambiguate.
run_stepsExecute an ordered batch of UI actions in ONE call via the native backend (idb/mobilecli, sub-second; Maestro fallback per step). Eliminates per-gesture MCP round-trips for fast continuous flows (login, navigation, form fill). Step actions: tap {x,y} · tapText {text|id} · type {text,submit} · key · swipe · waitFor {text,timeoutMs} · assertVisible {text} · waitMs · screenshot. Prefer waitFor over waitMs to act the instant the UI is ready instead of sleeping. WebView note: web-rendered text is invisible to tapText — use tap {x,y} for it; type uses real keystrokes so React onChange fires. Stops at the first failed step unless stopOnError:false. bundleId is only needed for the Maestro fallback (auto-detected otherwise). When to use: pick run_steps for >2 known sequential gestures (login, navigation, form fill); use run_flow for Maestro assertions/conditionals/loops/retries; use the individual gesture tools (tap_on, swipe, …) for a single exploratory action.
screen_sizeReturns the pixel dimensions of a booted iOS simulator screen by taking a temp screenshot and reading its pixel dimensions with sips.
screenshotTakes a screenshot of an iOS simulator. Returns the saved file path and byte size. Does NOT return base64 to keep payload small. saveTo defaults to a .png file in os.tmpdir().
set_locationSets the simulated GPS location on a running iOS simulator. Codifies the QA geo-spinner fix: use this to unblock location-gated features during QA testing without moving the physical device.
swipeSwipes in a direction or between two coordinates via an ephemeral Maestro flow. direction is always required; startX/startY/endX/endY are optional overrides expressed as percentage strings (e.g. '10%,50%') or pixel values.
tap_onTap, double-tap, or long-press an element on screen via an ephemeral Maestro flow. Target by text (regex), accessibility id, or absolute x/y coordinates. bundleId is REQUIRED — Maestro needs it for the appId flow header. WebView caution: text/id selectors only resolve native accessibility nodes. Web-rendered elements inside WKWebView are invisible — tap_on will report COMPLETED but nothing is tapped. Use x+y coordinates instead for WebView content. When to use: ONE native tap/long-press/double-tap. For >2 sequential gestures use run_steps; for canvas/WebGL game UIs use canvas_tap; for WebView DOM elements use webview_inspect to resolve coordinates first.
tap_with_fallbackSends a raw coordinate tap via the native backend (idb if installed, else a Maestro tapOn-point fallback). Useful for WKWebView game overlays where visual position differs from the DOM hit-test position. The Maestro fallback needs an app context: pass bundleId, or the foreground app is auto-detected. VERIFICATION: 'ok' is decided primarily by a change in the native accessibility element set before/after the tap (stable under animation/video). When no native backend is present it falls back to a screenshot byte-size delta (weak — animation can flip it). The result's oracle field reports which was used ('a11y-change' | 'screenshot-bytesize' | 'unverified'). For WebView-rendered targets the a11y tree won't change → oracle:'unverified'; confirm via webview_inspect. offsetStep defaults to 0 (tap the exact point); set it >0 only to deliberately probe nearby y-offsets on retry.
validate_flowReturns a trustworthy, evidenced verdict on whether a just-implemented flow works. Runs your visibility assertions through the oracle ladder (WebView-DOM > native a11y > Maestro; fail-closed on unverifiable) AND auto-checks app health: no recent crash, no error-level Metro logs, no failed (≥400) network requests. ok=true only when ALL assertions pass AND all applicable auto-checks are clean — never a bare 'looks ok'. State the expected outcome as assertions; this tool makes the AI's 'it works' auditable.
wait_for_elementPolls until an element/text is visible (via the oracle ladder), or fails on timeout. Use to act the instant the UI is ready instead of a blind sleep.
webview_evalEvaluates a JavaScript expression in a WebView's page context and returns the result. Use it to read web-app state (location.href, store values, feature flags, on-screen balances) or to assert conditions against the live DOM. Requires the app's WKWebView to be inspectable (isInspectable=true) — on by default in debug/staging builds, frequently disabled in production App Store builds.
webview_inspectLists embedded WebViews (WKWebView) on a booted simulator and, for the selected one, resolves a CSS selector to DOM elements WITH absolute on-screen tap coordinates. This is the answer to the 'WebView content is opaque' limitation of the coordinate tools: instead of eyeballing a screenshot, get tapX/tapY for a real DOM element and feed it straight into tap_on. Defaults to interactive elements when no selector is given. Requires the app's WKWebView to be inspectable (isInspectable=true) — on by default in debug/staging builds, frequently disabled in production App Store builds.
webview_navigateDrives a WebView's navigation: goto a URL, or back / forward / reload. On goto/reload it also installs Podium's Rive capture hook (unless captureRive:false) so Rive instances CONSTRUCTED AFTER navigation (the common SPA/lobby case: mount a screen → build the Rive canvas) are auto-discovered by rive_inspect without any app changes. Note: this transport injects via post-navigation eval (not document-start), so a Rive built synchronously during the initial hard load may be missed — expose window.__rive for that case. Requires the app's WKWebView to be inspectable (isInspectable=true) — on by default in debug/staging builds, frequently disabled in production App Store builds.
webview_networkCaptures HTTP traffic made INSIDE a WebView (fetch + XMLHttpRequest) and exports it as JSON or a redacted HAR 1.2 log. This is the network-debugging path for WebView-based apps — RN shells that host their UI in a WKWebView, where the API calls run in the web layer so metro_network (CDP Network domain) captures nothing. It injects a fetch/XHR recorder into the page, captures for durationMs while you drive the app, then returns request/response metadata (url, method, status, headers, timing). Only requests made AFTER capture starts are recorded. format:'har' emits a valid HAR 1.2 log (HAR-lite — no response bodies) openable in Chrome DevTools → Import HAR; pass saveTo to write the .har file. Sensitive headers (authorization/cookie/…) and request bodies are REDACTED by default — set redact:false to keep them (don't commit unredacted HAR: it leaks tokens). Requires the app's WKWebView to be inspectable (isInspectable=true) — on by default in debug/staging builds, frequently disabled in production App Store builds.
BEFORE YOU INSTALL
60 tools when we connected. rive_analyze, rive_e2e_init, rive_events, rive_fire, rive_inspect, rive_run, rive_set, rive_set_text and run_intent are available but undocumented; the documentation lists 51.
CONTEXT COST
Rated by the size of the 60 tool definitions that ride along on every turn, not by how many there are
INSTALLATION
git clone https://github.com/hoainho/podium-mcp.git
cd podium-mcp
npm install
npm run buildnpx -y podium-mcp{
"mcpServers": {
"podium": {
"command": "npx",
"args": [
"-y",
"podium-mcp"
]
}
}
}/plugin marketplace add github:hoainho/podium-mcp
/plugin install podium-mcp@podium{
"mcpServers": {
"podium": {
"type": "stdio",
"command": "node",
"args": [
"<YOUR_PATH_TO_PODIUM_MCP>/dist/index.js"
]
}
}
}