← Alle Beitrage

MCP-Server neben einer iOS-App: Zwei Agenten-Ökosysteme, eine Liste

Get Bananas, meine SwiftUI-Einkaufslisten-App, läuft auf iOS, macOS, watchOS und visionOS.1 Sie lebt zudem in Claude Desktop als .mcpb-MCP-Erweiterung und stellt fünf Tools bereit: get_shopping_list, add_item, remove_item, update_item, update_shopping_list.2

Get-Bananas-Einkaufsliste auf dem iPhone, dieselbe JSON-Datei, die der MCP-Server liest und schreibt Wenn Sie Claude bitten „Bananen, Milch und Brot auf meine Liste setzen”, ruft Claude add_item dreimal auf, und beim nächsten Öffnen der App auf meinem Handy sind die Einträge da. Kein Server. Kein Konto. Kein API-Schlüssel. Die Brücke ist eine einzige JSON-Datei plus fünf Schichten Schleifenvermeidung, die ich nachrüsten musste, nachdem v1.0 sich selbst in drei Minuten in eine 4-MB-Datei geschrieben hatte.

Die interessante Frage ist: wie? SwiftData ist ausschließlich Apple-Plattform-Laufzeit und aus einem Node.js-Prozess heraus nicht lesbar.3 Das native CloudKit-Framework benötigt das passende Entitlement com.apple.developer.icloud-services und die Apple Developer Team-Identifier; der MCP-Subprozess von Claude Desktop hat weder das eine noch das andere und kann CKContainer somit nicht so nutzen, wie meine signierte App es tut. CloudKit Web Services existiert zwar, dessen Verwendung würde aber das Pflegen einer separaten Token-/Auth-Brücke zwischen dem Desktop-Prozess und Apples Servern erfordern.4 Die naheliegenden Wege sind also versperrt.

Der Weg, den ich gegangen bin, ist älter und seltsamer. Die Get-Bananas-App und ihr MCP-Server teilen sich den Zustand über eine JSON-Datei in iCloud Drive. Die Swift-App führt ein SwiftData-Modell für die App-interne Persistenz und exportiert nach jeder Änderung über NSFileCoordinator eine Datei BananaList.json in ihren iCloud-Drive-Container. Der Node.js-MCP-Server liest und schreibt dieselbe Datei mit einem 5-Sekunden-Exklusiv-Dateisperre, Erkennung verwaister Sperren und atomarem Temp-File-Rename-Schreiben. iCloud Drive übernimmt die geräteübergreifende Synchronisierung. Heute liest und schreibt Claude Desktop dieselbe Quelle der Wahrheit vom Mac aus; der App-Intents-Adapter für Apple Intelligence ist die nächste Oberfläche – gegen dieselbe Datei.

Dieser Essay handelt davon, warum diese Anordnung funktioniert, was sie kostet und wo sie auseinanderfällt.

TL;DR

  • Get Bananas stellt seine Einkaufsliste über einen ausgelieferten MCP-Server für Claude Desktop bereit. Dasselbe Dateiformat wird als Nächstes einen App-Intents-Adapter für Apple Intelligence unterstützen.
  • Das Integrations-Substrat ist iCloud Drive plus eine JSON-Datei, kein CloudKit, kein Server, kein Dienst.
  • Swift-App: SwiftData @Model ShoppingItem für In-App-Geschwindigkeit; iCloud-Drive-JSON-Export für Portabilität.
  • MCP-Server: 575 Zeilen Node.js, Dateisperre mit Erkennung verwaister Sperren, läuft innerhalb des .mcpb-Bundles von Claude Desktop.
  • Trade-off: Dateibasiertes JSON synchronisiert langsamer als CloudKit und birgt Merge-Konflikt-Risiken, funktioniert aber mit jedem Agenten-Ökosystem, das eine Datei lesen kann.

Referenz-Architekturdiagramm des Model Context Protocol von Anthropic, das das Client/Server-Verbindungsmodell zeigt

