← Todos los articulos

Servidor MCP junto a una app de iOS: dos ecosistemas de agentes, una sola lista

Get Bananas, mi app de listas de compras hecha con SwiftUI, funciona en iOS, macOS, watchOS y visionOS.1 También vive dentro de Claude Desktop como una extensión MCP .mcpb que expone cinco herramientas: get_shopping_list, add_item, remove_item, update_item, update_shopping_list.2

Lista de compras de Get Bananas en el iPhone, el mismo archivo JSON que el servidor MCP lee y escribe Cuando le dices a Claude “agrega bananas, leche y pan a mi lista”, Claude llama a add_item tres veces y la próxima vez que abro la app en el teléfono, los elementos están ahí. Sin servidor. Sin cuenta. Sin clave de API. El puente es un único archivo JSON más cinco capas de prevención de bucles que tuve que añadir después de lanzar una v1.0 que se escribió a sí misma hasta convertirse en un archivo de 4MB en tres minutos.

La pregunta interesante es cómo. SwiftData solo funciona en tiempo de ejecución de las plataformas Apple y no se puede leer desde un proceso de Node.js.3 El framework nativo de CloudKit necesita la entitlement correspondiente com.apple.developer.icloud-services y el identificador de equipo de Apple Developer; el subproceso MCP de Claude Desktop no tiene ninguno de los dos, así que no puede usar CKContainer como sí lo hace mi app firmada. CloudKit Web Services existe, pero usarlo requeriría mantener un puente de tokens / autenticación independiente entre el proceso de escritorio y los servidores de Apple.4 Así que los caminos obvios están cerrados.

El camino que tomé es más antiguo y más extraño. La app Get Bananas y su servidor MCP comparten estado a través de un archivo JSON en iCloud Drive. La app Swift mantiene un modelo de SwiftData para la persistencia dentro de la app y exporta un archivo BananaList.json a su contenedor de iCloud Drive a través de NSFileCoordinator después de cada cambio. El servidor MCP de Node.js lee y escribe el mismo archivo con un bloqueo exclusivo de archivo de 5 segundos, detección de bloqueos obsoletos y escrituras atómicas mediante renombrado de archivo temporal. iCloud Drive se encarga de la sincronización entre dispositivos. Hoy, Claude Desktop lee y escribe la misma fuente de verdad desde el Mac; el adaptador App Intents para Apple Intelligence es la próxima superficie, contra el mismo archivo.

Este ensayo trata sobre por qué ese arreglo funciona, qué cuesta y dónde se rompe.

TL;DR

  • Get Bananas expone su lista de compras a Claude Desktop a través de un servidor MCP incluido. El mismo formato de archivo soportará a continuación un adaptador App Intents para Apple Intelligence.
  • El sustrato de integración es iCloud Drive más un archivo JSON, no CloudKit, no un servidor, no un servicio.
  • App Swift: SwiftData @Model ShoppingItem para velocidad dentro de la app; exportación JSON a iCloud Drive para portabilidad.
  • Servidor MCP: 575 líneas de Node.js, bloqueo de archivo con detección de bloqueos obsoletos, se ejecuta dentro del bundle .mcpb de Claude Desktop.
  • Compromiso: la sincronización JSON basada en archivos es más lenta que CloudKit y tiene riesgo de conflictos de fusión, pero funciona en cualquier ecosistema de agentes que pueda leer un archivo.

Diagrama de referencia de la arquitectura del Model Context Protocol de Anthropic que muestra el modelo de conexión cliente/servidor

La arquitectura del Model Context Protocol según la documenta Anthropic. Un host MCP (Claude Desktop) se conecta a uno o más servidores MCP (get-bananas.mcpb en este artículo), cada uno expone herramientas, recursos y prompts que el host puede invocar. Fuente: modelcontextprotocol.io.11

La arquitectura en una página

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

Dos superficies. Un archivo. Todo el puente es el archivo.

El lado Swift: SwiftData para velocidad, JSON para portabilidad

En la app, la lista de compras es un @Model de SwiftData. Código real de producción: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?
}

Esa es la verdad en memoria. Cada pulsación de tecla, cada toque de casilla, cada cambio de sección escribe en SwiftData. SwiftData impulsa las vistas de SwiftUI. La app se siente nativa porque es nativa. El disparador del respaldo está basado en hash: un observador .onChange(of: computeItemsHash()) se activa solo cuando el id, nombre, cantidad, sección, estado marcado u opcional de un elemento cambia, nunca en una edición sin operación pura.

