← Tous les articles

Serveur MCP aux côtés d'une application iOS : deux écosystèmes d'agents, une seule liste

Get Bananas, mon application de liste de courses SwiftUI, fonctionne sur iOS, macOS, watchOS et visionOS.1 Elle vit aussi à l’intérieur de Claude Desktop en tant qu’extension MCP .mcpb exposant cinq outils : get_shopping_list, add_item, remove_item, update_item, update_shopping_list.2

Liste de courses Get Bananas sur iPhone, le même fichier JSON que le serveur MCP lit et écrit Lorsque vous demandez à Claude « ajoute des bananes, du lait et du pain à ma liste », Claude appelle add_item trois fois et la prochaine fois que j’ouvre l’application sur mon téléphone, les articles y sont. Aucun serveur. Aucun compte. Aucune clé API. Le pont est un unique fichier JSON plus cinq couches de prévention de boucle que j’ai dû ajouter après avoir livré une v1.0 qui s’est écrite elle-même dans un fichier de 4 Mo en trois minutes.

La question intéressante est comment. SwiftData est limité à l’exécution sur les plateformes Apple et n’est pas lisible depuis un processus Node.js.3 Le framework natif CloudKit nécessite l’entitlement correspondant com.apple.developer.icloud-services et l’identifiant d’équipe Apple Developer ; le sous-processus MCP de Claude Desktop n’a ni l’un ni l’autre, il ne peut donc pas utiliser CKContainer comme le fait mon application signée. CloudKit Web Services existe, mais l’utiliser nécessiterait de maintenir un pont de jeton/auth séparé entre le processus desktop et les serveurs d’Apple.4 Les chemins évidents sont donc fermés.

Le chemin que j’ai pris est plus ancien et plus étrange. L’application Get Bananas et son serveur MCP partagent leur état via un fichier JSON dans iCloud Drive. L’application Swift conserve un modèle SwiftData pour la persistance interne et exporte un fichier BananaList.json vers son conteneur iCloud Drive via NSFileCoordinator après chaque modification. Le serveur MCP Node.js lit et écrit le même fichier avec un verrou de fichier exclusif de 5 secondes, une détection de verrou périmé et des écritures atomiques par renommage de fichier temporaire. iCloud Drive gère la synchronisation entre appareils. Aujourd’hui, Claude Desktop lit et écrit la même source de vérité depuis le Mac ; l’adaptateur App Intents pour Apple Intelligence est la prochaine surface, contre le même fichier.

Cet essai porte sur les raisons pour lesquelles cette configuration fonctionne, ce qu’elle coûte et où elle s’effondre.

TL;DR

  • Get Bananas expose sa liste de courses à Claude Desktop via un serveur MCP livré. Le même format de fichier prendra en charge un adaptateur App Intents pour Apple Intelligence ensuite.
  • Le substrat d’intégration est iCloud Drive plus un fichier JSON, pas CloudKit, pas un serveur, pas un service.
  • Application Swift : SwiftData @Model ShoppingItem pour la rapidité interne ; export JSON vers iCloud Drive pour la portabilité.
  • Serveur MCP : 575 lignes de Node.js, verrou de fichier avec détection de verrou périmé, fonctionne à l’intérieur du bundle .mcpb de Claude Desktop.
  • Compromis : la synchronisation JSON basée sur fichier est plus lente que CloudKit et présente un risque de conflit de fusion, mais elle fonctionne dans n’importe quel écosystème d’agent capable de lire un fichier.

Diagramme de référence de l'architecture Model Context Protocol d'Anthropic montrant le modèle de connexion client/serveur

L’architecture Model Context Protocol telle que documentée par Anthropic. Un hôte MCP (Claude Desktop) se connecte à un ou plusieurs serveurs MCP (get-bananas.mcpb dans cet article), chacun exposant des outils, des ressources et des prompts que l’hôte peut invoquer. Source : modelcontextprotocol.io.11

L’architecture sur une page

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

