Skip to main content
Glama

🦁 Safari MCP

The browser for your coding agent.

Your real Safari, logged in — no Chrome, no heat, no headless.

npm version npm downloads License: MIT macOS OpenSSF Scorecard

Install in VS Code · VS Code Insiders · Install in Cursor

98 tools · No Chrome/Puppeteer/Playwright needed · ~5ms per command · 60% less CPU than Chrome

Quick Start · All 98 Tools · Examples · Why Safari MCP? · Architecture · Changelog

Safari MCP Demo

❌ Without Safari MCP

Your AI agent needs to browse. So it either:

  • Spins up Chromium via Playwright — with no logins, no cookies, no sessions

  • Uses Chrome DevTools MCP — and melts your fan running a second browser

  • Relies on headless scrapers — blocked by Cloudflare, reCAPTCHA, and bot detection

Related MCP server: mcp-browser

✅ With Safari MCP

Your AI drives the Safari you're already logged into — Gmail, GitHub, Ahrefs, Slack, banking.

Native WebKit. ~60% less CPU. Background operation. 98 tools. One npx command. macOS only.

📰 Featured on freeCodeCamp: How to Connect Your AI Coding Agent to a Browser on macOS · HackerNoon: Reverse-Engineering React, Shadow DOM, and CSP

🍎 Apple ships an official Safari MCP — safaridriver --mcp, first in Safari Technology Preview 247 (July 2026) and in stable Safari since 27.0. It drives an isolated automation session for debugging. safari-mcp drives the real Safari you're already logged into — your cookies and sessions, in the background, with 98 tools. See the full comparison below.


Highlights

  • 98 tools — navigation, clicks, forms, screenshots, network, storage, accessibility, and more

  • Zero heat — native WebKit on Apple Silicon, ~60% less CPU than Chrome

  • Your real browser — keeps all logins, cookies, sessions (Gmail, GitHub, Ahrefs, etc.)

  • Background operation — Safari stays in the background, no window stealing

  • No browser dependencies — no Puppeteer, no Playwright, no WebDriver, no Chrome

  • Persistent process — reuses a single osascript process (~5ms per command vs ~80ms)

  • Framework-compatible — React, Vue, Angular, Svelte form filling via native setters


In users' own words

Not solicited testimonials — quotes lifted from the public issue tracker, each linked to the thread it came from.

"I run multiple Pi sessions/subagents against my normal Safari profile in parallel. Sharing its cookies and logins is intentional." — @maxim, on running concurrent agents against a real browser

"The server has a deliberate tab-ownership model … the code is careful about this, and for the default case that's the right safety posture." — @turner-moore, on why the guards refuse to touch your tabs

"Direct local validation from the package: Safari MCP doctor 6/6." — @jrepp, who found and fixed a queue-alignment bug in the focus helper


Quick Start

Prerequisites

  • macOS (any version with Safari)

  • Node.js 20+

  • Safari → Settings → Advanced → Show features for web developers ✓

  • Safari → Settings → Developer → Allow JavaScript from Apple Events ✓

Install (one command)

npx safari-mcp

That's it — no global install needed. Or install permanently:

npm install -g safari-mcp

Configure your MCP client

All clients run Safari MCP the same way — npx safari-mcp. Pick your editor:

claude mcp add safari -- npx safari-mcp

Or edit ~/.mcp.json:

{
  "mcpServers": {
    "safari": {
      "command": "npx",
      "args": ["safari-mcp"]
    }
  }
}

Edit ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "safari": {
      "command": "npx",
      "args": ["safari-mcp"]
    }
  }
}

Restart Claude Desktop after saving.

One-click: Install in Cursor

Or edit .cursor/mcp.json in your project:

{
  "mcpServers": {
    "safari": {
      "command": "npx",
      "args": ["safari-mcp"]
    }
  }
}

One-click: Install in VS Code

Or edit .vscode/mcp.json:

{
  "servers": {
    "safari": {
      "type": "stdio",
      "command": "npx",
      "args": ["safari-mcp"]
    }
  }
}

Edit .windsurf/mcp.json in your project (or ~/.codeium/windsurf/mcp_config.json globally):

{
  "mcpServers": {
    "safari": {
      "command": "npx",
      "args": ["safari-mcp"]
    }
  }
}

Open Cline in VS Code → click the MCP icon → Edit MCP Settings → add:

{
  "mcpServers": {
    "safari": {
      "command": "npx",
      "args": ["safari-mcp"]
    }
  }
}

Edit ~/.continue/config.yaml (or .continue/config.yaml in workspace):

mcpServers:
  - name: safari
    command: npx
    args:
      - safari-mcp

Edit ~/.config/goose/config.yaml:

extensions:
  safari:
    name: safari
    type: stdio
    cmd: npx
    args:
      - safari-mcp
    enabled: true

Open LM Studio → Settings → MCP Servers → Add Server:

  • Name: safari

  • Command: npx

  • Args: safari-mcp

Open Zed → Settings → search for "Context Servers" and add:

{
  "context_servers": {
    "safari": {
      "command": {
        "path": "npx",
        "args": ["safari-mcp"]
      }
    }
  }
}
brew install achiya-automation/tap/safari-mcp
git clone https://github.com/achiya-automation/safari-mcp.git
cd safari-mcp && npm install

Usage Workflow

The recommended pattern for AI agents using Safari MCP:

1. safari_snapshot        → Get page state (accessibility tree)
2. safari_click/fill/...  → Interact with elements by ref
3. safari_snapshot        → Verify the result

Element targeting — tools accept multiple targeting strategies:

Strategy

Example

Best for

CSS selector

#login-btn, .submit

Unique elements

Visible text

"Sign In", "Submit"

Buttons, links

Coordinates

x: 100, y: 200

Canvas, custom widgets

Ref from snapshot

ref: "e42"

Any element from accessibility tree

Tip: Start with safari_snapshot to get element refs, then use refs for precise targeting. This is faster and more reliable than CSS selectors.

After a click or a fill, safari_snapshot with diff: true returns only the lines that changed since that tab's previous snapshot: + lines are new and carry fresh refs, - lines are gone. The first call for a tab returns the full snapshot.


Running several agents at once

Multiple agents or subagents driving one Safari at the same time will fight over the active tab — unless you run them against a shared HTTP daemon instead of one process per client:

SAFARI_MCP_HTTP=1 SAFARI_MCP_HTTP_PORT=9225 npx safari-mcp

Then point every client at it:

{ "mcpServers": { "safari-mcp": { "type": "http", "url": "http://127.0.0.1:9225/mcp" } } }

One daemon, many sessions — and each session gets its own tab state. The server keys activeTabIndex, the ownership flag and a unique tab marker off the MCP session id, so session A physically cannot read or steer session B's tab.