El truco es que SwiftData no es la verdad entre procesos. Es la caché entre procesos. Cada cambio aplica un debounce de 500ms y luego escribe un archivo JSON al contenedor de iCloud Drive de la app a través del API de escritura coordinada de 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 es la forma soportada de escribir un archivo que otros procesos (y el daemon de iCloud Drive) podrían leer concurrentemente.16 Antes de que ocurra esa escritura, el manager lee el archivo existente y omite la escritura por completo si el JSON coincide byte por byte. Eso reduce la rotación redundante de iCloud Drive cada vez que un observador de cambios de SwiftData se dispara por una edición sin operación. En restore, el manager reintenta hasta tres veces con retroceso exponencial (1s, 2s, 4s, presupuesto total 7s), porque NSMetadataQuery reporta un cambio de archivo antes de que iCloud Drive haya descargado realmente los nuevos bytes.6

La forma Codable del archivo es intencionalmente permisiva. ShoppingListExport decodifica con valores por defecto para cada campo faltante y filtra los elementos con nombres vacíos: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
        }
    }
}

El decodificador defensivo es a propósito. Cualquier cosa que escriba el JSON a continuación (un servidor MCP, un futuro shortcut, un pegado manual) inevitablemente olvidará un campo. El lado Swift lo absorbe. El formato de archivo compartido es el contrato; el decodificador de Swift es la parte indulgente.

Get Bananas en macOS, el host donde se ejecuta el servidor MCP como subproceso de Claude Desktop y lee el mismo archivo de iCloud Drive

El lado Node: un servidor MCP de 575 líneas que lee el mismo archivo

El servidor MCP vive en mcp-extension/server/index.js, distribuido como get-bananas.mcpb para el sistema de extensiones de Claude Desktop. Abre el mismo archivo de iCloud Drive desde el host de macOS:2

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

Cinco herramientas: una de lectura pura (get_shopping_list), tres herramientas a nivel de elemento de leer-modificar-escribir (add_item, remove_item, update_item) y una herramienta de reemplazo masivo (update_shopping_list) que escribe sin leer primero. El servidor MCP también expone el archivo como un Resource separado de solo lectura para clientes que prefieren el API de recursos. Cada escritura pasa por un bloqueo de archivo con recuperación de bloqueos obsoletos:

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

El patrón de bloqueo es más antiguo que Node.js. fs.writeFileSync con la bandera 'wx' es la versión multiplataforma de O_EXCL | O_CREAT. Si el archivo de bloqueo existe y tiene más de 5 segundos, el servidor asume que el titular anterior se cayó y lo reclama. Si existe y es reciente, el servidor espera 50ms y reintenta. Después de 5 segundos en total, se rinde.8

El bloqueo solo sincroniza a los escritores del lado Node entre sí (una segunda invocación de MCP mientras la primera está a mitad de escritura). No coordina con la app Swift, que escribe a través de NSFileCoordinator y String.write(atomically:) y nunca toca BananaList.json.lock. La superposición genuina entre Swift/Node se deja a dos mecanismos más débiles: la app Swift aplica un debounce de 500ms antes de escribir, las escrituras de MCP ocurren solo bajo prompt del usuario, y cualquier colisión residual recae sobre la resolución last-write-wins de iCloud Drive a granularidad de archivo.

Las escrituras en sí mismas usan el patrón temporal-atómico-luego-renombrar, con una verificación de parseo de JSON entre medio:

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 en el mismo sistema de archivos es atómico según POSIX: un lector en otro proceso ve el archivo viejo o el nuevo, nunca bytes a medio escribir.17 Fijar la ruta temporal a process.pid evita que dos instancias del servidor MCP (raro, pero posible si el usuario reinstala Claude Desktop sin reiniciar) se pisen los archivos temporales entre sí. El JSON.parse a mitad de escritura es un paso paranoico: si la serialización en sí misma produjo JSON inválido, la función aborta antes del rename, dejando intacto el archivo canónico.

Por qué iCloud Drive, no CloudKit