Deux surfaces. Un fichier. L’ensemble du pont, c’est le fichier.

Le côté Swift : SwiftData pour la rapidité, JSON pour la portabilité

Dans l’application, la liste de courses est un @Model SwiftData. Vrai code de production :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?
}

C’est la vérité en mémoire. Chaque frappe de touche, chaque coche, chaque changement de section écrit dans SwiftData. SwiftData pilote les vues SwiftUI. L’application semble native parce qu’elle est native. Le déclencheur de sauvegarde repose sur un hash : un observateur .onChange(of: computeItemsHash()) ne se déclenche que lorsque l’id, le nom, la quantité, la section, l’état coché ou optionnel d’un article change, jamais lors d’une édition purement no-op.

L’astuce, c’est que SwiftData n’est pas la vérité inter-processus. C’est le cache inter-processus. Chaque modification est anti-rebondie de 500 ms puis écrit un fichier JSON dans le conteneur iCloud Drive de l’application via l’API d’écriture coordonnée d’Apple :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 est le moyen pris en charge pour écrire un fichier que d’autres processus (et le démon d’iCloud Drive) pourraient lire simultanément.16 Avant que cette écriture ne se produise, le gestionnaire lit le fichier existant et saute entièrement l’écriture si le JSON correspond octet par octet. Cela réduit le brassage redondant d’iCloud Drive chaque fois qu’un observateur de changement SwiftData se déclenche pour une édition no-op. À la restore, le gestionnaire réessaie jusqu’à trois fois avec un backoff exponentiel (1 s, 2 s, 4 s, budget total 7 s), car NSMetadataQuery signale un changement de fichier avant qu’iCloud Drive n’ait réellement téléchargé les nouveaux octets.6

La forme Codable du fichier est intentionnellement permissive. ShoppingListExport décode avec des valeurs par défaut pour chaque champ manquant et filtre les articles dont le nom est vide :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
        }
    }
}

Le décodeur défensif est intentionnel. Tout ce qui écrit le JSON ensuite (un serveur MCP, un raccourci futur, un collage manuel) oubliera inévitablement un champ. Le côté Swift l’absorbe. Le format de fichier partagé est le contrat ; le décodeur Swift est la partie indulgente.

Get Bananas sur macOS, l'hôte où le serveur MCP s'exécute en tant que sous-processus de Claude Desktop et lit le même fichier iCloud Drive

Le côté Node : un serveur MCP de 575 lignes qui lit le même fichier

Le serveur MCP vit dans mcp-extension/server/index.js, distribué sous la forme get-bananas.mcpb pour le système d’extensions de Claude Desktop. Il ouvre le même fichier iCloud Drive depuis l’hôte macOS :2

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

Cinq outils : une lecture pure (get_shopping_list), trois outils de niveau article en lecture-modification-écriture (add_item, remove_item, update_item), et un outil de remplacement en bloc (update_shopping_list) qui écrit sans lire au préalable. Le serveur MCP expose également le fichier en tant que Resource distincte en lecture seule pour les LLM qui préfèrent l’API ressource. Chaque écriture passe par un verrou de fichier avec récupération de verrou périmé :

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

Le motif de verrou est plus ancien que Node.js. fs.writeFileSync avec le drapeau 'wx' est la version multiplateforme de O_EXCL | O_CREAT. Si le fichier de verrou existe et a plus de 5 secondes, le serveur suppose que le détenteur précédent s’est planté et le récupère. S’il existe et est récent, le serveur attend 50 ms et réessaie. Après 5 secondes au total, il abandonne.8

Le verrou ne synchronise que les rédacteurs côté Node entre eux (une seconde invocation MCP pendant que la première est en cours d’écriture). Il ne se coordonne pas avec l’application Swift, qui écrit via NSFileCoordinator et String.write(atomically:) et ne touche jamais à BananaList.json.lock. Un véritable chevauchement Swift/Node est laissé à deux mécanismes plus faibles : l’application Swift anti-rebondit 500 ms avant d’écrire, les écritures MCP ne se produisent que sur sollicitation utilisateur, et toute collision résiduelle se résout par la résolution last-write-wins d’iCloud Drive à la granularité du fichier.