Die Model-Context-Protocol-Architektur, wie von Anthropic dokumentiert. Ein MCP-Host (Claude Desktop) verbindet sich mit einem oder mehreren MCP-Servern (get-bananas.mcpb in diesem Artikel), die jeweils Tools, Ressourcen und Prompts bereitstellen, die der Host aufrufen kann. Quelle: modelcontextprotocol.io.11

Die Architektur auf einer Seite

┌─────────────────────────────────────────────────────────┐
                    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                         
└─────────────────────────────────────────────────────────┘

Zwei Oberflächen. Eine Datei. Die gesamte Brücke ist die Datei.

Die Swift-Seite: SwiftData für Geschwindigkeit, JSON für Portabilität

In der App ist die Einkaufsliste ein SwiftData-@Model. Echter Produktionscode: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?
}

Das ist die In-Memory-Wahrheit. Jeder Tastenanschlag, jeder Checkbox-Tap, jede Sektionsänderung schreibt in SwiftData. SwiftData treibt die SwiftUI-Views. Die App fühlt sich nativ an, weil sie nativ ist. Der Backup-Trigger ist hash-basiert: ein .onChange(of: computeItemsHash())-Watcher feuert nur, wenn sich id, name, amount, section, checked oder optional eines Eintrags ändern – niemals bei einer reinen No-Op-Bearbeitung.

Der Trick ist, dass SwiftData nicht die prozessübergreifende Wahrheit ist. Es ist der prozessübergreifende Cache. Jede Änderung wird 500 ms entprellt und schreibt dann über Apples koordinierten Schreib-API eine JSON-Datei in den iCloud-Drive-Container der App: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 ist der unterstützte Weg, eine Datei zu schreiben, die andere Prozesse (und der iCloud-Drive-Daemon) gleichzeitig lesen könnten.16 Bevor dieser Schreibvorgang stattfindet, liest der Manager die existierende Datei und überspringt das Schreiben vollständig, wenn das JSON Byte für Byte übereinstimmt. Das reduziert überflüssigen iCloud-Drive-Traffic, sobald ein SwiftData-Änderungsbeobachter für eine No-Op-Bearbeitung feuert. Beim restore versucht der Manager bis zu drei Wiederholungen mit exponentiellem Backoff (1 s, 2 s, 4 s, Gesamtbudget 7 s), weil NSMetadataQuery eine Dateiänderung meldet, bevor iCloud Drive die neuen Bytes tatsächlich heruntergeladen hat.6

Die Codable-Form der Datei ist absichtlich tolerant. ShoppingListExport dekodiert mit Standardwerten für jedes fehlende Feld und filtert Einträge mit leeren Namen heraus: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
        }
    }
}

Der defensive Decoder ist Absicht. Was auch immer das JSON als Nächstes schreibt (ein MCP-Server, ein zukünftiger Shortcut, ein manuelles Einfügen), wird unweigerlich ein Feld vergessen. Die Swift-Seite fängt das ab. Das geteilte Dateiformat ist der Vertrag; der Swift-Decoder ist die nachsichtige Partei.

Get Bananas auf macOS, dem Host, auf dem der MCP-Server als Claude-Desktop-Subprozess läuft und dieselbe iCloud-Drive-Datei liest

Die Node-Seite: Ein 575-Zeilen-MCP-Server, der dieselbe Datei liest

Der MCP-Server lebt in mcp-extension/server/index.js, ausgeliefert als get-bananas.mcpb für das Erweiterungssystem von Claude Desktop. Er öffnet vom macOS-Host aus dieselbe iCloud-Drive-Datei:2

const ICLOUD_FILE_PATH = path.join(
  os.homedir(),
  "Library/Mobile Documents/iCloud~com~941apps~Banana-List/Documents/BananaList.json"
);