Two properties make this safe rather than merely tidy:

  • Tab identity is a marker, not an index. Each session stamps a unique id into the page it opens, so ownership survives navigation and survives the user reordering or closing other tabs. An index alone would silently drift onto the wrong tab.

  • It fails closed. If a session's marked tab can't be re-found, every tool refuses instead of falling back to whatever tab is in front — because that tab is usually yours:

    Tab tracking lost — refusing to fall back to "current tab of window"
    (would target the user's active tab). Call safari_new_tab to reopen.

This also drops process count sharply: ~17 node processes for 17 concurrent sessions becomes 1.

SAFARI_PROFILE stays optional — leave it unset and sessions bind to your ordinary Safari windows, cookies and logins intact. Details in docs/http-transport-design.md.

Prefer stdio (one process per agent) over a persistent daemon? That works too — isolation then comes from the process boundary itself. One caveat: if your client multiplexes agents through mcporter, mcporter caches a single MCP client for all of them — whichever transport you pick — so the server never sees distinct sessions and per-session isolation can't engage. mcporter-lanes (a pi extension by @maxim, born out of #76) fixes this upstream: each agent session gets its own daemon dir — and therefore its own safari-mcp — with an idle timeout so processes don't pile up.


Acting on a tab you already have open

By default the server touches only tabs it opened itself. Point it at one of yours and it refuses:

Tab safety: refusing "click" — current tab (https://mail.example.com/inbox) was not
opened by this MCP session. Use safari_new_tab or safari_switch_tab to target your own tab.

That default exists because early versions did click into and close people's tabs. But "read the article I'm looking at" and "fill in the form on my screen" are real, and reopening the page loses the session state that made your tab worth using. Set SAFARI_MCP_ALLOW_USER_TABS=1 and an explicit safari_switch_tab adopts the tab instead of refusing it; from then on the session works in it like one of its own, and says so:

{ "tabIndex": 3, "safeUrl": "https://mail.example.com/inbox", "note": "(user tab, opted-in)" }

What the flag deliberately does not do:

  • It unlocks adoption, not the guards. Only safari_switch_tab adopts, and only the tab you named. An ordinary click or navigate still never lands on whatever tab happens to be in front — the server acts on the tab you pointed it at, not the one you wandered to.

  • safari_close_tab still refuses. Closing is the one action whose cost you cannot undo, so an adopted tab is writable, never disposable. Close it yourself.

  • Adoption is session-local. Nothing is written to the shared ownership file, so it ends with the session rather than leaking to the next process on the machine.

safari_doctor prints the flag's state, and every operation on an adopted tab logs (user tab, opted-in) — so "why did it touch my tab" has an answer instead of being a mystery. Default off; set it only for agents you want working inside your own browsing session. Designed in #92.


Environment variables

Variable

Default

What it does

SAFARI_MCP_HTTP

off

Run one shared HTTP daemon instead of a process per client (see above).

SAFARI_MCP_HTTP_PORT

9225

Port for that daemon.

SAFARI_PROFILE

unset

Bind sessions to a named Safari profile. Unset = your ordinary windows.

SAFARI_MCP_ALLOW_USER_TABS

off

Let safari_switch_tab adopt a tab you already had open, instead of refusing it (see below).

MCP_MAX_TABS

6

Tabs one session may keep open. Opening one more closes that session's oldest tab, and safari_new_tab names it in evictedTab (its receipt).

SAFARI_MCP_RAISE_ON_NAVIGATE

off

Let navigation bring Safari to the front, and stop the focus guard from putting your previous app back.

SAFARI_MCP_TOOLS

all

core registers only the everyday tools (tabs, navigation, snapshot, reading, clicks, forms, evaluate, console, network, doctor), or give a comma list of tool names. For clients that load every tool definition up front.

SAFARI_MCP_SCREENSHOT_MAX_WIDTH

unset

Downscale every safari_screenshot to this pixel width (Retina captures are 2× the viewport). Per-call maxWidth overrides it.

SAFARI_MCP_KEEPALIVE_TAB

off

Keep one daemon-served page open in the profile window so Safari never parks the extension worker between commands.

SAFARI_MCP_OPEN_WINDOW_CMD

unset

Command run with the profile name when the profile window is absent (e.g. after a reboot). Must open the window without focusing Safari.

SAFARI_MCP_RAISE_ON_NAVIGATE=1 is for agents whose whole point is showing you a page — a voice assistant answering "open YouTube", a demo driver. Everything else should leave it off: by default Safari MCP works in the background and hands focus back to whatever app you were using, so an agent can drive a page while you keep typing somewhere else.


Tools (98)

Navigation (4)

Tool

Description

safari_navigate

Navigate to URL (auto HTTPS, wait for load)

safari_go_back

Go back in history

safari_go_forward

Go forward in history

safari_reload

Reload page (optional hard reload)

Page Reading (3)

Tool

Description

safari_read_page

Get title, URL, and text content

safari_get_source

Get full HTML source

safari_navigate_and_read

Navigate + read in one call

Click & Interaction (6)

Tool

Description

safari_click

Click by CSS selector, visible text, or coordinates

safari_double_click

Double-click (select word, etc.)

safari_right_click

Right-click (context menu)

safari_hover

Hover over element

safari_click_and_wait

Click + wait for navigation

safari_click_and_read

Click then return the updated page — saves a round-trip (React Router + full loads)

Form Input (11)

Tool

Description

safari_fill

Fill input (React/Vue/Angular compatible)

safari_clear_field

Clear input field

safari_select_option

Select dropdown option

safari_fill_form

Batch fill multiple fields

safari_fill_and_submit

Fill form + submit in one call

safari_type_text

Type real keystrokes (JS-based, no System Events)

safari_press_key

Press key with modifiers

safari_react_select_set

Set a react-select v5 value via React fiber — bypasses the menu UI

safari_react_select_list_options

List a react-select v5 dropdown's options without opening it

safari_replace_editor

Replace all content in a code editor (Monaco, CodeMirror, Ace, ProseMirror)

safari_verify_state

Verify an editor's framework-level state matches expected — catch stale DOM before Submit

Screenshots & PDF (3)

Tool

Description

safari_screenshot

Screenshot as PNG (viewport or full page)

safari_screenshot_element

Screenshot a specific element

safari_save_pdf

Export page as PDF

Scroll (3)

Tool

Description

safari_scroll

Scroll up/down by pixels

safari_scroll_to

Scroll to exact position

safari_scroll_to_element

Smooth scroll to element

Tab Management (5)

Tool

Description

safari_list_tabs

List all tabs (index, title, URL)

safari_new_tab

Open new tab (background, no focus steal)

safari_close_tab

Close tab

safari_switch_tab

Switch to tab by index

safari_wait_for_new_tab

Wait for a new tab (e.g. OAuth popup) and auto-switch to it

Wait (2)

Tool

Description

safari_wait_for

Wait for element, text, or URL change

safari_wait

Wait for specified milliseconds

JavaScript (3)

Tool

Description

safari_evaluate

Execute arbitrary JavaScript, return result

safari_eval_file

Execute JavaScript read from a file path (avoids huge inline scripts)

safari_list_frames

List every document in the tab (main page + each iframe) with frameId, URL and text length — pass a frameId to safari_evaluate's frame to run inside a cross-origin iframe

Element Inspection (4)

Tool

Description

safari_get_element

Element details (tag, rect, attrs, visibility)

safari_query_all

Find all matching elements

safari_get_computed_style

Computed CSS styles

safari_detect_forms

Auto-detect all forms with field selectors

Accessibility (2)

Tool

Description

safari_accessibility_snapshot

Full a11y tree: roles, ARIA, focusable elements

safari_snapshot

Accessibility tree with ref IDs for every interactive element — preferred way to see page state

Drag & Drop (1)

Tool

Description

safari_drag

Drag between elements or coordinates

File Operations (2)

Tool

Description

safari_upload_file

Upload file via JS DataTransfer (no file dialog!)

safari_paste_image

Paste image into editor (no clipboard touch!)

Dialog & Window (2)

Tool

Description

safari_handle_dialog

Handle alert/confirm/prompt

safari_resize

Resize browser window

Device Emulation (2)

Tool

Description

safari_emulate

Emulate device (iPhone, iPad, Pixel, Galaxy)

safari_reset_emulation

Reset to desktop

Cookies & Storage (11)

Tool

Description

safari_get_cookies

Get all cookies

safari_set_cookie

Set cookie with all options

safari_delete_cookies

Delete one or all cookies

safari_local_storage

Read localStorage

safari_set_local_storage

Write localStorage

safari_delete_local_storage

Delete/clear localStorage

safari_session_storage

Read sessionStorage

safari_set_session_storage

Write sessionStorage

safari_delete_session_storage

Delete/clear sessionStorage

safari_export_storage

Export all storage as JSON (backup/restore sessions)

safari_import_storage

Import storage state from JSON

Clipboard (2)

Tool

Description

safari_clipboard_read

Read clipboard text

safari_clipboard_write

Write text to clipboard

Network (6)

Tool

Description

safari_network

Quick network requests via Performance API

safari_start_network_capture

Start detailed capture (fetch + XHR)

safari_network_details

Get captured requests with headers/timing

safari_clear_network

Clear captured requests

safari_mock_route

Mock network responses (intercept fetch/XHR)

safari_clear_mocks

Remove all network mocks

Console (4)

Tool

Description

safari_start_console

Start capturing console messages

safari_get_console

Get all captured messages

safari_clear_console

Clear captured messages

safari_console_filter

Filter by level (log/warn/error)

Performance (2)

Tool

Description

safari_performance_metrics

Navigation timing, Web Vitals, memory

safari_throttle_network

Simulate slow-3g/fast-3g/4g/offline

Data Extraction (4)

Tool

Description

safari_extract_tables

Tables as structured JSON

safari_extract_meta

All meta: OG, Twitter, JSON-LD, canonical

safari_extract_images

Images with dimensions and loading info

safari_extract_links

Links with rel, external/nofollow detection

Advanced (7)

Tool

Description

safari_override_geolocation

Override browser geolocation

safari_list_indexed_dbs

List IndexedDB databases

safari_get_indexed_db

Read IndexedDB records

safari_css_coverage

Find unused CSS rules

safari_analyze_page

Full page analysis in one call

safari_doctor

Diagnose the macOS permission + daemon chain (Apple Events, Accessibility, Screen Recording, codesign) with per-failure fixes

safari_reload_extension

Hot-reload the Safari MCP Bridge extension without a manual toggle

Automation (1)

Tool

Description

safari_run_script

Run multiple actions in a single call (batch)

Native Input — CGEvent (4)

Tool

Description

safari_native_click

OS-level mouse click (CGEvent, isTrusted: true) — bypasses WAF/bot detection when safari_click is blocked (405/403)

safari_native_hover

OS-level cursor hover — triggers real :hover/mouseenter for tooltips and obfuscated UIs

safari_native_type

Insert text via the real paste pipeline — ProseMirror/Slate/Draft.js process it natively so Submit sends real data

safari_native_keyboard

OS-level keypress + modifiers to Safari, no focus steal — reaches React trust-gated handlers (Discord/Slack send)

iOS & WebKit Validation (4)

Tool

Description

safari_inspect_viewport

Validate the <meta name=viewport> tag for iOS Safari (device-width, zoom/WCAG, viewport-fit)

safari_safe_area_insets

Read live safe-area-inset values + viewport-fit / env() usage (notch / Dynamic Island)

safari_check_pwa

Audit iOS "Add to Home Screen" / PWA readiness (apple-touch-icon, manifest, theme-color, splash)

safari_webkit_compat

Check page CSS against this Safari via CSS.supports() — unsupported props, missing -webkit- prefixes, known quirks


Security

Safari MCP runs locally on your Mac with minimal attack surface:

Aspect

Detail

Network

No remote connections — all communication is local (stdio + localhost)

Permissions

macOS system permissions required (Screen Recording for screenshots)

Data

No telemetry, no analytics, no data sent anywhere

Extension

Communicates only with the local profile bridges (localhost:9224/9228/9232/9236), validated by Safari

Code

Fully open source (MIT) — audit every line


Safari MCP vs Alternatives

Feature

Safari MCP

Chrome DevTools MCP

Playwright MCP

CPU/Heat

🟢 Minimal

🔴 High

🟡 Medium

Your logins

✅ Yes

✅ Yes

❌ No

macOS native

✅ WebKit

❌ Chromium

❌ Chromium/WebKit

Browser dependencies

None

Chrome + debug port

Playwright runtime

Tools

98

~30

~25

File upload

JS (no dialog)

CDP

Playwright API

Image paste

JS (no clipboard)

CDP

Playwright API

Focus steal

❌ Background

❌ Background

❌ Headless

Network mocking

✅

❌

✅

Lighthouse

❌

✅

❌

Performance trace

❌

✅

❌

Tip: Use Safari MCP for daily browsing tasks (95% of work) and Chrome DevTools MCP only for Lighthouse/Performance audits.

vs Apple's Official Safari MCP (safaridriver)

In July 2026 Apple shipped an official Safari MCP server built on safaridriver — first in Safari Technology Preview 247, and in stable Safari since 27.0. That's great validation for the category — and it's built for a different job. Apple's server drives an isolated WebDriver automation session for debugging; safari-mcp drives the real Safari you're already logged into.

On macOS 27.0 with Safari 27.0, safaridriver --help lists --mcp and its tools/list answers with 17 entries (verified 2026-09-15); Safari 26.5.2 had no --mcp (verified 2026-07-23). Check your own machine with safaridriver --help | grep mcp.

Running both? Apple's setup guide registers its server under the name safari-mcp (claude mcp add safari-mcp -- /usr/bin/safaridriver --mcp) — the same name as this package. Give one of them a different name, e.g. claude mcp add safari-webdriver -- /usr/bin/safaridriver --mcp, so your client doesn't overwrite one with the other or leave you guessing which server answered.

🦁 safari-mcp (this repo)

Apple safaridriver --mcp

Your real logins / cookies

✅ Your actual Safari

⚠️ Isolated automation session — no access to AutoFill or browsing activity

Runs on

✅ Stable Safari, every Mac

⚠️ Safari 27 or later, or Safari Technology Preview 247+

Background (no focus steal)

✅ Yes

❌ Dedicated window with a "controlled by automation" banner

Page sees navigator.webdriver

✅ false — an ordinary tab

⚠️ true — required of every WebDriver session

Tools

98

17

Storage (cookies, localStorage, IndexedDB)

✅ 10 tools

❌

Network mocking + throttling

✅ Yes

❌ Read-only network inspection

Device emulation (iPhone, iPad)

✅ Yes

⚠️ Viewport + media type only

Setup

npx safari-mcp

Enable "Allow remote automation and external agents" in Safari's Developer settings, then point your client at safaridriver --mcp

Official Apple support

❌ Community (MIT)

✅ Apple, WebDriver-standard

About that navigator.webdriver row: the WebDriver standard requires every remote-controlled session to report true, so Apple's server does. safari-mcp evaluates JavaScript in an ordinary tab, so it reads false (checked on Safari 27.0, 2026-09-17). That is the difference if a site refuses WebDriver traffic. It is not an anti-detection feature — nothing else about the browser is disguised, and your real logins are in use, so treat every page as if it knows exactly who you are.

When Apple's server is the right pick: you specifically want a clean-room, WebDriver-standard session for compatibility debugging on Safari 27 or later. For everything else — daily automation on the browser you're already signed into, in the background — safari-mcp is built for exactly that.

Why Safari MCP and Not the Other Safari MCP Projects?

There are several "safari-mcp" projects floating around. Here's how they compare:

Feature

🦁 safari-mcp (this repo)

lxman/safari-mcp-server

Epistates/MCPSafari

HayoDev/safari-devtools-mcp

Tools

98

~10

23

~15

Install

npx safari-mcp

Manual

Binary

npx

Engine

Dual (Extension + AppleScript)

WebDriver

Extension only

DevTools Protocol

Keeps your real Safari logins

✅ Yes

⚠️ Limited

✅ Yes

❌ Debug session

Background (no focus steal)

✅ Yes

❌ No

⚠️ Sometimes

✅ Yes

Storage tools (cookies, localStorage, IndexedDB)

10

0

0

2

Data extraction (tables, meta, images, links)

4

0

0

0

Network mocking

✅ Yes

❌ No

❌ No

❌ No

Device emulation (iPhone, iPad, Pixel)

✅ Yes

❌ No

❌ No

❌ No

File upload (no dialog)

✅ JS DataTransfer

❌ No

❌ No

❌ No

Image paste (no clipboard touch)

✅ Yes

❌ No

❌ No

❌ No

PDF export

✅ Yes

❌ No

❌ No

❌ No

Console capture

4 tools

0

1

1

Performance metrics + Web Vitals

✅ Yes

❌ No

❌ No

⚠️ Partial

Active maintenance

✅ Multiple releases/week

🟡 Sporadic

🟡 Slow

🟡 Slow

License

MIT

MIT

None specified

MIT

In MCP Registry

✅

❌

❌

✅

In Awesome MCP

✅

❌

❌

❌

TL;DR — if you want the most complete Safari MCP with the smoothest install, the most tools, and active maintenance, this is the one.


Architecture

Safari MCP uses a dual-engine architecture — the Extension is preferred for speed and advanced capabilities, with AppleScript as an always-available fallback:

Claude/Cursor/AI Agent
        ↓ MCP Protocol (stdio)
   Safari MCP Server (Node.js)
        ↓                    ↓
   Extension (HTTP)     AppleScript + Swift daemon
   (~5-20ms/cmd)        (~5ms/cmd, always available)
        ↓                    ↓
   Content Script       do JavaScript in tab N
        ↓                    ↓
   Page DOM ←←←←←←←←←← Page DOM

Key design decisions:

  • Dual engine with automatic fallback — Extension is preferred; if not connected, AppleScript handles everything seamlessly

  • Persistent Swift helper — one long-running process instead of spawning per command (16x faster)

  • Tab-indexed operations — all JS runs on a specific tab by index, never steals visual focus

  • JS-first approach — typing, clicking, file upload all use JavaScript events (no System Events keyboard conflicts)

  • No activate — Safari is never brought to foreground


Safari Extension (Optional)

The Safari MCP Extension is optional but recommended. Without it, ~80% of functionality works via AppleScript alone. The extension adds capabilities that AppleScript cannot provide:

What the Extension Adds

Capability

With Extension

AppleScript Only

Closed Shadow DOM (Reddit, Web Components)

✅ Full access

❌ Invisible

Strict CSP sites

✅ Bypasses via MAIN world

❌ Often blocked

React/Vue/Angular state manipulation

✅ Deep (Fiber, ProseMirror)

⚠️ Basic

Loading state detection (spinners, skeletons)

✅ Smart detection

❌ No

Dialog handling (alert/confirm)

❌

✅ Only AppleScript

Native OS-level click (CGEvent)

❌

✅ Only AppleScript

PDF export

❌

✅ Only AppleScript

When do you need the extension? If you're automating modern SPAs with closed shadow DOM (e.g., Reddit), sites with strict Content Security Policy, or framework-heavy editors (Draft.js, ProseMirror, Slate).

Installing the Extension

The extension requires a one-time build with Xcode (free, included with macOS).

Note for npm users: The xcode/ directory is not included in the npm package. Clone the GitHub repository to build from source.

Prerequisites: Xcode (install from App Store — free)

# 1. Clone the repo (the npm package does not include the Xcode project)
git clone https://github.com/achiya-automation/safari-mcp.git
cd safari-mcp

# 2. Build a clean, Apple Development-signed app
# If needed, first choose your Team in Xcode → Signing & Capabilities.
xcodebuild -project "xcode/Safari MCP/Safari MCP.xcodeproj" \
  -scheme "Safari MCP (macOS)" -configuration Release \
  -allowProvisioningUpdates clean build

# 3. Locate the signed app
APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData/Safari_MCP-*/Build/Products/Release -name "Safari MCP.app" -maxdepth 2 | head -1)

# 4. Re-sign safari-helper with the Apple Events entitlement
# (helps macOS surface the TCC Automation prompt reliably).
# Keep the stable identifier: macOS keys the Accessibility grant to it.
codesign --sign - --force --identifier com.achiya-automation.safari-mcp \
  --entitlements safari-helper.entitlements safari-helper

# 5. Open the app (needed once so Safari registers the extension)
open "$APP_PATH"

Do not ad-hoc re-sign Safari MCP.app with codesign --deep --sign -. That replaces the Apple Development identity on the wrapper and embedded extension; current Safari can then silently disable or mark the extension as removed. If a previous build was ad-hoc signed, run the clean build command above again.

Alternatively, open xcode/Safari MCP/Safari MCP.xcodeproj directly in Xcode, select your Apple ID under Signing & Capabilities, and click Run. A free personal Apple Developer account is sufficient for local use.

Then in Safari:

  1. Safari → Settings → Advanced → enable Show features for web developers

  2. Safari → Settings → Developer → Allow unsigned extensions (required each Safari restart)

  3. Safari → Settings → Extensions → enable Safari MCP Bridge

The extension connects automatically to the first local bridge whose declared Safari profile matches its own. The default bridge ports are 9224, 9228, 9232, and 9236; a single-profile setup normally uses 9224.

Note: "Allow unsigned extensions" resets every time Safari restarts. You'll need to re-enable it in Safari → Settings → Developer after each restart. The extension itself stays installed.

Toolbar icon status:

  • ON — connected to MCP server

  • OFF — manually disabled via popup

  • (no badge) — server not running, will auto-reconnect


macOS Permissions

Safari MCP needs these one-time permissions:

Permission

Where

Why

JavaScript from Apple Events

Safari → Settings → Developer

Required for do JavaScript

Automation → Safari

System Settings → Privacy & Security → Automation

Required for all AppleScript-backed tools

Screen Recording

System Settings → Privacy & Security → Screen & System Audio Recording

Required for safari_screenshot without the extension, and for safari_save_pdf

Accessibility (safari-helper)

System Settings → Privacy & Security → Accessibility (named Device Control and Data Access on macOS 27)

Required for safari_native_click, safari_native_keyboard, safari_native_hover and safari_save_pdf

Granting Accessibility to safari-helper (required for safari_native_*)

The safari_native_click, safari_native_keyboard and safari_native_hover tools inject OS-level CGEvent events into Safari without stealing focus. macOS requires the underlying helper binary to be approved in Accessibility before those events can reach a non-frontmost window.

  1. Open System Settings → Privacy & Security → Accessibility (on macOS 27 the pane is named Device Control and Data Access).

  2. Click + (unlock with your password if needed).

  3. Navigate to the helper binary and add it:

    • npm global install: $(npm root -g)/safari-mcp/safari-helper

    • npx / project install: ./node_modules/safari-mcp/safari-helper

    • From source clone: /path/to/safari-mcp/safari-helper

  4. Make sure the toggle next to it is ON.

The postinstall script re-signs the helper with a stable identifier (com.achiya-automation.safari-mcp) so this permission survives future upgrades — without that step, every npm update would silently revoke approval because the binary's adhoc-signed identifier changes per build.

If safari_native_click reports success but the page doesn't react (no isTrusted: true click events fire), the helper is most likely missing this approval. The safari_* (non-native_) tools don't need it.

Granting Automation → Safari (important for IDE users)

macOS TCC grants Automation permission to the parent process that spawns the MCP server, not to safari-mcp itself. So you need to grant Automation → Safari to the app that runs Claude Code / Cursor / Windsurf — typically Visual Studio Code or Terminal.

If the permission dialog never appears automatically, run this command once from a Terminal that already has Automation permission:

osascript -e 'tell application "Safari" to get URL of current tab of window 1'

That call registers the Terminal app in the Automation database and then triggers the prompt for Safari. After you approve it, subsequent MCP calls from any child process chain will work.


Troubleshooting

Issue

Fix

"AppleScript error"

Enable "Allow JavaScript from Apple Events" in Safari → Settings → Developer

"Not authorized to send Apple events to Safari"

Grant Automation → Safari to your IDE (see above)

"Not authorized" after npm update

Updating changes the binary's cdhash — macOS silently revokes Automation permission. Re-run the osascript one-liner above to re-grant it

safari_native_click reports success but page doesn't react

Add safari-helper to System Settings → Privacy & Security → Accessibility (see Granting Accessibility above). Confirm by attaching a click listener with {capture:true} in the page console — without the grant, no isTrusted: true event fires

Screenshots empty

Grant Screen & System Audio Recording to Terminal/VS Code — or, when safari-mcp runs as a LaunchAgent, to the node binary itself (a Homebrew node upgrade changes its path, so re-grant after upgrading). safari_doctor names the exact binary

Extension missing after enabling "Allow unsigned extensions" (macOS 27)

Known macOS 27.0 issue (183044008): with the setting on, rebuild the extension app (see Installing the Extension) and it appears

Tab not found

Call safari_list_tabs to refresh tab indices

Hebrew keyboard issues

All typing uses JS events — immune to keyboard layout

HTTPS blocked

safari_navigate auto-tries HTTPS first, falls back to HTTP

Safari steals focus

Ensure you're on latest version — newTab restores your active tab


FAQ

No. Every AppleScript path targets application "Safari" and the native helper looks for the bundle id com.apple.Safari, so STP (com.apple.SafariTechnologyPreview) is a different application that safari-mcp never addresses. Run it against stable Safari.

Note that Apple's own safaridriver --mcp server is the one that needs STP 247+ (or Safari 27+) — see Safari MCP vs Alternatives. The two are unrelated.

Not as a supported mode, and it is untested. No tool opens a private window, and Safari disables extensions in Private Browsing unless you turn each one on explicitly, so the extension bridge — and everything in the "With Extension" column of Safari Extension (Optional) — is off there by default.

If you want an isolated identity, use a separate Safari profile instead: profile windows are a first-class concept here (see Running several agents at once).

Yes. Tab lookups walk every window rather than assuming window 1, and a tab is pinned by its window's (index, tabCount) pair, because an index on its own is ambiguous once two windows are open. Profile windows are recognised by Safari's ProfileName — Tab Title window naming.

The reliable handle is still the receipt that safari_new_tab returns: pass it to every later call and the tab is addressed directly, no matter how many windows move around it.

Any macOS with Safari — the package declares "os": ["darwin"] and no minimum OS, and there is no version gate in the code. Node.js 20+ is the hard requirement ("engines": { "node": ">=20" }).

Two places where the OS does matter in practice:

  • On macOS 27 the Accessibility pane is renamed Device Control and Data Access — same grant, new name (see macOS Permissions).

  • Safari 26 and 27 changed several web-platform behaviours that specific tools account for; those are handled, not blocked.

Yes — that is what Running several agents at once covers. Each session owns its own tabs, and ownership is tracked per session rather than by tab index, so one agent cannot act on (or close) a tab another agent owns.

One process binds the extension bridge port; the others proxy through it and take the port over if that process exits.

No. safari_new_tab opens a background tab and never steals focus, and the few commands macOS forces an implicit activate on restore your previous frontmost app afterwards — with a guard that leaves focus alone if you were typing in Safari yourself a moment earlier.

The two exceptions are the deliberately native tools — safari_native_click, safari_native_keyboard, safari_native_hover — which post real OS events and therefore land wherever the frontmost window is. Do not use those while you are working in another Safari tab.

Your existing extensions keep running normally; safari-mcp drives the same Safari you use, so content blockers and password managers behave exactly as they do for you.

safari-mcp also ships its own optional extension. Without it roughly 80% of the tools still work over AppleScript alone; with it you additionally get closed shadow DOM, strict-CSP sites, deep framework state and loading-state detection. Dialog handling, native clicks and PDF export go the other way — those are AppleScript-only. The full split is in Safari Extension (Optional).

Anything that evaluates JavaScript fails — safari_evaluate, safari_read_page, form filling, extraction — while pure AppleScript actions such as opening or switching tabs still work. That is the usual cause of "it opens the tab but reads nothing".

Turn it on in Safari → Settings → Advanced → Show features for web developers, then Safari → Settings → Developer → Allow JavaScript from Apple Events. Both are listed under Prerequisites. safari_doctor walks the rest of the chain (Automation, native helper, Accessibility, Screen Recording) and names this setting in its fix hint — but its Apple Events check only counts windows, which Automation permission alone satisfies, so it will not fail on this one for you.

Works With

Safari MCP works with any MCP-compatible client:

Client

Status

Claude Code

✅ Tested daily

Claude Desktop

✅ Tested

Cursor

✅ Tested

Windsurf

✅ Compatible

VS Code + Continue

✅ Compatible


Contributing

PRs welcome! See CONTRIBUTING.md for setup instructions.

The codebase is two files:

  • safari.js — Safari automation layer (AppleScript + JavaScript)

  • index.js — MCP server with tool definitions


Sponsors

Safari MCP is free and open source. If it saves you time or CPU cycles, consider supporting its development:

Your support funds:

  • 🧪 Testing across macOS versions and Safari releases

  • 🛠️ New tools and features

  • 📖 Documentation and examples

Become the first sponsor!


Commercial Support

Need Safari MCP integrated into your product or agent stack? Achiya Automation offers:

  • Priority bug fixes and custom tool development for your use case

  • Integration consulting — wiring Safari MCP into production agent systems (Claude, Cursor, n8n, custom)

  • Private deployment support — multi-user Safari MCP, non-standard macOS environments, CI/CD

  • Training workshops for engineering teams adopting MCP-based automation

Built by the author of Safari MCP. Start a conversation →


What agents unlock with Safari MCP

When an AI agent drives Safari MCP, it gets things a headless browser can't:

  • Real authenticated sessions — Gmail, GitHub, Ahrefs, Slack, banking dashboards are all already logged in

  • Framework-aware form filling — safari_fill_and_submit calls React/Vue/Angular setters natively, no guessing whether input events fired

  • Background operation — the agent works in parallel while you keep using your Mac

  • One MCP call per workflow — safari_run_script batches navigation + clicks + extraction into a single roundtrip

The pattern holds across models: drive the browser the human already trusts — you inherit logins, cookies, extensions, and the user's exact environment in one step.


Community

6,000+ monthly npm downloads — developers are building AI agents on macOS with Safari MCP.


Ecosystem

Other macOS MCP servers that complement Safari MCP:

Project

What it does

When to use

mcp-server-macos-use

OS-level macOS automation (accessibility, screen control)

System-wide interactions beyond Safari

chrome-devtools-mcp

Chrome DevTools Protocol

Lighthouse audits, Chrome-specific performance traces

Using Safari MCP alongside Chrome DevTools MCP? Safari handles 95% of daily browsing (zero overhead), Chrome handles the 5% that needs Lighthouse or Chrome-specific traces.


Listed On

Glama MCP Registry MCP Scoreboard CI


License

MIT — use it however you want.


Built by Achiya Automation — automation & AI agents for business.

Available Tools

98 tools
safari_accessibility_snapshotA

Get the accessibility tree of the page (roles, ARIA labels, focusable elements, form states). Essential for a11y auditing.

Inputs: receipt, maxDepth, selector.

safari_analyze_pageA

Full page analysis in ONE call: title, URL, meta tags, OG, headings, link stats, image stats, forms, and text preview. Perfect for SEO/audit.

Inputs: no named parameters.

safari_check_pwaA

Audit the page for iOS 'Add to Home Screen' / PWA readiness: apple-mobile-web-app-capable, apple-touch-icon (incl. 180x180), theme-color, status-bar style, web app manifest, splash screens. Returns a pass/total checklist.

Inputs: no named parameters.

safari_clear_consoleA

Clear all captured console messages

Inputs: no named parameters.

safari_clear_fieldB

Clear an input field

Inputs: selector (required), receipt.

safari_clear_mocksA

Remove all network route mocks (restore real network behavior)

Inputs: no named parameters.

safari_clear_networkA

Clear all captured network requests

Inputs: no named parameters.

safari_clickA

Click element. Use ref (from snapshot), selector, text, or x/y. Works on React/Airtable/virtual DOM apps via full PointerEvent+MouseEvent sequence + React Fiber fallback. Pure JS — never touches user's mouse. When using ref, always take a FRESH safari_snapshot first — refs expire after each new snapshot.

Inputs: x, y, ref, text, +2 more.

safari_click_and_readA

Click an element then return the updated page — saves 1 full round-trip vs separate click+read_page. Handles both React Router navigation and full page loads.

Inputs: x, y, text, wait, +3 more.

safari_click_and_waitA

Click an element AND wait for the result (page load or element). Use instead of click + wait_for separately.

Inputs: text, timeout, waitFor, selector.

safari_clipboard_readA

Read the current clipboard content (text)

Inputs: no named parameters.

safari_clipboard_writeB

Write text to the system clipboard

Inputs: text (required).

safari_close_tabA

Close the current tab. After a daemon/session restart, pass the opaque receipt returned by safari_new_tab or safari_list_tabs.

Inputs: url, receipt.

safari_console_filterA

Get console messages filtered by level (must call safari_start_console first)

Inputs: level (required).

safari_css_coverageA

Analyze CSS coverage: find unused CSS rules across all stylesheets. Shows coverage percentage per stylesheet.

Inputs: no named parameters.

safari_delete_cookiesA

Delete a specific cookie or all cookies for the current page

Inputs: all, name.

safari_delete_local_storageA

Delete a localStorage key, or clear all localStorage (omit key to clear all)

Inputs: key.

safari_delete_session_storageA

Delete a sessionStorage key, or clear all sessionStorage (omit key to clear all)

Inputs: key.

safari_detect_formsA

Auto-detect all forms on the page with their fields, types, selectors, and submit buttons. Great for automated form filling.

Inputs: no named parameters.

safari_doctorA

Diagnose the macOS permission + daemon chain in one shot: Safari running, Apple Events/Automation, native helper daemon, Accessibility (native clicks), Screen Recording, and the helper's codesign identity. Returns a pass/fail checklist with the exact System Settings fix per failure, then the extension bridge: which worker holds the profile lease, every worker that tried and what the profile probe answered it, and what each Safari profile's extension storage holds. Run this FIRST when clicks/screenshots/startup 'don't work even with permissions granted' or when tools answer 'profile extension unavailable'.

Inputs: no named parameters.

safari_double_clickA

Double-click an element by CSS selector or x/y coordinates (e.g. to select a word in text)

Inputs: x, y, receipt, selector.

safari_dragB

Drag an element to another element or position. Use CSS selectors or x/y coordinates.

Inputs: sourceX, sourceY, targetX, targetY, +2 more.

safari_emulateA

Emulate a mobile device: sizes the Safari window to the device's CSS viewport and overrides navigator.userAgent/platform/maxTouchPoints (iOS/iPadOS 27 values) in the current page — JavaScript only: the HTTP User-Agent header is unchanged and a reload or navigation clears it. Devices: iphone-18-pro, iphone-18-pro-max, iphone-air, iphone-17, iphone-17e, iphone-16, iphone-14, iphone-14-pro-max, ipad, ipad-air-11, ipad-air-13, ipad-pro-11, ipad-pro-13, ipad-pro, pixel-7, galaxy-s24. Or pass width/height (and userAgent).

Inputs: width, device, height, userAgent.

safari_eval_fileA

Execute JavaScript read from a FILE path (avoids passing huge scripts inline / manual copy). Same engine as safari_evaluate: extension-first (no focus steal), AppleScript fallback. Use to upload binary via a generated .js containing base64.

Inputs: path (required), receipt.

safari_evaluateA

Execute JavaScript in the current page (a returned Promise is awaited — fetch/timers work in background tabs; requestAnimationFrame never fires there). Automatically falls back to AppleScript when CSP blocks execution (e.g. Google Search Console, LinkedIn). For reading data, prefer safari_read_page or safari_snapshot. For interactions, prefer safari_click/fill with refs.

Inputs: script (required), frame, receipt.

safari_export_storageA

Export all storage state (cookies + localStorage + sessionStorage) as JSON — useful for saving and restoring login sessions

Inputs: no named parameters.

safari_extract_imagesA

Extract all images with src, alt, dimensions, loading strategy, viewport visibility

Inputs: limit.

safari_extract_metaA

Extract all meta tags: title, description, canonical, OG tags, Twitter cards, JSON-LD, alternate languages, RSS feeds

Inputs: no named parameters.

safari_extract_tablesA

Extract HTML tables as structured JSON (headers + rows). Perfect for scraping data tables.

Inputs: limit, selector.

safari_fillA

Fill/replace value in an input, textarea, select, OR contenteditable (rich text). Handles React controlled inputs, ProseMirror, Draft.js, and Google Closure editors automatically. Use for SETTING a value (replaces existing). For code editors (Monaco/CodeMirror/Ace), use safari_replace_editor instead. For character-by-character typing in search boxes, use safari_type_text. IMPORTANT: When using ref, always take a FRESH safari_snapshot first — refs expire after each new snapshot (prefix changes: 5_xx → 6_xx).

Inputs: value (required), ref, receipt, selector.

safari_fill_and_submitA

Fill a form AND submit it in one operation. Finds submit button automatically if not specified.

Inputs: fields (required), submitSelector.

safari_fill_formB

Fill multiple form fields at once

Inputs: fields (required), receipt.

safari_get_computed_styleA

Get computed CSS styles for an element. Optionally filter specific properties.

Inputs: selector (required), properties.

safari_get_consoleA

Get captured console messages (must call safari_start_console first)

Inputs: no named parameters.

safari_get_cookiesA

Get cookies for the current page

Inputs: no named parameters.

safari_get_elementB

Get detailed info about an element (tag, text, rect, attributes, visibility)

Inputs: selector (required), receipt.

safari_get_indexed_dbB

Read records from an IndexedDB database store

Inputs: dbName (required), storeName (required), limit.

safari_get_sourceC

Get HTML source of current page

Inputs: maxLength.

safari_go_backB

Go back in browser history

Inputs: receipt.

safari_go_forwardB

Go forward in browser history

Inputs: receipt.

safari_handle_dialogB

Set up handler for the next alert/confirm/prompt dialog

Inputs: text, action, receipt.

safari_hoverB

Hover over element. Use ref, selector, or x/y

Inputs: x, y, ref, receipt, +1 more.

safari_import_storageB

Import storage state from JSON (as exported by safari_export_storage) — restores cookies, localStorage, sessionStorage

Inputs: state (required).

safari_inspect_viewportA

Validate the page's against iOS Safari best practices: width=device-width, initial-scale, disabled-zoom (WCAG 1.4.4), viewport-fit=cover. Returns parsed attributes + severity-tagged issues (error/warning/info).

Inputs: no named parameters.

safari_list_framesA

List every document in the tab — the main page plus each iframe — with its frameId, URL and text length. Use when a page's content lives in a cross-origin iframe (an embedded app, a micro-frontend shell, a payment field): safari_read_page and safari_evaluate see only the main document there and come back empty or with a loader. Pass the frameId or a URL substring as safari_evaluate's frame to run inside it.

Inputs: receipt.

safari_list_indexed_dbsA

List all IndexedDB databases on the current page

Inputs: no named parameters.

safari_list_tabsA

List every tab in this session's Safari window (titles + URLs, including the user's own tabs — verbose and rarely needed). Tabs this session owns carry a receipt. Prefer keeping the receipt from safari_new_tab over listing.