Les écritures elles-mêmes utilisent le motif temp-atomique-puis-renommer, avec une vérification d’analyse JSON entre les deux :

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 sur le même système de fichiers est POSIX-atomique : un lecteur sur un autre processus voit soit l’ancien fichier, soit le nouveau, jamais des octets à moitié écrits.17 Épingler le chemin temporaire à process.pid empêche deux instances de serveur MCP (rare, mais possible si l’utilisateur réinstalle Claude Desktop sans redémarrer) d’écraser les fichiers temporaires l’une de l’autre. Le JSON.parse en cours d’écriture est une étape de paranoïa : si la sérialisation elle-même a produit un JSON invalide, la fonction abandonne avant le renommage, laissant le fichier canonique intact.

Pourquoi iCloud Drive, pas CloudKit

Le choix qui fait fonctionner l’architecture, c’est utiliser iCloud Drive (basé sur fichier) au lieu de CloudKit (basé sur enregistrement) pour la vérité inter-processus. CloudKit est ce qu’Apple recommande pour la synchronisation app-à-app. Il offre une résolution de conflits de plus haut niveau, un push côté serveur et un partitionnement par zone.9 L’API native CKContainer est limitée aux plateformes Apple et soumise à entitlement, donc un sous-processus de Claude Desktop ne peut pas l’utiliser comme le fait mon application signée. Apple publie bien CloudKit Web Services pour les LLM hors plateformes Apple, mais l’utiliser nécessiterait de provisionner un jeton serveur-à-serveur, de le câbler dans le serveur MCP et de maintenir un pont d’authentification séparé : pas impossible, mais une quantité substantielle d’infrastructure pour une liste de courses.4

Le serveur MCP s’exécute non-sandboxé sur macOS en tant que sous-processus de Claude. Il n’a aucune chaîne de signature Apple Developer, aucune correspondance d’identifiant d’équipe avec le conteneur CloudKit de mon application, et aucun jeton CloudKit Web Services configuré.

iCloud Drive, en revanche, s’expose comme un emplacement de système de fichiers ordinaire. L’API prise en charge par Apple est FileManager.url(forUbiquityContainerIdentifier:) côté application ;14 sur macOS, l’emplacement résolu pour Get Bananas est ~/Library/Mobile Documents/iCloud~com~941apps~Banana-List/Documents/BananaList.json. Ce chemin est un détail d’implémentation spécifique à macOS de l’endroit où iCloud Drive expose le conteneur, mais pour Claude Desktop fonctionnant sur le même Mac, c’est juste un fichier. Tout processus avec un accès en lecture au répertoire personnel de l’utilisateur peut le lire et l’écrire. Tout comme un futur raccourci, un futur plugin SwiftBar, un futur script llama.cpp que l’utilisateur exécute localement. Tout ce qui peut lire un fichier peut s’intégrer.

Le coût, c’est que la synchronisation d’iCloud Drive est plus lente que celle de CloudKit (secondes, pas sous-seconde) et a une sémantique de conflit plus faible (last-write-wins à la granularité du fichier, pas de fusion au niveau de l’enregistrement). Pour une liste de courses d’environ 30 articles, aucun de ces coûts n’a d’importance. Pour une application à fort volume d’écritures avec 10 000 lignes et des éditeurs concurrents, les deux coûts domineraient.

Cinq couches de prévention de boucle

Le morceau de code le plus délicat côté Swift est la prévention de boucle. Sans elle : le serveur MCP écrit le JSON, iCloud Drive le synchronise vers iOS, le NSMetadataQuery de l’application iOS détecte le changement, l’application réimporte le JSON dans SwiftData, l’import déclenche un observateur de changement SwiftData, l’observateur de changement déclenche une sauvegarde anti-rebondie, la sauvegarde anti-rebondie écrit le JSON, iCloud Drive le resynchronise. J’ai livré la version naïve en v1.0 et regardé une liste de courses de 30 articles gonfler à 4 Mo en trois minutes pendant les tests.10