Fünf Tools: ein reines Lese-Tool (get_shopping_list), drei Read-Modify-Write-Tools auf Eintragsebene (add_item, remove_item, update_item) und ein Bulk-Replace-Tool (update_shopping_list), das schreibt, ohne vorher zu lesen. Der MCP-Server stellt die Datei zudem als separate, schreibgeschützte Resource für Clients bereit, die den Resource-API bevorzugen. Jeder Schreibvorgang läuft über eine Dateisperre mit Wiederherstellung verwaister Sperren:

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.");
}

Das Lock-Pattern ist älter als Node.js. fs.writeFileSync mit dem 'wx'-Flag ist die plattformübergreifende Variante von O_EXCL | O_CREAT. Existiert die Lock-Datei und ist älter als 5 Sekunden, geht der Server davon aus, dass der vorherige Halter abgestürzt ist, und übernimmt sie. Existiert sie und ist frisch, wartet der Server 50 ms und versucht es erneut. Nach insgesamt 5 Sekunden gibt er auf.8

Die Sperre synchronisiert ausschließlich Node-seitige Schreiber untereinander (eine zweite MCP-Aufruf während die erste noch mitten im Schreiben ist). Sie koordiniert nicht mit der Swift-App, die über NSFileCoordinator und String.write(atomically:) schreibt und BananaList.json.lock niemals berührt. Echte Swift/Node-Überlappungen werden zwei schwächeren Mechanismen überlassen: Die Swift-App entprellt 500 ms vor dem Schreiben, MCP-Schreibvorgänge erfolgen nur auf Benutzeranfrage, und jede Restkollision fällt in iCloud Drives Last-Write-Wins-Auflösung auf Datei-Granularität durch.

Die Schreibvorgänge selbst nutzen das Atomic-Temp-then-Rename-Pattern, mit einer JSON-Parse-Prüfung dazwischen:

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 auf demselben Dateisystem ist POSIX-atomar: Ein Leser in einem anderen Prozess sieht entweder die alte oder die neue Datei, niemals halbgeschriebene Bytes.17 Den Temp-Pfad an process.pid zu binden verhindert, dass zwei MCP-Server-Instanzen (selten, aber möglich, falls der Benutzer Claude Desktop neu installiert ohne neu zu starten) sich gegenseitig die Temp-Dateien überschreiben. Das JSON.parse mitten im Schreibvorgang ist ein Paranoia-Schritt: Sollte die Serialisierung selbst ungültiges JSON erzeugt haben, bricht die Funktion vor dem Rename ab und lässt die kanonische Datei unangetastet.

Warum iCloud Drive, nicht CloudKit

Die Entscheidung, die diese Architektur überhaupt funktionieren lässt, ist die Verwendung von iCloud Drive (dateibasiert) statt CloudKit (record-basiert) für die prozessübergreifende Wahrheit. CloudKit ist das, was Apple für App-zu-App-Synchronisierung empfiehlt. Es bietet höhere Konfliktauflösung, serverseitige Push-Mechanismen und zonenbasierte Partitionierung.9 Der native CKContainer-API ist Apple-Plattform-only und entitlement-gated, sodass ein Claude-Desktop-Subprozess ihn nicht so verwenden kann wie meine signierte App. Apple veröffentlicht zwar CloudKit Web Services für Nicht-Apple-Plattform-Clients, aber dessen Nutzung würde das Bereitstellen eines Server-zu-Server-Tokens, die Einbindung in den MCP-Server und die Pflege einer separaten Auth-Brücke erfordern: nicht unmöglich, aber ein erheblicher Infra-Aufwand für eine Einkaufsliste.4

Der MCP-Server läuft unsandboxed auf macOS als Subprozess von Claude. Er hat keine Apple-Developer-Signaturkette, keinen Team-Identifier-Match mit dem CloudKit-Container meiner App und keinen konfigurierten CloudKit-Web-Services-Token.

