Cupertino 1.3.1
Version 1.3.1 ·
Added
Maps is the eighth surface, and the first that cannot send an Apple Event.
@mgcrea/mcp-apple-mapsreads the places saved on this Mac — favourites, collections (Guides) and recents — with real coordinates and addresses, throughapple_maps_list_favorites,apple_maps_list_collections,apple_maps_list_collection_places,apple_maps_list_recents,apple_maps_search_places,apple_maps_get_placeandapple_maps_diagnostics. Configuration isAPPLE_MAPS_STORE,APPLE_MAPS_INDEX_MODE,APPLE_MAPS_MAX_RESULTSandAPPLE_MAPS_EXPOSE_PROMPTS.Maps ships no scripting dictionary, so
usesAppleEventsis FALSE for the first time insurfaces.json— which means adding this surface did not widen the Apple Events consent string the app asks for. It also means there is no second lane: without Full Disk Access this server returns an error rather than an empty list, because an empty list of favourites reads exactly like a person who has saved none.Read-only, and for a firmer reason than Safari's. The store is mirrored to iCloud by
NSPersistentCloudKitContainer: a write is an edit to one replica of a synchronising object graph, underneath a running app that is also editing it, withNSCK*bookkeeping tables a third-party writer would not maintain.APPLE_MAPS_ALLOW_WRITESis accepted and ignored, andtools.test.tsasserts the tool list is identical with it on and off.Two bugs were caught only by running against a real store, and both are the kind a comfortable fixture cannot reach.
ZMUIDis a 64-bit Apple place id — a real one reads-2679868148951248105— andnode:sqliteTHROWS pastNumber.MAX_SAFE_INTEGERrather than truncating, so reading it as a number failed the entire favourites listing rather than that one field; it is nowCAST(... AS TEXT), which is the ruledocs/surfaces.mdalready stated. And 12 of 20 linked favourites carryZMUID = 0, a sentinel rather than an id, now reported as null so two unrelated places cannot be taken for the same place.Columns are resolved by COVERAGE rather than by name, which no other surface has needed:
ZHISTORYITEMcarries bothZLATITUDE(1 row of 33) andZLATITUDE1(19 of 33), and taking the first recognised name would report that Maps holds almost no coordinates.pnpm probe:maps, and two spikes recording how this surface was nearly lost.scripts/probe-maps.mjsreports the entity shapes, resolves the id bridge by running the join rather than reading column names, and detects the date epoch.scripts/spike-maps-store.mjsanswers "is there a store behind the grant at all", and refuses to report a negative unless it can first open four stores that shipped surfaces read daily — Maps was declared "no file lane" three times by processes that never checked they could see anything.scripts/spike-maps-ax.mjsis the record of the Accessibility lane that was measured and rejected: readable, but ~14 s per read with no coordinates and no stable identifier, against 0 ms and both from the file lane.
Fixed
looksLikeDateColumnnever matched a single Core Data column. Its token boundary was_, written for snake_case schemas like Calendar'sstart_date; Core Data has no underscores, soZCREATETIMEandZLASTVISITEDDATEsilently returned false in every Notes, Contacts and Maps store. A date detector that quietly finds nothing is exactly whatdocs/surfaces.mdmeans by "dates are the richest source of silent errors". The new rule matches on SUFFIX rather than substring, because substring matching recreates thecalENDar_idtrap in a new alphabet — "update" contains "date", soZUPDATECOUNTandZVALIDATEDwould have read as timestamps.Wire a single project folder, instead of every session on the Mac. Settings ▸ Clients has a Project folders section: choose a folder, and Cupertino's servers are wired for that folder alone. Until now the app only offered
claude mcp add --scope user, which is right for someone with three projects and wrong at ninety — measured on a real install, 12 of 93 tracked project directories had ever called a Cupertino tool, so 87% of sessions were carrying ~73 tool definitions they never used.A radio picks which file holds the entry. Both are read by Claude Code; only one is a file this app is willing to write.
- Your Claude Code config adds it to
~/.claude.jsonunder the folder's path, leaving the folder untouched. The app will not write that file — it holds API credentials and running sessions write to it concurrently — so this hands over a command to paste, with thecdincluded, because local scope files the server under whatever directory the CLI ran from and a command pasted in the wrong terminal wires the wrong folder and reports success. - A .mcp.json file writes.mcp.jsonin the folder, where any Claude Code session opened there picks it up. That file is strict JSON with servers undermcpServers, the same shape as the four clients the app already writes, so it goes through the same merge, backup and atomic swap.The scope is a control next to the button rather than a preference in Settings: the choice is per-folder, and a global setting would be right for whichever kind of repo someone has more of and quietly wrong for the rest. The last choice is remembered, which is the part a preference would have bought.
Fixed
Settings reported Full Disk Access as granted while every protected store was unreadable.
Permissions.diskAccess()walked the surface stores and returnedgrantedon the first one it could open — andLibrary/Application Support/AddressBookopens without the grant, because it is gated by the Contacts TCC service, a different permission. So Contacts answered the question on behalf of Mail, Messages, Safari, Notes, Reminders and Calendar, and answered it wrongly. Measured with the grant absent: those six all denied, AddressBook readable, a green tick in Settings in the same second thatapple_mail_diagnosticsreportedfullDiskAccess: deniedand every mail lane fell back to Apple Events. The probe is now TCC's own database, which is present on every Mac and readable under exactly one condition. A permission row that lies is worse than no row: it sends someone looking for the fault everywhere except where it is.The Settings item and its ⌘, were missing from the app menu. Not because inserting them failed — the insertion succeeded at launch, with no error logged — but because SwiftUI installs its own
NSApp.mainMenuafter the delegate returns, and builds another whenever the activation policy flips, each replacement discarding what came before. Re-asserting the item ondidBecomeActiveand after every policy change did not help either: the rebuild lands after those hooks, so the race cannot be won by inserting more often. The item is now declared as a SwiftUI command, which makes it part of what gets rebuilt.A composer failure could not say which permission was missing. Filling a Mail reply goes through System Events, which sits behind two grants — Accessibility, and Automation to System Events, a separate grant from Automation to Mail.
prop()swallows the exception either way, so both arrive as the same "no composer window" error, indistinguishable from Mail never having opened one. The old message named Accessibility outright; it was a guess with the other possibilities hidden, and it sent at least one investigation to re-grant a permission that was already in place. Diagnostics now reports the two separately,-1743(refused) is distinguished from-1744(never asked), and the reply and forward tools pre-flight the check rather than opening a window they cannot reach. A failed reply no longer leaves an empty composer on screen for someone to close by hand — every attempt used to add another — and one that opens and cannot be filled is closed again, unless something landed in it that could not be read back, which is left alone because discarding that would destroy the text with no undo.A green Accessibility row in Settings while every reply failed. Diagnostics asked
AXIsProcessTrusted, which answers for an identity, and an identity turned out to be ambiguous:tccutil reset Accessibility io.mgcrea.cupertinoreported clearing four separate entries on one machine — an installed copy, a development build and earlier reinstalls each leaving their own row, all shown as a single "Cupertino" in the pane. The app's check matched one and the checks made on its behalf matched another, so granting it again only ever added a fifth. Attribution was never at fault;launchctl procinfoputs the responsible pid of each server at the app, as designed. Diagnostics now reportscomposerUiRead— whether Mail's windows could actually be named — beside the flag, and that is the line to believe when the two disagree. The cure is to clear the identifier and grant once, from the bundle that is running.The website's surface tool lists and captions had drifted from the shipped counts.