Inputs: no named parameters.

safari_local_storageA

Get localStorage data for the current page

Inputs: key.

safari_mock_routeA

Intercept network requests matching a URL pattern and return a mock response. Works with both fetch and XHR. Useful for testing API error states, offline behavior, or replacing API responses.

Inputs: response (required), urlPattern (required).

safari_native_clickA

OS-level mouse click via macOS CGEvent — produces isTrusted: true events that pass WAF/bot detection (G2, Cloudflare, etc.). Use when regular safari_click fails with 405/403 errors or form submissions are blocked. Trade-off: physically moves the mouse cursor and requires Safari window to be visible. Use ref (from snapshot), selector, text, or x/y. When using ref, always take a FRESH safari_snapshot first.

Inputs: x, y, ref, text, +2 more.

safari_native_hoverA

OS-level mouse hover via macOS CGEvent — moves the real cursor to an element to trigger native :hover / mouseenter handlers. Use for obfuscated UIs where JS-dispatched mouseenter isn't enough, like Discord server sidebars (tooltips only appear on real hover) or portal-rendered tooltips. After hover, call safari_wait_for or safari_evaluate to read the tooltip. Dwells for dwellMs to let tooltips render, then restores the original cursor position by default. Requires Safari window to be visible.

Inputs: x, y, ref, text, +3 more.