La elección que hace que la arquitectura funcione es usar iCloud Drive (basado en archivos) en lugar de CloudKit (basado en registros) para la verdad entre procesos. CloudKit es lo que Apple recomienda para la sincronización entre apps. Tiene resolución de conflictos de más alto nivel, push del lado del servidor y particionado por zonas.9 El API nativo CKContainer es exclusivo de las plataformas Apple y está restringido por entitlement, así que un subproceso de Claude Desktop no puede usarlo como sí lo hace mi app firmada. Apple sí publica CloudKit Web Services para clientes que no son de plataformas Apple, pero usarlo requeriría aprovisionar un token servidor-a-servidor, conectarlo al servidor MCP y mantener un puente de autenticación independiente: no imposible, pero una cantidad sustancial de infraestructura para una lista de compras.4

El servidor MCP se ejecuta sin sandbox en macOS como subproceso de Claude. No tiene cadena de firma de Apple Developer, ni coincidencia de identificador de equipo con el contenedor de CloudKit de mi app, ni token de CloudKit Web Services configurado.

iCloud Drive, en cambio, se expone a sí mismo como una ubicación regular del sistema de archivos. El API soportado por Apple es FileManager.url(forUbiquityContainerIdentifier:) para el lado de la app;14 en macOS, la ubicación resuelta para Get Bananas es ~/Library/Mobile Documents/iCloud~com~941apps~Banana-List/Documents/BananaList.json. Esa ruta es un detalle de implementación específico de macOS sobre dónde iCloud Drive expone el contenedor, pero para Claude Desktop ejecutándose en el mismo Mac, es solo un archivo. Cualquier proceso con acceso de lectura al directorio home del usuario puede leerlo y escribirlo. También puede hacerlo un futuro shortcut, un futuro plugin de SwiftBar, un futuro script de llama.cpp que el usuario ejecute localmente. Cualquier cosa que pueda leer un archivo puede integrarse.

El costo es que la sincronización de iCloud Drive es más lenta que la de CloudKit (segundos, no sub-segundos) y tiene semánticas de conflicto más débiles (last-write-wins a granularidad de archivo, no fusión a nivel de registro). Para una lista de compras con quizá 30 elementos, ninguno de los costos importa. Para una app de alto volumen de escrituras con 10K filas y editores concurrentes, ambos costos dominarían.

Cinco capas de prevención de bucles

La pieza de código más complicada del lado Swift es la prevención de bucles. Sin ella: el servidor MCP escribe el JSON, iCloud Drive lo sincroniza a iOS, el NSMetadataQuery de la app iOS detecta el cambio, la app reimporta el JSON a SwiftData, la importación dispara un observador de cambios de SwiftData, el observador de cambios dispara un respaldo con debounce, el respaldo con debounce escribe el JSON, iCloud Drive lo sincroniza de vuelta. Lancé la versión ingenua en la v1.0 y vi una lista de compras de 30 elementos crecer hasta 4MB en tres minutos durante las pruebas.10

La versión lanzada usa cinco guardias apilados, no uno. Cada uno protege un caso límite de timing diferente:

// 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
}
Capa Dónde Qué atrapa
1. Contador de sync > 0 Camino de escritura saliente Escrituras reentrantes disparadas mientras una sincronización-desde-iCloud está activamente en curso
2. Ventana de 5s post-sync Camino de escritura saliente Callbacks onChange retrasados de @Model que SwiftData dispara después de que la importación se haya asentado
3. Ventana de 2s post-backup Manejador entrante de NSMetadataQuery Eventos del sistema de archivos local disparados justo después de la propia escritura de la app
4. Coincidencia exacta de mod-date Manejador entrante iCloud Drive haciendo eco de nuestro propio respaldo de vuelta a nosotros entre dispositivos
5. Mod-date monotónico Manejador entrante NSMetadataQuery disparando tanto DidUpdate como DidFinishGathering para un solo cambio

Las dos primeras capas regulan el camino de escritura saliente: ¿debería exportar a iCloud ahora mismo? Las tres restantes regulan el manejador entrante de NSMetadataQuery: ¿debería importar este cambio a SwiftData? Cualquiera de los lados por sí solo es insuficiente. Una sola ida y vuelta de sincronización puede pasar a través de los guardias de ambos lados dependiendo de qué evento se dispare primero, así que cada camino necesita sus propias defensas.

