Native WebKit for Mac. Human approval and persistent sign-ins. Requires the companion app.
Native WebKit automation for Apple Silicon, exposed through MCP and designed for an LLM rather than a human.
This repository is a Swift rewrite. The retained TypeScript/Playwright files are prior art only and are not the implementation being extended.
WKWebView runtime using the persistent system WebKit data store for real authenticated sessions.address_resolution_failed, address_now_ambiguous, logical_target_changed, node_replaced_but_semantic_locator_recovered, and coordinate_invalidated_by_layout_change.think: false, strict budgets, and deterministic fallback.browser_session can set a bounded 320×240–3840×2160 CSS viewport and can
navigate back, forward, or reload using WebKit's own history list. History
movement is confirmed against the destination WebKit reports; reloading a page
produced by a form submission is refused before confirmation to prevent replay.WKUIDelegate. The observation names the requesting origin and permission, with
repeated requests counted and distinct records bounded; there is no MCP operation
that can grant one.formaction overrides its form's action, so a control whose
accessible name reads harmlessly can post elsewhere.handoff_start immediately returns an opaque session-bound resume token while the actual WebKit session becomes a visible window. handoff_status is non-blocking; handoff_resume consumes the token only after local confirmation and returns a fresh observation. The agent remains locked out throughout human control.handoff_focus without changing who controls the page. Session status says whether that surface is currently presented, and restricted authentication status exposes only bounded rendering booleans—never page text, field values, private URL components, or network payloads.session_in_use with
wait_only=true and cannot observe, navigate, act, invalidate addresses, or
request duplicate user control in it. Ownership transfers after the owning
client disconnects, or through a human-confirmed client_handoff. Native
confirmations are shown one at a time across the app, and each names the
requesting agent as a quoted, self-reported name.browser_session(operation: "open") and keep
its returned session_id for all page operations. On a shared transport, a
full broker refuses an open that would return an already-owned page. Status
without an ID selects the caller's sole session; when it owns several, it asks
for an explicit ID. A new client with spare capacity is directed to open its
own page. Host lease metadata reports the broker's remaining session slots.window.opener, at most four; all close when the agent resumes. Under
agent control new windows stay refused and reported.WKWebView attached to one opaque window. Agent control orders that window
out; human handoff presents the same view without reparenting it. A packaged
visual smoke mode checks the actual WindowServer pixels.open(wait_timeout_ms: ...) can wait up to 60 seconds for the local
host lease without stealing it; dead transports release their session owner.
browser_session(operation: "client_handoff") can transfer an idle session
only after local human confirmation and a final no-active-call check.credential_not_found; WebkitUIMCP
then offers a native human handoff for manual sign-in and addition/update in
SiliconPass. No credential value crosses MCP, JSON, logs, or the clipboard.HttpOnly authenticated cookie plus localStorage and sessionStorage survive in the same view/data-store lifetime.browser_act exposes click, native submit-control click, bounded non-sensitive input/textarea fill, Enter/Tab/Escape, blur, explicit input commit, and answering the one JavaScript alert/confirm/prompt dialog a page is suspended on (dialog_accept, dialog_dismiss, dialog_accept_value). A pending dialog is reported in the observation as untrusted site content, blocks every other operation until it is answered, is never answered automatically, and resolves to an explicit indeterminate outcome if nobody answers it. beforeunload is out of scope: the public macOS SDK exposes no before-unload panel to an embedder. Native approval also routes public text fills through AppKit insertion with a measured trusted input receipt. Fill verifies both the freshly re-resolved semantic target's exact value and that its live validation state is not invalid. Other actions support exact URL or URL prefix, title, heading, exact/contains semantic text, checked, selected, enabled, semantic target absence (target_absent with value="true"), value, validation state, character count, bounded state attributes, dialog, named panel, or selected-option postconditions. A same-URL SPA mutation returns same_url_page_state_changed guidance instead of implying success. UI state never proves backend commit.browser_download converts authenticated attachment responses to WKDownload from either a fresh observed control or a protected same-origin URL fallback. It requires exact native confirmation plus a save-panel destination, never overwrites an existing file, and succeeds only after an on-disk receipt reports the HTTP status, suggested/final filename, MIME type, byte count, SHA-256, and decoded provisioning-profile UUID when available. Cookies, headers, and the absolute local destination path stay outside MCP.input/change events, so site handlers may autosave or cause server effects. It is destructive and human-confirmed; password controls require local human handoff.approval_mode: "native" sends confirmed click/submit and Enter/Tab/Escape through public AppKit NSEvent handling on the freshly re-resolved WKWebView target. An isolated message handler must observe the matching DOM event with event.isTrusted == true before the receipt reports trust. Missing/mismatched receipts fail indeterminate; no flag is synthesized. approval_mode: "mcp", blur, and commit remain JavaScript-dispatched and report untrusted. Native public-text fill reports trust only when its matching AppKit insertion receipt is observed.confirmation_mode, dispatch_mode, and trusted_gesture_state. Native confirmation alone never establishes event trust.readyState=complete and mutation quiescence while remaining blank;
navigation, observation, and text reads report empty_or_unusable in that case and
explicitly forbid treating the absence as page truth. A rendered textless canvas or
media document remains usable.browser_read_text reads only currently rendered virtualized lines. Use bounded scroll plus another read for additional ranges.WKWebView ContentMode API—and
set_emulated_media is deliberately absent because it does not help operate a
person's site.WKWebsiteDataStore.proxyConfigurations is public from macOS 14 and is Apple DTS's own recommendation for reading a WKWebView's request contents, and a bundled WKWebExtension with the webRequest permission is public from macOS 15.4. Neither is free: the proxy route needs a trusted root certificate to see inside HTTPS, and the extension route reports no headers and cannot block. What is reported instead is narrow — the download receipt's HTTP status, and the egress proxy's accepted, blocked, pinned and timed-out connection counts. A main-frame navigation result carries no HTTP status today, and the proxy does not name the hosts it allowed or refused.safaridriver dispatches NSEvent through [window sendEvent:] exactly as this does, and the WebDriver specification requires every conformant driver to produce trusted events. What differs is the measurement: an action whose trusted DOM receipt is missing or mismatched fails indeterminate here, where Playwright's hit-target interceptor treats an absent event as success.WKFormInfo submission gate is implemented, unit-tested, and does not fire. WebKit did not call webView(_:willSubmitForm:submissionHandler:) on either macOS 27.0 build measured here, 26A5416b and 26A5419a — not for submit(), not for requestSubmit(), and not for a native trusted-gesture click. It is therefore not a second live gate and must not be read as one; it will decide if WebKit begins delivering the callback. The live defence against a submit control that posts to another site is the destination line in the confirmation.THIRD_PARTY_EMBED provenance and a sanitized origin. Their geometry is explicitly
frame-local; frameActionModes (or compact frame_action_modes) names eligible
attempts even when actionable=false means no native pointer geometry. A confirmed
hover or select_option runs only as untrusted JavaScript in the exact child
frame; non-sensitive press_key and fill can use AppKit only
when that frame returns a matching trusted DOM receipt. A native pointer click has
no public frame-to-window coordinate transform, refuses before confirmation with
cross_origin_native_geometry_unavailable, and points to live human handoff.
This is bounded control reach, not proof of hosted checkout completion. Sensitive
and authentication controls still require a human. Frames that cannot be
evaluated, and frames beyond the bounded native registry, remain explicitly
unreadable rather than being treated as empty page content.WKWebView (for example a WebAuthn-only step without the
browser passkey entitlement). Those return full_browser_required with an internal
safari_compatibility requirement. Any already-restricted authentication origin may
also use compatibility_start as an explicit manual fallback when its native human
handoff is present but unusable. The operation opens the exact private URL in Safari
only after native confirmation; it never auto-switches merely because a page timed out,
never replaces the native handoff, provides no Safari observation/control, and cannot
return a Safari login to this MCP session. Complete the blocked workflow manually in
the external browser. WebKitUI never copies cookies, passkeys, AutoFill data, or
credentials between backends.takeSnapshot may omit GPU-composited effects.WebKitRuntime(protectedWebsiteDataStore:); the lower-level WebKitRuntime(websiteDataStore:) initializer is intentionally unprotected for fixtures and embedding.docs/network-boundary.md.Apple's Safari MCP server ships inside safaridriver and is started with
safaridriver --mcp; Apple's own help text for the flag reads "Run as an MCP (Model
Context Protocol) server using stdio transport." It exposes seventeen tools — page
content, screenshots, network requests, console logs, JavaScript evaluation, DOM
interaction, viewport and media emulation, tab management. It needs two settings in two
different panes: "Show features for web developers" in Safari > Settings > Advanced, and
"Allow remote automation and external agents" in Safari > Settings > Developer. Apple's
floor is Safari 27 beta or Safari Technology Preview 247 or later, and Apple documents
Safari 27 beta on macOS 26 and macOS Sequoia as well as macOS 27, so it is not a macOS 27
requirement.
Apple aims it at web developers: it "gives your agent the ability to know how your code actually renders in the browser", and both sets of release notes file it under WebDriver > New Features. Apple also states it "runs entirely on your local machine and makes no network calls of its own" and that it "does not have access to your personal information in Safari (e.g. AutoFill or other browser activity)". For developing a site it is the better tool, it is included with Safari and Apple lists no separate price, and this project does not compete with it.
The difference is what happens when the site is not yours and you are signed in to it:
| Safari MCP server | WebKitUI MCP | |
|---|---|---|
| Purpose | inspect and debug a site you are developing | act on a site you are signed in to |
| Approval | no confirmation step is documented, and none of the seventeen tools requests one | native macOS confirmation before every exposed click and open-world navigation |
| Tool annotations | none: every tool carries only name, description and inputSchema, so a client gets no signal separating page reading from JavaScript evaluation | readOnlyHint, destructiveHint, idempotentHint and openWorldHint on every tool |
| JavaScript evaluation | exposed as a tool | absent, with no CDP or coordinate fallback |
| Session | Apple's WebDriver documentation states automation windows are "isolated from normal browsing windows, user settings, and preferences" and that a session "always starts from a clean slate" | its own WKWebView and its own persistent profile, which is what authenticated work requires |
| Protocol | reports 2024-11-05 | 2026-07-28, the current revision |
| Network detail | full request inspection, including headers, body and timing | no per-request inspection: the download receipt's HTTP status, and the egress proxy's connection counts without host names |
| Verification | inspection tools | an explicit postcondition per action, plus separate confirmation_mode, dispatch_mode and trusted_gesture_state |
The isolation wording above is Apple's WebDriver documentation, about automation sessions generally. Apple has published no equivalent statement about the MCP server itself.
Both can be installed at once, and for most developers both should be.
Two things this project will not say about Apple's server, because Apple does not: that it cannot reach your cookies or your logged-in sessions — Apple's MCP post names AutoFill and "other browser activity" only — and that Apple states there is no confirmation step. The absence is documented; a denial is not.
The product targets macOS 15+ on Apple silicon. Building the optional
WKJSHandle probe requires the macOS 27 SDK; those APIs stay feature-gated.
swift build -c release --arch arm64
xcrun swift-format lint --strict --recursive Sources Tests Package.swift
swift test --arch arm64
swift test -c release --arch arm64
For a reproducible verification of the current checkout plus the signed local installation and its two-client transport:
scripts/verify-native-installed.sh
Run the server:
swift run -c release --arch arm64 webkitui-mcp
Before registration, run the secret-free local readiness check:
webkitui-mcp doctor
It checks the native confirmation helper, local owner-authentication policy, architecture, macOS version, and locally stored license state. It does not open a website, read browser data or credentials, authenticate the user, or contact the license service.
webkitui-mcp --help prints the stdio contract without starting the server.
The sibling webkitui-mcp-confirm helper owns legacy native dialogs so closing
an AppKit alert cannot terminate or corrupt the long-lived stdio server.
The process reads newline-delimited JSON-RPC from stdin, writes protocol responses only to stdout, and reserves stderr for diagnostics.
For a disposable direct process, install the CLI and register it normally:
release_bin="$(swift build --show-bin-path -c release --arch arm64)/webkitui-mcp"
release_dir="$(dirname "$release_bin")"
install -m 0755 "$release_bin" "$HOME/.local/bin/webkitui-mcp"
install -m 0755 "$release_dir/webkitui-mcp-confirm" "$HOME/.local/bin/webkitui-mcp-confirm"
codex mcp add webkitui-mcp -- "$HOME/.local/bin/webkitui-mcp"
claude mcp add --scope user webkitui-mcp -- "$HOME/.local/bin/webkitui-mcp"
Install the two together and leave them side by side. Before showing a confirmation the server checks the helper next to it, and what it can demand depends on how the server itself is signed:
Mixing the two is refused in both directions. If you re-sign one binary, re-sign the other with the same identity, or the confirmation will not appear and every navigation will fail closed.
The confirmation uses a nonactivating panel: it can receive keyboard input while the caller remains the active application, without adding a separate helper icon to the Dock. Two preferences, shared by a source build and the notarized app, decide what the keyboard may do:
# Return cancels and Escape closes after 1 s of initial keyboard focus (default).
defaults write com.lorislab.webkitui-mcp ConfirmationKeyboardDefault cancel
# Return/Escape have no default action; click, or Tab to a button then Space.
defaults write com.lorislab.webkitui-mcp ConfirmationKeyboardDefault none
# Arming delay for Return/Escape, 0 to 5 seconds (default 1).
defaults write com.lorislab.webkitui-mcp ConfirmationArmingDelaySeconds -float 2
The arming delay starts after the initial display and key-window acquisition.
Queued Return/Escape input is checked against its original event timestamp. Initially no button has focus:
stray Return/Space press nothing, and Escape
is ignored until armed. Deliberately selecting a button with Tab and Space or
clicking still works. In none mode Escape stays disabled.
Approve is never a keyboard default and cannot be made one. No per-project or
per-session auto-approval exists; the design constraints for one are recorded
in docs/research/2026-09-01-goal-delegation-and-browser-addressing-sota.md.
For authenticated sessions that must survive Codex conversation reconnects, build the self-contained app instead:
scripts/package-preview.sh dist
scripts/verify-package-preview.sh dist
Unzip WebKitUI-MCP-0.6.24-preview.zip, move WebKitUI MCP.app to the
Applications folder, and open it. In the status window:
The Activity journal button opens the local privacy-safe event history.
Its files live under ~/Library/Application Support/WebkitUIMCP/Activity with
owner-only permissions. Rotation retains at most seven 5 MiB archives plus the
active file. Clear removes activity files but preserves transaction
receipts; Export writes only the same redacted event schema.
The app uses Apple's Service Management API and does not install a mutable
plist in ~/Library/LaunchAgents. Prepare to uninstall disables Launch at
Login while preserving receipts and license data; then quit the app and move it
to the Trash. The preview archive is unsigned. Signing, notarization, clean-Mac
installation and publication are separate release gates.
Each MCP client receives isolated observations, confirmations, transactions,
and capability grants over the same durable browser. A logical
browser_session close detaches that client authority but deliberately keeps
the live browser; quitting the app destroys that in-memory page session.
Restart conversations opened before registration: MCP tool catalogs are fixed for a running conversation and are not retroactively replaced.
browser_session { operation: "profiles" }, then browser_session { operation: "open", profile_id: "default" }.browser_navigate opens a native exact-destination approval dialog by
default. Use approval_mode: "mcp" only when the client reliably supports
multi-round elicitation.browser_observe; use element_scroll_into_view, browser_scroll, or
browser_session { operation: "set_viewport", ... }, then observe again when
needed. back, forward, and reload are native-confirmed session operations.observationID and elementID once.browser_fill_siliconpass. If it returns
credential_not_found, accept the native handoff and add or update the
credential directly in SiliconPass.browser_act returns input_required; approve the exact bound action.ReceiptV1, or reconcile the receipt with
browser_transaction. One redacted canonical object produces both JSON and
Markdown; only the SHA-256 of the idempotency key is exported. Exported
evidence never authorizes replay.Large semantic pages can be read without oversized MCP results: browser_observe
supports role/name filters, maximum_elements, maximum_field_characters, and
element_offset; follow nextElementOffset until it is absent.
The enforced limits and the remaining release measurements are documented in
docs/performance-budgets.md.
The authority boundaries, attack paths, current controls and residual release
gates are maintained in
docs/security-threat-model.md.
Local data lifetimes and explicit-export boundaries are documented in
docs/privacy-retention.md.
The stable diagnostic allowlist and forbidden support data are documented in
docs/support-diagnostics.md.
The manual signed-update, rollback, support and incident rules are documented
in docs/release-maintenance-policy.md.
While a person holds the human control window, its bar offers Fill with
SiliconPass when the SiliconPass broker is installed. The person asks, not the
agent: WebKitUI binds the one visible password form in the frame being used,
including a restricted sign-in frame such as idmsa.apple.com, and SiliconPass
matches the saved sign-in by that frame's exact host. The values are typed into the
two fields through AppKit, so the page sees real input events; nothing is
submitted and nothing reaches MCP, logs or the agent.
SiliconPass credential release is deliberately stronger than ordinary browser
confirmation. After the exact native fill summary is approved, macOS evaluates
deviceOwnerAuthentication uses zero Touch ID reuse and delegates the
available authentication method to macOS. WebKitUI does not infer that Touch ID,
Apple Watch, password fallback, or closed-lid authentication will be available
on a particular Mac. If the Mac is locked or authentication UI cannot be
presented, WebKitUI receives only
user_presence_unavailable: no secret is released and no automatic retry is
performed.
The protected browser never connects to private address space. The one exception
is a service on the owner's Tailscale network: navigating to a name that resolves
only into 100.64.0.0/10 asks for a native Allow Tailscale Access confirmation
for that exact origin and port. Once allowed, the proxy connects to that host on that
port only, pinned against rebinding; every other private address, and the same name
on another port, stays refused. Grants last until the app quits. Any other private
destination returns privateNetworkDestination instead of an opaque WebKit error.
Restricted authentication origins such as idmsa.apple.com return only their
canonical origin and a local handoff requirement. Observe, text, capture,
scroll, credential fill, and actuation remain blocked until the human leaves
that origin. A cross-origin redirect is not reported as an ambiguous WebKit
failure: it returns redirect_requires_human_approval with source and target
origins only.
When App Store Connect embeds that restricted Apple Account origin, the sign-in
takes the native human handoff like any other restricted origin. It used to return
full_browser_required: the frame measured as stalled was a hidden-page defect of
the parked window (fixed 2026-09-23), not an engine limit.
Navigation blocks on an exact native macOS confirmation by default and returns
a normal terminal tool result. Human handoff is
two calls: the first transfers control to the visible window; after completing
the sensitive step, call browser_session { operation: "handoff", ... } again
and approve the native resume dialog. MCP 2026-07-28 clients continue to use
multi-round input_required results.
Use browser_session { operation: "handoff" } when a human must control the same local WebKit session. Declining resume leaves human control active.
If a previous handoff token survives while agent control is active, status and
handoff_start point to handoff for recovery. A local confirmation is required
before invalidating the old token and starting a new human handoff. A confirmed
tokenless resume also invalidates earlier tokens. Another client's handoff remains
excluded, and human work in progress is reported as wait-only.
When a click's cross-origin navigation is refused, the action result and status
report a navigation_blocker containing only the source and destination origins
and a human-handoff instruction. The click is not automatically replayed, and the
private authentication request is not exposed.
Semantic appearance and target absence postconditions require an unfiltered, untruncated baseline:
otherwise postcondition_baseline_incomplete refuses the action before dispatch.
Observe again with enough elements and field characters to establish absence.
Covered controls name the hit-tested covering element in coveredBy (compact:
covered_by). Name filters normalize common apostrophes; visible inline onclick
controls are included even without a pointer cursor. Identically sized link wrappers
without independent state or explicit handlers are omitted in favour of their link.
The handoff window becomes a regular foreground Mac app with a Dock icon and
uses the same rendered view as semantic observation and capture.
linux/ is a separate, ephemeral Playwright worker for disposable workloads on
a dedicated Linux VM. It does not inherit Mac cookies or passwords and it does
not replace native WebKit. Route authenticated sessions and human handoff to the
Mac; route public, unauthenticated headless work to Linux. See
linux/README.md and
docs/architecture/linux-headless-worker.md.
docs/architecture/docs/architecture/private-remote-transport.mddocs/research/Benchmarks/README.mddocs/architecture/public-private-boundary.mdSECURITY.mdThe dated adversarial corpus measurement records 53 deterministic tests across 57 concrete fixture executions for provenance, dialog truth, destinations, and export leakage. It measures those product boundaries, not human or model attack success.
The first measured local lane uses 30 runs of the same deterministic fixture at 2560×1600. It compares WKWebView with Playwright 1.61.1 driving installed Chrome 151. It does not yet measure full process-tree memory, visible-window behavior, authenticated task success, or Playwright's pinned Chromium binary; no broader superiority claim is made.
WebKitUI MCP 0.6.22 Developer Preview is available under the
Business Source License 1.1. The source is readable, auditable and
modifiable; production use is granted for personal noncommercial, qualifying
noncommercial organization and evaluation use, and commercial production use
requires an agreement with the Licensor. This version converts to Apache 2.0 on
2030-08-28. See LICENSING.md for the exact scope, third-party
boundary and treatment of earlier revisions.
The model cannot mint capability handles. Page content never becomes trusted policy. Password values are omitted from observations. Unknown dispatch is indeterminate and is never automatically retried. No commit, push, deployment, or external account mutation is performed by the project itself.
Navigation actor attribution uses the first main-frame navigation within two seconds of an action. It is diagnostic evidence, not proof of causality or authorization: autonomous or more delayed navigation can be misattributed.
A disappearing or renamed Save/Done control cannot satisfy enabled_equals=false: that predicate requires the original semantic target to remain present and disabled. Use target_absent with value="true" for disappearance, or a heading/text predicate for the resulting page. Absence is proved only by a complete observation; it means the original semantic identity is no longer observed, and does not prove server-side persistence. An indeterminate result after dispatch requires inspection or receipt reconciliation, never automatic replay.
This listing does not have a supported local package template. Use the maintainer’s documentation for its hosted endpoint, authentication, and client-specific setup. No install command has been inferred.
https://github.com/lowrisk75/webkitui-mcp/releases/download/v0.6.24/webkitui-mcp-0.6.24-macos-arm64.mcpbotherWebKitUI MCP works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.