safari_native_keyboardA

OS-level keyboard event via macOS CGEvent — sends a real keypress (with optional modifiers) to the Safari window WITHOUT activating Safari or stealing focus. Use when safari_press_key's JS path doesn't reach React trust-gated handlers (Discord ProseMirror Enter, Slack send, virtualized editors). Keys: enter, return, tab, escape, space, delete, backspace, up/down/left/right, home, end, pageup, pagedown, f1-f6, a-z, 0-9 and common punctuation. Modifiers: cmd, shift, alt, ctrl. Produces isTrusted:true events. Never activates Safari — runs entirely in the background.

Inputs: key (required), modifiers.

safari_native_typeA

Insert text into ANY editor via OS-level clipboard paste (CGEvent Cmd+V targeted to Safari window). Unlike safari_fill which manipulates DOM directly (breaking React/ProseMirror state), this goes through the real paste pipeline — ProseMirror/Slate/Draft.js process the paste event natively and update their internal model. After native_type, pressing Enter (via safari_native_keyboard) will actually submit the form because the framework state matches the DOM. Saves and restores the user's clipboard. No focus stealing. Use for Discord, Slack, and any editor where safari_fill works visually but the content isn't 'really there' when you try to submit.

Inputs: value (required), ref, selector.

safari_navigateA