La lección se generaliza a cualquier arquitectura de “archivo compartido entre dos escritores”: la detección de cambios basada en mod-time es necesaria pero no suficiente. Necesitas una identidad estable para “escrituras que yo causé” que sobreviva al menos una ida y vuelta a través de la capa de sincronización. Lo más cercano que iCloud Drive te da es la fecha de modificación del archivo en el momento en que lo escribiste. Aférrate a ella. Compara en el camino de regreso.

Qué construiría diferente

Cuatro lecciones de haber lanzado esto.

El Codable defensivo del lado Swift se gana su sustento. El servidor MCP ha sido reescrito tres veces. Cada reescritura olvidó establecer un campo al menos una vez. El decodificador de Swift absorbió cada variante y la app nunca se cayó. Si volviera a empezar, llevaría más campos a “decodificar con valor por defecto” en lugar de “requerido”. El contrato entre los dos escritores es frágil por diseño.7

El timeout del bloqueo debería ser consciente del contenido, no solo del mtime. Cinco segundos es poco. Si el Mac del usuario está en Wi-Fi lento o el dispositivo iOS se está restaurando después de un largo segundo plano, una sincronización de iCloud Drive de BananaList.json.lock puede tardar más de 5 segundos en propagarse. El servidor MCP entonces ve un bloqueo obsoleto que en realidad sigue retenido. La solución es condicionar la verificación de bloqueo obsoleto al PID escrito dentro del archivo de bloqueo: si kill(pid, 0) reporta un proceso aún en ejecución, no rompas el bloqueo sin importar qué tan viejo se vea el mtime. El código actual escribe el PID pero nunca lo lee de vuelta.

La herramienta update_shopping_list fue un error. Reemplaza la lista entera. Claude Desktop ocasionalmente la llama cuando una operación a nivel de un solo elemento bastaría, y entonces una porción no trivial de la lista del usuario desaparece. Solo debería haber lanzado las cuatro herramientas a nivel de elemento (get, add, remove, update) y forzar a Claude a componerlas. La anotación destructiveHint: true del protocolo MCP marca la herramienta como destructiva,11 pero Claude no siempre lo muestra al usuario antes de llamarla. La herramienta de reemplazo masivo es conveniente para el LLM y peligrosa para el usuario. La presencia de una salvaguarda en la capa del protocolo no sustituye a no lanzar el arma de pie.

La exportación JSON compartida necesita un campo de versión. ShoppingListExport decodifica con valores por defecto permisivos, lo que funciona hasta el día en que renombre un campo en lugar de añadir uno. Un schemaVersion: 1 al inicio del JSON permitiría que cualquiera de los lados detecte un futuro cambio incompatible y rechace la lectura en lugar de producir silenciosamente un modelo malformado. Las migraciones seguirían siendo manuales, pero al menos el modo de fallo sería ruidoso en lugar de pérdida silenciosa de datos.

Cuándo no usar este patrón

Negarse es parte del diseño.

Si los datos están regulados (salud, finanzas, cualquier cosa con una política de retención por cumplimiento), el sistema de archivos controlado por el usuario de iCloud Drive es el sustrato equivocado. CloudKit tiene logs y trazas de auditoría; los archivos JSON legibles por el usuario no.

Si el presupuesto de latencia entre procesos es sub-segundo, iCloud Drive no lo cumplirá. En mis pruebas, la sincronización de iCloud Drive normalmente tarda segundos en lugar de sub-segundos en una conexión saludable; Apple no publica un SLA más estricto, y las redes lentas lo alargan más. La entrega basada en push de CloudKit es materialmente más rápida para actualizaciones a nivel de registro. Un producto de colaboración en tiempo real necesita CloudKit (o un servidor de sincronización dedicado).

Si el esquema evoluciona rápido, el patrón Codable-con-valores-por-defecto acumula deuda. Cada nuevo campo requiere una decisión de “valor por defecto para archivos viejos” que envejece rápidamente. La sincronización de archivos JSON es mejor para esquemas estables con cambios mayormente aditivos.

Qué significa esto para apps que quieren ser alcanzables por múltiples ecosistemas de agentes

El patrón es lo bastante simple como para repetirlo. Tres piezas:

  1. Un @Model de SwiftData para la persistencia dentro de la app. Impulsa la UI, rápido, nativo.
  2. Una exportación Codable JSON escrita a iCloud Drive ante cambios con debounce. Decodificador defensivo. Esquema estable. El archivo compartido es el contrato.
  3. Un pequeño adaptador para cada ecosistema de agentes que lea y escriba el mismo archivo con un bloqueo de archivo. Node.js para Claude Desktop. Un futuro App Intent + AppEntity para Apple Intelligence. Un futuro shell script para lo que se lance a continuación.

