MCP 伺服器與 iOS 應用程式並存:兩個__TERM_23__生態系統,一份清單
Get Bananas 是我用 SwiftUI 開發的購物清單應用程式,可在 iOS、macOS、watchOS 與 visionOS 上執行。1 它同時也以 .mcpb MCP 擴充套件的形式存在於 Claude Desktop 中,提供五個工具:get_shopping_list、add_item、remove_item、update_item、update_shopping_list。2
當您對 Claude 說「將香蕉、牛奶和麵包加到我的清單」,Claude 會呼叫 add_item 三次,下次我在手機上開啟應用程式時,這些項目就會在那裡。沒有伺服器。沒有帳號。沒有 API 金鑰。 橋樑就是一個 JSON 檔案,加上五層防迴圈機制——這是我在 v1.0 上線後不得不加上的,因為當時這個版本三分鐘內就把自己寫成了一個 4MB 的檔案。
有趣的問題是:這怎麼做到的。SwiftData 只能在 Apple 平台執行階段使用,Node.js 程序無法讀取。3 原生的 CloudKit 框架需要對應的 com.apple.developer.icloud-services 權限與 Apple Developer 團隊識別碼;Claude Desktop 的 MCP 子程序兩者皆無,因此無法像我簽名過的應用程式那樣使用 CKContainer。CloudKit Web Services 是存在的,但若要使用,就得在桌面程序與 Apple 伺服器之間維護一座獨立的權杖/認證橋樑。4 所以那些顯而易見的路徑都行不通。
我採取的路徑既古老又奇特。Get Bananas 應用程式與其 MCP 伺服器透過 iCloud Drive 中的一個 JSON 檔案共享狀態。 Swift 應用程式以 SwiftData 模型維持應用程式內部的持久化,並在每次變更後透過 NSFileCoordinator 將 BananaList.json 檔案匯出到其 iCloud Drive 容器中。Node.js MCP 伺服器使用 5 秒獨佔檔案鎖、過期鎖偵測,以及原子化的暫存檔重新命名寫入機制,讀寫同一份檔案。iCloud Drive 處理跨裝置同步。今日,Claude Desktop 從 Mac 端讀寫同一份事實來源;下一個介面將是針對 Apple Intelligence 的 App Intents 轉接器,操作的也是同一份檔案。
這篇文章談的是這個安排為什麼可行、代價是什麼,以及它在哪裡會崩壞。
TL;DR
- Get Bananas 透過內建的 MCP 伺服器將購物清單暴露給 Claude Desktop。同樣的檔案格式接下來會支援針對 Apple Intelligence 的 App Intents 轉接器。
- 整合基底是 iCloud Drive 加上一個 JSON 檔案,不是 CloudKit、不是伺服器、也不是某個服務。
- Swift 應用程式:以 SwiftData
@Model ShoppingItem換取應用程式內部速度;以 iCloud Drive JSON 匯出換取可攜性。 - MCP 伺服器:575 行 Node.js 程式碼,具備過期鎖偵測的檔案鎖,執行於 Claude Desktop 的
.mcpb套件內。 - 取捨:基於檔案的 JSON 同步比 CloudKit 慢,並有合併衝突風險,但它能在任何能讀取檔案的LLM生態系統中運作。