Navigate this session's tab to a URL (pass receipt to pin the tab). Waits for the page to load. Returns {title, url} — plus a fresh receipt when the origin changed; use that one from then on.

Inputs: url (required), receipt.

safari_navigate_and_readA

Navigate to a URL and return the page content in one step — saves 1 full round-trip vs navigate+read_page. Use instead of safari_navigate + safari_read_page. Like safari_navigate, returns a fresh receipt when the origin changed; use that one from then on.

Inputs: url (required), receipt, timeout, maxLength.

safari_networkA

Quick network overview via Performance API (no setup needed). Shows URLs and timing for resources loaded by the page. For detailed request/response info (headers, status codes, POST bodies), use safari_start_network_capture + safari_network_details instead.

Inputs: limit.

safari_network_detailsA

Get captured network requests with full details (must call safari_start_network_capture first)

Inputs: limit, filter, receipt.

safari_new_tabA

Open a new background tab (never steals focus), optionally with a URL. Returns {tabIndex, safeUrl, receipt}. KEEP THE RECEIPT and pass it as receipt to every later call on this tab — it survives MCP reconnects, daemon restarts, parallel sessions and subagents, and makes safari_list_tabs / safari_switch_tab unnecessary.

Inputs: url.

safari_override_geolocationB

Override the browser's geolocation API to return custom coordinates