La version livrée utilise cinq garde-fous empilés, pas un. Chacun protège un cas limite de timing différent :

// 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
}
Couche Ce qu’elle attrape
1. Compteur de sync > 0 Chemin d’écriture sortant Écritures réentrantes déclenchées pendant qu’une sync depuis iCloud est activement en cours
2. Fenêtre post-sync de 5 s Chemin d’écriture sortant Callbacks @Model onChange retardés que SwiftData déclenche après que l’import s’est stabilisé
3. Fenêtre post-sauvegarde de 2 s Gestionnaire NSMetadataQuery entrant Événements de système de fichiers locaux déclenchés juste après l’écriture de l’application elle-même
4. Correspondance exacte de date de modification Gestionnaire entrant iCloud Drive renvoyant en écho notre propre sauvegarde entre les appareils
5. Date de modification monotone Gestionnaire entrant NSMetadataQuery déclenchant à la fois DidUpdate et DidFinishGathering pour un seul changement

Les deux premières couches contrôlent le chemin d’écriture sortant : dois-je exporter vers iCloud maintenant ? Les trois restantes contrôlent le gestionnaire NSMetadataQuery entrant : dois-je importer ce changement dans SwiftData ? L’un ou l’autre côté seul est insuffisant. Un seul aller-retour de synchronisation peut traverser des garde-fous des deux côtés selon l’événement qui se déclenche en premier, donc chaque chemin a besoin de ses propres défenses.

La leçon se généralise à toute architecture « fichier partagé entre deux rédacteurs » : la détection de changement basée sur la date de modification est nécessaire mais pas suffisante. Vous avez besoin d’une identité stable pour les “écritures que j’ai causées” qui survive à au moins un aller-retour à travers la couche de synchronisation. Ce qui s’en rapproche le plus avec iCloud Drive, c’est la date de modification du fichier au moment où vous l’avez écrit. Conservez-la. Comparez-la au retour.

Ce que je construirais différemment

Quatre leçons tirées de la livraison de ceci.

Le Codable défensif côté Swift gagne sa place. Le serveur MCP a été réécrit trois fois. Chaque réécriture a oublié de définir un champ au moins une fois. Le décodeur Swift a absorbé chaque variante et l’application n’a jamais planté. Si je recommençais, je pousserais davantage de champs vers « décoder avec valeur par défaut » plutôt que « requis ». Le contrat entre les deux rédacteurs est fragile par conception.7

Le délai d’expiration du verrou devrait être conscient du contenu, pas seulement de mtime. Cinq secondes, c’est court. Si le Mac de l’utilisateur est sur un Wi-Fi lent ou si l’appareil iOS est en cours de restauration après un long arrière-plan, une synchronisation iCloud Drive de BananaList.json.lock peut prendre plus de 5 secondes à se propager. Le serveur MCP voit alors un verrou périmé qui est en fait toujours détenu. La correction consiste à conditionner la vérification de verrou périmé au PID écrit à l’intérieur du fichier de verrou : si kill(pid, 0) signale un processus toujours en cours d’exécution, ne brisez pas le verrou, peu importe à quel point la mtime semble ancienne. Le code actuel écrit le PID mais ne le relit jamais.

L’outil update_shopping_list était une erreur. Il remplace toute la liste. Claude Desktop l’appelle parfois alors qu’une opération sur un seul article ferait l’affaire, puis une partie non triviale de la liste de l’utilisateur disparaît. J’aurais dû ne livrer que les quatre outils de niveau article (get, add, remove, update) et forcer Claude à les composer. L’annotation destructiveHint: true du protocole MCP marque bien l’outil comme destructif,11 mais Claude ne le présente pas toujours à l’utilisateur avant l’appel. L’outil de remplacement en bloc est pratique pour le LLM et dangereux pour l’utilisateur. La présence d’un garde-fou au niveau du protocole ne remplace pas le fait de ne pas livrer le piège.

