Install
$ agentstack add mcp-appiumtestdistribution-appclaw ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ● Shell / process execution Used
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
AppClaw
AI-powered mobile automation agent for Android and iOS. Tell it what to do in plain English — it figures out what to tap, type, and swipe.
You: "Send a WhatsApp message to Mom
saying good morning"
AppClaw:
Step 1: Open WhatsApp
Step 2: Search for Mom
Step 3: Open chat with Mom
Step 4: Type "good morning"
Step 5: Tap Send
Step 6: Done
✅ Goal completed in 6 steps.
Prerequisites
- Node.js 18+
- Device connected — USB, emulator, or simulator
- LLM API key from any supported provider (Anthropic, OpenAI, Google, Groq, or local Ollama)
Installation
From npm
npm install -g appclaw
Create a .env file in your working directory:
cp .env.example .env
Local development
git clone https://github.com/AppiumTestDistribution/appclaw.git
cd appclaw
npm install
cp .env.example .env
Edit .env based on your preferred mode:
Vision mode (recommended)
Screenshot-first mode using Stark (df-vision + Gemini) for element location. Requires a Gemini API key.
LLM_PROVIDER=gemini
LLM_API_KEY=your-gemini-api-key
LLM_MODEL=gemini-3.1-flash-lite
AGENT_MODE=vision
DOM mode
Uses XML page source to find elements by accessibility ID, xpath, etc. No vision needed — works with any LLM provider.
LLM_PROVIDER=gemini # or anthropic, openai, groq, ollama
LLM_API_KEY=your-api-key
AGENT_MODE=dom
Usage
Platform & device selection
AppClaw supports both Android and iOS (simulators + real devices). On macOS, you'll get an interactive prompt to choose. For CI or to skip prompts, use flags:
# Android (default — no flags needed)
appclaw "Open Settings"
# iOS Simulator (auto-selects the booted simulator)
appclaw --platform ios --device-type simulator "Open Settings"
# iOS Simulator — pick by name
appclaw --platform ios --device-type simulator --device "iPhone 17 Pro" "Open Settings"
# iOS Real Device — pick by UDID
appclaw --platform ios --device-type real --udid 00008120-XXXX "Open Settings"
# Env vars work too (great for .env or CI)
PLATFORM=ios DEVICE_TYPE=simulator appclaw "Open Settings"
> Tip: If only one simulator is booted, it's auto-selected — no --udid needed.
Agent mode (LLM-driven)
# Interactive mode (prompts for platform + goal)
appclaw
# Pass goal directly
appclaw "Open Settings"
appclaw "Search for cats on YouTube"
appclaw "Turn on WiFi"
appclaw "Send hello on WhatsApp to Mom"
# Or with npx (no global install)
npx appclaw "Open Settings"
When running from a local clone, use npm start instead:
npm start
npm start "Open Settings"
Export a replayable test — pass --export and the agent's trajectory is written as a runnable vitest spec when the goal completes:
# Default path: $EXPORT_DIR/.test.ts (defaults to .appclaw/exports/)
appclaw --export "Open YouTube and search for Appium 3.0"
# Bare filename → EXPORT_DIR/
appclaw --export youtube.test.ts "Open YouTube and search for Appium 3.0"
# Path with directory hint → used verbatim
appclaw --export tests/e2e/youtube.test.ts "Open YouTube"
# Override the directory for one run
appclaw --export-dir tests/recorded --export youtube.test.ts "Open YouTube"
# Or set it persistently in .env
echo 'EXPORT_DIR=tests/recorded' >> .env
The export drops the wrong-direction branch when verification rejected a done, preserves the auto-launched app as the first step, and translates internal agent tools back to natural language so the generated test reads like English. See the [SDK section](#sdk-typescript--javascript) below for what the test file looks like.
YAML flows (no LLM needed)
Run declarative automation steps from a YAML file — fast, repeatable, zero LLM cost:
appclaw --flow examples/flows/google-search.yaml
Flows support both structured and natural language syntax:
Structured:
appId: com.android.settings
name: Turn on WiFi
---
- launchApp
- wait: 2
- tap: 'Connections'
- tap: 'Wi-Fi'
- done: 'Wi-Fi turned on'
Natural language:
name: YouTube search
---
- open YouTube app
- click on search icon
- type "Appium 3.0" in the search bar
- perform search
- scroll down until "TestMu AI" is visible
- verify video from TestMu AI is visible
- done
Supported natural language patterns include: open , click/tap , type "text", scroll up/down, swipe left/right, scroll down until "X" is visible, wait N seconds, go back, press home, verify/assert is visible, press enter, and done. Questions like "whats on the screen?" or "how many items are there?" are answered via vision without executing any action.
Parallel & suite runs
Run the same flow on N devices simultaneously, or distribute a suite of flows across N workers:
Same flow, N devices — add parallel: N to the flow's metadata:
name: youtube_parallel
platform: android
parallel: 2
---
- open YouTube app
- search for "Appium 3.0"
- assert "TestMu AI" is visible
- done
appclaw --flow youtube.yaml # spins up 2 devices, runs flow on both concurrently
Suite: different flows, N workers — a suite YAML lists flows and a worker count:
name: youtube_suite
platform: android
parallel: 2
flows:
- flows/login.yaml
- flows/search.yaml
- flows/playback.yaml
appclaw --flow youtube-suite.yaml # 2 devices pull from queue until all 3 flows finish
The VS Code extension shows a live multi-device grid — each device card updates in real time with a per-device step log, progress bar, and pass/fail result. Failed flows can be re-run with Re-run Failed from the summary notification.
Playground (interactive REPL)
Build YAML flows interactively on a real device — type commands and watch them execute live:
appclaw --playground
# iOS simulator
appclaw --playground --platform ios --device-type simulator
# Specific device
appclaw --playground --platform ios --device-type simulator --device "iPhone 17 Pro"
Features:
- Type natural-language commands that execute immediately on the device
- Steps accumulate as you go
- Export to a YAML flow file or a runnable SDK test (vitest spec) — format picked from the file extension
- Slash commands:
/help,/steps,/export,/clear,/device,/disconnect
Export formats — /export dispatches by extension:
> /export my-flow.yaml # YAML flow (default behaviour)
> /export tests/youtube.test.ts # SDK vitest spec
> /export youtube.test.ts # bare filename → EXPORT_DIR/youtube.test.ts
Bare filenames for SDK tests land in EXPORT_DIR (default .appclaw/exports); paths with a directory hint are used verbatim. YAML files stay in the current directory regardless.
SDK (TypeScript / JavaScript)
Drive a device programmatically from a vitest / jest / mocha test. The SDK exposes the same natural-language layer as the playground, so steps you build interactively can be lifted into a test file as-is.
import { AppClaw, AppClawAssertionError } from 'appclaw';
import { describe, it } from 'vitest';
import 'dotenv/config';
describe('YouTube smoke', () => {
it('searches and verifies a result', async () => {
const app = new AppClaw({
provider: 'gemini',
apiKey: process.env.LLM_API_KEY,
platform: 'android',
agentMode: 'vision',
video: true, // record screen, embed in report
mcpDebug: false, // silence appium-mcp stderr noise
});
await app.run('open YouTube app');
await app.run('tap search icon');
await app.run('type "Appium 3.0"');
await app.run('tap first result');
// Throws AppClawAssertionError on failure — vitest marks the test red.
// The error message includes the original claim, the LLM's reason (in
// vision mode), and a snapshot of visible texts (in DOM mode).
await app.verify('the video by TestMu AI is visible');
await app.teardown(); // closes the appium session, writes the report
}, 120_000);
});
Three execution surfaces:
| Method | Use when | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | app.run(instruction, options?) | One natural-language step. Returns { success, action, message } — does NOT throw on failure. options overrides wait/scroll config for this call only. | | app.verify(claim) | Assertion. Throws AppClawAssertionError so the test framework records a failure. | | app.runFlow(path) | Execute a YAML flow file end-to-end. | | app.runGoal(goal, opts?) | Hand the goal to the LLM agent (same as CLI agent mode). Supports exportPath (see below). |
Recording goal runs as replayable tests — pass exportPath to runGoal() and the agent's trajectory is written as a runnable vitest spec when it finishes:
await app.runGoal('open YouTube, search for Appium 3.0, play first result', {
exportPath: 'tests/e2e/youtube.test.ts',
});
The exported file replays via app.run(...) calls — no LLM cost, no agent loop. The exporter:
- Prepends the synthetic
launch_appstep the preprocessor handled (so the launch isn't missing from the replay) - Drops the entire branch before a rejected
donedecision — the recovery path the agent took after a verification failure is the only one preserved - Translates internal agent tools (
find_and_click,find_and_type, etc.) back into natural language so the test reads like English
Read the four caveats embedded in the generated file header before running it in CI — replays don't inherit the agent's safety net.
Per-command overrides — app.run() takes an optional second argument that wins over the instance defaults for that one step. Useful for a slow screen that needs a longer wait, or a tight list that needs a shorter scroll:
// Wait up to 20s for this specific (slow-loading) screen
await app.run('click on Dashboard', { waitTimeout: 20000 });
// Scroll a short distance, up to 5 times, to find an item
await app.run('scroll down until Karma is visible', { scrollMode: 'short', scrollTimes: 5 });
// A single full-screen swipe
await app.run('swipe up', { scrollMode: 'full' });
| RunOptions field | Purpose | | ------------------ | --------------------------------------------------------------------------------------- | | waitTimeout | Implicit-wait timeout (ms) for this command's target element. Overrides instance value. | | waitInterval | Poll cadence (ms) for this command's implicit wait. | | scrollMode | Scroll/swipe distance: short (~30%) / medium (~60%) / full (~90%) of the screen. | | scrollTimes | Repeat count (plain swipe) or max scroll attempts (scroll … until …). |
scrollMode / scrollTimes can also be set on the constructor as instance-wide defaults.
Constructor options (all optional — env vars fall through):
| Option | Default | Purpose | | -------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | provider | gemini | LLM provider (anthropic / openai / gemini / groq / ollama) | | apiKey | env | API key for the chosen provider | | model | (auto) | Model ID override | | platform | android | Target platform | | deviceUdid | (auto) | Pin to a specific device — required when running tests in parallel | | agentMode | dom | dom or vision — vision-mode verify() failures include the LLM's reason | | waitTimeout | 10000 | Implicit wait (ms) — each action polls until its target is on screen before acting, so you don't need wait … steps between calls. 0 = fail-fast | | waitInterval | 300 | Poll cadence (ms) for waitTimeout | | scrollMode | (engine ~60%) | Default scroll/swipe distance: short / medium / full. Override per call via run() | | scrollTimes | (parsed) | Default scroll/swipe repeat / max-scroll count. Override per call via run() | | video | false | Record screen and embed in the auto-generated report | | report | true | Write an HTML report to .appclaw/runs/ on teardown | | mcpDebug | env / false | Stream [appium-mcp] subprocess logs. Overrides MCP_DEBUG=1 | | silent | false | Suppress the per-step ✓ #N tap "label" log lines. Default off — SDK consumers see device activity to match the playground UX. |
TypeScript types — the package ships full type declarations (package.json → types: dist/sdk/index.d.ts), so editors give autocomplete on every option and tsc rejects typos before a test runs. The public types are importable by name:
import { AppClaw, AppClawStepError, AppClawAssertionError } from 'appclaw';
import type { AppClawOptions, RunOptions, ScrollDistance, RunResult, FlowResult } from 'appclaw';
Definitions:
/** How far each scroll/swipe travels, as a fraction of the screen. */
type ScrollDistance = 'short' | 'medium' | 'full'; // ~30% / ~60% / ~90%
/** Constructor config — every field optional; unset falls back to env / defaults. */
interface AppClawOptions {
provider?: 'anthropic' | 'openai' | 'gemini' | 'groq' | 'ollama';
apiKey?: string;
model?: string;
platform?: 'android' | 'ios';
deviceUdid?: string;
agentMode?: 'dom' | 'vision';
maxSteps?: number;
stepDelay?: number;
waitTimeout?: number; // implicit-wait timeout (ms), default 10000
waitInterval?: number; // poll cadence (ms), default 300
scrollMode?: ScrollDistance;
scrollTimes?: number;
silent?: boolean;
failOnError?: boolean;
report?: boolean;
reportName?: string;
video?: boolean;
mcpTransport?: 'stdio' | 'sse';
mcpHost?: string;
mcpPort?: number;
mcpDebug?: boolean;
}
/** Per-command overrides — the optional 2nd arg to `app.run()`. */
interface RunOptions {
wai
…
## Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [AppiumTestDistribution](https://github.com/AppiumTestDistribution)
- **Source:** [AppiumTestDistribution/AppClaw](https://github.com/AppiumTestDistribution/AppClaw)
- **License:** Apache-2.0
- **Homepage:** https://appclaw.in/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.