# Opensafari

> Smart. Fast. Parallel. iOS Safari automation MCP server via Xcode Simulator.

- **Type:** MCP server
- **Install:** `agentstack add mcp-shaun0927-opensafari`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [shaun0927](https://agentstack.voostack.com/s/shaun0927)
- **Installs:** 0
- **Category:** [Integrations](https://agentstack.voostack.com/c/integrations)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [shaun0927](https://github.com/shaun0927)
- **Source:** https://github.com/shaun0927/opensafari

## Install

```sh
agentstack add mcp-shaun0927-opensafari
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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](https://github.com/shaun0927/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](https://github.com/shaun0927/opensafari/issues/590); AppleScript fallback otherwise ([#491](https://github.com/shaun0927/opensafari/issues/491)) |
| WebView in Native | ✅ | ⚠️ Partial | ⚠️ | Bundle metadata (`appId`&#124;`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](https://github.com/shaun0927/opensafari/issues/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](https://github.com/shaun0927/opensafari/issues/491)) | ❌ ([XCUI focus](https://appium.io/docs/en/2.0/ecosystem/drivers/)) | ✅ `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-size ` elements with font-size ` 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.

```bash
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

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

```jsonc
// 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

```typescript
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:

```typescript
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:

```typescript
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](https://github.com/shaun0927/openchrome) can run simultaneously.

### Custom Port

Set the `OPENSAFARI_PROXY_PORT` environment variable to use a different device port:

```bash
# 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](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](https://github.com/shaun0927)
- **Source:** [shaun0927/opensafari](https://github.com/shaun0927/opensafari)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-shaun0927-opensafari
- Seller: https://agentstack.voostack.com/s/shaun0927
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