Inputs: latitude (required), longitude (required), accuracy.

safari_paste_imageA

Paste an image from a local file into the focused element via JS DataTransfer (no clipboard, no focus steal). Works on Medium, dev.to, HackerNoon, TOI, etc.

Inputs: filePath (required), receipt.

safari_performance_metricsA

Get detailed performance metrics: navigation timing, Web Vitals (FCP, LCP, CLS), resource breakdown, memory usage

Inputs: no named parameters.

safari_press_keyB

Press a keyboard key (enter, tab, escape, arrows, etc). Supports modifiers (cmd, shift, alt, ctrl).

Inputs: key (required), receipt, modifiers.

safari_query_allA

Find all elements matching a CSS selector (returns tag, text, href, value). x/y are relative to each element's own frame viewport — for matches inside an iframe, click by selector or snapshot ref, not by these coordinates

Inputs: selector (required), limit, receipt.

safari_react_select_list_optionsA

List available options of a react-select v5 dropdown without opening the menu. Returns JSON {ok, total, options:[{label,value}…]}. Useful when safari_react_select_set returns 'option not found' and you need to see exact labels (e.g. 'Email Routing Rules' vs 'Email Routing'). Either ref or selector required.

Inputs: ref, selector.

safari_react_select_setA

Set a value in a react-select v5 dropdown by walking React fiber to find the Select component and invoking onChange directly — bypasses the menu UI entirely. Use when safari_click on the chevron or option keeps failing (Cloudflare custom token forms after a few rows, portal-rendered selects that intercept synthetic events). Returns JSON {ok, selected} on success, or {ok:false, error, available:[…]} listing up to 30 option labels on miss. Match is by label, value, or case-insensitive label. Either ref or selector required. NOTE: For Permissions-levels combos that are disabled until a Permission is selected, set the Permissions value first — the level combo becomes enabled and its props.options populate.

