Safari Web Inspector Bridge
# 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 › Safari › Advanced › 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
Scored across 14 tools
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.
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.
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.
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.