El patrón es portátil porque el sustrato de integración es el sistema de archivos. Cada runtime de agente que existe hoy (Claude Desktop, Cursor, Goose, Cline) y la mayoría de los que se lancen el próximo año pueden leer un archivo.11 CloudKit no puede. Los motores de sincronización nativos no pueden. El mínimo común denominador gana cuando el objetivo es alcance entre ecosistemas de LLM.

Anthropic y Apple no se ponen de acuerdo en cómo debería verse un agente. App Intents dice que es una declaración tipada de Swift que Apple Intelligence resuelve en el dispositivo. MCP dice que es un servidor JSON-RPC con una lista de herramientas que cualquier LLM puede llamar. Ambos son correctos en sus propios ecosistemas. Get Bananas no trata a ninguno como la fuente de verdad y deja que el sistema de archivos medie.12

La próxima vez que lance una app que quiera dos superficies de agente, empezaré con el formato de archivo antes que el modelo de entidad.

FAQ

¿Qué es .mcpb y cómo funciona?

Un .mcpb es el formato de bundle de extensión MCP de Anthropic para Claude Desktop. Es un archivo zip que contiene un manifest.json describiendo las herramientas, el punto de entrada del servidor MCP (Node.js, Python, etc.), un icono y metadatos. Claude Desktop lo instala como una extensión de navegador con un solo clic y ejecuta el servidor como un subproceso local. El servidor MCP habla JSON-RPC sobre stdio.1115 Get Bananas envía su servidor empaquetado de esta manera.

¿Por qué no usar el nuevo puente de App Intents a MCP?

No hay uno. App Intents (el framework de Apple) y MCP (el protocolo de Anthropic) son independientes. Apple Intelligence llama a App Intents a través de su propio resolutor. Claude Desktop llama a servidores MCP a través de su propio runtime. Una app que quiera ambas superficies envía ambas; no hay puente automático.1213

¿Podrías hacer esto sin iCloud Drive?

Sí, con salvedades. Cualquier ubicación de archivo compartida y escribible funciona: una carpeta en ~/Documents, un recurso compartido de red, un sistema de archivos FUSE montado en S3. iCloud Drive es conveniente porque ya está en cada Mac que ejecuta Claude Desktop y en cada dispositivo iOS que el usuario posee. Un archivo no-iCloud forzaría al usuario a configurar la sincronización por separado.

¿Qué ocurre cuando hay un conflicto de escritura?

El bloqueo de archivo de 5 segundos más los reintentos de 50ms manejan a los escritores del lado MCP concurrentes (p. ej., una segunda invocación de MCP que llega mientras la primera está a mitad de escritura). No coordina con la app Swift, que escribe a través de su propio coordinador. Cuando Swift y Node se superponen genuinamente (raro, dado el debounce de 500ms de Swift y que las escrituras de MCP solo se disparan por prompts del usuario), iCloud Drive resuelve a granularidad de archivo: la última escritura gana. El filtro isValid del decodificador de Swift entonces descarta cualquier cosa malformada.

¿Por qué no CRDTs o transformación operacional?

Excesivo para listas de compras de 30 elementos. Los CRDTs son la elección correcta cuando las ediciones concurrentes superpuestas son comunes y necesitas semánticas de fusión deterministas (editores de documentos colaborativos, juegos multijugador). Para una lista de compras donde una persona añade elementos vía Claude y otra los marca vía la app iOS de camino a la tienda, last-write-wins-con-debounce es correcto.


Dos ecosistemas de agentes, una sola lista de compras. El puente es iCloud Drive más un archivo JSON con un decodificador indulgente, y eso es suficiente. El mínimo común denominador no es una limitación. Es lo único en lo que ambos ecosistemas están de acuerdo.