Anthropic 文件中所記載的 Model Context Protocol 架構。MCP 主機(Claude Desktop)連接到一個或多個 MCP 伺服器(本文中為 get-bananas.mcpb),每個伺服器都暴露主機可呼叫的工具、資源與提示。資料來源:modelcontextprotocol.io。11
一頁概覽的架構
┌─────────────────────────────────────────────────────────┐
│ Get Bananas iOS app │
│ SwiftUI views → SwiftData @Model ShoppingItem │
│ ↓ (debounced 0.5s, atomic write) │
│ iCloudBackupManager.swift │
│ ↓ │
│ ~/Library/Mobile Documents/.../BananaList.json │
└──────────────────────────┬──────────────────────────────┘
│
iCloud Drive sync
│
┌──────────────────────────┴──────────────────────────────┐
│ Claude Desktop on Mac │
│ ↑ │
│ get-bananas.mcpb (Node.js MCP server) │
│ - acquireLock() with 5s timeout │
│ - readShoppingList() / writeShoppingList() │
│ - 5 tools: get/add/remove/update/replace │
│ ↑ │
│ JSON-RPC (stdio) ← Claude │
└─────────────────────────────────────────────────────────┘
兩個介面。一份檔案。整座橋樑就是這份檔案。
Swift 端:用 SwiftData 換速度,用 JSON 換可攜性
在應用程式中,購物清單是一個 SwiftData @Model。實際上線的程式碼:5
@Model
final class ShoppingItem {
@Attribute(.unique) var id: UUID
var name: String
var amount: String
var section: String
var isChecked: Bool
var isOptional: Bool
var sortOrder: Int
var lastModified: Date?
}
那是記憶體中的真實。每次按鍵、每次勾選、每次區塊變更都會寫入 SwiftData。SwiftData 驅動 SwiftUI 視圖。應用程式感覺起來原生,因為它就是原生的。備份觸發採用雜湊判斷:.onChange(of: computeItemsHash()) 監視器只在項目的 id、名稱、數量、區塊、勾選或可選狀態變更時觸發,純粹的無作用編輯絕不會觸發。
訣竅在於:SwiftData 並不是跨程序的事實。它是跨程序的快取。每次變更會延遲 500 毫秒,然後透過 Apple 的協調寫入 API 將 JSON 檔案寫到應用程式的 iCloud Drive 容器:6
// iCloudBackupManager.swift, paraphrased
private let fileName = "BananaList.json"
static let backupDebounceDelay: TimeInterval = 0.5
static let ignoreBackupAfterSyncWindow: TimeInterval = 5.0
static let maxRetries = 3
// Real pre-write content check + NSFileCoordinator
let coordinator = NSFileCoordinator(filePresenter: nil)
coordinator.coordinate(writingItemAt: backupURL, options: [], error: &err) { url in
try jsonString.write(to: url, atomically: true, encoding: .utf8)
}
NSFileCoordinator 是寫入其他程序(以及 iCloud Drive 守護程序)可能並行讀取的檔案時,Apple 官方支援的方式。16 在那次寫入發生之前,管理器會讀取現有檔案,並在 JSON 與既有內容逐位元組相符時完全略過寫入。這在 SwiftData 變更觀察者因無作用編輯觸發時,可減少多餘的 iCloud Drive 流量。restore 時,管理器會以指數退避(1 秒、2 秒、4 秒,總預算 7 秒)重試最多三次,因為 NSMetadataQuery 在 iCloud Drive 真正下載新位元組之前就會回報檔案變更。6
該檔案的 Codable 結構刻意保持寬容。ShoppingListExport 對每個缺漏欄位都以預設值解碼,並過濾掉名稱為空的項目:7
struct ShoppingListExport: Codable {
var sections: [String]
var items: [ShoppingItemData]
struct ShoppingItemData: Codable {
var id: UUID
var name: String
var amount: String
var section: String
var optional: Bool
var checked: Bool
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID()
self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
self.amount = try container.decodeIfPresent(String.self, forKey: .amount) ?? ""
// ...
}
var isValid: Bool {
!name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
}
}
防禦性的解碼器是刻意設計的。下一個寫入這份 JSON 的東西(MCP 伺服器、未來的捷徑、手動貼上)勢必會忘記某個欄位。Swift 端會吸收這一切。共享檔案格式就是合約;Swift 解碼器是寬容的一方。

