Skip to main content
Glama
andesco

Safari Web Inspector Bridge

by andesco
README.md
# Safari Web Inspector Bridge

Safari Web Inspector Bridge is an MCP server that gives AI agents the same capabilities a developer gets from Safari Web Inspector — inspect, observe, and automate WKWebViews running on connected iOS devices.

## Architecture

```mermaid
graph TD
    Agent["AI Agent<br/>Claude, Codex, etc."]
    Bridge["MCP Server:<br /><code>safari-web-inspector-bridge</code>"]
    Proxy["managed child process: <code>ios-webkit-debug-proxy</code>"]
    Device["iOS Device<br/>WKWebView"]

    Agent <-->|"MCP <code>stdio</code>"| Bridge
    Bridge <-->|"WebSocket<br/>WebKit Inspector Protocol"| Proxy
    Proxy <-->|"usbmuxd<br/>(USB / Wi-Fi)"| Device

    style Agent fill:#f0f0f0
    style Bridge fill:#f0f0f0
    style Proxy fill:#f0f0f0
    style Device fill:#f0f0f0
```

The server spawns `ios-webkit-debug-proxy` as a child process, connects to the WebKit Inspector Protocol over WebSocket, and exposes everything as MCP tools. The proxy is managed for its full lifecycle -- started on init, health-checked, auto-restarted on crash, and killed on shutdown.

### Proxy Lifecycle

```mermaid
stateDiagram-v2
    [*] --> Checking: Server starts
    Checking --> Spawning: Binary found
    Checking --> Error: Binary missing
    Spawning --> HealthCheck: Process started
    HealthCheck --> Ready: localhost:9221 responds
    HealthCheck --> Error: Timeout (10s)
    Ready --> Crashed: Process exits
    Crashed --> Spawning: Restart (once)
    Crashed --> Error: Already retried
    Ready --> Stopped: Graceful shutdown
    Stopped --> [*]
```

### 13 MCP Tools

**Discovery:** `list_devices` | `list_inspectable_pages` | `connect`

**Observation:** `get_url` | `get_dom` | `get_network_log` | `get_console_log` | `screenshot`

**Automation:** `navigate` | `execute_javascript` | `click_element` | `type_text` | `wait_for`

## Prerequisites

- **`macOS`** required for `usbmuxd` and iOS device connectivity
- **`ios-webkit-debug-proxy`**: `brew install ios-webkit-debug-proxy`
- **`iOS`**: Settings &rsaquo; Safari &rsaquo; Advanced &rsaquo; Web Inspector: enabled
- target app WKWebView must have `isInspectable = true`

## Installation

```bash
git clone https://github.com/andesco/safari-web-inspector-bridge.git
cd safari-web-inspector-bridge
npm install
npm run build
```

### Add to Claude Code

```bash
claude mcp add safari-web-inspector-bridge node /path/to/safari-web-inspector-bridge/dist/index.js
```

### Add to any MCP client

Add to your MCP configuration file:

```json
{
  "mcpServers": {
    "safari-web-inspector-bridge": {
      "command": "node",
      "args": ["/path/to/safari-web-inspector-bridge/dist/index.js"],
      "env": {
        "SWIB_NETWORK_CAPTURE": "true",
        "SWIB_CONSOLE_CAPTURE": "true"
      }
    }
  }
}
```

## Tools

### Device & Connection

| Tool | Parameters | Returns |
|------|-----------|---------|
| `list_devices` | _(none)_ | `[{ udid, name, os_version }]` |
| `list_inspectable_pages` | `device_udid?` string -- filter to one device | `[{ page_id, title, url, app_bundle_id, device_udid }]` |
| `connect` | `page_id` string -- from `list_inspectable_pages` | `{ connected, page_id, url, title, warnings? }` |

### Observation

| Tool | Parameters | Returns |
|------|-----------|---------|
| `get_url` | _(none)_ | `{ url }` |
| `get_dom` | `selector?` string -- CSS selector (default: `document.documentElement`); `outer_html?` boolean (default: `true`) -- outerHTML vs textContent | `{ html }` or `{ text }` |
| `get_network_log` | `clear?` boolean (default: `false`); `filter_url?` string -- regex; `filter_status?` string -- e.g. `"302"`, `"4xx"` | `[{ request_id, method, url, status, mime_type, response_headers, request_headers, redirected_from, redirected_to, timing, error }]` |
| `get_console_log` | `clear?` boolean (default: `false`); `level?` `"log" \| "warn" \| "error" \| "info"` | `[{ level, text, timestamp, source_url, line_number }]` |
| `screenshot` | _(none)_ | MCP image content (base64 PNG) |

### Automation

| Tool | Parameters | Returns |
|------|-----------|---------|
| `navigate` | `url` string | `{ url, title, status }` |
| `execute_javascript` | `expression` string; `await_promise?` boolean (default: `true`) | `{ result }` -- JSON-serialized return value |
| `click_element` | `selector` string -- CSS selector; `index?` number (default: `0`) -- which match to click | `{ clicked, selector, tag_name }` |
| `type_text` | `text` string; `selector?` string -- focus this element first | `{ typed: true }` |
| `wait_for` | One of: `selector?` string, `url_contains?` string, `network_idle?` number (ms); plus `timeout_ms?` number (default: `10000`) | `{ matched: true, elapsed_ms }` |