Referencias


  1. Get Bananas del autor, una app de listas de compras con SwiftUI + SwiftData para iOS, macOS, watchOS y visionOS, publicada por 941 Apps. 

  2. Get Bananas envía un servidor MCP (Model Context Protocol) empaquetado como get-bananas.mcpb para Claude Desktop. Herramientas expuestas: get_shopping_list, add_item, remove_item, update_item, update_shopping_list. El servidor son 575 líneas de Node.js en mcp-extension/server/index.js

  3. Apple Developer, framework “SwiftData”. Disponible en iOS 17+, macOS 14+, watchOS 10+, visionOS 1+. Solo en tiempo de ejecución; sin enlaces del lado del servidor o entre procesos. 

  4. Apple Developer, framework “CloudKit”. El API nativo CKContainer requiere la entitlement com.apple.developer.icloud-services y el identificador de equipo de Apple Developer correspondiente. Apple también publica CloudKit Web Services para clientes que no son de plataformas Apple, pero usarlo requiere un puente de tokens / autenticación independiente que Get Bananas no mantiene. 

  5. Código de producción en Banana List/Item.swift. El campo lastModified se añadió más tarde para la resolución de conflictos de sincronización con iCloud. 

  6. Código de producción en Banana List/iCloudBackupManager.swift. Las constantes viven en Banana List/Constants.swift

  7. Código de producción en Banana List/ShoppingListExport.swift. Decodificador personalizado con valores por defecto decodeIfPresent más filtro isValid en la importación. 

  8. Semánticas POSIX O_EXCL | O_CREAT; Node.js expone la misma atomicidad vía fs.writeFileSync(path, data, { flag: 'wx' }). Ver documentación de fs de Node.js

  9. Apple Developer, “Designing for CloudKit”. Sincronización basada en push, resolución de conflictos a nivel de registro, particionado por zonas. 

  10. Notas de depuración del autor. El incidente de bucle infinito produjo un BananaList.json de 4MB a partir de una lista de compras de 30 elementos en 3 minutos antes de que aterrizara la lógica del contador de sincronización. 

  11. Anthropic, “Model Context Protocol”. Protocolo abierto para uso de herramientas por LLM; multi-runtime (Claude Desktop, Cline, Goose, etc.). 

  12. Análisis del autor en App Intents Are Apple’s New API to Your App. La tesis de los dos contratos paralelos aplicada a través de superficies sistema-IA (Apple) y uso de herramientas entre LLM (Anthropic). 

  13. Apple Developer, framework “App Intents”. Superficie tipado-declarativa de uso de herramientas de Apple para Siri, Spotlight y Apple Intelligence. 

  14. Apple Developer, “FileManager url(forUbiquityContainerIdentifier:)”. El API soportado para resolver la URL del contenedor de iCloud Drive de una app. La ruta de macOS bajo ~/Library/Mobile Documents/ es el detalle de implementación del SO host de dónde iCloud Drive expone el contenedor; el API simbólico es lo que las apps deberían llamar. 

  15. Anthropic, “Desktop Extensions”. El formato .mcpb es un archivo zip que contiene manifest.json, el punto de entrada del servidor MCP, icono y metadatos. Instalación con un solo clic en Claude Desktop; ejecuta el servidor empaquetado como subproceso local sobre JSON-RPC por stdio. 

  16. Apple Developer, “NSFileCoordinator”. Coordina lecturas y escrituras a un archivo entre procesos que opten por el mismo protocolo de coordinación; requerido cuando el daemon bird de iCloud Drive, los observadores impulsados por NSMetadataQuery y la app misma pueden tocar todos la misma ruta. 

  17. POSIX rename(2) debe ser atómico cuando el origen y el destino están en el mismo sistema de archivos. El espejo local de iCloud Drive bajo ~/Library/Mobile Documents/ es un solo volumen APFS, así que fs.renameSync entre un archivo temporal hermano y la ruta canónica es atómico desde la perspectiva de cualquier lector. Ver especificación POSIX rename

Artículos relacionados

Única fuente de verdad: SwiftData, MCP, iCloud

Tres llamadores pueden escribir en la misma lista de compras: una persona, Apple Intelligence y un agente externo. La ve…

17 min de lectura

App Intents vs MCP: la cuestión del enrutamiento

Dos protocolos, una sola app. App Intents expone tu app a Apple Intelligence. MCP expone el mismo dominio a Claude, Chat…

15 min de lectura

Tu agente tiene un intermediario que no verificaste

Investigadores probaron 28 routers de API de LLM pagos. 17 tocaron credenciales canary de AWS. Uno drenó ETH de una clav…

14 min de lectura