iCloud Drive hingegen präsentiert sich als regulärer Dateisystem-Speicherort. Apples unterstützter API ist FileManager.url(forUbiquityContainerIdentifier:) für die App-Seite;14 auf macOS ist der aufgelöste Speicherort für Get Bananas ~/Library/Mobile Documents/iCloud~com~941apps~Banana-List/Documents/BananaList.json. Dieser Pfad ist ein macOS-spezifisches Implementierungsdetail dafür, wo iCloud Drive den Container offenlegt, aber für Claude Desktop, das auf demselben Mac läuft, ist es einfach eine Datei. Jeder Prozess mit Lesezugriff auf das Home-Verzeichnis des Benutzers kann sie lesen und schreiben. Das gilt auch für einen zukünftigen Shortcut, ein zukünftiges SwiftBar-Plugin, ein zukünftiges llama.cpp-Skript, das der Benutzer lokal ausführt. Alles, was eine Datei lesen kann, kann sich integrieren.

Der Preis ist, dass die iCloud-Drive-Synchronisierung langsamer ist als CloudKit (Sekunden, nicht Sub-Sekunden) und schwächere Konfliktsemantik hat (Last-Write-Wins auf Datei-Granularität, kein Merge auf Record-Ebene). Für eine Einkaufsliste mit vielleicht 30 Einträgen spielt keiner dieser Kosten eine Rolle. Für eine schreibintensive App mit 10.000 Zeilen und gleichzeitigen Editoren würden beide Kosten dominieren.

Fünf Schichten Schleifenvermeidung

Das kniffligste Stück Code auf der Swift-Seite ist die Schleifenvermeidung. Ohne sie: Der MCP-Server schreibt das JSON, iCloud Drive synchronisiert es nach iOS, das NSMetadataQuery der iOS-App bemerkt die Änderung, die App importiert das JSON wieder in SwiftData, der Import löst einen SwiftData-Änderungsbeobachter aus, der Beobachter feuert ein entprelltes Backup, das entprellte Backup schreibt das JSON, iCloud Drive synchronisiert es zurück. Ich habe die naive Version in v1.0 ausgeliefert und beim Testen zugesehen, wie eine 30-Eintrag-Einkaufsliste in drei Minuten auf 4 MB anwuchs.10

Die ausgelieferte Version verwendet fünf gestapelte Schutzmechanismen, nicht nur einen. Jeder schützt einen anderen Timing-Edge-Case:

// 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
}
Schicht Wo Was sie abfängt
1. Sync-Zähler > 0 Ausgehender Schreibpfad Wiedereintretende Schreibvorgänge, die ausgelöst werden, während ein Sync-from-iCloud aktiv läuft
2. 5-s-Post-Sync-Fenster Ausgehender Schreibpfad Verzögerte @Model-onChange-Callbacks, die SwiftData nach Abschluss des Imports feuert
3. 2-s-Post-Backup-Fenster Eingehender NSMetadataQuery-Handler Lokale Dateisystem-Events, die direkt nach dem eigenen Schreibvorgang der App feuern
4. Exakter Mod-Date-Match Eingehender Handler iCloud Drive, das das eigene Backup geräteübergreifend zurückspielt
5. Monotonisches Mod-Date Eingehender Handler NSMetadataQuery, das sowohl DidUpdate als auch DidFinishGathering für eine einzelne Änderung feuert

Die ersten beiden Schichten sichern den ausgehenden Schreibpfad: Soll ich gerade nach iCloud exportieren? Die übrigen drei sichern den eingehenden NSMetadataQuery-Handler: Soll ich diese Änderung in SwiftData importieren? Keine Seite alleine reicht aus. Ein einzelner Sync-Round-Trip kann je nachdem, welches Event zuerst feuert, durch Schutzmechanismen auf beiden Seiten kommen, daher braucht jeder Pfad seine eigenen Verteidigungen.

