Servidor MCP ao lado de um app iOS: dois ecossistemas de agentes, uma lista
Get Bananas, meu app de lista de compras em SwiftUI, roda em iOS, macOS, watchOS e visionOS.1 Também vive dentro do Claude Desktop como uma extensão MCP .mcpb expondo cinco ferramentas: get_shopping_list, add_item, remove_item, update_item, update_shopping_list.2
Quando você pede ao Claude “adicione bananas, leite e pão à minha lista”, o Claude chama add_item três vezes e, na próxima vez que eu abrir o app no meu telefone, os itens estão lá. Sem servidor. Sem conta. Sem chave de API. A ponte é um único arquivo JSON mais cinco camadas de prevenção de loop que tive de adicionar depois de lançar uma v1.0 que se escreveu para dentro de um arquivo de 4MB em três minutos.
A pergunta interessante é como. SwiftData é exclusivo do runtime das plataformas Apple e não é legível por um processo Node.js.3 O framework nativo CloudKit precisa do entitlement correspondente com.apple.developer.icloud-services e do identificador da equipe de Apple Developer; o subprocesso MCP do Claude Desktop não tem nenhum dos dois, então não consegue usar CKContainer como meu app assinado faz. CloudKit Web Services existe, mas usá-lo exigiria manter uma ponte separada de token / autenticação entre o processo desktop e os servidores da Apple.4 Então os caminhos óbvios estão fechados.
O caminho que escolhi é mais antigo e mais estranho. O app Get Bananas e seu servidor MCP compartilham estado por meio de um arquivo JSON no iCloud Drive. O app Swift mantém um modelo SwiftData para persistência dentro do app e exporta um arquivo BananaList.json para seu container do iCloud Drive através do NSFileCoordinator após cada mudança. O servidor MCP em Node.js lê e escreve no mesmo arquivo com um lock de arquivo exclusivo de 5 segundos, detecção de lock obsoleto e escritas atômicas via temp-file-rename. O iCloud Drive cuida da sincronização entre dispositivos. Hoje, o Claude Desktop lê e escreve a mesma fonte da verdade a partir do Mac; o adaptador App Intents para Apple Intelligence é a próxima superfície, contra o mesmo arquivo.
Este ensaio é sobre por que esse arranjo funciona, o que ele custa e onde ele falha.
TL;DR
- Get Bananas expõe sua lista de compras ao Claude Desktop por meio de um servidor MCP embarcado. O mesmo formato de arquivo dará suporte a um adaptador App Intents para Apple Intelligence em seguida.
- O substrato de integração é iCloud Drive mais um arquivo JSON, não CloudKit, não um servidor, não um service.
- App Swift: SwiftData
@Model ShoppingItempara velocidade dentro do app; exportação JSON via iCloud Drive para portabilidade. - Servidor MCP: 575 linhas de Node.js, lock de arquivo com detecção de lock obsoleto, roda dentro do bundle
.mcpbdo Claude Desktop. - Trade-off: a sincronização baseada em arquivo JSON é mais lenta que CloudKit e tem risco de conflito de merge, mas funciona em qualquer ecossistema de agente que consiga ler um arquivo.