Node 端:讀取同一份檔案的 575 行 MCP 伺服器
這個 MCP 伺服器位於 mcp-extension/server/index.js,以 get-bananas.mcpb 的形式發佈到 Claude Desktop 的擴充套件系統。它從 macOS 主機開啟同一份 iCloud Drive 檔案:2
const ICLOUD_FILE_PATH = path.join(
os.homedir(),
"Library/Mobile Documents/iCloud~com~941apps~Banana-List/Documents/BananaList.json"
);
五個工具:一個純讀取(get_shopping_list)、三個項目層級的讀-改-寫工具(add_item、remove_item、update_item),以及一個無需先讀取就直接寫入的批次替換工具(update_shopping_list)。MCP 伺服器同時將該檔案以獨立唯讀 Resource 的形式暴露給偏好資源 API 的用戶端。每次寫入都經過具備過期鎖恢復機制的檔案鎖:
const LOCK_FILE_PATH = ICLOUD_FILE_PATH + ".lock";
const LOCK_TIMEOUT_MS = 5000;
async function acquireLock() {
const startTime = Date.now();
while (Date.now() - startTime < LOCK_TIMEOUT_MS) {
try {
fs.writeFileSync(LOCK_FILE_PATH, String(process.pid), { flag: 'wx' });
return true;
} catch (err) {
if (err.code === 'EEXIST') {
const stat = fs.statSync(LOCK_FILE_PATH);
if (Date.now() - stat.mtimeMs > LOCK_TIMEOUT_MS) {
fs.unlinkSync(LOCK_FILE_PATH); // stale lock recovery
continue;
}
await new Promise(resolve => setTimeout(resolve, 50));
} else {
throw err;
}
}
}
throw new Error("Could not acquire file lock; please try again.");
}
這個鎖的模式比 Node.js 還要古老。fs.writeFileSync 搭配 'wx' 旗標,是 O_EXCL | O_CREAT 的跨平台版本。如果鎖檔存在且已超過 5 秒,伺服器就假設先前的持有者已當機,並回收鎖。如果鎖檔存在且新鮮,伺服器會等待 50 毫秒後重試。總共 5 秒後,就放棄。8
這把鎖只同步 Node 端寫入者之間的競爭(例如第一次 MCP 寫入仍在進行中,第二次 MCP 呼叫又進來)。它不會與 Swift 應用程式協調——後者透過 NSFileCoordinator 與 String.write(atomically:) 寫入,從不碰 BananaList.json.lock。真正的 Swift/Node 重疊,留給兩個較弱的機制處理:Swift 應用程式寫入前會延遲 500 毫秒,MCP 寫入只在使用者提示時發生,任何殘餘的衝突就交給 iCloud Drive 在檔案層級的「最後寫入者勝出」機制解決。
寫入本身採用原子化暫存檔加重新命名的模式,中間插入 JSON 解析檢查:
const tempPath = ICLOUD_FILE_PATH + ".tmp." + process.pid;
fs.writeFileSync(tempPath, jsonString, "utf8");
// Verify the temp file is valid JSON before renaming
JSON.parse(fs.readFileSync(tempPath, "utf8"));
// Atomic rename - either the new file or the old file, never partial
fs.renameSync(tempPath, ICLOUD_FILE_PATH);
fs.renameSync 在同一檔案系統上是 POSIX 原子操作:其他程序的讀者要麼看到舊檔、要麼看到新檔,絕不會看到寫到一半的位元組。17 將暫存路徑綁定到 process.pid,可避免兩個 MCP 伺服器執行個體(罕見,但若使用者重新安裝 Claude Desktop 而未重啟,就有可能)互相覆蓋彼此的暫存檔。寫入中途的 JSON.parse 是一道偏執步驟:萬一序列化本身產出無效的 JSON,函式會在重新命名前中止,讓正典檔案保持原樣。
為什麼選 iCloud Drive,而非 CloudKit
讓這套架構成立的關鍵選擇,在於用 iCloud Drive(基於檔案)而非 CloudKit(基於記錄)作為跨程序的事實。 CloudKit 是 Apple 推薦的應用程式對應用程式同步方式。它具備更高層級的衝突解決、伺服器端推送,以及基於區域的分區。9 原生的 CKContainer API 僅限 Apple 平台,且受權限把關,因此 Claude Desktop 的子程序無法像我簽名過的應用程式那樣使用它。Apple 確實有為非 Apple 平台用戶端發佈 CloudKit Web Services,但若要使用,就必須佈建伺服器對伺服器的權杖、把它接到 MCP 伺服器中,並維護一座獨立的認證橋樑——並非不可能,但對一個購物清單而言,這是相當可觀的基礎設施。4
MCP 伺服器在 macOS 上以 Claude 的子程序、未沙盒化的方式執行。它沒有 Apple Developer 簽章鏈、沒有與我應用程式 CloudKit 容器相符的團隊識別碼,也沒有設定 CloudKit Web Services 的權杖。
相對地,iCloud Drive 將自己暴露為一般的檔案系統位置。應用程式端 Apple 官方支援的 API 是 FileManager.url(forUbiquityContainerIdentifier:);14 在 macOS 上,Get Bananas 解析出的位置是 ~/Library/Mobile Documents/iCloud~com~941apps~Banana-List/Documents/BananaList.json。那條路徑是 macOS 特有的實作細節,描述 iCloud Drive 在哪裡呈現容器,但對在同一台 Mac 上執行的 Claude Desktop 來說,它就只是個檔案。任何具備使用者主目錄讀取權限的程序都能讀寫它。未來的捷徑、未來的 SwiftBar 外掛、使用者本機執行的未來 llama.cpp 指令稿也都可以。任何能讀取檔案的東西都能整合。
代價是,iCloud Drive 的同步比 CloudKit 慢(以秒計,而非次秒級),且具備較弱的衝突語意(檔案層級的最後寫入者勝出,而非記錄層級的合併)。對一份大概只有 30 個項目的購物清單而言,這兩項代價都不重要。對一個有 1 萬列資料、且編輯者並行的高寫入量應用程式而言,這兩項代價會構成主導性問題。
五層防迴圈機制
Swift 端最棘手的程式碼是防迴圈機制。如果沒有它:MCP 伺服器寫 JSON,iCloud Drive 同步到 iOS,iOS 應用程式的 NSMetadataQuery 注意到變更,應用程式重新將 JSON 匯入 SwiftData,匯入觸發 SwiftData 變更觀察者,變更觀察者觸發延遲備份,延遲備份寫 JSON,iCloud Drive 又把它同步回去。我在 v1.0 推出了這個天真版本,然後在測試時眼看著一份 30 個項目的購物清單在三分鐘內膨脹到 4MB。10
上線版本使用了五層堆疊式防護,而非一層。每層守護的是不同的時序邊角案例:
// Layer 1: Thread-safe sync counter (re-entrant guard)
private let syncLock = NSLock()
private var _syncCount: Int = 0
var isSyncing: Bool { syncCount > 0 }
// Layers 1 + 2: shouldSkipBackup gates outbound writes
var shouldSkipBackup: Bool {
if isSyncing { return true } // Layer 1
if let lastSync = lastSyncTime,
Date().timeIntervalSince(lastSync) < Constants.ignoreBackupAfterSyncWindow {
return true // Layer 2
}
return false
}
// Layer 3 (in NSMetadataQuery handler): drop changes within 2s of our own backup
if let lastBackup = lastBackupTime,
Date().timeIntervalSince(lastBackup) < Constants.ignoreChangesWindow {
return
}
// Layer 4: exact mod-date match = our own backup coming back via iCloud
if let lastBackupMod = lastBackupModificationDate, modDate == lastBackupMod {
return
}
// Layer 5: monotonic mod-date guard against re-processing the same version
if let lastSynced = lastSyncedModificationDate, modDate <= lastSynced {
return
}
| 層 | 位置 | 攔截的情境 |
|---|---|---|
| 1. 同步計數器 > 0 | 對外寫入路徑 | 來自 iCloud 的同步進行中時所觸發的可重入寫入 |
| 2. 同步後 5 秒視窗 | 對外寫入路徑 | SwiftData 在匯入結束之後才觸發的延遲 @Model onChange 回呼 |
| 3. 備份後 2 秒視窗 | 對內 NSMetadataQuery 處理器 |
應用程式自己寫入後立刻觸發的本機檔案系統事件 |
| 4. 修改日期完全相符 | 對內處理器 | iCloud Drive 把我們自己的備份跨裝置回送過來 |
| 5. 修改日期單調遞增 | 對內處理器 | NSMetadataQuery 對單一變更同時觸發 DidUpdate 與 DidFinishGathering |
前兩層守護對外寫入路徑:我現在該不該匯出到 iCloud? 後三層守護對內 NSMetadataQuery 處理器:我該不該把這次變更匯入 SwiftData? 任一側單獨都不夠。一次同步往返可以根據哪個事件先觸發,而通過兩側的防護,因此每條路徑都需要自己的防禦。
這個教訓可推廣到任何「兩個寫入者共享檔案」的架構:基於修改時間的變更偵測是必要但不充分。你需要一個能撐過至少一次同步往返的、穩定的『我引發的寫入』身分標記。 iCloud Drive 能給你的最接近的東西,是檔案在你寫入當下的修改日期。記住它。回來時拿來比對。
我會做哪些不同的選擇
從上線中得到的四個教訓。
Swift 端的防禦性 Codable 物超所值。 MCP 伺服器已被改寫三次。每次改寫至少都忘了設定一個欄位。Swift 解碼器吸收了所有變體,應用程式從未當機。如果重來一次,我會把更多欄位推向「以預設值解碼」而非「必填」。兩個寫入者之間的合約,本來就是設計上脆弱的。7
鎖逾時應該以內容為依據,而非單看 mtime。 5 秒太短了。如果使用者的 Mac 處於緩慢的 Wi-Fi,或 iOS 裝置在長時間背景之後正在還原,iCloud Drive 同步 BananaList.json.lock 可能需要超過 5 秒才能傳播。MCP 伺服器於是會看到一把實際上仍被持有的「過期」鎖。修復方式是,讓過期鎖檢查的條件,改為依據鎖檔內寫入的 PID:如果 kill(pid, 0) 回報該程序仍在執行,那就無論 mtime 看起來多舊都不要破鎖。目前的程式碼會寫入 PID,但從不讀回。
update_shopping_list 工具是個錯誤。 它會替換整份清單。Claude Desktop 偶爾會在單項操作就足夠時呼叫它,然後使用者清單中相當大的一部分就消失了。我應該只發佈四個項目層級工具(get、add、remove、update),強制 Claude 自行組合它們。MCP 協定的 destructiveHint: true 註解確實會將該工具標記為破壞性,11 但 Claude 不一定會在呼叫前向使用者揭示。批次替換工具對LLM方便,對使用者危險。協定層的護欄,並不能取代「不要發佈這個自爆腳」。
共享的 JSON 匯出需要一個版本欄位。 ShoppingListExport 用寬容預設值解碼,這在我哪天重新命名某個欄位(而非新增一個)之前都行得通。在 JSON 頂端加上 schemaVersion: 1,可以讓任一側偵測到未來的破壞性變更,並拒絕讀取,而不是默默產出畸形模型。遷移仍然需要手動,但至少失敗模式會大聲響起,而非默默丟失資料。
何時不該用這套模式
拒絕也是設計的一部分。
如果資料是受規範的(健康、金融,或任何具備合規保留政策的資料),iCloud Drive 那種使用者可控的檔案系統就是錯誤的基底。CloudKit 有日誌與稽核軌跡;使用者可讀的 JSON 檔案則沒有。
如果跨程序延遲預算是次秒級,iCloud Drive 達不到。在我的測試中,iCloud Drive 在健康連線上同步通常要花數秒,而非次秒;Apple 並未公佈更嚴格的 SLA,網路慢時會更久。CloudKit 基於推送的傳遞,在記錄層級更新上明顯更快。即時協作產品需要 CloudKit(或專屬的同步伺服器)。
如果結構演進得快,「Codable 加預設值」的模式會讓技術債複利累積。每個新欄位都需要一個「舊檔的預設值」決策,而這種決策老化得很快。JSON 檔案同步最適合穩定、以新增為主的結構變更。
這對想被多個LLM生態系統觸及的應用程式意味著什麼
這套模式簡單到可以複製。三件事:
- 一個用於應用程式內持久化的 SwiftData
@Model。 驅動 UI、快速、原生。 - 一份 Codable JSON 匯出,在延遲變更時寫到 iCloud Drive。 防禦性解碼器。穩定結構。共享檔案就是合約。
- 每個LLM生態系統的小型轉接器,使用檔案鎖讀寫同一份檔案。Claude Desktop 用 Node.js。未來給 Apple Intelligence 用的 App Intent + AppEntity。未來下一個推出的產品則用 shell 指令稿。
這套模式之所以可攜,是因為整合基底就是檔案系統。今日存在的每個LLM執行環境(Claude Desktop、Cursor、Goose、Cline)以及明年會推出的大多數,都能讀取檔案。11 CloudKit 不行。原生同步引擎不行。當目標是跨越LLM生態系統的觸及面時,最低共通分母會勝出。
Anthropic 與 Apple 對LLM該長什麼樣子有不同看法。 App Intents 認為它是 Apple Intelligence 在裝置端解析的型別化 Swift 宣告。MCP 認為它是任何LLM都可呼叫、帶有工具清單的 JSON-RPC 伺服器。兩者在各自的生態系統中都正確。Get Bananas 將兩者皆不視為事實來源,讓檔案系統居中協調。12
下一次當我推出一個想要兩個LLM介面的應用程式時,我會在實體模型之前先從檔案格式開始。
FAQ
.mcpb 是什麼,它怎麼運作?
.mcpb 是 Anthropic 為 Claude Desktop 設計的 MCP 擴充套件套件格式。它是一個 zip 壓縮檔,包含描述工具的 manifest.json、MCP 伺服器進入點(Node.js、Python 等)、圖示與中繼資料。Claude Desktop 透過單擊像安裝瀏覽器擴充套件那樣安裝它,並以本機子程序執行該伺服器。MCP 伺服器透過 stdio 講 JSON-RPC。1115 Get Bananas 的伺服器就是以這種方式包裝發佈的。
為什麼不用新的 App Intents 對 MCP 橋樑?
並沒有這樣的橋樑。App Intents(Apple 的框架)與 MCP(Anthropic 的協定)是各自獨立的。Apple Intelligence 透過自己的解析器呼叫 App Intents。Claude Desktop 透過自己的執行環境呼叫 MCP 伺服器。想要兩個介面的應用程式,就得兩者都實作;沒有自動橋樑。1213
不用 iCloud Drive 也能做到嗎?
可以,但有但書。任何共享的可寫檔案位置都行:~/Documents 中的資料夾、網路共享磁碟、掛載成 FUSE 的 S3 檔案系統。iCloud Drive 之所以方便,是因為它已經存在於每一台執行 Claude Desktop 的 Mac 上,以及使用者擁有的每一台 iOS 裝置上。非 iCloud 的檔案會迫使使用者另行設定同步。
寫入衝突發生時會怎樣?
5 秒檔案鎖加上 50 毫秒重試,處理的是 MCP 端並行的寫入者(例如第一次 MCP 寫入進行到一半時,第二次 MCP 呼叫到達)。它不與 Swift 應用程式協調,後者透過自己的協調器寫入。當 Swift 與 Node 真的重疊時(罕見,因為 Swift 有 500 毫秒延遲,而 MCP 寫入只在使用者提示時觸發),iCloud Drive 在檔案層級解決衝突:最後寫入者勝出。Swift 解碼器的 isValid 過濾器再丟掉任何畸形內容。
為什麼不用 CRDT 或 operational transform?
對 30 個項目的購物清單來說太過頭了。CRDT 適用於並行重疊編輯常見、且需要決定性合併語意的場景(協作文件編輯器、多人遊戲)。對一份購物清單而言——一個人透過 Claude 加項目,另一個人在前往商店的路上透過 iOS 應用程式逐項勾選——「最後寫入者勝出加上延遲」就是正確的選擇。
兩個LLM生態系統,一份購物清單。橋樑是 iCloud Drive 加上一個帶寬容解碼器的 JSON 檔案,而這就足夠了。最低共通分母不是限制。它是兩個生態系統唯一都能達成共識的事物。
References
-
Author’s Get Bananas, a SwiftUI + SwiftData shopping list app for iOS, macOS, watchOS, and visionOS, published by 941 Apps. ↩
-
Get Bananas ships an MCP (Model Context Protocol) server bundled as
get-bananas.mcpbfor Claude Desktop. Tools exposed:get_shopping_list,add_item,remove_item,update_item,update_shopping_list. The server is 575 lines of Node.js inmcp-extension/server/index.js. ↩↩ -
Apple Developer, “SwiftData” framework. Available iOS 17+, macOS 14+, watchOS 10+, visionOS 1+. Runtime-only; no server-side or cross-process bindings. ↩
-
Apple Developer, “CloudKit” framework. The native
CKContainerAPI requires thecom.apple.developer.icloud-servicesentitlement and matching Apple Developer team identifier. Apple also publishes CloudKit Web Services for non-Apple-platform clients, but using it requires a separate token / auth bridge that Get Bananas does not maintain. ↩↩ -
Production code in
Banana List/Item.swift. ThelastModifiedfield was added later for iCloud sync conflict resolution. ↩ -
Production code in
Banana List/iCloudBackupManager.swift. Constants live inBanana List/Constants.swift. ↩↩ -
Production code in
Banana List/ShoppingListExport.swift. Custom decoder withdecodeIfPresentdefaults plusisValidfilter on import. ↩↩ -
POSIX
O_EXCL | O_CREATsemantics; Node.js exposes the same atomicity viafs.writeFileSync(path, data, { flag: 'wx' }). See Node.js fs documentation. ↩ -
Apple Developer, “Designing for CloudKit”. Push-based sync, record-level conflict resolution, zone partitioning. ↩
-
Author’s debugging notes. The infinite-loop incident produced a 4MB
BananaList.jsonfrom a 30-item shopping list in 3 minutes before sync-counter logic landed. ↩ -
Anthropic, “Model Context Protocol”. Open protocol for LLM tool use; multi-runtime (Claude Desktop, Cline, Goose, etc.). ↩↩↩↩
-
Author’s analysis in App Intents Are Apple’s New API to Your App. The two parallel-contracts thesis applied across system-AI surfaces (Apple) and cross-LLM tool use (Anthropic). ↩↩
-
Apple Developer, “App Intents framework”. Apple’s typed-declarative tool-use surface for Siri, Spotlight, and Apple Intelligence. ↩
-
Apple Developer, “FileManager url(forUbiquityContainerIdentifier:)”. The supported API for resolving an app’s iCloud Drive container URL. The macOS path under
~/Library/Mobile Documents/is the host-OS implementation detail of where iCloud Drive surfaces the container; the symbolic API is what apps should call. ↩ -
Anthropic, “Desktop Extensions”. The
.mcpbformat is a zip archive containingmanifest.json, MCP server entry point, icon, and metadata. Single-click install in Claude Desktop; runs the bundled server as a local subprocess over stdio JSON-RPC. ↩ -
Apple Developer, “NSFileCoordinator”. Coordinates reads and writes to a file across processes that opt into the same coordination protocol; required when iCloud Drive’s
birddaemon,NSMetadataQuery-driven observers, and the app itself can all touch the same path. ↩ -
POSIX
rename(2)is required to be atomic when the source and destination are on the same filesystem. iCloud Drive’s local mirror under~/Library/Mobile Documents/is a single APFS volume, sofs.renameSyncbetween a sibling temp file and the canonical path is atomic from any reader’s perspective. See POSIX rename specification. ↩