Die Lehre verallgemeinert sich auf jede „geteilte Datei mit zwei Schreibern”-Architektur: Mod-Time-basierte Änderungserkennung ist notwendig, aber nicht hinreichend. Sie brauchen eine stabile Identität für „Schreibvorgänge, die ich verursacht habe”, die mindestens einen Round-Trip durch die Sync-Schicht überlebt. Das Nächstliegende, was iCloud Drive Ihnen dafür bietet, ist das Modifikationsdatum der Datei zum Zeitpunkt des Schreibens. Halten Sie es fest. Vergleichen Sie es beim Rückweg.

Was ich anders bauen würde

Vier Lehren aus dem Ausliefern.

Der defensive Codable auf der Swift-Seite verdient seinen Platz. Der MCP-Server wurde dreimal umgeschrieben. Bei jedem Umschreiben wurde mindestens einmal vergessen, ein Feld zu setzen. Der Swift-Decoder hat jede Variante absorbiert und die App ist nie abgestürzt. Würde ich nochmal anfangen, würde ich noch mehr Felder in „decode mit Default” statt „erforderlich” verschieben. Der Vertrag zwischen den beiden Schreibern ist konstruktionsbedingt fragil.7

Das Lock-Timeout sollte inhaltsbewusst sein, nicht nur mtime-basiert. Fünf Sekunden sind kurz. Wenn der Mac des Benutzers auf langsamem WLAN läuft oder das iOS-Gerät nach langer Hintergrundzeit wiederhergestellt wird, kann eine iCloud-Drive-Synchronisierung von BananaList.json.lock länger als 5 Sekunden brauchen, um sich zu propagieren. Der MCP-Server sieht dann eine verwaiste Sperre, die in Wirklichkeit noch gehalten wird. Die Lösung ist, die Stale-Lock-Prüfung an die im Lock-File geschriebene PID zu koppeln: Meldet kill(pid, 0) einen noch laufenden Prozess, brechen Sie die Sperre nicht, egal wie alt die mtime aussieht. Der aktuelle Code schreibt die PID, liest sie aber nie zurück.

Das Tool update_shopping_list war ein Fehler. Es ersetzt die gesamte Liste. Claude Desktop ruft es gelegentlich auf, wenn eine Einzeleintrags-Operation ausgereicht hätte, woraufhin ein nicht-trivialer Teil der Liste des Benutzers verschwindet. Ich hätte nur die vier Tools auf Eintragsebene (get, add, remove, update) ausliefern und Claude zwingen sollen, sie zu komponieren. Die Annotation destructiveHint: true des MCP-Protokolls markiert das Tool zwar als destruktiv,11 aber Claude legt das nicht immer dem Benutzer offen, bevor es aufgerufen wird. Das Bulk-Replace-Tool ist bequem für den LLM und gefährlich für den Benutzer. Das Vorhandensein eines Schutzmechanismus auf Protokollebene ersetzt nicht das Nicht-Ausliefern der Fußangel.

Der gemeinsame JSON-Export braucht ein Versionsfeld. ShoppingListExport dekodiert mit toleranten Defaults, was funktioniert, bis zu dem Tag, an dem ich ein Feld umbenenne, statt eines hinzuzufügen. Ein schemaVersion: 1 am Anfang des JSON würde es beiden Seiten erlauben, eine zukünftige Breaking Change zu erkennen und den Lesevorgang abzulehnen, statt stillschweigend ein fehlerhaftes Modell zu erzeugen. Migrationen wären weiterhin manuell, aber zumindest wäre der Fehlermodus laut statt stiller Datenverlust.

Wann dieses Pattern nicht zu verwenden ist

Ablehnung ist Teil des Designs.

Sind die Daten reguliert (Gesundheit, Finanzen, alles mit einer Compliance-Aufbewahrungsrichtlinie), ist das benutzerkontrollierte Dateisystem von iCloud Drive das falsche Substrat. CloudKit verfügt über Logging und Audit Trails; benutzerlesbare JSON-Dateien nicht.