Inputs: value (required), ref, selector.

safari_read_pageA

Read page text content (title, URL, body text). Use for reading article text or page content. For interacting with elements, prefer safari_snapshot (gives ref IDs). Use selector to read specific element. Use maxLength to limit output.

Inputs: receipt, selector, maxLength.

safari_reloadB

Reload the current page

Inputs: hard, receipt.

safari_reload_extensionA

Hot-reload the Safari MCP Bridge extension — forces it to reload its own code from disk without requiring manual Safari Preferences → Extensions → toggle. Use after editing extension/background.js or extension/content.js in the safari-mcp repo. The extension briefly disconnects during reload and auto-reconnects within ~2 seconds. NOTE: this tool itself requires the extension version already installed to support the reload_extension command (added in v2.9.1+). If your extension is older, trigger a manual reload once to pick up this feature.

Inputs: no named parameters.

safari_replace_editorA

Replace ALL content in a code editor (Monaco, CodeMirror, Ace, ProseMirror). Use ONLY for code editors — Airtable automations, GitHub gists, CodePen, n8n code nodes, etc. NOT for rich text editors like Medium/LinkedIn (use safari_fill for those). Detects ProseMirror/Draft.js/CodeMirror/Monaco/Ace and uses their native API.

Inputs: text (required), receipt.

safari_reset_emulationA

Reset device emulation: restores the window size from before safari_emulate and removes its navigator overrides (no reload)

Inputs: no named parameters.

safari_resizeC

Resize the Safari window

Inputs: width (required), height (required).

safari_right_clickB

Right-click (context menu) an element by CSS selector or x/y coordinates

Inputs: x, y, receipt, selector.

safari_run_scriptA

Batch Safari actions in one MCP session. Named profiles use the verified extension and support: newTab, switchTab, getReceipt, listTabs, closeTab, navigate, navigateAndRead, readPage, snapshot, getElementInfo, querySelectorAll, waitFor, waitForTime, click, clickAndOpenPopup, fill, fillForm, clearField, typeText, selectOption, pressKey, scroll, scrollTo, scrollToElement, hover, evaluate, reload, goBack, goForward. switchTab accepts an index or an opaque receipt; a valid receipt recovers the exact owned tab across Safari windows without focusing one. clickAndOpenPopup takes exactly one selector or snapshot ref, targets one exact frame, performs one click, captures a blocked HTTP(S) window.open, and opens it as a background tab without focusing Safari; it refuses CAPTCHA/challenge targets and never returns URL query/hash data. Non-profile mode retains the legacy action set plus getReceipt; there, closeTab and switchTab take a tab index, not a receipt, and getReceipt needs the tab's receipt in args. After a cross-origin redirect, rotate the tab's receipt with {"action":"getReceipt","args":{"receipt":""}}.

Inputs: steps (required).

safari_safe_area_insetsA