L’export JSON partagé a besoin d’un champ de version. ShoppingListExport décode avec des valeurs par défaut permissives, ce qui fonctionne jusqu’au jour où je renomme un champ plutôt que d’en ajouter un. Un schemaVersion: 1 en haut du JSON permettrait à l’un ou l’autre côté de détecter un futur changement cassant et de refuser la lecture au lieu de produire silencieusement un modèle malformé. Les migrations resteraient manuelles, mais au moins le mode de défaillance serait bruyant plutôt qu’une perte de données silencieuse.

Quand ne pas utiliser ce motif

Le refus fait partie de la conception.

Si les données sont réglementées (santé, finance, tout ce qui a une politique de rétention de conformité), le système de fichiers contrôlé par l’utilisateur d’iCloud Drive est le mauvais substrat. CloudKit dispose de journaux et de pistes d’audit ; les fichiers JSON lisibles par l’utilisateur, non.

Si le budget de latence inter-processus est sous-seconde, iCloud Drive ne le respectera pas. Dans mes tests, la synchronisation iCloud Drive prend généralement des secondes plutôt qu’une sous-seconde sur une connexion saine ; Apple ne publie pas de SLA plus serré, et les réseaux lents allongent la durée. La livraison push de CloudKit est matériellement plus rapide pour les mises à jour au niveau de l’enregistrement. Un produit de collaboration en temps réel a besoin de CloudKit (ou d’un serveur de synchronisation dédié).

Si le schéma évolue rapidement, le motif Codable-avec-valeurs-par-défaut accumule de la dette. Chaque nouveau champ nécessite une décision « valeur par défaut pour les anciens fichiers » qui vieillit rapidement. La synchronisation par fichier JSON convient le mieux aux schémas stables avec des changements principalement additifs.

Ce que cela signifie pour les applications qui veulent être accessibles par plusieurs écosystèmes d’agents

Le motif est suffisamment simple pour être répété. Trois pièces :

  1. Un @Model SwiftData pour la persistance interne. Pilote l’interface, rapide, natif.
  2. Un export JSON Codable écrit vers iCloud Drive lors d’un changement anti-rebondi. Décodeur défensif. Schéma stable. Le fichier partagé est le contrat.
  3. Un petit adaptateur pour chaque écosystème d’agent qui lit et écrit le même fichier avec un verrou de fichier. Node.js pour Claude Desktop. Un futur App Intent + AppEntity pour Apple Intelligence. Un futur script shell pour ce qui sera livré ensuite.

Le motif est portable parce que le substrat d’intégration, c’est le système de fichiers. Tous les runtimes d’agents qui existent aujourd’hui (Claude Desktop, Cursor, Goose, Cline) et la plupart de ceux qui seront livrés l’année prochaine peuvent lire un fichier.11 CloudKit ne peut pas. Les moteurs de synchronisation natifs ne peuvent pas. Le plus petit dénominateur commun gagne lorsque l’objectif est la portée à travers les écosystèmes de LLM.

Anthropic et Apple ne sont pas d’accord sur ce à quoi un agent devrait ressembler. Les App Intents disent qu’il s’agit d’une déclaration Swift typée qu’Apple Intelligence résout sur l’appareil. MCP dit qu’il s’agit d’un serveur JSON-RPC avec une liste d’outils que n’importe quel LLM peut appeler. Les deux sont corrects dans leurs propres écosystèmes. Get Bananas ne traite ni l’un ni l’autre comme la source de vérité et laisse le système de fichiers servir d’intermédiaire.12

La prochaine fois que je livrerai une application qui veut deux surfaces d’agents, je commencerai par le format de fichier avant le modèle d’entité.