Liegt das prozessübergreifende Latenzbudget unter einer Sekunde, wird iCloud Drive es nicht erreichen. In meinen Tests dauert die iCloud-Drive-Synchronisierung bei einer gesunden Verbindung üblicherweise Sekunden statt Sub-Sekunden; Apple veröffentlicht kein engeres SLA, und langsame Netzwerke verlängern es. CloudKits push-basierte Zustellung ist für Record-Level-Updates spürbar schneller. Ein Echtzeit-Kollaborationsprodukt benötigt CloudKit (oder einen dedizierten Sync-Server).

Entwickelt sich das Schema schnell weiter, häuft das Codable-mit-Defaults-Pattern Schulden an. Jedes neue Feld erfordert eine „Default für alte Dateien”-Entscheidung, die schnell altert. JSON-Dateisynchronisierung eignet sich am besten für stabile Schemata mit überwiegend additiven Änderungen.

Was das für Apps bedeutet, die von mehreren Agenten-Ökosystemen erreichbar sein wollen

Das Muster ist einfach genug zum Wiederholen. Drei Bestandteile:

  1. Ein SwiftData-@Model für die App-interne Persistenz. Treibt die UI an, schnell, nativ.
  2. Ein Codable-JSON-Export, der bei entprellter Änderung in iCloud Drive geschrieben wird. Defensiver Decoder. Stabiles Schema. Die geteilte Datei ist der Vertrag.
  3. Ein kleiner Adapter für jedes Agenten-Ökosystem, der dieselbe Datei mit einer Dateisperre liest und schreibt. Node.js für Claude Desktop. Ein zukünftiger App Intent + AppEntity für Apple Intelligence. Ein zukünftiges Shell-Skript für was auch immer als Nächstes erscheint.

Das Muster ist portabel, weil das Integrations-Substrat das Dateisystem ist. Jede heute existierende Agenten-Laufzeit (Claude Desktop, Cursor, Goose, Cline) und die meisten, die nächstes Jahr erscheinen werden, können eine Datei lesen.11 CloudKit kann das nicht. Native Sync-Engines können das nicht. Der kleinste gemeinsame Nenner gewinnt, wenn das Ziel Reichweite über LLM-Ökosysteme hinweg ist.

Anthropic und Apple sind sich uneinig darüber, wie ein Agent aussehen sollte. App Intents sagen: Es ist eine typisierte Swift-Deklaration, die Apple Intelligence on-device auflöst. MCP sagt: Es ist ein JSON-RPC-Server mit einer Tool-Liste, die jeder LLM aufrufen kann. Beide haben in ihren jeweiligen Ökosystemen recht. Get Bananas behandelt keines von beiden als Quelle der Wahrheit und lässt das Dateisystem vermitteln.12

Beim nächsten Mal, wenn ich eine App ausliefere, die zwei Agenten-Oberflächen haben soll, werde ich mit dem Dateiformat beginnen, vor dem Entitätsmodell.

FAQ

Was ist .mcpb und wie funktioniert es?

Ein .mcpb ist das Bundle-Format für MCP-Erweiterungen von Anthropic für Claude Desktop. Es ist ein ZIP-Archiv, das eine manifest.json mit der Beschreibung der Tools, den Einstiegspunkt des MCP-Servers (Node.js, Python usw.), ein Icon und Metadaten enthält. Claude Desktop installiert es per Einzelklick wie eine Browser-Erweiterung und führt den Server als lokalen Subprozess aus. Der MCP-Server spricht JSON-RPC über stdio.1115 Get Bananas liefert seinen Server auf diese Weise gebündelt aus.

Warum nicht die neue App-Intents-zu-MCP-Brücke verwenden?

