Skip to content

Commit 9c443ef

Browse files
feat(ios): complete iOS integration with docs and exports
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 251e4bd commit 9c443ef

12 files changed

Lines changed: 435 additions & 131 deletions

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,50 @@ yarn build && yalc publish
14541454
yalc add @metamask/client-mcp-core
14551455
```
14561456

1457+
## iOS Simulator Support (Experimental)
1458+
1459+
### Overview
1460+
1461+
This package includes experimental support for automating MetaMask Mobile on iOS simulators using XCUITest. The same tool interface (`mm_click`, `mm_type`, etc.) works on both browser extensions and iOS apps through the `IPlatformDriver` abstraction.
1462+
1463+
### Architecture
1464+
1465+
- `IPlatformDriver` — Platform-agnostic interface for element interaction, discovery, and screenshots
1466+
- `PlaywrightPlatformDriver` — Browser automation via Playwright (default)
1467+
- `IOSPlatformDriver` — iOS simulator automation via XCUITest HTTP server
1468+
- `XCUITestClient` — HTTP client for the XCUITest runner
1469+
1470+
### Prerequisites
1471+
1472+
See [docs/ios-setup.md](docs/ios-setup.md) for setup instructions.
1473+
1474+
### Platform Support Matrix
1475+
1476+
| Tool | Browser | iOS |
1477+
| ------------------------- | --------------- | --------------- |
1478+
| mm_click |||
1479+
| mm_type |||
1480+
| mm_wait_for |||
1481+
| mm_screenshot |||
1482+
| mm_accessibility_snapshot |||
1483+
| mm_list_testids |||
1484+
| mm_describe_screen |||
1485+
| mm_get_state |||
1486+
| mm_build | ✅ (capability) | ✅ (capability) |
1487+
| mm_seed_contract | ✅ (capability) | ✅ (capability) |
1488+
| mm_clipboard || ❌ (CDP) |
1489+
| mm_switch_to_tab || ❌ (tabs) |
1490+
| mm_close_tab || ❌ (tabs) |
1491+
| mm_wait_for_notification || ❌ (tabs) |
1492+
1493+
### Usage
1494+
1495+
To launch an iOS session, set `platform: 'ios'` in the launch input:
1496+
1497+
```typescript
1498+
{ platform: 'ios', simulatorDeviceId: '<UDID>', appBundlePath: '/path/to/MetaMask.app' }
1499+
```
1500+
14571501
## License
14581502

14591503
MIT

scripts/test-ios-integration.sh

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
#!/usr/bin/env bash
2+
# iOS Integration Test Script
3+
#
4+
# Runs a simple end-to-end integration test for the iOS platform support.
5+
# This script is macOS-only and requires:
6+
# - Xcode 15+ with iOS simulator runtimes
7+
# - A booted simulator (or one will be booted)
8+
# - The XCUITest runner built via scripts/build-ios-runner.sh
9+
#
10+
# This script is NOT intended for CI — it requires a macOS machine with
11+
# Xcode and simulator access.
12+
#
13+
# Usage:
14+
# ./scripts/test-ios-integration.sh [--device-udid <UDID>]
15+
#
16+
# If --device-udid is not provided, the script will use the first booted
17+
# simulator or boot the first available iPhone simulator.
18+
19+
set -euo pipefail
20+
21+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
22+
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
23+
CACHE_DIR="$HOME/.metamask-mcp/ios-runner"
24+
DERIVED_DATA="$CACHE_DIR/DerivedData"
25+
RUNNER_PID=""
26+
RUNNER_PORT=""
27+
DEVICE_UDID=""
28+
29+
RED='\033[0;31m'
30+
GREEN='\033[0;32m'
31+
YELLOW='\033[1;33m'
32+
NC='\033[0m'
33+
34+
cleanup() {
35+
echo ""
36+
echo "🧹 Cleaning up..."
37+
38+
if [ -n "$RUNNER_PID" ] && kill -0 "$RUNNER_PID" 2>/dev/null; then
39+
echo " Stopping runner (PID: $RUNNER_PID)..."
40+
kill "$RUNNER_PID" 2>/dev/null || true
41+
wait "$RUNNER_PID" 2>/dev/null || true
42+
fi
43+
44+
echo -e "${GREEN}✓ Cleanup complete${NC}"
45+
}
46+
47+
trap cleanup EXIT
48+
49+
parse_args() {
50+
while [[ $# -gt 0 ]]; do
51+
case $1 in
52+
--device-udid)
53+
DEVICE_UDID="$2"
54+
shift 2
55+
;;
56+
*)
57+
echo "Unknown option: $1"
58+
echo "Usage: $0 [--device-udid <UDID>]"
59+
exit 1
60+
;;
61+
esac
62+
done
63+
}
64+
65+
# Step 1: Validate prerequisites
66+
step_validate_prerequisites() {
67+
echo "=========================================="
68+
echo "Step 1: Validate Prerequisites"
69+
echo "=========================================="
70+
71+
if ! bash "$SCRIPT_DIR/validate-ios-prerequisites.sh"; then
72+
echo -e "${RED}Prerequisites check failed. Aborting.${NC}"
73+
exit 1
74+
fi
75+
echo ""
76+
}
77+
78+
# Step 2: Boot simulator if needed
79+
step_boot_simulator() {
80+
echo "=========================================="
81+
echo "Step 2: Boot Simulator"
82+
echo "=========================================="
83+
84+
if [ -n "$DEVICE_UDID" ]; then
85+
echo "Using provided device UDID: $DEVICE_UDID"
86+
BOOTED_STATE=$(xcrun simctl list devices | grep "$DEVICE_UDID" | grep -c "Booted" || true)
87+
if [ "$BOOTED_STATE" -eq 0 ]; then
88+
echo "Booting device $DEVICE_UDID..."
89+
xcrun simctl boot "$DEVICE_UDID"
90+
sleep 5
91+
else
92+
echo "Device already booted."
93+
fi
94+
else
95+
BOOTED_UDID=$(xcrun simctl list devices booted -j | python3 -c "
96+
import json, sys
97+
data = json.load(sys.stdin)
98+
for runtime, devices in data.get('devices', {}).items():
99+
for d in devices:
100+
if d.get('state') == 'Booted':
101+
print(d['udid'])
102+
sys.exit(0)
103+
" 2>/dev/null || true)
104+
105+
if [ -n "$BOOTED_UDID" ]; then
106+
DEVICE_UDID="$BOOTED_UDID"
107+
echo "Using already-booted simulator: $DEVICE_UDID"
108+
else
109+
echo "No booted simulator found. Booting first available iPhone..."
110+
DEVICE_UDID=$(xcrun simctl list devices available -j | python3 -c "
111+
import json, sys
112+
data = json.load(sys.stdin)
113+
for runtime, devices in data.get('devices', {}).items():
114+
if 'iOS' not in runtime:
115+
continue
116+
for d in devices:
117+
if 'iPhone' in d.get('name', ''):
118+
print(d['udid'])
119+
sys.exit(0)
120+
print('')
121+
" 2>/dev/null || true)
122+
123+
if [ -z "$DEVICE_UDID" ]; then
124+
echo -e "${RED}No available iPhone simulator found. Create one first.${NC}"
125+
exit 1
126+
fi
127+
128+
echo "Booting simulator: $DEVICE_UDID"
129+
xcrun simctl boot "$DEVICE_UDID"
130+
sleep 5
131+
fi
132+
fi
133+
134+
echo -e "${GREEN}✓ Simulator ready: $DEVICE_UDID${NC}"
135+
echo ""
136+
}
137+
138+
# Step 3: Build XCUITest runner
139+
step_build_runner() {
140+
echo "=========================================="
141+
echo "Step 3: Build XCUITest Runner"
142+
echo "=========================================="
143+
144+
bash "$SCRIPT_DIR/build-ios-runner.sh"
145+
echo ""
146+
}
147+
148+
# Step 4: Start XCUITest runner
149+
step_start_runner() {
150+
echo "=========================================="
151+
echo "Step 4: Start XCUITest Runner"
152+
echo "=========================================="
153+
154+
XCTESTRUN_FILE=$(find "$DERIVED_DATA" -name "*.xctestrun" -type f | head -1)
155+
if [ -z "$XCTESTRUN_FILE" ]; then
156+
echo -e "${RED}No .xctestrun file found. Build may have failed.${NC}"
157+
exit 1
158+
fi
159+
160+
echo "Starting runner with: $XCTESTRUN_FILE"
161+
162+
xcodebuild test-without-building \
163+
-xctestrun "$XCTESTRUN_FILE" \
164+
-destination "platform=iOS Simulator,id=$DEVICE_UDID" \
165+
> /tmp/ios-runner-output.log 2>&1 &
166+
RUNNER_PID=$!
167+
168+
echo "Runner started (PID: $RUNNER_PID). Waiting for port..."
169+
170+
TIMEOUT=60
171+
ELAPSED=0
172+
while [ $ELAPSED -lt $TIMEOUT ]; do
173+
if grep -q "AGENT_DEVICE_RUNNER_PORT=" /tmp/ios-runner-output.log 2>/dev/null; then
174+
RUNNER_PORT=$(grep -o 'AGENT_DEVICE_RUNNER_PORT=[0-9]*' /tmp/ios-runner-output.log | head -1 | cut -d= -f2)
175+
break
176+
fi
177+
178+
if ! kill -0 "$RUNNER_PID" 2>/dev/null; then
179+
echo -e "${RED}Runner process exited unexpectedly.${NC}"
180+
cat /tmp/ios-runner-output.log
181+
exit 1
182+
fi
183+
184+
sleep 1
185+
ELAPSED=$((ELAPSED + 1))
186+
done
187+
188+
if [ -z "$RUNNER_PORT" ]; then
189+
echo -e "${RED}Runner did not emit port within ${TIMEOUT}s.${NC}"
190+
cat /tmp/ios-runner-output.log
191+
exit 1
192+
fi
193+
194+
echo -e "${GREEN}✓ Runner ready on port: $RUNNER_PORT${NC}"
195+
echo ""
196+
}
197+
198+
# Step 5: Run test sequence
199+
step_run_tests() {
200+
echo "=========================================="
201+
echo "Step 5: Run Integration Tests"
202+
echo "=========================================="
203+
204+
local BASE_URL="http://127.0.0.1:$RUNNER_PORT/command"
205+
local TESTS_PASSED=0
206+
local TESTS_FAILED=0
207+
208+
run_test() {
209+
local test_name=$1
210+
local payload=$2
211+
local response
212+
213+
echo -n " Testing $test_name... "
214+
response=$(curl -s -X POST "$BASE_URL" \
215+
-H "Content-Type: application/json" \
216+
-d "$payload" \
217+
--max-time 30 2>&1)
218+
219+
if echo "$response" | python3 -c "import json,sys; d=json.load(sys.stdin); sys.exit(0 if d.get('ok') else 1)" 2>/dev/null; then
220+
echo -e "${GREEN}PASS${NC}"
221+
((TESTS_PASSED++))
222+
else
223+
echo -e "${RED}FAIL${NC}"
224+
echo " Response: $response"
225+
((TESTS_FAILED++))
226+
fi
227+
}
228+
229+
run_test "healthcheck" '{"command":"healthcheck"}'
230+
run_test "snapshot" '{"command":"snapshot"}'
231+
run_test "screenshot" '{"command":"screenshot"}'
232+
233+
echo ""
234+
echo "Results: ${TESTS_PASSED} passed, ${TESTS_FAILED} failed"
235+
236+
if [ "$TESTS_FAILED" -gt 0 ]; then
237+
echo -e "${RED}Some tests failed!${NC}"
238+
return 1
239+
fi
240+
241+
echo -e "${GREEN}✓ All integration tests passed${NC}"
242+
echo ""
243+
}
244+
245+
# Main
246+
main() {
247+
parse_args "$@"
248+
249+
echo ""
250+
echo "╔══════════════════════════════════════════╗"
251+
echo "║ iOS Integration Test Suite ║"
252+
echo "║ @metamask/client-mcp-core ║"
253+
echo "╚══════════════════════════════════════════╝"
254+
echo ""
255+
256+
step_validate_prerequisites
257+
step_boot_simulator
258+
step_build_runner
259+
step_start_runner
260+
step_run_tests
261+
262+
echo "=========================================="
263+
echo -e "${GREEN}Integration test complete ✓${NC}"
264+
echo "=========================================="
265+
}
266+
267+
main "$@"

src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22
export type * from './capabilities/types.js';
33
export * from './capabilities/context.js';
44

5+
// Platform Abstraction (TargetType excluded — already exported from mcp-server/utils)
6+
export type {
7+
PlatformType,
8+
ClickActionResult,
9+
TypeActionResult,
10+
PlatformScreenshotOptions,
11+
IPlatformDriver,
12+
} from './platform/index.js';
13+
export * from './platform/playwright-driver.js';
14+
15+
// iOS Platform Support
16+
export * from './platform/ios/index.js';
17+
export * from './platform/ios/ios-driver.js';
18+
export * from './platform/ios/xcuitest-client.js';
19+
export * from './platform/ios/simctl.js';
20+
export * from './platform/ios/runner-lifecycle.js';
21+
522
// MCP Server - Session Manager Interface
623
export * from './mcp-server/session-manager.js';
724

src/mcp-server/tools/clipboard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export async function handleClipboard(
3535
* @returns Promise resolving to clipboard operation result
3636
*/
3737
execute: async (context) => {
38-
const { page } = context;
38+
const page = context.page!;
3939
const cdpSession = await page.context().newCDPSession(page);
4040

4141
try {

0 commit comments

Comments
 (0)