A arquitetura do Model Context Protocol conforme documentada pela Anthropic. Um host MCP (Claude Desktop) conecta-se a um ou mais servidores MCP (get-bananas.mcpb neste artigo), cada um expondo ferramentas, recursos e prompts que o host pode invocar. Fonte: modelcontextprotocol.io.11
A arquitetura em uma 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 │
└─────────────────────────────────────────────────────────┘
Duas superfícies. Um arquivo. A ponte inteira é o arquivo.
O lado Swift: SwiftData para velocidade, JSON para portabilidade
No app, a lista de compras é um @Model do SwiftData. Código de produção real: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?
}
Essa é a verdade em memória. Cada digitação, cada toque em checkbox, cada mudança de seção escreve no SwiftData. SwiftData alimenta as views SwiftUI. O app parece nativo porque é nativo. O gatilho de backup é baseado em hash: um observador .onChange(of: computeItemsHash()) dispara apenas quando o id, nome, quantidade, seção, estado checked ou optional de um item muda, nunca em uma edição que não muda nada.
O truque é que SwiftData não é a verdade entre processos. Ele é o cache entre processos. Cada mudança aplica debounce de 500ms e então escreve um arquivo JSON no container do iCloud Drive do app através da API de escrita coordenada da 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 é a forma suportada de escrever um arquivo que outros processos (e o daemon do iCloud Drive) podem ler concorrentemente.16 Antes dessa escrita acontecer, o manager lê o arquivo existente e pula a escrita inteiramente se o JSON for byte por byte idêntico. Isso corta a churn redundante no iCloud Drive sempre que um observador de mudança do SwiftData dispara para uma edição que não muda nada. No restore, o manager tenta novamente até três vezes com backoff exponencial (1s, 2s, 4s, orçamento total de 7s), porque NSMetadataQuery reporta uma mudança de arquivo antes de o iCloud Drive ter de fato baixado os novos bytes.6
A forma Codable do arquivo é intencionalmente permissiva. ShoppingListExport decodifica com defaults para cada campo ausente e filtra itens com nomes vazios: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
}
}
}
O decoder defensivo é proposital. O que quer que escreva o JSON a seguir (um servidor MCP, um futuro shortcut, um colar manual) inevitavelmente vai esquecer um campo. O lado Swift absorve isso. O formato de arquivo compartilhado é o contrato; o decoder Swift é a parte tolerante.