Es gibt keine. App Intents (Apples Framework) und MCP (das Protokoll von Anthropic) sind unabhängig voneinander. Apple Intelligence ruft App Intents über seinen eigenen Resolver auf. Claude Desktop ruft MCP-Server über seine eigene Laufzeit auf. Eine App, die beide Oberflächen wünscht, liefert beide aus; eine automatische Brücke gibt es nicht.1213

Könnte man das ohne iCloud Drive machen?

Ja, mit Einschränkungen. Jeder gemeinsame, beschreibbare Dateispeicherort funktioniert: ein Ordner in ~/Documents, eine Netzwerkfreigabe, ein S3-gemountetes FUSE-Dateisystem. iCloud Drive ist bequem, weil es bereits auf jedem Mac ist, der Claude Desktop ausführt, und auf jedem iOS-Gerät, das der Benutzer besitzt. Eine Nicht-iCloud-Datei würde den Benutzer zwingen, die Synchronisierung separat einzurichten.

Was passiert bei einem Schreibkonflikt?

Die 5-Sekunden-Dateisperre plus 50-ms-Wiederholungen behandeln gleichzeitige MCP-seitige Schreiber (z. B. einen zweiten MCP-Aufruf, der eintrifft, während der erste mitten im Schreiben ist). Sie koordiniert nicht mit der Swift-App, die über ihren eigenen Coordinator schreibt. Wenn Swift und Node sich tatsächlich überlappen (selten, angesichts der 500-ms-Entprellung von Swift und der Tatsache, dass MCP-Schreibvorgänge nur auf Benutzeranfragen feuern), löst iCloud Drive auf Datei-Granularität auf: Last Write Wins. Der isValid-Filter des Swift-Decoders verwirft anschließend alles Fehlerhafte.

Warum keine CRDTs oder Operational Transform?

Übertrieben für Einkaufslisten mit 30 Einträgen. CRDTs sind die richtige Wahl, wenn überlappende gleichzeitige Bearbeitungen üblich sind und Sie deterministische Merge-Semantik benötigen (kollaborative Dokumenten-Editoren, Mehrspieler-Spiele). Für eine Einkaufsliste, bei der eine Person über Claude Einträge hinzufügt und eine andere sie auf dem Weg zum Laden über die iOS-App abhakt, ist Last-Write-Wins-mit-Entprellung korrekt.


Zwei Agenten-Ökosysteme, eine Einkaufsliste. Die Brücke ist iCloud Drive plus eine JSON-Datei mit einem nachsichtigen Decoder, und das genügt. Der kleinste gemeinsame Nenner ist keine Einschränkung. Er ist das Einzige, worauf sich beide Ökosysteme einigen.

