Skip to main content
Glama

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 for the full technical design. For Universal Link channel recipes (Notes paste-and-tap, captureLogs, channel matrix), see 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

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.

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 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.

Long-running MCP sessions are soak-tested nightly — see memory-soak workflow.


Related MCP server: SilbercueSwift

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-size < 16px

  • position: fixed behavior with real software keyboard

  • 100vh viewport height inconsistencies with address bar

  • Safe area insets (notch, home indicator) on real device frames

  • color-scheme dark mode forced rendering

  • Touch target minimum size requirements (44×44px)

2. Parallel Multi-Device Testing

Test across multiple devices simultaneously with a single command:

opensafari serve --devices "iphone-17e,iphone-17,iphone-17-pro-max,ipad-pro"

# 4 simulators boot in parallel
# Each gets its own Safari instance + WebKit Protocol connection
# All share login state via cookie injection

3. Persistent Login Sessions

Log in once, test forever. Cookies and localStorage are extracted directly from the real Safari session:

# First time: login captured from real Safari
opensafari auth save --site myapp.com
  → Exports cookies via WebKit Protocol Network.getAllCookies
  → Saves to ~/.opensafari/auth/myapp.json

# Every subsequent run: auto-restored
opensafari serve
  → Injects cookies via Network.setCookie into each simulator's Safari
  → All simulators start already logged in

Auth profiles are JSON files stored under ~/.opensafari/auth/. Each profile contains the site name, capture timestamp, current URL, cookies, cookie domain groups, localStorage, and sessionStorage needed to restore login state. On POSIX systems, OpenSafari creates new auth profile directories as private 0700 directories and profile files as private 0600 files, then updates profiles with an atomic same-directory replacement.

Delete saved login state with opensafari auth delete myapp.com, or remove the matching ~/.opensafari/auth/myapp.com.json file.

4. iOS-Specific Auto-Detection

Built-in QA checks that run on real Safari — no approximation:

Check

What It Detects

Auto-Zoom Guard

<input> elements with font-size < 16px

Safe Area Validator

Content hidden behind notch or home indicator

Keyboard Overlap

Fixed elements covered by real software keyboard

Touch Target Audit

Clickable elements smaller than 44×44px

Dark Mode Diff

Visual differences via simctl ui appearance toggle

Viewport Compare

Layout breaks across different screen sizes

Scroll Lock Check

Body scroll not restored after modal close

5. MCP Native

Works with any MCP client — Claude Code, Cursor, VS Code, or custom agents:

// .mcp.json
{
  "mcpServers": {
    "opensafari": {
      "command": "npx",
      "args": ["-y", "opensafari-mcp", "serve"]
    }
  }
}

6. Shared DNA with OpenChrome

OpenSafari shares battle-tested infrastructure with OpenChrome:

Module

Source

Status

MCP Server core

OpenChrome

Shared

Transport (stdio/HTTP)

OpenChrome

Shared

Security (sanitizer, guard, audit)

OpenChrome

Shared

Watchdog (event loop, disk, health)

OpenChrome

Shared

Orchestration (workflow engine)

OpenChrome

Adapted

Simulator Manager

NEW

OpenSafari

Safari Client (WebKit Protocol)

NEW

OpenSafari

iOS QA Engine

NEW

OpenSafari


Tools

Core Tools (Tier 1)

Tool

Description

navigate

Open URL in real Safari

click

Tap element by CSS selector or coordinates

type

Type text into form elements

scroll

Scroll page in any direction

screenshot

Capture real Safari screen via WebKit Protocol

read_page

Extract visible text content

query_dom

CSS selector queries with element details

javascript

Execute JavaScript in page context via Runtime.evaluate

inspect

Element CSS, accessibility, and layout inspection

cookies

Get/set/clear real Safari cookies via Network domain

Device Management (Tier 1)

Tool

Description

device_list

List available simulator device types

device_boot

Boot a specific device (iPhone SE, 16, iPad, etc.)

device_shutdown

Shutdown simulator

device_rotate

Toggle portrait/landscape

appearance_toggle

