An experimental take on replicating the Claude Verifier Chrome extension for Safari. This is my approach to bringing the same browser verification capabilities to Safari using a native Safari Web Extension and WebDriver.
MCP server that verifies web pages in Safari via a native Safari Web Extension or WebDriver.
- Quick Start
- Overview
- Architecture
- Prerequisites
- Installation
- Safari Extension Setup
- Running the Server
- Client Configuration
- Verification Modes
- Tool Reference
- Browser Control Tools
- Writing Assertions
- Report Artifacts
- Port Configuration
- Development
- Project Structure
- Troubleshooting
- Future Enhancements
Claude Verifier Safari is a Model Context Protocol (MCP) server that lets AI assistants verify web pages in Safari. It navigates to a URL, runs JavaScript assertions against the page, captures screenshots, and returns structured verification reports.
The server communicates over MCP stdio transport and is macOS-only.
Two verification modes are available:
| Mode | How it works | Safari setup |
|---|---|---|
| Extension (default) | Safari Web Extension with toolbar badge and popup UI | Install and enable the extension |
| WebDriver | Controls Safari externally via safaridriver |
Enable "Allow Remote Automation" |
Claude Desktop / AI Assistant
| MCP (stdio)
v
Node.js MCP Server
| HTTP + WebSocket bridge (localhost:52678)
v
Swift Host App (SafariWebExtensionHandler)
| Native Messaging
v
Safari Web Extension (background.js)
| browser.scripting / browser.tabs
v
Safari Browser --> assertions + screenshot --> report
The MCP server runs an HTTP bridge on localhost:52678 with WebSocket support. The Safari Web Extension connects via WebSocket for instant task delivery, falling back to HTTP polling when the WebSocket is unavailable. When a task arrives, the extension navigates to the URL, runs assertions via browser.scripting.executeScript(), captures a screenshot, and posts the result back.
Claude Desktop / AI Assistant
| MCP (stdio)
v
Node.js MCP Server
| W3C WebDriver HTTP (localhost:4444)
v
safaridriver --> Safari Browser --> assertions + screenshot --> report
The MCP server talks directly to safaridriver using the W3C WebDriver protocol. The extension and Swift layers are bypassed entirely.
- macOS (any recent version)
- Node.js 18+ (required for native
fetch()) - Safari installed
Additional requirements per mode:
| Requirement | Extension Mode | WebDriver Mode |
|---|---|---|
| Xcode (to build the extension) | Yes | No |
| Safari extension enabled | Yes | No |
| "Allow Remote Automation" in Safari | No | Yes |
Three one-time steps:
- Install the Safari extension — build via Xcode or install from the Mac App Store (when available)
- Enable the extension in Safari > Settings > Extensions
- Register the MCP server:
npx claude-verifier-safari setupThat's it. All 24 MCP tools are now available in every Claude Code session.
npx claude-verifier-safari setupThis registers the MCP server globally in Claude Code (user scope). Under the hood it runs:
claude mcp add -s user claude-verifier-safari -- npx claude-verifier-safariTo remove:
npx claude-verifier-safari uninstallcd webdriver
npm installThen register manually:
claude mcp add claude-verifier-safari -s local -- npx tsx /absolute/path/to/webdriver/src/cli.tsThe extension must be built and installed via Xcode before using extension mode.
open xcode/Safari\ Verifier/Safari\ Verifier.xcodeprojIn Xcode:
- Select the Claude Verifier Safari (macOS) scheme
- Build and run with Cmd+R
This installs both the host app and the Safari Web Extension.
Since the extension is not signed for distribution, Safari hides it by default:
- Open Safari > Develop > Allow Unsigned Extensions (enter your password when prompted)
- This setting resets every time you quit Safari, so you'll need to re-enable it each session during development
If you don't see the Develop menu: Safari > Settings > Advanced > Show features for web developers.
- Open Safari > Settings > Extensions (or Preferences > Extensions on older macOS)
- Check the box next to Claude Verifier Safari
- Leave website access set to Ask (the default) — Safari will prompt you to allow access for each site when the extension tries to verify it
After enabling, you should see the Claude Verifier Safari icon in the toolbar. Click it to open the popup, which shows the current status (idle/running/pass/fail).
The MCP server starts automatically when Claude Code invokes the verify_safari tool. You don't need to start it manually.
For development, from the webdriver/ directory:
npm startThis starts the MCP server via tsx src/index.ts. On startup it:
- Launches the HTTP bridge on port
52678 - Connects to MCP stdio transport
- Logs
Bridge listening on port 52678to stderr
The server keeps running until you stop it with Ctrl+C or send SIGINT/SIGTERM. On shutdown it cleans up the bridge and any active safaridriver process.
Recommended: Use the setup command (registers globally, available in every project):
npx claude-verifier-safari setupManual registration from source (local to one project):
claude mcp add claude-verifier-safari -s local -- npx tsx /absolute/path/to/webdriver/src/cli.tsNote:
npx claude-verifier-safarionly works when the package is installed from npm. For local development, always use thenpx tsx .../src/cli.tsform above. The entry point must becli.ts(notindex.ts).
Verify the server connects:
claude mcp listYou should see claude-verifier-safari: ✓ Connected. All tools will be available in your next Claude Code session.
Add the following to your Claude Desktop config file (claude_desktop_config.json):
{
"mcpServers": {
"claude-verifier-safari": {
"command": "npx",
"args": ["claude-verifier-safari"]
}
}
}After saving, restart Claude Desktop. All tools will be available in your conversations.
Best for most use cases. The extension runs assertions inside Safari's own scripting context, which gives the most accurate browser behavior. No special Safari settings required beyond enabling the extension.
mode: "extension"
Features:
- Toolbar badge shows verification status (idle/running/pass/fail)
- Popup UI with details
- Screenshots via
browser.tabs.captureVisibleTab() - HTML dump support
- DOM overlay banner
- WebSocket push transport (instant task delivery)
- Disconnected state detection with badge indicator
- No "Allow Remote Automation" needed
Limitations:
- Requires building and installing the Xcode project
Useful for CI/headless scenarios or when you don't want to install the extension.
mode: "webdriver"
Features:
- Full HTML dump support
- DOM overlay banner during verification
- No extension installation needed
Limitations:
- Requires Safari > Develop > Allow Remote Automation to be enabled
safaridrivermust be available in PATH (ships with macOS)
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
url |
string | yes | — | Page URL (http:// or https:// only) |
assertions |
string[] | yes | — | JavaScript expressions (1–50, each non-empty) |
screenshot |
boolean | no | true |
Capture a PNG screenshot |
htmlDump |
boolean | no | false |
Capture raw page HTML (both modes) |
timeout |
number | no | 30 |
Timeout in seconds (1–300) |
overlay |
boolean | no | true |
Show DOM overlay banner (both modes) |
notifications |
boolean | no | true |
Send macOS notifications on start/finish |
mode |
string | no | "extension" |
"extension" or "webdriver" |
The tool returns:
- A markdown summary with pass/fail status per assertion
- An inline screenshot image (when enabled)
- Report artifacts saved to disk
Beyond verify_safari, the server exposes 23 individual tools for fine-grained browser control. These work in extension mode only and operate on the active Safari tab.
| Tool | Description |
|---|---|
navigate |
Navigate to a URL and wait for load |
navigate_history |
Go back or forward in browser history |
| Tool | Description |
|---|---|
click |
Click an element by CSS selector |
hover |
Hover over an element by CSS selector |
fill |
Fill a single input field |
fill_form |
Fill multiple form fields at once |
scroll |
Scroll the page or a specific element |
handle_dialog |
Accept or dismiss alert/confirm/prompt dialogs |
| Tool | Description |
|---|---|
evaluate_script |
Run arbitrary JavaScript in the page context |
take_screenshot |
Capture a PNG screenshot of the visible tab |
dom_snapshot |
Get a simplified accessibility tree of the page |
wait_for |
Poll until a JavaScript condition becomes truthy |
resize_window |
Change the browser window dimensions |
| Tool | Description |
|---|---|
new_page |
Open a new tab (optionally with a URL) |
list_pages |
List all open tabs with URLs and titles |
select_page |
Switch to a tab by ID |
close_page |
Close a tab by ID |
| Tool | Description |
|---|---|
list_console_messages |
Retrieve captured console messages (log, warn, error, info) |
list_network_requests |
List captured fetch/XHR requests (auto-injects capture hook) |
get_network_request |
Get details of a specific request by index or URL pattern |
performance_start_trace |
Start collecting performance entries via PerformanceObserver |
performance_stop_trace |
Stop tracing and return all collected entries |
performance_analyze_insight |
Analyze performance data: page load timing, FCP/LCP, slow resources |
Network capture works by monkey-patching fetch and XMLHttpRequest in the page context. It captures metadata only (method, URL, status, headers, timing, body size) — not response bodies. The capture hook is injected automatically on first call to list_network_requests or get_network_request.
Performance tracing uses PerformanceObserver to collect entries. Safari supports: resource, navigation, paint, largest-contentful-paint, mark, measure. The performance_analyze_insight tool runs analysis server-side and returns structured output including page load timing, render metrics (FP, FCP, LCP), resource summary with slow resources (>500ms), and custom marks/measures.
Each assertion is a JavaScript expression evaluated in the page context. It must return a truthy value to pass.
// Check page title
document.title === "My App"
// Check an element exists
document.querySelector("h1") !== null
// Check text content
document.querySelector(".hero-title").textContent.includes("Welcome")
// Check element count
document.querySelectorAll(".product-card").length >= 3
// Check visibility
getComputedStyle(document.querySelector(".modal")).display !== "none"
// Check form state
document.querySelector("#email-input").value !== ""
// Check meta tags
document.querySelector('meta[name="viewport"]')?.content.includes("width=device-width")
// Check link href
document.querySelector('a.cta-button')?.href.includes("/signup")Keep assertions simple and focused. Each assertion should test one thing.
Each verification run writes artifacts to .claude-verifier-reports/<timestamp>/ (relative to the current working directory):
| File | Always created | Description |
|---|---|---|
report.json |
Yes | Structured report with status, assertions, and metadata |
screenshot.png |
When screenshot: true |
Full-page PNG screenshot |
page.html |
When htmlDump: true |
Raw HTML source |
log.txt |
Yes | Human-readable run summary |
{
"target": "safari-extension",
"timestamp": "2025-01-15T10:30:00.000Z",
"status": "success",
"summary": "All 3 assertions passed",
"assertions": [
{ "expression": "document.title === 'Example'", "passed": true, "error": null },
{ "expression": "document.querySelector('h1') !== null", "passed": true, "error": null },
{ "expression": "document.querySelectorAll('a').length > 0", "passed": true, "error": null }
],
"artifacts": {
"screenshots": ["screenshot.png"],
"html_dump": null,
"logs": "log.txt"
},
"metadata": {
"url": "https://example.com",
"git_sha": "8de16c2",
"safari_version": "18.1",
"macos_version": "15.2"
}
}The HTTP bridge between the MCP server and the Safari extension uses a fixed default port.
| Component | Default port | Purpose |
|---|---|---|
| HTTP bridge | 52678 |
MCP server <-> extension communication |
| safaridriver | 4444 |
WebDriver protocol (WebDriver mode only) |
The bridge port can be overridden by passing a custom port to startBridge() in the code. The port is also written to ~/Library/Application Support/claude-verifier-safari/port for diagnostics.
All commands run from webdriver/:
npm start # Run the MCP server (dev mode via tsx)
npm run build # Compile TypeScript to dist/
npm run lint # ESLint with strict TypeScript rules
npm test # Vitest unit tests
npx tsc --noEmit # Type-check without emitting| Tool | Version | Purpose |
|---|---|---|
| TypeScript | 5.7 | Type-safe source code |
| ESM | — | import/export, "type": "module" |
| MCP SDK | 1.12 | Model Context Protocol server |
| Zod | 3.24 | Input validation |
| ESLint | 9.x | Linting (flat config, strict type-checked rules) |
| Vitest | 4.x | Unit testing |
| tsx | 4.x | TypeScript execution (no compile step) |
- Strict TypeScript — no
any,strict: truein tsconfig - ESM only —
import/export, norequire - ESLint strict —
typescript-eslintstrict type-checked rules - Small functions — single responsibility, under 30 lines
- Minimal comments — explain why, not what
npm testTests are co-located with source files as *.test.ts (excluded from tsconfig and ESLint). Currently covers error classes, feedback script generation, and WebDriver response parsing.
Create a file next to the module you want to test:
webdriver/src/
mymodule.ts
mymodule.test.ts <-- add this
Vitest picks up any src/**/*.test.ts file automatically.
safari/
README.md
CLAUDE.md Development guidelines
extension/
manifest.json Web extension manifest (Manifest V3)
background.js Polls bridge for tasks, runs assertions
popup.html/js/css Toolbar popup UI
icons/ Extension icons (48-256px)
xcode/
Claude Verifier Safari/
Claude Verifier Safari.xcodeproj
Shared (App)/ Shared app resources and view controller
Shared (Extension)/
SafariWebExtensionHandler.swift Native messaging bridge (HTTP proxy)
macOS (App)/ macOS host app target
macOS (Extension)/ macOS extension target
iOS (App)/ iOS host app target
iOS (Extension)/ iOS extension target
webdriver/
package.json Dependencies, scripts, version, bin field
tsconfig.json TypeScript config (ES2022, strict)
eslint.config.js ESLint flat config
vitest.config.ts Test runner config
dist/ Compiled output (npm publish ships this)
src/
cli.ts CLI entry point (setup/uninstall/start)
index.ts MCP server setup, orchestration
bridge.ts HTTP bridge for extension communication (port 52678)
webdriver.ts W3C WebDriver HTTP client
safaridriver.ts safaridriver process lifecycle
errors.ts Custom error classes
feedback.ts DOM overlay and macOS notifications
types.ts TypeScript interfaces
tools/
register.ts Imports and registers all tool modules
verify.ts verify_safari (full-page verification)
navigate.ts navigate, navigate_history
click.ts click
fill.ts fill, fill_form
scroll.ts scroll
wait.ts wait_for
evaluate.ts evaluate_script
screenshot.ts take_screenshot
resize.ts resize_window
tabs.ts new_page, list_pages, select_page, close_page
hover.ts hover
dialog.ts handle_dialog
snapshot.ts dom_snapshot
console.ts list_console_messages
network.ts list_network_requests, get_network_request
performance.ts performance_start/stop/analyze tools
The MCP server is registered but Claude Code cannot start it. Common causes:
- Server not registered — run
npx claude-verifier-safari setupto register globally - Node.js not found — ensure
node(18+) andnpxare on your PATH - Check server status — run
claude mcp listto see connection status, orclaude mcp get claude-verifier-safarifor full details - Re-register — if using a source install with absolute paths, verify the path points to
cli.ts(notindex.ts):claude mcp remove claude-verifier-safari -s local claude mcp add claude-verifier-safari -s local -- npx tsx /absolute/path/to/webdriver/src/cli.ts
The extension is not polling the bridge. Check:
- Is the extension enabled? Safari > Settings > Extensions > Claude Verifier Safari must be checked
- Is the server running?
npm startshould showBridge listening on port 52678 - Is the extension loaded? Look for the Claude Verifier Safari icon in the toolbar
- Check the extension console: Safari > Develop > Web Extension Background Pages > Claude Verifier Safari. Look for errors in the console
- Verify safaridriver exists:
which safaridriver(should be/usr/bin/safaridriver) - Enable remote automation: Safari > Develop > Allow Remote Automation
- If the Develop menu is not visible: Safari > Settings > Advanced > Show features for web developers
If another process is using port 52678, you can override the bridge port by modifying the startBridge() call in src/index.ts. The extension's Swift handler will fall back to reading the port from ~/Library/Application Support/claude-verifier-safari/port.
- Ensure you're building the macOS scheme, not iOS
- Check that signing is set to "Automatically manage signing"
- Clean the build folder: Product > Clean Build Folder (Shift+Cmd+K)
- Assertions run in the page context after navigation completes, but dynamic content may not be loaded yet. Use assertions that check for specific elements rather than timing-dependent state
- In extension mode, navigation waits for
tabs.onUpdatedwithstatus: "complete"before running assertions - In WebDriver mode, the
timeoutparameter controls how long to wait for page load
- Task queuing — Queue incoming verification requests instead of rejecting with a conflict error when a task is already running. This would let the AI assistant fire multiple verifications without waiting for each one to complete
- Configurable bridge port via environment variable — Allow setting the bridge port through an env var (e.g.
BRIDGE_PORT) instead of requiring a code change, making it easier to avoid port conflicts - Persistent verification history — Store past run summaries in a lightweight database (SQLite) so the AI assistant can query trends, compare runs, or detect regressions across multiple verifications
- Multi-tab parallel verification — Run assertions on multiple URLs concurrently using separate Safari tabs, returning a combined report. Useful for verifying cross-page flows or comparing environments