Quellenverzeichnis


  1. Get Bananas des Autors, eine SwiftUI- + SwiftData-Einkaufslisten-App für iOS, macOS, watchOS und visionOS, veröffentlicht von 941 Apps. 

  2. Get Bananas liefert einen MCP- (Model Context Protocol) Server, gebündelt als get-bananas.mcpb für Claude Desktop. Bereitgestellte Tools: get_shopping_list, add_item, remove_item, update_item, update_shopping_list. Der Server umfasst 575 Zeilen Node.js in mcp-extension/server/index.js

  3. Apple Developer, „SwiftData”-Framework. Verfügbar ab iOS 17, macOS 14, watchOS 10, visionOS 1. Nur zur Laufzeit; keine serverseitigen oder prozessübergreifenden Bindings. 

  4. Apple Developer, „CloudKit”-Framework. Der native CKContainer-API erfordert das Entitlement com.apple.developer.icloud-services und einen passenden Apple Developer Team-Identifier. Apple veröffentlicht zudem CloudKit Web Services für Nicht-Apple-Plattform-Clients, deren Verwendung jedoch eine separate Token-/Auth-Brücke erfordert, die Get Bananas nicht pflegt. 

  5. Produktionscode in Banana List/Item.swift. Das Feld lastModified wurde später für die Konfliktauflösung der iCloud-Synchronisierung hinzugefügt. 

  6. Produktionscode in Banana List/iCloudBackupManager.swift. Konstanten leben in Banana List/Constants.swift

  7. Produktionscode in Banana List/ShoppingListExport.swift. Custom Decoder mit decodeIfPresent-Defaults plus isValid-Filter beim Import. 

  8. POSIX-Semantik von O_EXCL | O_CREAT; Node.js stellt dieselbe Atomarität über fs.writeFileSync(path, data, { flag: 'wx' }) bereit. Siehe Node.js fs documentation

  9. Apple Developer, „Designing for CloudKit”. Push-basierte Synchronisierung, Konfliktauflösung auf Record-Ebene, Zonen-Partitionierung. 

  10. Debugging-Notizen des Autors. Der Endlosschleifen-Vorfall erzeugte aus einer 30-Eintrag-Einkaufsliste in 3 Minuten eine 4-MB-BananaList.json, bevor die Sync-Counter-Logik gelandet war. 

  11. Anthropic, „Model Context Protocol”. Offenes Protokoll für die LLM-Tool-Nutzung; Multi-Runtime (Claude Desktop, Cline, Goose usw.). 

  12. Analyse des Autors in App Intents Are Apple’s New API to Your App. Die These der zwei parallelen Verträge angewandt auf System-AI-Oberflächen (Apple) und plattformübergreifende LLM-Tool-Nutzung (Anthropic). 

  13. Apple Developer, „App Intents framework”. Apples typisiert-deklarative Tool-Nutzungs-Oberfläche für Siri, Spotlight und Apple Intelligence. 

  14. Apple Developer, „FileManager url(forUbiquityContainerIdentifier:)”. Der unterstützte API zum Auflösen der iCloud-Drive-Container-URL einer App. Der macOS-Pfad unter ~/Library/Mobile Documents/ ist das Implementierungsdetail des Host-OS, wo iCloud Drive den Container offenlegt; der symbolische API ist das, was Apps aufrufen sollten. 

  15. Anthropic, „Desktop Extensions”. Das .mcpb-Format ist ein ZIP-Archiv mit manifest.json, MCP-Server-Einstiegspunkt, Icon und Metadaten. Einzelklick-Installation in Claude Desktop; führt den gebündelten Server als lokalen Subprozess über stdio JSON-RPC aus. 

  16. Apple Developer, „NSFileCoordinator”. Koordiniert Lese- und Schreibvorgänge auf eine Datei zwischen Prozessen, die sich auf dasselbe Koordinationsprotokoll einlassen; erforderlich, wenn der bird-Daemon von iCloud Drive, NSMetadataQuery-getriebene Beobachter und die App selbst denselben Pfad berühren können. 

  17. POSIX rename(2) ist atomar erforderlich, wenn Quelle und Ziel auf demselben Dateisystem liegen. Der lokale Mirror von iCloud Drive unter ~/Library/Mobile Documents/ ist ein einzelnes APFS-Volume, sodass fs.renameSync zwischen einer benachbarten Temp-Datei und dem kanonischen Pfad aus Sicht jedes Lesers atomar ist. Siehe POSIX rename specification

Verwandte Beiträge

Single Source Of Truth: SwiftData, MCP, iCloud

Drei Aufrufer können dieselbe Einkaufsliste schreiben: ein Mensch, Apple Intelligence und ein externer Agent. Die Wahrhe…

13 Min. Lesezeit

App Intents vs MCP: Die Frage des Routings

Zwei Protokolle, eine App. App Intents öffnen Ihre App für Apple Intelligence. MCP öffnet dieselbe Domäne für Claude, Ch…

12 Min. Lesezeit

Ihr Agent hat einen Mittelsmann, den Sie nicht geprüft haben

Forscher testeten 28 LLM API-Router. 17 griffen auf AWS-Canary-Credentials zu. Einer leerte ETH von einem Private Key. D…

12 Min. Lesezeit