Switch light/dark mode via simctl ui

device_network_set

Toggle host-level network state (online / offline / airplane) so native apps see real SocketException / NSURLErrorNotConnectedToInternet — see docs/tools/device-network.md

device_network_get

Read the current simulated network state set by device_network_set

App Lifecycle (Tier 2)

Tool

Description

app_launch

Launch app by bundle ID with optional args and env vars

app_terminate

Terminate a running app by bundle ID

app_activate

Bring app to foreground (launches if not running)

app_list_running

List running foreground apps with PIDs

app_context

Report the current mobile context and optionally guard on an expected bundle

app_reset

Reset app state: terminate, clear permissions, uninstall

app_notes_paste_and_tap_url

Reviewer-equivalent Universal Link tap: launches Notes.app, paste-injects the URL, waits for iOS Data Detector to produce an AXLink, and taps it — see docs/recipes/universal-link-channels.md

Auth Tools (Tier 3)

Tool

Description

auth_save

Capture cookies + localStorage from current session

auth_restore

Restore saved auth state into a simulator

auth_list

List saved auth profiles

Parallel & Orchestration (Tier 2)

Tool

Description

batch_screenshot

Capture same URL across all active devices

batch_execute

Run JS across all simulators in parallel

batch_navigate

Open same URL on all devices simultaneously

cross_viewport_compare

Side-by-side visual comparison across devices

iOS QA Engine (Tier 3)

Tool

Description

qa_auto_zoom

Detect inputs triggering iOS auto-zoom

qa_touch_targets

Find elements below 44×44px minimum

qa_safe_area

Check content behind notch/home indicator

qa_keyboard_overlap

Detect fixed elements hidden by real keyboard

qa_dark_mode

Compare light vs dark mode rendering

qa_full_audit

Run all QA checks and generate report


Flutter QA Workflow

OpenSafari drives Flutter apps running in the iOS Simulator using the same native-app tools used for UIKit/SwiftUI. The only wrinkle is that Flutter's accessibility tree is lazy — it only populates when an assistive technology (VoiceOver, XCTest) connects. OpenSafari auto-activates it so widget labels are queryable.

Auto-activation (no app changes required)

app_tree, app_query, app_inspect, app_tap_element, app_wait_for, and app_assert_element all call ensureSemanticsActive() before reading the accessibility tree. The activator:

  1. Quick-checks the tree — if it already has ≥5 nodes, semantics is already active.

  2. Toggles com.apple.Accessibility.AccessibilityEnabled via xcrun simctl spawn defaults write — this triggers Flutter's SemanticsBinding without enabling VoiceOver spoken feedback.

  3. Polls the tree (up to 3s) until it populates, or falls back gracefully if activation never succeeds.

// 1. Boot simulator and launch the Flutter app
await device_boot('iPhone 16');
await app_launch({ bundleId: 'com.example.flutterApp' });

// 2. Terminate Safari if it's running — its background elements can
//    dominate the macOS AX tree and hide Flutter widgets.
await app_terminate({ bundleId: 'com.apple.mobilesafari' });
await app_switch_app({ bundleId: 'com.example.flutterApp' });

// 3. Read the tree — Flutter Semantics nodes appear automatically.
const tree = await app_tree({ max_depth: 5 });

// 4. Query widgets by label, identifier, or role.
const button = await app_query({ label: 'Login' });
const email  = await app_query({ identifier: 'email-field' });

Making widgets queryable

Flutter API

Queryable via

Notes

Semantics(label: 'Login')

app_query({ label: 'Login' })

Preferred for human-readable labels

Semantics(identifier: 'login-btn')

app_query({ identifier: 'login-btn' })

Flutter 3.19+ — stable selector for tests

Plain Text('Counter: 42')

app_query({ text: 'Counter' })

Works when the text widget auto-synthesizes semantics

Key('login-btn')

Not queryable

Keys are a Flutter-internal reference — they do not surface to the native AX tree

Release builds

Approach A (simctl defaults write) works for both debug and release builds. If a release app still reports an empty tree, add the explicit opt-in to main.dart:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SemanticsBinding.instance.ensureSemantics(); // enable for OpenSafari QA
  runApp(const MyApp());
}