O lado Node: um servidor MCP de 575 linhas que lê o mesmo arquivo
O servidor MCP vive em mcp-extension/server/index.js, distribuído como get-bananas.mcpb para o sistema de extensões do Claude Desktop. Ele abre o mesmo arquivo do iCloud Drive a partir do host macOS:2
const ICLOUD_FILE_PATH = path.join(
os.homedir(),
"Library/Mobile Documents/iCloud~com~941apps~Banana-List/Documents/BananaList.json"
);
Cinco ferramentas: uma leitura pura (get_shopping_list), três ferramentas read-modify-write em nível de item (add_item, remove_item, update_item) e uma ferramenta de substituição em massa (update_shopping_list) que escreve sem ler antes. O servidor MCP também expõe o arquivo como um Resource separado somente leitura para clientes que preferem a API de resource. Cada escrita passa por um lock de arquivo com recuperação de lock obsoleto:
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.");
}
O padrão de lock é mais antigo que o Node.js. fs.writeFileSync com a flag 'wx' é a versão multiplataforma de O_EXCL | O_CREAT. Se o arquivo de lock existe e tem mais de 5 segundos, o servidor assume que o detentor anterior travou e o reivindica. Se existe e está fresco, o servidor espera 50ms e tenta novamente. Após 5 segundos no total, ele desiste.8
O lock só sincroniza writers do lado Node entre si (uma segunda invocação MCP enquanto a primeira está no meio de uma escrita). Ele não coordena com o app Swift, que escreve via NSFileCoordinator e String.write(atomically:) e nunca toca em BananaList.json.lock. A sobreposição genuína entre Swift e Node é deixada para dois mecanismos mais fracos: o app Swift faz debounce de 500ms antes de escrever, escritas MCP acontecem apenas mediante prompt do usuário, e qualquer colisão residual cai na resolução last-write-wins do iCloud Drive em granularidade de arquivo.
As escritas em si usam o padrão temp-atômico-depois-rename, com uma checagem de parse JSON entre as etapas:
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 no mesmo sistema de arquivos é atômico segundo POSIX: um leitor em outro processo vê o arquivo antigo ou o arquivo novo, nunca bytes escritos pela metade.17 Fixar o caminho temp ao process.pid impede que duas instâncias do servidor MCP (raras, mas possíveis se o usuário reinstalar o Claude Desktop sem reiniciar) sobrescrevam os arquivos temp uma da outra. O JSON.parse no meio da escrita é uma etapa de paranoia: se a serialização em si produziu JSON inválido, a função aborta antes do rename, deixando o arquivo canônico intocado.
Por que iCloud Drive, não CloudKit
A escolha que faz a arquitetura funcionar é usar iCloud Drive (baseado em arquivo) em vez de CloudKit (baseado em registro) para a verdade entre processos. CloudKit é o que a Apple recomenda para sincronização app-a-app. Ele tem resolução de conflito de mais alto nível, push do lado do servidor e particionamento por zona.9 A API nativa CKContainer é exclusiva das plataformas Apple e protegida por entitlement, então um subprocesso do Claude Desktop não consegue usá-la como meu app assinado faz. A Apple publica CloudKit Web Services para clientes fora das plataformas Apple, mas usá-lo exigiria provisionar um token server-to-server, plumbá-lo no servidor MCP e manter uma ponte de auth separada: não impossível, mas uma quantidade substancial de infra para uma lista de compras.4
O servidor MCP roda sem sandbox no macOS como subprocesso do Claude. Ele não tem cadeia de assinatura Apple Developer, nenhum casamento de identificador de equipe com o container CloudKit do meu app e nenhum token CloudKit Web Services configurado.
O iCloud Drive, por outro lado, expõe-se como um local regular do sistema de arquivos. A API suportada pela Apple é FileManager.url(forUbiquityContainerIdentifier:) para o lado do app;14 no macOS, o local resolvido para Get Bananas é ~/Library/Mobile Documents/iCloud~com~941apps~Banana-List/Documents/BananaList.json. Esse caminho é um detalhe de implementação específico do macOS de onde o iCloud Drive expõe o container, mas para o Claude Desktop rodando no mesmo Mac, é apenas um arquivo. Qualquer processo com acesso de leitura ao diretório home do usuário pode lê-lo e escrevê-lo. Assim como um futuro shortcut, um futuro plugin SwiftBar, um futuro script llama.cpp que o usuário roda localmente. Qualquer coisa que consiga ler um arquivo pode integrar.
O custo é que a sincronização do iCloud Drive é mais lenta que CloudKit (segundos, não sub-segundo) e tem semântica de conflito mais fraca (last-write-wins em granularidade de arquivo, não merge em nível de registro). Para uma lista de compras com talvez 30 itens, nenhum dos custos importa. Para um app de alto volume de escrita com 10K linhas e editores concorrentes, ambos os custos dominariam.
Cinco camadas de prevenção de loop
A peça de código mais delicada do lado Swift é a prevenção de loop. Sem ela: o servidor MCP escreve o JSON, o iCloud Drive sincroniza para o iOS, o NSMetadataQuery do app iOS percebe a mudança, o app reimporta o JSON para o SwiftData, a importação dispara um observador de mudança do SwiftData, o observador de mudança dispara um backup com debounce, o backup com debounce escreve o JSON, o iCloud Drive sincroniza de volta. Eu lancei a versão ingênua na v1.0 e assisti uma lista de compras de 30 itens inflar para 4MB em três minutos durante os testes.10
A versão lançada usa cinco guardas empilhadas, não uma. Cada uma protege um caso 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
}
| Camada | Onde | O que ela captura |
|---|---|---|
| 1. Contador sync > 0 | Caminho de escrita de saída | Escritas re-entrantes disparadas enquanto uma sync-do-iCloud está ativamente em andamento |
| 2. Janela de 5s pós-sync | Caminho de escrita de saída | Callbacks onChange atrasados de @Model que o SwiftData dispara depois da importação ter assentado |
| 3. Janela de 2s pós-backup | Handler de entrada NSMetadataQuery |
Eventos do sistema de arquivos local disparados logo após a própria escrita do app |
| 4. Casamento exato de mod-date | Handler de entrada | iCloud Drive ecoando nosso próprio backup de volta para nós entre dispositivos |
| 5. Mod-date monotônico | Handler de entrada | NSMetadataQuery disparando tanto DidUpdate quanto DidFinishGathering para uma única mudança |
As duas primeiras camadas regulam o caminho de escrita de saída: devo exportar para o iCloud agora? As três restantes regulam o handler de entrada NSMetadataQuery: devo importar essa mudança para o SwiftData? Qualquer lado sozinho é insuficiente. Um único round-trip de sincronização pode passar pelas guardas em ambos os lados dependendo de qual evento dispara primeiro, então cada caminho precisa das próprias defesas.
A lição se generaliza para qualquer arquitetura de “arquivo compartilhado entre dois writers”: detecção de mudança baseada em mod-time é necessária mas não suficiente. Você precisa de uma identidade estável para “escritas que eu causei” que sobreviva a pelo menos um round trip pela camada de sync. A coisa mais próxima que o iCloud Drive te dá é a data de modificação do arquivo no momento em que você o escreveu. Segure-a. Compare na volta.
O que eu construiria de outra forma
Quatro lições de ter lançado isto.
O Codable defensivo do lado Swift se paga. O servidor MCP foi reescrito três vezes. Cada reescrita esqueceu de definir um campo pelo menos uma vez. O decoder Swift absorveu cada variante e o app nunca crashou. Se eu estivesse começando do zero, empurraria mais campos para “decode com default” em vez de “obrigatório”. O contrato entre os dois writers é frágil por design.7
O timeout do lock deveria ser ciente do conteúdo, não apenas do mtime. Cinco segundos é pouco. Se o Mac do usuário está em Wi-Fi lento ou o dispositivo iOS está restaurando após um background longo, uma sincronização do BananaList.json.lock pelo iCloud Drive pode levar mais de 5 segundos para propagar. O servidor MCP então vê um lock obsoleto que na verdade ainda está sendo mantido. A correção é condicionar a checagem de lock obsoleto ao PID escrito dentro do arquivo de lock: se kill(pid, 0) reportar um processo ainda em execução, não quebre o lock por mais velho que o mtime pareça. O código atual escreve o PID mas nunca o lê de volta.
A ferramenta update_shopping_list foi um erro. Ela substitui a lista inteira. O Claude Desktop ocasionalmente a chama quando uma operação de item único bastaria, e então um pedaço não-trivial da lista do usuário desaparece. Eu deveria ter lançado apenas as quatro ferramentas em nível de item (get, add, remove, update) e forçado o Claude a compô-las. A anotação destructiveHint: true do protocolo MCP de fato sinaliza a ferramenta como destrutiva,11 mas o Claude nem sempre apresenta isso ao usuário antes de chamar. A ferramenta de substituição em massa é conveniente para o LLM e perigosa para o usuário. A presença de uma proteção na camada de protocolo não substitui não lançar a arma no pé.
A exportação JSON compartilhada precisa de um campo de versão. ShoppingListExport decodifica com defaults permissivos, o que funciona até o dia em que eu renomear um campo em vez de adicionar um. Um schemaVersion: 1 no topo do JSON permitiria a qualquer lado detectar uma futura mudança incompatível e recusar a leitura em vez de produzir silenciosamente um modelo malformado. Migrações ainda seriam manuais, mas pelo menos o modo de falha seria barulhento em vez de perda silenciosa de dados.
Quando não usar este padrão
Recusa é parte do design.
Se os dados são regulados (saúde, financeiro, qualquer coisa com política de retenção de compliance), o sistema de arquivos controlado pelo usuário do iCloud Drive é o substrato errado. CloudKit tem logging e trilhas de auditoria; arquivos JSON legíveis pelo usuário não.
Se o orçamento de latência entre processos é sub-segundo, o iCloud Drive não vai atingi-lo. Nos meus testes, a sincronização do iCloud Drive geralmente leva segundos em vez de sub-segundo em uma conexão saudável; a Apple não publica um SLA mais apertado, e redes lentas tornam mais demorado. A entrega baseada em push do CloudKit é materialmente mais rápida para atualizações em nível de registro. Um produto de colaboração em tempo real precisa de CloudKit (ou de um servidor de sync dedicado).
Se o schema está evoluindo rápido, o padrão Codable-com-defaults acumula dívida. Cada novo campo exige uma decisão de “default para arquivos antigos” que envelhece rápido. Sincronização via arquivo JSON é melhor para schemas estáveis com mudança majoritariamente aditiva.
O que isso significa para apps que querem ser alcançáveis por múltiplos ecossistemas de agentes
O padrão é simples o suficiente para repetir. Três peças:
- Um
@ModelSwiftData para persistência dentro do app. Alimenta a UI, rápido, nativo. - Uma exportação Codable JSON escrita no iCloud Drive em mudança com debounce. Decoder defensivo. Schema estável. O arquivo compartilhado é o contrato.
- Um pequeno adaptador para cada ecossistema de agente que lê e escreve o mesmo arquivo com um lock de arquivo. Node.js para o Claude Desktop. Um futuro App Intent + AppEntity para Apple Intelligence. Um futuro shell script para o que vier a seguir.
O padrão é portátil porque o substrato de integração é o sistema de arquivos. Cada runtime de agente que existe hoje (Claude Desktop, Cursor, Goose, Cline) e a maioria que vai chegar no próximo ano consegue ler um arquivo.11 CloudKit não consegue. Engines de sync nativas não conseguem. O menor denominador comum vence quando o objetivo é alcance entre ecossistemas de LLM.
A Anthropic e a Apple discordam sobre como um agente deveria parecer. App Intents dizem que é uma declaração tipada em Swift que a Apple Intelligence resolve no dispositivo. MCP diz que é um servidor JSON-RPC com uma lista de ferramentas que qualquer LLM pode chamar. Ambos estão corretos em seus próprios ecossistemas. Get Bananas trata nenhum dos dois como fonte da verdade e deixa o sistema de arquivos mediar.12
Da próxima vez que eu lançar um app que queira duas superfícies de agente, vou começar pelo formato de arquivo antes do modelo de entidade.
FAQ
O que é .mcpb e como funciona?
Um .mcpb é o formato de bundle de extensão MCP da Anthropic para o Claude Desktop. É um arquivo zip contendo um manifest.json descrevendo as ferramentas, o entry point do servidor MCP (Node.js, Python, etc.), um ícone e metadata. O Claude Desktop o instala como uma extensão de browser via clique único e roda o servidor como um subprocesso local. O servidor MCP fala JSON-RPC sobre stdio.1115 O Get Bananas lança seu servidor empacotado dessa forma.
Por que não usar a nova ponte App Intents-para-MCP?
Não existe uma. App Intents (framework da Apple) e MCP (protocolo da Anthropic) são independentes. A Apple Intelligence chama App Intents através do seu próprio resolver. O Claude Desktop chama servidores MCP através do seu próprio runtime. Um app que quer ambas as superfícies lança ambas; não há ponte automática.1213
Você poderia fazer isso sem iCloud Drive?
Sim, com ressalvas. Qualquer local de arquivo compartilhado e gravável funciona: uma pasta em ~/Documents, um network share, um sistema de arquivos FUSE montado em S3. O iCloud Drive é conveniente porque já está em todo Mac que roda Claude Desktop e em todo dispositivo iOS que o usuário possui. Um arquivo não-iCloud forçaria o usuário a configurar a sync separadamente.
O que acontece quando há conflito de escrita?
O lock de arquivo de 5 segundos mais retries de 50ms cuidam de writers concorrentes do lado MCP (por exemplo, uma segunda invocação MCP chegando enquanto a primeira está no meio de uma escrita). Ele não coordena com o app Swift, que escreve através do seu próprio coordinator. Quando Swift e Node se sobrepõem genuinamente (raro, dado o debounce de 500ms do Swift e que escritas MCP só disparam em prompts do usuário), o iCloud Drive resolve em granularidade de arquivo: last write wins. O filtro isValid do decoder Swift então descarta qualquer coisa malformada.
Por que não CRDTs ou operational transform?
Exagero para listas de compras de 30 itens. CRDTs são a escolha certa quando edições concorrentes sobrepostas são comuns e você precisa de semântica determinística de merge (editores de documentos colaborativos, jogos multi-usuário). Para uma lista de compras onde uma pessoa adiciona itens via Claude e outra os marca como concluídos via app iOS no caminho para o mercado, last-write-wins-com-debounce está correto.
Dois ecossistemas de agentes, uma lista de compras. A ponte é o iCloud Drive mais um arquivo JSON com um decoder tolerante, e isso é suficiente. O menor denominador comum não é uma limitação. É a única coisa em que ambos os ecossistemas concordam.
Referências
-
Get Bananas do autor, um app de lista de compras em SwiftUI + SwiftData para iOS, macOS, watchOS e visionOS, publicado pela 941 Apps. ↩
-
O Get Bananas lança um servidor MCP (Model Context Protocol) empacotado como
get-bananas.mcpbpara o Claude Desktop. Ferramentas expostas:get_shopping_list,add_item,remove_item,update_item,update_shopping_list. O servidor tem 575 linhas de Node.js emmcp-extension/server/index.js. ↩↩ -
Apple Developer, framework “SwiftData”. Disponível em iOS 17+, macOS 14+, watchOS 10+, visionOS 1+. Apenas runtime; sem bindings server-side ou entre processos. ↩
-
Apple Developer, framework “CloudKit”. A API nativa
CKContainerrequer o entitlementcom.apple.developer.icloud-servicese o identificador da equipe Apple Developer correspondente. A Apple também publica CloudKit Web Services para clientes fora das plataformas Apple, mas usá-lo exige uma ponte separada de token / autenticação que o Get Bananas não mantém. ↩↩ -
Código de produção em
Banana List/Item.swift. O campolastModifiedfoi adicionado depois para resolução de conflito de sincronização do iCloud. ↩ -
Código de produção em
Banana List/iCloudBackupManager.swift. As constantes vivem emBanana List/Constants.swift. ↩↩ -
Código de produção em
Banana List/ShoppingListExport.swift. Decoder customizado com defaultsdecodeIfPresentmais filtroisValidna importação. ↩↩ -
Semântica POSIX
O_EXCL | O_CREAT; o Node.js expõe a mesma atomicidade viafs.writeFileSync(path, data, { flag: 'wx' }). Veja documentação fs do Node.js. ↩ -
Apple Developer, “Designing for CloudKit”. Sync baseada em push, resolução de conflito em nível de registro, particionamento por zona. ↩
-
Notas de debugging do autor. O incidente de loop infinito produziu um
BananaList.jsonde 4MB a partir de uma lista de compras de 30 itens em 3 minutos antes da lógica de contador de sync entrar. ↩ -
Anthropic, “Model Context Protocol”. Protocolo aberto para uso de ferramentas por LLM; multi-runtime (Claude Desktop, Cline, Goose, etc.). ↩↩↩↩
-
Análise do autor em App Intents Are Apple’s New API to Your App. A tese de contratos paralelos aplicada entre superfícies de IA do sistema (Apple) e uso de ferramentas entre LLMs (Anthropic). ↩↩
-
Apple Developer, framework “App Intents”. Superfície de uso de ferramentas tipada e declarativa da Apple para Siri, Spotlight e Apple Intelligence. ↩
-
Apple Developer, “FileManager url(forUbiquityContainerIdentifier:)”. A API suportada para resolver a URL do container do iCloud Drive de um app. O caminho macOS sob
~/Library/Mobile Documents/é o detalhe de implementação do host-OS de onde o iCloud Drive expõe o container; a API simbólica é o que os apps deveriam chamar. ↩ -
Anthropic, “Desktop Extensions”. O formato
.mcpbé um arquivo zip contendomanifest.json, entry point do servidor MCP, ícone e metadata. Instalação por clique único no Claude Desktop; roda o servidor empacotado como subprocesso local sobre JSON-RPC via stdio. ↩ -
Apple Developer, “NSFileCoordinator”. Coordena leituras e escritas em um arquivo entre processos que optam pelo mesmo protocolo de coordenação; necessário quando o daemon
birddo iCloud Drive, observadores baseados emNSMetadataQuerye o próprio app podem todos tocar no mesmo caminho. ↩ -
O
rename(2)POSIX é obrigado a ser atômico quando origem e destino estão no mesmo sistema de arquivos. O espelho local do iCloud Drive sob~/Library/Mobile Documents/é um único volume APFS, entãofs.renameSyncentre um arquivo temp irmão e o caminho canônico é atômico do ponto de vista de qualquer leitor. Veja especificação POSIX rename. ↩