Install
$ agentstack add mcp-shaun0927-opensafari ✓ 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 Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● 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.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
OpenSafari
Smart. Fast. Parallel. iOS Safari automation MCP server via Xcode Simulator.
Headless mobile QA automation — drive real Safari, Flutter, and native iOS apps on Xcode Simulator without stealing your mouse or requiring Simulator.app focus.
How OpenSafari compares
| | OpenSafari | Playwright WebKit | BrowserStack | Manual Testing | |---|:---:|:---:|:---:|:---:| | Engine | Real Safari (Xcode Sim) | Bundled WebKit (approximation) | Real devices (cloud) | Real devices | | Protocol | WebKit Remote Debugging (direct) | Playwright API (wrapper) | Proprietary | N/A | | iOS Fidelity | exact | close but diverges | exact | exact | | Parallel sessions | N simulators | N browsers | limited by plan | 1 device | | Login persistence | built-in (real Safari cookies) | manual | manual | manual | | LLM integration | MCP native | none | none | none | | Cost | free (Xcode) | free | $29+/mo | device cost | | iOS-specific QA | auto-detect (zoom, safe area, keyboard) | none | manual | manual |
> tl;dr — OpenSafari controls the real Safari inside Xcode Simulator via WebKit Remote Debugging Protocol — the same way OpenChrome controls real Chrome via CDP. No middleware, no bundled browsers. Just direct protocol access to the actual Safari.app.
Headless Capabilities
OpenSafari runs fully headless on CI — no display server, no mouse focus, no Simulator.app window required. See [docs/headless-architecture.md](docs/headless-architecture.md) for the full technical design. For Universal Link channel recipes (Notes paste-and-tap, captureLogs, channel matrix), see [docs/recipes/universal-link-channels.md](docs/recipes/universal-link-channels.md).
| Scenario | Query (AX Tree) | Input (Tap/Type) | Headless | Backend | |---|---|---|---|---| | Safari (Web) | ✅ | ✅ | ✅ | WebKit Remote Debug | | Flutter App | ✅ | ✅ | ✅ | FlutterVMInputBackend | | Native iOS App (Xcode ≤ 16) | ✅ | ✅ | ✅ | SimulatorKitHID (Tier 1) / simctl | | Native iOS App (Xcode 26+, element-targeted app_tap_element / app_type_element) | ✅ | ✅ | ✅ | AX-press (Tier 1.5) when the element advertises AXPress — see [docs/headless-architecture.md](docs/headless-architecture.md) | | Native iOS App (Xcode 26+, coordinate app_tap({x,y}) / app_swipe_native) | ✅ | ⚠️ Experimental (opt-in) | ⚠️ | PointerService opt-in via OPENSAFARI_ENABLE_POINTERSERVICE=1 — see #590; AppleScript fallback otherwise (#491) | | WebView in Native | ✅ | ⚠️ Partial | ⚠️ | Bundle metadata (appId|bundleId) requires a newer ios-webkit-debug-proxy build — older proxies fall back to URL-scheme heuristics. HTTPS WebViews (e.g. payment-return pages) must pass bundleId to app_webview_connect for bundle_match classification; without it they default to safari via url_scheme — see #592 |
> ✅ Supported and stable. ⚠️ Partially supported — see linked docs for current status and limitations.
Stability commitments (stable vs opt-in vs experimental) are catalogued in [docs/simhid-ios26-investigation.md#stability-commitments](docs/simhid-ios26-investigation.md#stability-commitments).
Headless input vs other iOS automation tools
| | OpenSafari | Appium | idb | XCUITest | |---|:---:|:---:|:---:|:---:| | Headless native input (no mouse focus, no Simulator.app activation) | ✅ AX-press + SimulatorKit HID (element-targeted tap & keys/buttons headless on Xcode 26+; coordinate-only tap/swipe pending #491) | ❌ (XCUI focus) | ✅ FBSimulatorHID | ❌ | | Works on Xcode 26+ (after simctl io input removal) | ✅ Safari, Flutter, and element-targeted native taps; ⚠️ coordinate-only native tap/swipe pending | ⚠️ driver-dependent | ✅ | ✅ | | Flutter native taps (no OS-level input) | ✅ Dart VM PointerDataPacket | ⚠️ 3rd-party plugin | ❌ | ❌ | | MCP / LLM integration | ✅ native | ❌ | ❌ | ❌ | | Private API dependency | SimulatorKit (documented, sentinel-guarded) | UIAutomation / XCUI | SimulatorKit | none |
> See [docs/private-apis.md](docs/private-apis.md) for the SimulatorKit contract, the daily sentinel CI that detects BC breaks, and the rollback plan if Apple changes symbols.
For CI setup recipes (GitHub Actions, Buildkite, GitLab CI), see [docs/ci-recipes.md](docs/ci-recipes.md).
Long-running MCP sessions are soak-tested nightly — see [memory-soak workflow](.github/workflows/memory-soak.yml).
What is OpenSafari?
Imagine testing your e-commerce site on iPhone 17e, iPhone 17, iPhone 17 Pro Max, and iPad — all at the same time, already logged in, with an AI agent that automatically finds iOS-specific bugs. That's OpenSafari.
You: Check our checkout flow for mobile issues across all iPhone sizes
AI: [4 parallel simulators, all devices simultaneously]
iPhone 17e: ⚠ Credit card input triggers iOS auto-zoom (font-size: 14px)
iPhone 17: ✓ Layout OK
iPhone 17 PM: ⚠ "Place Order" button only 38×32px (below 44px touch target)
iPad: ⚠ Shipping form hidden behind keyboard when focused
Time: 8s | All screenshots captured and analyzed.
| | Manual QA | OpenSafari | |---|:---:|:---:| | 4-device test | ~30 min | ~10s (parallel) | | Login | Each device, each time | Never (persisted) | | iOS bug detection | Human eye | Automatic (LLM vision) | | Consistency | Varies by tester | Deterministic |
Core Architecture
OpenSafari follows the same direct-protocol philosophy as OpenChrome:
OpenChrome: CDPClient → Chrome DevTools Protocol → Real Chrome
OpenSafari: SafariClient → WebKit Remote Debugging Protocol → Real Safari in Simulator
No middleware. No bundled browsers. Direct connection.
Claude Code / AI Agent (MCP Client)
│
│ JSON-RPC (stdio / HTTP)
▼
┌─────────────────────────────────────┐
│ OpenSafari MCP Server │
│ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ Simulator │ │ Safari │ │
│ │ Manager │ │ Client │ │
│ │ (simctl) │ │ (WebKit │ │
│ │ │ │ Protocol) │ │
│ └──────┬──────┘ └──────┬───────┘ │
│ │ │ │
│ boot/shutdown navigate/click │
│ rotate/appear screenshot │
│ multi-device DOM/JS/cookies │
│ │ │ │
│ ┌──────▼────────────────▼───────┐ │
│ │ Xcode Simulator(s) │ │
│ │ ┌────────┐ ┌────────┐ │ │
│ │ │ iPhone │ │ iPhone │ ... │ │
│ │ │ SE │ │ 16 PM │ │ │
│ │ │ Safari │ │ Safari │ │ │
│ │ └────────┘ └────────┘ │ │
│ └───────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ Shared Infrastructure │ │
│ │ (from OpenChrome) │ │
│ │ • Security (sanitizer, │ │
│ │ domain guard, audit) │ │
│ │ • Watchdog (event loop, │ │
│ │ disk, health endpoint) │ │
│ │ • Orchestration (workflow │ │
│ │ engine, parallel workers)│ │
│ │ • Session persistence │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
Key Features
1. Real Safari, Real Bugs
OpenSafari controls the actual Safari.app inside Xcode Simulator via WebKit Remote Debugging Protocol — not a bundled approximation. Every iOS-specific quirk is faithfully reproduced:
- iOS auto-zoom on inputs with
font-sizeelements with font-sizeunless you intentionally pass--http-insecure-localfor local-only testing. Provide the token withOPENSAFARIHTTPTOKENor--http-token`; OpenSafari never prints the token value.
Browser CORS for /mcp is restricted to local origins (localhost, 127.0.0.1, ::1) plus any comma-separated origins passed with --http-allow-origin. Use --http-host only when you intentionally need a non-loopback bind.
HTTP mode also blocks high-risk MCP tools that execute page/app code or move authentication material: javascript, flutter_evaluate, auth_save, auth_restore, and cookies. Stdio mode is unchanged. To intentionally expose those tools over HTTP, start with --http-enable-high-risk-tools or set OPENSAFARI_HTTP_ENABLE_HIGH_RISK_TOOLS=1; allowed and blocked high-risk HTTP calls are audit-logged with sensitive arguments redacted.
OPENSAFARI_HTTP_TOKEN="$OPENSAFARI_HTTP_TOKEN" opensafari serve --http 3100
curl -H "Authorization: Bearer $OPENSAFARI_HTTP_TOKEN" \
-H "Content-Type: application/json" \
http://127.0.0.1:3100/mcp \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
MCP Client Configuration
// Claude Code: .mcp.json
{
"mcpServers": {
"opensafari": {
"command": "npx",
"args": ["-y", "opensafari-mcp", "serve"]
}
}
}
// Claude Desktop: claude_desktop_config.json
{
"mcpServers": {
"opensafari": {
"command": "npx",
"args": ["-y", "opensafari-mcp", "serve", "--all-tools"]
}
}
}
Tool Tiers
Tools are organized into 3 tiers for progressive disclosure. The default surface is Tier 1 (12 tools, ~4.5 KB of schema); higher tiers are opt-in so the tools/list payload stays small for MCP clients that load every tool schema into model context.
| Tier | Tools | Access | |------|-------|--------| | Tier 1 | navigate, screenshot, click, type, scroll, readpage, querydom, javascript, cookies, deviceboot, deviceshutdown, diagnose | Default | | Tier 2 | inspect, waitfor, press, swipe, longpress, devicelist, app native tools, flutter tools, qa detectors, network tools | OPENSAFARI_TOOL_TIER=2 or setTier(2) | | Tier 3 | authsave, authrestore, authlist, batch, workflow, qafullaudit, crossviewportcompare | --all-tools or OPENSAFARI_TOOL_TIER=3 |
Every registered tool must have an explicit entry in src/config/tool-tiers.ts; tools without one fall back to Tier 3 (enforced by tests/unit/tool-tier-drift.test.ts).
> Migration note (0.7.x): earlier releases exposed Tier 2 (132 tools, ~22k tokens of schema) by default. If you relied on Tier 2 tools being listed without configuration, set OPENSAFARI_TOOL_TIER=2 in the server environment or pass --all-tools.
Programmatic API
import { createServer } from 'opensafari-mcp';
// Start with the default Tier 1 tool surface over stdio
await createServer();
// Expose all tool tiers immediately (same as opensafari serve --all-tools)
await createServer({ allTools: true });
// Or start with HTTP transport
await createServer({
transport: 'http',
port: 3100,
authToken: process.env.OPENSAFARI_HTTP_TOKEN,
});
WebKitClient
Direct WebKit protocol access for custom automation:
import { WebKitClient } from 'opensafari-mcp';
const client = new WebKitClient({ host: 'localhost', port: 9322 });
await client.connect({ retries: 5, retryDelay: 2000 });
// Navigate and evaluate
await client.navigate({ url: 'https://example.com', waitUntil: 'load' });
const title = await client.evaluate('document.title');
// Screenshot (returns PNG buffer)
const png = await client.screenshot();
// Cookies
const cookies = await client.getCookies();
await client.setCookies([{ name: 'key', value: 'val', domain: '.example.com',
path: '/', expires: -1, httpOnly: false, secure: false }]);
// DOM interaction
await client.click('#submit-btn');
await client.type('#email-input', 'user@example.com');
await client.disconnect();
SimulatorManager
Programmatic simulator lifecycle control:
import { SimulatorManager } from 'opensafari-mcp';
const manager = new SimulatorManager();
// Boot a device
const device = await manager.boot('iPhone 17 Pro');
console.log(device.udid, device.state); // "XXXX-..." "Booted"
// Open Safari
await manager.openUrl(device.udid, 'https://example.com');
// List booted devices
const booted = await manager.listBooted();
// Shutdown
await manager.shutdown(device.udid);
Requirements
- macOS (Xcode Simulator is macOS only)
- Xcode with iOS Simulator runtime installed
- Node.js >= 18
- ios-webkit-debug-proxy —
brew install ios-webkit-debug-proxy
WebInspector Proxy Configuration
OpenSafari uses ios_webkit_debug_proxy to bridge WebKit Remote Debugging from Xcode Simulator. The proxy is auto-started by the device_boot tool — no manual setup is needed in most cases.
Default Ports
| Port | Purpose | |------|---------| | 9321 | Device list (HTML) — serves the proxy's device listing page | | 9322 | Device connection (JSON) — WebKit debugging targets for connected simulators |
Port 9322 is deliberately offset from Chrome DevTools (9222) so OpenSafari and OpenChrome can run simultaneously.
Custom Port
Set the OPENSAFARI_PROXY_PORT environment variable to use a different device port:
# Use port 9500 instead of the default 9322
OPENSAFARI_PROXY_PORT=9500 opensafari serve
Port resolution order:
- Explicit
portoption (programmatic use) OPENSAFARI_PROXY_PORTenvironment variable- Default: 9322
Multi-Session Usage
Multiple Claude Code sessions can share the same proxy. When a session detects a healthy proxy already running on its target port, it reuses it instead of starting a new one. When the owning session exits, only its own proxy is terminated — other sessions' proxies remain unaffected.
Input Backend Selection
OpenSafari dispatches native input (app_tap, app_swipe_native, app_scroll_native, app_double_tap, app_type_text, app_key_input) through a 5-tier fallback chain and surfaces the selected path in each tool result via a backend field. Coordinate and element-targeted tap tools also surface whether the interaction was verified by a post-action AX-tree check. Transport success alone is no longer treated as interaction success when no observable UI effect can be confirmed. app_tap_element now applies the same contract after it falls back from ax-press to a coordinate backend: callers get verified: false with effect: "verification_unavailable" when the AX proof is unavailable, or a typed TAP_NO_EFFECT result when the UI stays unchanged after the dispatched tap.
| Tier | Backend | Identifier | Headless? | When used | |------|---------|------------|-----------|-----------| | 0 | FlutterVMInputBackend | flutter-vm | Yes | Flutter app reachable over Dart VM Service + DDS | | 1 | SimulatorKitHIDInputBackend | simhid | Yes | Any app — sim-hid-bridge resolves and SimulatorKit.framework loads (covers Xcode 26+) | | 2 | SimctlInputBackend | simctl | Yes | Xcode ≤16 legacy path (where simctl io input is still available) | | 3 | WebKitInputBackend | webkit | Yes | Xcode 26+ with an active Safari / WebView connection | | 4 | AppleScriptInputBackend | applescript | No | Opt-in only — moves the mouse cursor and activates Simulator.app |
See [docs/headless-architecture.md](docs/headless-architecture.md) for the decision flowchart and the full scenario matrix. Tool responses also include _meta: { backendKind, headless, deviceId } so CI can assert _meta.headless === true.
Raw bridge consumers can now ask for the same foreground diagnostics directly:
dist/ax-bridge context --device [--expect-bundle ] [--require-match true]dist/sim-hid-bridge context [--expect-bundle ] [--require-match true]
dist/sim-hid-bridge tap|swipe also appends classification, verified, frontmost, and `expectedBundle
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: shaun0927
- Source: shaun0927/opensafari
- License: MIT
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.