About Apple Safari
An MCP server for macOS Safari that reads browsing history, bookmarks, and the Reading List, and can inspect open tabs and page content. Its file, Apple Events, and extension lanes expose different data and actions and require their respective permissions.
Key features
- 01Search history and see visit counts
- 02List tabs, bookmarks, and Reading List
- 03Read page text, HTML, and elements
- 04Opt-in page network records
- 05Open URLs and act on pages when writes are enabled
- 06Optional code extraction from page text
Use cases
- 01Find a previously visited page
- 02Inspect open-tab content and form fields
- 03Review network requests from an allowed page
- 04Fill forms or save Reading List items with writes enabled
MCP support
Provides MCP clients with Safari history, tab, bookmark, Reading List, and page tools. Page access, network records, and write actions are governed by separate permissions and configuration gates.
Configuration
Local server config is the same for every client. Copy it into your client’s MCP config file.
npx -y @mgcrea/mcp-apple-safariAPPLE_SAFARI_ALLOW_WRITESRegister the mutating tools. Off by default, and the gate decides whether the write tools exist at all rather than merely refusing — an agent with writes off cannot see them.APPLE_SAFARI_ALLOW_CODESReturn one-time codes: a one-time-code field in apple_safari_page_elements, and apple_safari_find_codes for a code a page shows as text. Its own gate rather than allowWrites, because it is a read. Credential fields stay redacted whatever this says.APPLE_SAFARI_ALLOW_NETWORKRegister apple_safari_network_record, _list and _get, which record what a page you allowed the extension on fetches and read the responses. Its own gate rather than allowWrites, because it is a read; off means the tools do not exist. Headers named like credentials are masked, bodies are not. Reloading a tab to record and saving a body to a file still need APPLE_SAFARI_ALLOW_WRITES.APPLE_SAFARI_DOWNLOAD_DIRWhere apple_safari_network_get's saveAs writes a file. Defaults to ~/Downloads. A name is kept to its basename and an existing file is never replaced unless asked.APPLE_SAFARI_NETWORK_CHUNK_CHARSCharacters of a body the server asks the page for per command when it reads a large body. Defaults to 262144, which was measured to work; how far above that Safari's native-message channel goes is unmeasured.Local servers need a runtime on your machine (e.g. Node.js 18+). Config file locations differ by client; see your client’s docs.
Tools
Local servers expose tools only once running on your machine, and the maintainer has not provided a tool list.
Details
From the project README@mgcrea/mcp-apple-safari
MCP server for the macOS Safari browser: what you have visited, what is open now, and what is on the page in front of you.
{
"mcpServers": {
"apple-safari": { "command": "npx", "args": ["-y", "@mgcrea/mcp-apple-safari"] }
}
}
Three lanes that are not fallbacks for each other
This is the thing to understand before anything else. Every other surface in this bundle has two routes to the same data and the choice is about speed. Safari's lanes see almost disjoint things, and each needs a different permission — so an ungranted Safari server is not a slower Safari server, it is a different and much smaller one.
| Lane | Needs | Answers |
|---|---|---|
| File | Full Disk Access | the past: history, bookmarks, the Reading List |
| Apple Events | an Automation grant, Safari running | the present: which tabs are open |
| Extension | the extension enabled AND allowed on that site | the page: its text, its elements, acting on it |
The extension lane needs no TCC grant of any kind. Safari consents to it one website at a time, which the user can see and revoke per site.
Tools
Read
| Tool | What |
|---|---|
apple_safari_search_history | history by text or date range, with visit counts |
apple_safari_get_page | one history item by ref |
apple_safari_list_tabs | tabs open right now, optionally enriched from history |
apple_safari_list_bookmarks | the bookmark tree |
apple_safari_list_reading_list | saved items, and which are unread |
apple_safari_read_page | a captured page as text or HTML |
apple_safari_page_elements | what is clickable or typeable on a live page |
apple_safari_diagnostics | which lanes work, and why one does not |
Opt-in read — APPLE_SAFARI_ALLOW_CODES
| Tool | What |
|---|---|
apple_safari_find_codes | a one-time 2FA code in the TEXT of an open page |
Opt-in read — APPLE_SAFARI_ALLOW_NETWORK
| Tool | What |
|---|---|
apple_safari_network_record | arm, stop, clear or report recording on a page (reload needs writes) |
apple_safari_network_list | what the page fetched: method, URL, status, timing, no bodies |
apple_safari_network_get | one request's headers and body; saveAs writes a file (needs writes) |
Write — APPLE_SAFARI_ALLOW_WRITES
| Tool | What |
|---|---|
apple_safari_open_url | open a URL, in a new tab or the current |
apple_safari_add_reading_list_item | save a URL for later |
apple_safari_click | click an element on a live page |
apple_safari_fill | type into one |
apple_safari_scroll | scroll a page |
Configuration
| Variable | Default | |
|---|---|---|
APPLE_SAFARI_ALLOW_WRITES | off | Register the five write tools at all. |
APPLE_SAFARI_ALLOW_CODES | off | One-time codes — see below. |
APPLE_SAFARI_ALLOW_NETWORK | off | Network capture — see below. |
APPLE_SAFARI_DOWNLOAD_DIR | ~/Downloads | Where network_get saveAs writes. |
APPLE_SAFARI_NETWORK_CHUNK_CHARS | 262144 | Characters per native message when pulling a body. |
APPLE_SAFARI_LIVE_TABS | on | Use the Apple Events lane. Off = no Apple Event. |
APPLE_SAFARI_STORE | auto | Explicit History.db path. |
APPLE_SAFARI_BOOKMARKS | auto | Explicit Bookmarks.plist path. |
APPLE_SAFARI_PAGES | auto | Explicit extension capture directory. |
APPLE_SAFARI_ACTION_TIMEOUT_MS | 12000 | How long an action waits for the page to answer. |
APPLE_SAFARI_LIVE_READ_TIMEOUT_MS | 2500 | How long read_page waits on the tab. 0 = capture. |
APPLE_SAFARI_READING_LIST_CONFIRM_MS | 1500 | Beat before re-reading to confirm a Reading List add. |
APPLE_SAFARI_INDEX_MODE | auto | auto | ro | immutable | off. |
APPLE_SAFARI_DEFAULT_RANGE_DAYS | 30 | Window when only a start is given. |
APPLE_SAFARI_MAX_RANGE_DAYS | 3660 | Hard ceiling on a single range. |
APPLE_SAFARI_MAX_RESULTS | — | Default page size. |
APPLE_SAFARI_EXPOSE_PROMPTS | off | Register prompts and cupertino:// resources. |
APPLE_SAFARI_DEBUG | off | Verbose logging. |
APPLE_SAFARI_ALLOW_CODES
Two things move together when it is on: find_codes is registered, and page_elements returns the
value of a one-time-code field. It is a read, and deliberately not folded into
ALLOW_WRITES — reaching a read through the write gate would mean granting the right to click a
button in order to see a number.
A credential field is withheld either way. A password, a card number or a CVC comes back
redacted: "credential" with no value and no setting that returns it. This flag is named for codes
and does not widen past them. In both cases hasValue still says whether the field is filled, which
is usually what you needed.
It is weaker than the Messages gate of the same name, and worth knowing which. There, off means
the tool does not exist and the alternative is sifting whole threads. Here read_page stays ungated,
so off removes the targeted field read and the live DOM scan — not every byte of a page.
See docs/passwords.md in the repo for why the Passwords app itself is unreachable by any lane, and
why reading a code a website shows is a different question from reading the vault.
APPLE_SAFARI_ALLOW_NETWORK
Registers network_record, network_list and network_get, which show what a page you have
allowed the extension on fetched: fetch and XMLHttpRequest calls from the top frame, console
errors, and resource-timing entries. It is a read gate and is not folded into ALLOW_WRITES,
for the reason ALLOW_CODES is not: the traffic of a signed-in page is session material, and that
is a different exposure from a one-time code.
- Bodies are not redacted, and neither are URLs. Headers named
authorization,proxy-authorization,cookieorset-cookie, or containingtoken,secret,api-keyorapikey, come back as<redacted: N chars>. A body has no names to match, so it is returned as the page saw it, and URLs are returned in full, query strings included, so a token in one comes back too. Turn the gate on knowing that. - Opt-in per tab, and it expires. Nothing is wrapped until
network_recordstarts, recording survives a reload for 15 minutes by default (120 at most), andstopends it at once. - Two things need
ALLOW_WRITESas well:reload: trueonnetwork_record, which re-runs the page, andsaveAsonnetwork_get, which writes intoAPPLE_SAFARI_DOWNLOAD_DIR(a bare filename, and never over an existing file unlessoverwrite: true). Each fails with an error namingAPPLE_SAFARI_ALLOW_WRITES. - What it cannot see: subframes, web and service workers,
<img>/<script>bodies and WebSockets. Request ids die when the page reloads or navigates. On a page whose Content-Security-Policy refuses inline scripts there is a fallback that loads the recorder by URL instead. It has not been verified in Safari; if it works it arrives late, so the page's first requests can be missing, and if it does not,hook.injectionsays"blocked". - A file inside an API response:
network_getwithjsonPathto the field,decode: "base64"andsaveAs. The bytes go to disk, not into the conversation.
Buffer limits: 1,000 records and 50 MiB of bodies per page; bodies are kept up to maxBodyBytes
(10 MiB by default, 50 MiB at most). See docs/safari.md.
Notes that will bite you
- An unmatched tab is normal, not an error. The only join key between a live tab and history is
the URL, and it is trivially lossy: measured match rates were 60.7%, 55.3% and 8.3% across
three runs. A single-page app reached by
pushStatecommits no history row at all, so the page was genuinely visited and genuinely is not in the store. Treathistory: nullas NOT FOUND, never as "never visited". - An exact URL match is not proof the row is about that page. One measured tab on
http://localhost:4321/matched a history row for an entirely different project. Any local address carries this. read_pageis live when the tab can answer, and a snapshot when it cannot. It asks the open tab first and waitsAPPLE_SAFARI_LIVE_READ_TIMEOUT_MS; a visible tab answers within a second. A hidden tab checks in only every ten, so it — like a closed one — gets the capture the extension stored at load and as the page settled, withsource: "capture",ageSeconds, andliveReadsaying why. The acting tools always go to the live page, which is why they cost about a second on a visible tab and up to ten on a hidden one.- Read a form with
page_elements, notread_page. Field values are not page text. Elements carryvalue(a select's shown option too),checkedon checkboxes and radios, anddisabled: trueon greyed-out controls, which are listed rather than dropped. Values are cut at 200 characters unlessmaxValueCharsraises it, and a cut one says so withvalueTruncated. - Set a form with
fillandclick.fillwrites through the element's native setter, which is what makes React register the change — a plainel.value =shows the text and leaves React's state on the old value. On a select it takes the option textpage_elementsreports. Checkboxes and radios are clicked, not filled, and a click toggles, so readcheckedfirst. File inputs cannot be set: a content script has no way to reach a file on disk. - A timeout from an action means nothing was listening — the extension off, not allowed on that site, or the page closed. It never means the page refused. Commands are at-most-once: one that is handed out and lost is not retried, because a click that MIGHT have landed must not be repeated.
- Element ids die on navigation. They come from a
page_elementscall and are valid for that page only. Never construct one, and re-enumerate after any click that loaded something. - A Reading List add hits the network. Safari fetches the URL in the background to build the
entry, and overwrites any
withTitleyou pass with the page's own title.verifiedhas three states and is neverfalse: the file lags the add by about 2 s, and reporting an unconfirmed write as a failed one makes callers retry — into a list with no remove verb. - The write lane needs no Full Disk Access. Both Apple Events writes work on a machine where the
read lane does not.
APPLE_SAFARI_LIVE_TABS=falseoutranks the write gate, because that flag promises a server that sends no Apple Event at all. open_urlacceptshttpandhttpsonly. Ajavascript:URL would bedo JavaScriptthrough the front door — measured: Safari accepts one through the navigation verb — andfile:is a navigation verb that reads local files.
Licence
MIT.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.