For debug/profile builds, flutter_connect + flutter_widget_tree additionally expose the Dart VM Service, which returns the full Flutter widget hierarchy (including render-tree nodes that never reach the native AX bridge).

See docs/troubleshooting.md for common failure modes (empty trees, missing labels, Safari shadowing). For stateful semantic mobile QA, selector quality, settle policy, and scenario v2 guidance, see docs/mobile-semantic-qa.md.


Quick Start

# Install
npm install -g opensafari-mcp

# Run (stdio mode — for MCP clients like Claude Code)
opensafari serve

# HTTP mode (binds to 127.0.0.1 and requires a bearer token for /mcp)
OPENSAFARI_HTTP_TOKEN="replace-with-a-random-token" opensafari serve --http 3100

# With all tool tiers exposed
opensafari serve --all-tools

# With specific devices auto-booted
opensafari serve --devices "iphone-17e,iphone-17-pro-max"

# With auth state
opensafari serve --auth ~/.opensafari/auth/mysite.json

HTTP transport security

HTTP mode listens on 127.0.0.1 by default. The /health endpoint is unauthenticated, but /mcp requires Authorization: Bearer <token> unless you intentionally pass --http-insecure-local for local-only testing. Provide the token with OPENSAFARI_HTTP_TOKEN or --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, read_page, query_dom, javascript, cookies, device_boot, device_shutdown, diagnose

Default

Tier 2

inspect, wait_for, press, swipe, long_press, device_list, app_* native tools, flutter_* tools, qa_* detectors, network_* tools

OPENSAFARI_TOOL_TIER=2 or setTier(2)

Tier 3

auth_save, auth_restore, auth_list, batch_*, workflow_*, qa_full_audit, cross_viewport_compare

