Safari Web Inspector Bridge — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Safari Web Inspector Bridge (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
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:#f0f0f0The 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.
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 --> [*]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
usbmuxd and iOS device connectivitybrew install ios-webkit-debug-proxyisInspectable = truegit clone https://github.com/andesco/safari-web-inspector-bridge.git
cd safari-web-inspector-bridge
npm install
npm run buildclaude mcp add safari-web-inspector-bridge node /path/to/safari-web-inspector-bridge/dist/index.jsAdd to your MCP configuration file:
{
"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"
}
}
}
}| 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? } |
| 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) |
| 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 } |
| 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_CAPTUREandSWIB_CONSOLE_CAPTUREdefault totrue— set tofalseto disable.SWIB_AUTO_CONNECTdefaults tofalse— set totrueto enable.
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 contentnpm 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)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~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.