MCP サーバーを iOS アプリと同居させる:2つのエージェントエコシステム、1つのリスト
Get Bananas は私が開発した SwiftUI 製の買い物リストアプリで、iOS、macOS、watchOS、visionOS で動作します。1 さらに Claude Desktop の中では .mcpb MCP 拡張機能として存在し、5つのツール(get_shopping_list、add_item、remove_item、update_item、update_shopping_list)を公開しています。2
Claude に「バナナと牛乳とパンをリストに追加して」と頼むと、Claude は add_item を3回呼び出し、次に iPhone でアプリを開くと、それらのアイテムがそこにあります。サーバーなし。アカウントなし。API キーなし。 橋渡しとなるのは1つの JSON ファイルと、出荷した v1.0 が3分間で自分自身を 4MB のファイルに書き込んでしまった後に追加せざるを得なかった5層のループ防止策です。
興味深いのは、その仕組みです。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 より同期が遅く、マージ衝突のリスクがありますが、ファイルを読めるあらゆるエージェントエコシステムで動作します。

Anthropic が文書化している Model Context Protocol アーキテクチャ。MCP ホスト(Claude Desktop)が1つ以上の MCP サーバー(本記事では get-bananas.mcpb)に接続し、それぞれがホストから呼び出せるツール、リソース、プロンプトを公開しています。出典:modelcontextprotocol.io。11
1ページに収めたアーキテクチャ
┌─────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────┘
2つの表面、1つのファイル。橋渡しの全体がそのファイルなのです。
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、name、amount、section、checked、または optional の状態が変化したときにのみ発火し、純粋な no-op の編集では発火しません。
トリックは、SwiftData がプロセス間の真実ではないということです。それはプロセス間のキャッシュなのです。すべての変更は500msデバウンスされ、その後 Apple のコーディネートされた書き込み API を通じて、アプリの iCloud Drive コンテナに JSON ファイルを書き込みます: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 のデーモン)が同時に読み取る可能性のあるファイルへの書き込みをサポートする方法です。16 その書き込みが行われる前に、マネージャーは既存のファイルを読み取り、JSON がバイト単位で一致する場合は書き込みを完全にスキップします。これにより、SwiftData の変更オブザーバーが no-op の編集に対して発火するたびに発生する冗長な iCloud Drive のチャーンが削減されます。restore では、マネージャーは指数バックオフ(1秒、2秒、4秒、合計予算7秒)で最大3回リトライします。なぜなら 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 にあり、Claude Desktop の拡張機能システム向けに get-bananas.mcpb として配布されています。これは macOS ホストから同じ iCloud Drive ファイルを開きます:2
const ICLOUD_FILE_PATH = path.join(
os.homedir(),
"Library/Mobile Documents/iCloud~com~941apps~Banana-List/Documents/BananaList.json"
);
5つのツール:純粋な読み取り1つ(get_shopping_list)、3つの read-modify-write のアイテムレベルツール(add_item、remove_item、update_item)、そして最初に読まずに書き込む一括置換ツール1つ(update_shopping_list)。MCP サーバーはまた、リソース API を好むクライアント向けに、ファイルを別の読み取り専用 Resource として公開します。すべての書き込みは、古いロック回復付きのファイルロックを経由します:
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 より古いものです。'wx' フラグ付きの fs.writeFileSync は、O_EXCL | O_CREAT のクロスプラットフォーム版です。ロックファイルが存在し、5秒以上経過している場合、サーバーは前のホルダーがクラッシュしたと仮定してそれを再取得します。存在し、新しい場合、サーバーは50ms待ってリトライします。合計5秒経過すると、諦めます。8
ロックは Node 側のライター同士(最初の書き込み中に到着する2回目の MCP 呼び出し)を同期させるだけです。Swift アプリとは協調せず、Swift アプリは NSFileCoordinator と String.write(atomically:) を介して書き込み、BananaList.json.lock には決して触れません。Swift と Node の本物の重なりは、2つの弱い仕組みに任されています。Swift アプリは書き込み前に500msデバウンスし、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 に固定することで、2つの MCP サーバーインスタンス(稀ですが、ユーザーが Claude Desktop を再起動せずに再インストールした場合に発生する可能性があります)が互いの一時ファイルを上書きすることを防ぎます。書き込み中の JSON.parse は念のための手順です。シリアライゼーション自体が無効な JSON を生成した場合、関数はリネームの前に中断し、正規のファイルはそのまま残ります。
なぜ CloudKit ではなく iCloud Drive なのか
このアーキテクチャを機能させる選択は、プロセス間の真実のためにCloudKit(レコードベース)ではなく iCloud Drive(ファイルベース)を使うことです。CloudKit は Apple がアプリ間同期に推奨するものです。より高レベルの衝突解決、サーバーサイドのプッシュ、ゾーンベースのパーティショニングを備えています。9 ネイティブの CKContainer API は Apple プラットフォーム専用かつエンタイトルメントゲートが必要なので、Claude Desktop のサブプロセスは私の署名済みアプリのように使うことはできません。Apple は Apple プラットフォーム以外のクライアント向けに CloudKit Web Services を公開していますが、これを使うにはサーバー間トークンのプロビジョニング、それを MCP サーバーに配線すること、そして別の認証ブリッジを維持することが必要になります。不可能ではありませんが、買い物リストには相当な量のインフラです。4
MCP サーバーは Claude のサブプロセスとして macOS 上でサンドボックス化されずに動作します。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 です。そのパスは iCloud Drive がコンテナを公開する場所の macOS 固有の実装の詳細ですが、同じ Mac 上で動作する Claude Desktop にとっては単なるファイルです。ユーザーのホームディレクトリへの読み取りアクセスを持つ任意のプロセスがそれを読み書きできます。将来のショートカット、将来の SwiftBar プラグイン、ユーザーがローカルで実行する将来の llama.cpp スクリプトもです。ファイルを読めるものなら何でも統合できます。
代償は、iCloud Drive の同期が CloudKit よりも遅い(サブ秒ではなく秒単位)ことと、衝突セマンティクスが弱い(レコードレベルのマージではなく、ファイル粒度での最後の書き込みが勝つ)ことです。約30アイテムの買い物リストでは、どちらのコストも問題になりません。1万行と同時編集者を持つ書き込み量の多いアプリでは、両方のコストが支配的になるでしょう。
5層のループ防止
Swift 側で最もトリッキーなコードは、ループ防止です。それなしでは、MCP サーバーが JSON を書き込み、iCloud Drive がそれを iOS に同期し、iOS アプリの NSMetadataQuery が変更を検知し、アプリが JSON を SwiftData に再インポートし、インポートが SwiftData の変更オブザーバーをトリガーし、変更オブザーバーがデバウンスされたバックアップを発火させ、デバウンスされたバックアップが JSON を書き込み、iCloud Drive がそれを同期し戻します。私は v1.0 でナイーブなバージョンを出荷し、テスト中に30アイテムの買い物リストが3分間で 4MB に膨れ上がるのを目撃しました。10
出荷バージョンでは、1つではなく5つの積み重ねたガードを使っています。それぞれが異なるタイミングのエッジケースをガードします:
// 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 の両方を発火 |
最初の2層はアウトバウンド書き込みパスをガードします:今、iCloud にエクスポートすべきか? 残りの3層はインバウンド NSMetadataQuery ハンドラーをガードします:この変更を SwiftData にインポートすべきか? どちらか一方だけでは不十分です。単一の同期ラウンドトリップは、どのイベントが先に発火するかによって両側のガードを通り抜ける可能性があるので、各パスには独自の防御が必要です。
教訓は「2つのライターにわたる共有ファイル」アーキテクチャ全般に一般化できます:修正時刻ベースの変更検出は必要ですが十分ではありません。少なくとも1回の同期層へのラウンドトリップを生き残る、「自分が引き起こした書き込み」のための安定した識別子が必要です。 iCloud Drive が提供する最も近いものは、書き込んだ瞬間のファイルの修正日です。それを保持してください。戻ってきたときに比較してください。
私だったら別の作り方をすること
これを出荷して得た4つの教訓。
Swift 側の防御的な Codable はその価値があります。 MCP サーバーは3回書き直されました。各書き直しは少なくとも1回はフィールドを設定し忘れていました。Swift デコーダーはすべてのバリアントを吸収し、アプリは一度もクラッシュしませんでした。やり直すなら、「必須」ではなく「デフォルト付きでデコード」にもっと多くのフィールドを押し込むでしょう。2つのライター間の契約は、設計上脆弱なのです。7
ロックタイムアウトは mtime のみではなく、コンテンツを意識すべきです。 5秒は短いです。ユーザーの Mac が遅い Wi-Fi にあるか、iOS デバイスが長いバックグラウンドの後に復元中である場合、BananaList.json.lock の iCloud Drive 同期は伝播に5秒以上かかることがあります。MCP サーバーはそのとき、実際にはまだ保持されている古いロックを見ます。修正は、ロックファイル内に書き込まれた PID で古いロックチェックをゲートすることです:kill(pid, 0) がまだ実行中のプロセスを報告する場合、mtime がどれほど古く見えても、ロックを破ってはいけません。現在のコードは PID を書き込みますが、それを読み戻すことはありません。
update_shopping_list ツールは間違いでした。 これはリスト全体を置き換えます。Claude Desktop は単一アイテム操作で済む場合でも時々それを呼び出し、その結果ユーザーのリストの相当な部分が消えてしまいます。私は4つのアイテムレベルのツール(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-with-defaults パターンは負債を複利で蓄積します。新しいフィールドごとに「古いファイルのデフォルト」決定が必要で、それはすぐに陳腐化します。JSON ファイル同期は、ほぼ追加的な変更を持つ安定したスキーマに最適です。
複数のエージェントエコシステムから到達可能でありたいアプリにとって、これが意味すること
パターンは繰り返すのに十分シンプルです。3つの要素:
- アプリ内永続化のための SwiftData
@Model。 UI を駆動し、高速で、ネイティブ。 - デバウンスされた変更時に iCloud Drive に書き込まれる Codable JSON エクスポート。 防御的デコーダー。安定したスキーマ。共有ファイルが契約。
- 各エージェントエコシステム用の小さなアダプターで、ファイルロックを使って同じファイルを読み書きする。Claude Desktop には Node.js。Apple Intelligence には将来の App Intent + AppEntity。次に出荷されるものには将来のシェルスクリプト。
このパターンが可搬なのは、統合の基盤がファイルシステムだからです。今日存在するすべてのエージェントランタイム(Claude Desktop、Cursor、Goose、Cline)と、来年出荷されるほとんどのものは、ファイルを読めます。11 CloudKit はできません。ネイティブ同期エンジンはできません。LLM エコシステム間でのリーチを目標とするとき、最小公倍数が勝つのです。
Anthropic と Apple は、エージェントがどのように見えるべきかについて意見が異なります。 App Intents は、Apple Intelligence がデバイス上で解決する型付き Swift 宣言だと言います。MCP は、任意の LLM が呼び出せるツールリストを持つ JSON-RPC サーバーだと言います。両方とも自身のエコシステムでは正しいのです。Get Bananas はどちらも真実のソースとして扱わず、ファイルシステムに仲介させています。12
次にエージェント表面を2つ持ちたいアプリを出荷するときは、エンティティモデルの前にファイル形式から始めるでしょう。
FAQ
.mcpb とは何で、どう動作しますか?
.mcpb は Anthropic の Claude Desktop 向けの MCP 拡張機能バンドル形式です。ツールを記述する manifest.json、MCP サーバーのエントリーポイント(Node.js、Python など)、アイコン、メタデータを含む zip アーカイブです。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 内のフォルダ、ネットワーク共有、S3 マウントの FUSE ファイルシステム。iCloud Drive が便利なのは、Claude Desktop を実行するすべての Mac とユーザーが所有するすべての iOS デバイスにすでにあるからです。iCloud 以外のファイルだと、ユーザーは別途同期を設定する必要があります。
書き込みの衝突が起きたらどうなりますか?
5秒のファイルロックと50msのリトライが、同時の MCP 側ライター(例:最初の書き込み中に到着する2回目の MCP 呼び出し)を処理します。Swift アプリとは協調しません。Swift アプリは独自のコーディネーターを通じて書き込みます。Swift と Node が本当に重なるとき(Swift の500msデバウンスと、MCP の書き込みがユーザープロンプト時にのみ発火することを考えると稀です)、iCloud Drive はファイル粒度で解決します:最後の書き込みが勝ちます。Swift デコーダーの isValid フィルターが、不正なものを除外します。
CRDT や operational transform はなぜ使わないのですか?
30アイテムの買い物リストにはオーバーキルです。CRDT は、重なる同時編集が一般的で、決定論的なマージセマンティクス(共同ドキュメントエディタ、マルチユーザーゲーム)が必要なときに正しい選択です。一人が Claude 経由でアイテムを追加し、もう一人が店に行く途中で iOS アプリ経由でチェックを入れる買い物リストには、デバウンス付きの最後の書き込みが勝つが正しいのです。
2つのエージェントエコシステム、1つの買い物リスト。橋渡しとなるのは 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. ↩