--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<string>('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-proxybrew 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:

  1. Explicit port option (programmatic use)

  2. OPENSAFARI_PROXY_PORT environment variable

  3. 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 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 <udid> [--expect-bundle <bundle>] [--require-match true]

  • dist/sim-hid-bridge context <udid> [--expect-bundle <bundle>] [--require-match true]

dist/sim-hid-bridge tap|swipe also appends classification, verified, frontmost, and expectedBundleMatched to its JSON result so downstream QA can tell the difference between a clean in-app tap and a wrong-foreground outcome.

Example tool result:

{
  "status": "tapped",
  "x": 100,
  "y": 200,
  "deviceId": "…",
  "backend": "simhid",
  "verified": true,
  "effect": "subtree_changed",
  "_meta": { "backendKind": "simhid", "headless": true, "deviceId": "…" }
}

Raw dist/ax-bridge Contract

dist/ax-bridge is a Node.js wrapper that sits in front of the compiled Swift binary dist/ax-bridge-native (Mach-O). The wrapper intercepts --help / -h before argument validation, runs ensureSemanticsActive() for tree-read commands by default (opt out with --ensure-semantics off), then delegates every other invocation unchanged to dist/ax-bridge-native.

Bridge resolution order:

  1. dist/ax-bridge-native next to the script (standard installed layout)

  2. Swift interpreter fallback — only when dist/ax-bridge.swift is present

Contract: commands that cannot expose app content return a typed error code as JSON on stdout (exit 1) instead of an empty-success tree:

Code

Meaning

DEVICE_RESOLUTION_FAILED

Requested device not found / not booted

DEVICE_RESOLUTION_AMBIGUOUS

Multiple booted simulators match

DEVICE_WINDOW_NOT_FOUND

No AX window matched the requested device

DEVICE_CONTENT_ROOT_EMPTY

Window resolved but no app-semantics content (#40)

APP_CONTENT_NOT_EXPOSED

Tree is Simulator chrome only after bootstrap (#41)

EXPECTED_BUNDLE_MISMATCH

(context) Expected bundle not foreground

BRIDGE_NOT_FOUND

ax-bridge-native / ax-bridge.swift missing

AX_WRAPPER_FAILED

Wrapper-level unexpected error

BAD_ARGS

Invalid or missing CLI flags

UNKNOWN_COMMAND

Command not recognized

Example invocations:

# dump — success: full JSON accessibility tree on stdout, exit 0
node dist/ax-bridge dump --device booted
# dump — error: chrome-only tree after bootstrap, exit 1
# stdout: {"error":"...","code":"APP_CONTENT_NOT_EXPOSED"}

# query — success: matched elements on stdout, exit 0
node dist/ax-bridge query --device booted --label "Sign In"
# query — error: device not found, exit 1
# stdout: {"error":"...","code":"DEVICE_RESOLUTION_FAILED"}

# inspect — success: single element detail on stdout, exit 0
node dist/ax-bridge inspect --device booted --path "0/1/2"
# inspect — error: empty content root, exit 1
# stdout: {"error":"...","code":"DEVICE_CONTENT_ROOT_EMPTY"}

# press — success: {"ok":true,"code":"OK",...} on stdout, exit 0
node dist/ax-bridge press --device booted --path "0/1/2"

# context — success: foreground bundle info on stdout, exit 0
node dist/ax-bridge context --device booted
# context — expected bundle mismatch, exit 1
# stdout: {"error":"...","code":"EXPECTED_BUNDLE_MISMATCH"}

Note: The higher-level app_* MCP tools provide the same semantics activation plus richer heuristics (retry, partial-tree promotion, and cross-session device resolution). Use the raw bridge for downstream harnesses that prefer direct CLI access without the MCP layer.

Focus-theft protection (OPENSAFARI_ALLOW_FOCUS_INPUT)

Tier 3 is default-deny. On Xcode 26+ with no Safari connection, getInputBackend() throws HeadlessInputUnavailableError instead of silently moving the physical mouse cursor. To re-enable the legacy AppleScript/CGEvent fallback (for example, to automate a non-Safari native app), opt in with an environment variable:

# Only set this if you understand the consequences — it WILL move your
# mouse cursor and bring Simulator.app to the foreground.
OPENSAFARI_ALLOW_FOCUS_INPUT=1 opensafari serve

Accepted values are 1 and true; anything else is ignored. When the opt-in is honored, a one-time warning is logged to stderr at the first tool call.


Relationship to OpenChrome

OpenSafari is the Safari/iOS counterpart to OpenChrome. Same philosophy, same architecture — different browser.

OpenChrome

OpenSafari

Browser

Real Chrome

Real Safari (in Simulator)

Protocol

Chrome DevTools Protocol (CDP)

WebKit Remote Debugging Protocol

Client

CDPClient (puppeteer-core)

SafariClient (WebKit Protocol)

Execution

chrome --remote-debugging-port

xcrun simctl + WebKit debug socket

Use Case

Desktop web automation

Mobile web QA & debugging

Parallel

N tabs in 1 Chrome

N simulators, each with Safari

Login

Real Chrome sessions

Real Safari sessions

Together, they provide complete browser coverage — Chrome for desktop, Safari for iOS — both controlled by AI agents through MCP with direct protocol connections. No middleware, no bundled browsers.


Documentation

Document

Description

Getting Started

Setup guide and first steps

API Reference

Programmatic API documentation

Troubleshooting

Common issues and solutions

CI Integration

Using OpenSafari in CI pipelines

CI Recipes

Copy-paste GitHub Actions, Buildkite, and GitLab CI recipes

Memory Budget

Per-cache retention budgets and eviction policies

Diagnose Tool Reference

diagnose output schema and memory block reference

RFC: Native App Backend

Architecture RFC for native-app automation in Xcode Simulator

Native App Tool Surface

Proposed MCP tool surface for native-app automation

WebKit Protocol Research

WebKit Remote Debugging Protocol research notes


License

MIT


A
license - permissive license
-
quality - not tested
A
maintenance

Maintenance

Maintainers
14hResponse time
2dRelease cycle
37Releases (12mo)
Commit activity
Issues opened vs closed

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

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/shaun0927/opensafari'

If you have feedback or need assistance with the MCP directory API, please join our Discord server