FAQ

Qu’est-ce que .mcpb et comment cela fonctionne-t-il ?

Un .mcpb est le format de bundle d’extension MCP d’Anthropic pour Claude Desktop. C’est une archive zip contenant un manifest.json décrivant les outils, le point d’entrée du serveur MCP (Node.js, Python, etc.), une icône et des métadonnées. Claude Desktop l’installe comme une extension de navigateur via un seul clic et exécute le serveur en tant que sous-processus local. Le serveur MCP parle JSON-RPC sur stdio.1115 Get Bananas livre son serveur empaqueté de cette façon.

Pourquoi ne pas utiliser le nouveau pont App Intents-vers-MCP ?

Il n’y en a pas. App Intents (le framework d’Apple) et MCP (le protocole d’Anthropic) sont indépendants. Apple Intelligence appelle App Intents via son propre résolveur. Claude Desktop appelle les serveurs MCP via son propre runtime. Une application qui veut les deux surfaces livre les deux ; il n’y a pas de pont automatique.1213

Pourriez-vous faire cela sans iCloud Drive ?

Oui, avec des réserves. Tout emplacement de fichier partagé inscriptible fonctionne : un dossier dans ~/Documents, un partage réseau, un système de fichiers FUSE monté sur S3. iCloud Drive est pratique parce qu’il est déjà sur tous les Mac qui exécutent Claude Desktop et sur tous les appareils iOS que possède l’utilisateur. Un fichier non-iCloud forcerait l’utilisateur à configurer la synchronisation séparément.

Que se passe-t-il en cas de conflit d’écriture ?

Le verrou de fichier de 5 secondes plus les nouvelles tentatives de 50 ms gère les rédacteurs concurrents côté MCP (par exemple, une seconde invocation MCP qui arrive pendant que la première est en cours d’écriture). Il ne se coordonne pas avec l’application Swift, qui écrit via son propre coordinateur. Lorsque Swift et Node se chevauchent réellement (rare, étant donné l’anti-rebond de 500 ms de Swift et le fait que les écritures MCP ne se déclenchent que sur sollicitation utilisateur), iCloud Drive résout à la granularité du fichier : la dernière écriture l’emporte. Le filtre isValid du décodeur Swift élimine ensuite tout ce qui est malformé.

Pourquoi pas des CRDT ou de la transformation opérationnelle ?

Surdimensionné pour des listes de courses de 30 articles. Les CRDT sont le bon choix lorsque les éditions concurrentes qui se chevauchent sont fréquentes et que vous avez besoin d’une sémantique de fusion déterministe (éditeurs de documents collaboratifs, jeux multi-utilisateurs). Pour une liste de courses où une personne ajoute des articles via Claude et une autre les coche via l’application iOS sur le chemin du magasin, last-write-wins-with-debounce est correct.


Deux écosystèmes d’agents, une liste de courses. Le pont, c’est iCloud Drive plus un fichier JSON avec un décodeur indulgent, et cela suffit. Le plus petit dénominateur commun n’est pas une limitation. C’est la seule chose sur laquelle les deux écosystèmes s’accordent.