Read the live CSS safe-area-inset values (top/right/bottom/left) as the page sees them, whether viewport-fit=cover is set, and whether env(safe-area-inset-*) is used in any stylesheet. For notch / Dynamic Island layout debugging.

Inputs: no named parameters.

safari_save_pdfA

Save the current page as a PDF file. Uses screencapture + PDF rendering (no Safari UI interaction needed).

Inputs: path (required).

safari_screenshotA

Take a visual screenshot (base64 JPEG). EXPENSIVE — use safari_snapshot instead for most tasks. Only use screenshot when you need to verify visual layout, styling, images, or colors that snapshot can't show. Pass maxWidth (e.g. 1024) for layout checks — far fewer tokens.

Inputs: receipt, fullPage, maxWidth.

safari_screenshot_elementA

Take a screenshot of a specific element (by CSS selector). Returns a base64 image.

Inputs: selector (required), receipt.

safari_scrollC

Scroll the page up or down by a specified amount

Inputs: amount, receipt, direction.

safari_scroll_toB

Scroll to a specific position on the page

Inputs: x, y, receipt.

safari_scroll_to_elementA

Scroll to element by CSS selector OR text. For virtual DOM (Airtable) use text — scrolls down until text appears in DOM.

Inputs: text, block, receipt, timeout, +1 more.

safari_select_optionA

Select an option in a native dropdown. Sets .value and dispatches change event. Pass ref (from safari_snapshot) for a select inside an iframe or shadow DOM — a plain selector only reaches the top document. For custom dropdowns (React/LinkedIn), use safari_click on the dropdown trigger, then safari_click on the option instead.

Inputs: value (required), ref, receipt, selector.

safari_session_storageA

Get sessionStorage data for the current page

Inputs: key.

safari_set_local_storageB

Set a value in localStorage

Inputs: key (required), value (required).

safari_set_session_storageB

Set a value in sessionStorage

Inputs: key (required), value (required).

safari_snapshotA

PREFERRED way to see page state. Returns accessibility tree with ref IDs for every interactive element. Use refs with click/fill/type instead of CSS selectors. Workflow: snapshot → see refs → click({ref:'0_5'}). PREFER THIS over safari_screenshot (cheaper, structured text vs heavy image) and over safari_read_page (includes interactive refs). Use safari_screenshot only when you need to see visual layout/styling.

Inputs: diff, receipt, selector.

safari_start_consoleA

Start capturing console messages (log, warn, error, info). Call once per page.

Inputs: no named parameters.

safari_start_network_captureA

Start capturing detailed network requests (fetch + XHR) with headers, status, timing. Call once per page. Intercepts fetch/XHR — captures requests AFTER this call only. For quick overview of already-loaded resources, use safari_network instead.

Inputs: no named parameters.

safari_switch_tabA

Switch the MCP session's logical target without focusing Safari. Pass an index for the current session window, or an opaque receipt to recover the exact owned tab across Safari windows; a valid receipt takes precedence over any supplied index. Rarely needed: passing receipt on each tool call already targets that tab.

Inputs: url, index, receipt.

safari_throttle_networkA

Simulate slow network conditions. Profiles: slow-3g, fast-3g, 4g, offline. Or custom latency/speed. Call with no args to reset.

Inputs: latency, profile, downloadKbps.

safari_type_textA

Type text character-by-character with realistic key events. Best for: search boxes (triggers autocomplete), chat inputs, and fields that react to each keystroke. For rich text editors (Medium, HackerNoon, LinkedIn), use safari_fill instead — it uses framework-native APIs. For code editors (Monaco/CodeMirror), use safari_replace_editor. When using ref, always take a FRESH safari_snapshot first — refs expire after each new snapshot.

Inputs: text (required), ref, receipt, selector.

safari_upload_fileA

Upload a file to a element via JavaScript DataTransfer — NO file dialog, NO UI interaction. IMPORTANT: Do NOT click the file input before calling this tool — just provide the selector and file path. If a file dialog is already open, this tool will close it first. NOTE: 'verified 0 files' may appear even on success if the site uses a custom upload handler — check visually with safari_snapshot. For an IMAGE going into a composer/editor that should show a thumbnail, pass verifyPreview:true — some sites (Google Business Profile) accept the file handle and flip their UI to 'attached' while ingesting nothing, and the post then publishes with no image.

Inputs: filePath (required), selector (required), receipt, forceNative, +1 more.

safari_verify_stateA

Verify the framework-level state of an editor/input matches the expected value. Returns JSON {match, mode, actual, expected, hint?}. Modern editors (ProseMirror, Lexical, Closure, React-controlled inputs) maintain state separately from the DOM — .value or .textContent may show new text while the internal store still holds old data, so a Submit click sends stale data. Call this AFTER safari_fill and BEFORE clicking Submit on critical forms (Featured.com, LinkedIn share, Medium, Reddit).

Inputs: expected (required), selector (required), receipt.

safari_waitA

Wait for a fixed time in milliseconds. Use only when you need a brief pause between actions. PREFER safari_wait_for (waits for element/text to appear) — it's smarter and doesn't waste time.

Inputs: ms (required).

safari_wait_forC

Wait for an element or text to appear on the page

Inputs: text, receipt, timeout, selector.

safari_wait_for_new_tabA

Wait for a new tab to appear (e.g. after OAuth login click opens popup). Automatically switches to the new tab.

Inputs: timeout, urlContains.

safari_webkit_compatA

Check every CSS property used on the page against THIS Safari via CSS.supports() — reports unsupported properties, properties that need a -webkit- prefix, and known Safari rendering quirks (e.g. position:sticky inside overflow ancestors). Tested in the live engine, so no false positives.

Inputs: no named parameters.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.4/5.0

    Scored across 98 tools

    Disambiguation3/5

    The set contains many near-duplicate interaction primitives (safari_click, safari_native_click, safari_click_and_read, safari_click_and_wait, safari_double_click/right_click; safari_fill vs safari_type_text vs safari_native_type) plus multiple page-reading tools. Descriptions largely explain when to use each, so boundaries are recoverable, but the sheer number of options creates real misselection risk.

    Naming Consistency5/5

    All 98 tools use the safari_ prefix and snake_case, with predictable verb/object naming for most (safari_click, safari_navigate, safari_get_cookies). A few noun-style names (safari_snapshot, safari_doctor, safari_network) are minor deviations, not convention mixing.

    Tool Count1/5

    98 tools is far beyond a well-scoped browser automation surface; many are specialized variants (native_*, *_and_read/wait, storage/console/network micro-tools). Even if individually useful, this is an extreme tool-count mismatch for agent selection.

    Completeness5/5

    Coverage is exceptionally broad: navigation, interaction, forms, storage, cookies, network mocking/capture, console, screenshots, accessibility, PWA, CSS coverage, emulation, iframes, uploads, and dialogs. No obvious lifecycle gap for browser automation.

    Maintenance

    ActivityActive
    ResponsivenessUsually handled

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      D
      maintenance
      Provides AI assistants with Safari browser automation and developer tools access, enabling LLMs to control Safari, access console logs, monitor network activity, and perform browser automation tasks.
      13
      9 npm
      33
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Native macOS browser (SwiftUI + WKWebView) that exposes an MCP server in-process so AI agents can drive a real local browser with the user's existing logged-in sessions. ~40 tools (navigate, click, fill, screenshot, eval_js, network log, accessibility tree). Local-only with bearer-token auth.
      2
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Lightweight browser automation server for AI agents, enabling fast (10-25ms per action), structured observations and semantic selectors with zero token overhead.
      6
      Apache 2.0
    • F
      license
      Not graded
      quality
      B
      maintenance
      Native WebKit browser automation for MCP agents on Apple silicon Macs (macOS 15+), with persistent sign-ins, human approval and local handoff. Runs locally through a companion app; Developer Preview under BUSL-1.1, with a separate agreement required for commercial production use.
      1
      -