## Configuration

| Env Var | Default | Description |
|---------|---------|-------------|
| `SWIB_AUTO_CONNECT` | `false` | Auto-connect to the first inspectable page on startup |
| `SWIB_NETWORK_CAPTURE` | `true` | Capture network requests on connect |
| `SWIB_CONSOLE_CAPTURE` | `true` | Capture console messages on connect |
| `SWIB_PROXY_PORT` | `9222` | Starting port for `ios-webkit-debug-proxy` device ports |

> [!note]
> `SWIB_NETWORK_CAPTURE` and `SWIB_CONSOLE_CAPTURE` default to `true` — set to `false` to disable.
> `SWIB_AUTO_CONNECT` defaults to `false` — set to `true` to enable.

## Example Workflow

```mermaid
sequenceDiagram
    participant Agent as AI Agent
    participant Bridge as MCP Server
    participant Proxy as ios-webkit-debug-proxy
    participant Device as iOS WKWebView

    Agent->>Bridge: list_inspectable_pages()
    Bridge->>Proxy: GET /json (devices + pages)
    Proxy-->>Bridge: page_id: "1", title: "Banks"
    Bridge-->>Agent: [{ page_id, title, url, app_bundle_id }]

    Agent->>Bridge: connect({ page_id: "1" })
    Bridge->>Proxy: WebSocket connect
    Proxy->>Device: WebKit Inspector attach
    Device-->>Proxy: Connected
    Bridge-->>Agent: { connected: true }

    Agent->>Bridge: get_network_log({ filter_url: "scotiabank" })
    Bridge-->>Agent: [{ url, status: 302, redirected_to: "scotiabank://..." }]

    Agent->>Bridge: execute_javascript("document.title")
    Bridge->>Device: Runtime.evaluate
    Device-->>Bridge: "Banks"
    Bridge-->>Agent: { result: "Banks" }

    Agent->>Bridge: click_element({ selector: "button.next" })
    Bridge->>Device: Runtime.evaluate (querySelector + click)
    Device-->>Bridge: clicked
    Bridge-->>Agent: { clicked: true, tag_name: "button" }

    Agent->>Bridge: wait_for({ url_contains: "/dashboard" })
    loop Poll until match
        Bridge->>Device: Runtime.evaluate (location.href)
        Device-->>Bridge: current URL
    end
    Bridge-->>Agent: { matched: true, elapsed_ms: 1230 }

    Agent->>Bridge: screenshot()
    Bridge->>Device: Page.snapshotRect
    Device-->>Bridge: base64 PNG
    Bridge-->>Agent: image content
```

## Development

```bash
npm run build        # Compile TypeScript to dist/
npm run dev          # Watch mode (tsc --watch)
npm test             # Run tests (vitest)
npm run test:watch   # Watch mode tests
npm start            # Run the MCP server (node dist/index.js)
```

### Project structure

```
src/
  index.ts               # Entry point, server setup, lifecycle
  types.ts               # Interfaces and config loader
  proxy-manager.ts       # Spawns and manages ios-webkit-debug-proxy
  device-discovery.ts    # Queries proxy for devices and pages
  webkit-connection.ts   # WebSocket connection to WebKit Inspector Protocol
  network-buffer.ts      # Ring buffer for network request entries (1000 max)
  tools/
    device-tools.ts      # list_devices, list_inspectable_pages, connect
    observation-tools.ts # get_url, get_dom, get_network_log, get_console_log, screenshot
    automation-tools.ts  # navigate, execute_javascript, click_element, type_text, wait_for
```

TDQS

A3.5/5.0

Scored across 14 tools

Disambiguation5/5

Each tool has a clearly distinct role: enumeration (list_devices, list_inspectable_pages), connection, state retrieval (get_url, get_dom, get_network_log, get_console_log), actions (navigate, execute_javascript, click_element, type_text, wait_for), and debugging (screenshot, debug_protocol). No two tools target the same resource or action in an ambiguous way.

Naming Consistency4/5

Most tools follow a consistent verb_noun pattern (list_devices, get_url, click_element). A few single-word verbs (connect, navigate, screenshot) deviate slightly but are still clear and fit the overall imperative style. No mixed naming conventions like camelCase or inconsistent prefixes.

Tool Count5/5

14 tools is well within the ideal range for a comprehensive browser automation/inspection server. Each tool covers a necessary aspect of the workflow without redundancy or bloat.

Completeness4/5

The toolset covers the core lifecycle of inspecting and automating WKWebViews: discovery, connection, navigation, DOM access, interaction, waiting, and logging. A minor gap is lack of an explicit clear operation for network/console logs despite descriptions mentioning 'last clear', but this can be worked around with reconnection.

Maintenance

ActivityInactive
ResponsivenessNo issues