Références


  1. Get Bananas de l’auteur, une application de liste de courses SwiftUI + SwiftData pour iOS, macOS, watchOS et visionOS, publiée par 941 Apps. 

  2. Get Bananas livre un serveur MCP (Model Context Protocol) empaqueté sous le nom get-bananas.mcpb pour Claude Desktop. Outils exposés : get_shopping_list, add_item, remove_item, update_item, update_shopping_list. Le serveur compte 575 lignes de Node.js dans mcp-extension/server/index.js

  3. Apple Developer, framework « SwiftData ». Disponible sur iOS 17+, macOS 14+, watchOS 10+, visionOS 1+. Limité à l’exécution ; aucune liaison côté serveur ou inter-processus. 

  4. Apple Developer, framework « CloudKit ». L’API native CKContainer nécessite l’entitlement com.apple.developer.icloud-services et un identifiant d’équipe Apple Developer correspondant. Apple publie également CloudKit Web Services pour les LLM hors plateformes Apple, mais l’utiliser nécessite un pont de jeton/auth séparé que Get Bananas ne maintient pas. 

  5. Code de production dans Banana List/Item.swift. Le champ lastModified a été ajouté plus tard pour la résolution des conflits de synchronisation iCloud. 

  6. Code de production dans Banana List/iCloudBackupManager.swift. Les constantes vivent dans Banana List/Constants.swift

  7. Code de production dans Banana List/ShoppingListExport.swift. Décodeur personnalisé avec des valeurs par défaut decodeIfPresent plus un filtre isValid à l’import. 

  8. Sémantique POSIX O_EXCL | O_CREAT ; Node.js expose la même atomicité via fs.writeFileSync(path, data, { flag: 'wx' }). Voir la documentation fs de Node.js

  9. Apple Developer, « Designing for CloudKit ». Synchronisation par push, résolution de conflits au niveau de l’enregistrement, partitionnement par zone. 

  10. Notes de débogage de l’auteur. L’incident de boucle infinie a produit un BananaList.json de 4 Mo à partir d’une liste de courses de 30 articles en 3 minutes avant que la logique de compteur de synchronisation n’arrive. 

  11. Anthropic, « Model Context Protocol ». Protocole ouvert pour l’utilisation d’outils par les LLM ; multi-runtime (Claude Desktop, Cline, Goose, etc.). 

  12. Analyse de l’auteur dans App Intents Are Apple’s New API to Your App. La thèse des deux contrats parallèles appliquée à travers les surfaces système-IA (Apple) et l’utilisation d’outils inter-LLM (Anthropic). 

  13. Apple Developer, « framework App Intents ». La surface d’utilisation d’outils typée-déclarative d’Apple pour Siri, Spotlight et Apple Intelligence. 

  14. Apple Developer, « FileManager url(forUbiquityContainerIdentifier:) ». L’API prise en charge pour résoudre l’URL du conteneur iCloud Drive d’une application. Le chemin macOS sous ~/Library/Mobile Documents/ est le détail d’implémentation de l’OS hôte de l’endroit où iCloud Drive expose le conteneur ; l’API symbolique est ce que les applications devraient appeler. 

  15. Anthropic, « Desktop Extensions ». Le format .mcpb est une archive zip contenant manifest.json, le point d’entrée du serveur MCP, une icône et des métadonnées. Installation en un clic dans Claude Desktop ; exécute le serveur empaqueté en tant que sous-processus local sur JSON-RPC stdio. 

  16. Apple Developer, « NSFileCoordinator ». Coordonne les lectures et écritures vers un fichier entre des processus qui adhèrent au même protocole de coordination ; requis lorsque le démon bird d’iCloud Drive, les observateurs pilotés par NSMetadataQuery et l’application elle-même peuvent tous toucher au même chemin. 

  17. POSIX rename(2) doit être atomique lorsque la source et la destination sont sur le même système de fichiers. Le miroir local d’iCloud Drive sous ~/Library/Mobile Documents/ est un seul volume APFS, donc fs.renameSync entre un fichier temporaire frère et le chemin canonique est atomique du point de vue de tout lecteur. Voir la spécification POSIX rename

Articles connexes

Single Source Of Truth: SwiftData, MCP, iCloud

Three callers can write to the same shopping list: a human, Apple Intelligence, and an external agent. Truth has to live…

17 min de lecture

App Intents vs MCP : la question du routage

Deux protocoles, une seule application. Les App Intents exposent votre application à Apple Intelligence. MCP expose le m…

15 min de lecture

Votre agent a un intermédiaire que vous n'avez pas vérifié

Des chercheurs ont testé 28 routeurs LLM API. 17 ont touché aux identifiants canari AWS. Un a vidé de l'ETH d'une clé pr…

14 min de lecture