Serwer MCP obok aplikacji iOS: dwa ekosystemy agentów, jedna lista
Get Bananas, moja aplikacja SwiftUI z listą zakupów, działa na iOS, macOS, watchOS i visionOS.1 Żyje również wewnątrz Claude Desktop jako rozszerzenie MCP .mcpb udostępniające pięć narzędzi: get_shopping_list, add_item, remove_item, update_item, update_shopping_list.2
Gdy poprosi Pan/Pani Claude „dodaj banany, mleko i chleb do mojej listy”, Claude wywołuje add_item trzy razy, a następnym razem, gdy otworzę aplikację na telefonie, pozycje tam są. Bez serwera. Bez konta. Bez klucza API. Mostem jest pojedynczy plik JSON plus pięć warstw zapobiegania pętlom, które musiałem dodać po wypuszczeniu wersji v1.0, która sama zapisała się w pliku o rozmiarze 4 MB w trzy minuty.
Interesujące pytanie brzmi: jak. SwiftData jest dostępny tylko w środowisku uruchomieniowym platform Apple i nie da się go odczytać z procesu Node.js.3 Natywny framework CloudKit wymaga pasującego uprawnienia com.apple.developer.icloud-services oraz identyfikatora zespołu Apple Developer; podproces MCP w Claude Desktop nie posiada żadnego z nich, więc nie może używać CKContainer tak, jak robi to moja podpisana aplikacja. Istnieje CloudKit Web Services, ale jego użycie wymagałoby utrzymywania osobnego mostu token/auth między procesem desktopowym a serwerami Apple.4 Tak więc oczywiste ścieżki są zamknięte.
Ścieżka, którą wybrałem, jest starsza i dziwniejsza. Aplikacja Get Bananas i jej serwer MCP współdzielą stan przez plik JSON w iCloud Drive. Aplikacja Swift utrzymuje model SwiftData do trwałego przechowywania w aplikacji i eksportuje plik BananaList.json do swojego kontenera iCloud Drive za pośrednictwem NSFileCoordinator po każdej zmianie. Serwer MCP w Node.js odczytuje i zapisuje ten sam plik z 5-sekundową wyłączną blokadą pliku, wykrywaniem przestarzałych blokad i atomowymi zapisami z przemianowaniem pliku tymczasowego. iCloud Drive obsługuje synchronizację międzyurządzeniową. Dziś Claude Desktop odczytuje i zapisuje to samo źródło prawdy z Maca; adapter App Intents dla Apple Intelligence będzie kolejną powierzchnią, działającą na tym samym pliku.
Ten esej jest o tym, dlaczego ten układ działa, ile kosztuje i gdzie zawodzi.
TL;DR
- Get Bananas udostępnia swoją listę zakupów w Claude Desktop poprzez dostarczany serwer MCP. Ten sam format pliku obsłuży kolejny adapter App Intents dla Apple Intelligence.
- Substratem integracji jest iCloud Drive plus plik JSON, nie CloudKit, nie serwer, nie usługa.
- Aplikacja Swift: SwiftData
@Model ShoppingItemdla szybkości w aplikacji; eksport JSON do iCloud Drive dla przenośności. - Serwer MCP: 575 linii Node.js, blokada pliku z wykrywaniem przestarzałych blokad, działa wewnątrz pakietu
.mcpbClaude Desktop. - Kompromis: synchronizacja oparta na pliku JSON jest wolniejsza niż CloudKit i niesie ryzyko konfliktów scalania, ale działa w dowolnym ekosystemie agenta, który potrafi odczytać plik.

Architektura Model Context Protocol udokumentowana przez Anthropic. Host MCP (Claude Desktop) łączy się z jednym lub większą liczbą serwerów MCP (get-bananas.mcpb w tym artykule), z których każdy udostępnia narzędzia, zasoby i prompty, jakie host może wywołać. Źródło: modelcontextprotocol.io.11
Architektura na jednej stronie
┌─────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────┘
Dwie powierzchnie. Jeden plik. Cały most to ten plik.
Strona Swift: SwiftData dla szybkości, JSON dla przenośności
W aplikacji lista zakupów jest modelem SwiftData @Model. Prawdziwy kod produkcyjny: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?
}
To jest prawda w pamięci. Każde uderzenie w klawisz, każde stuknięcie w pole wyboru, każda zmiana sekcji jest zapisywana do SwiftData. SwiftData napędza widoki SwiftUI. Aplikacja sprawia natywne wrażenie, ponieważ jest natywna. Wyzwalacz kopii zapasowej oparty jest na haszu: obserwator .onChange(of: computeItemsHash()) uruchamia się tylko wtedy, gdy zmienia się id, nazwa, ilość, sekcja, stan zaznaczenia lub opcjonalność pozycji, nigdy przy czystej edycji bez zmian.
Sztuczka polega na tym, że SwiftData nie jest prawdą międzyprocesową. Jest pamięcią podręczną międzyprocesową. Każda zmiana jest opóźniona o 500 ms, a następnie zapisuje plik JSON do kontenera iCloud Drive aplikacji za pomocą skoordynowanego zapisu API 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 to wspierany sposób zapisu pliku, który inne procesy (oraz demon iCloud Drive) mogą odczytywać współbieżnie.16 Zanim ten zapis nastąpi, menedżer odczytuje istniejący plik i całkowicie pomija zapis, jeśli JSON jest bajt po bajcie taki sam. Eliminuje to nadmiarowy ruch w iCloud Drive za każdym razem, gdy obserwator zmian SwiftData uruchamia się dla edycji bez zmian. Przy restore menedżer ponawia próbę do trzech razy z wykładniczym wycofywaniem (1 s, 2 s, 4 s, łącznie do 7 s), ponieważ NSMetadataQuery zgłasza zmianę pliku, zanim iCloud Drive faktycznie pobierze nowe bajty.6
Kształt Codable pliku jest celowo permisywny. ShoppingListExport dekoduje z domyślnymi wartościami dla każdego brakującego pola i odfiltrowuje pozycje z pustymi nazwami: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
}
}
}
Defensywny dekoder jest zamierzony. Cokolwiek zapisze następne JSON (serwer MCP, przyszły skrót, ręczne wklejenie), nieuchronnie zapomni o jakimś polu. Strona Swift to absorbuje. Wspólny format pliku jest kontraktem; dekoder Swift jest stroną wybaczającą.

Strona Node: 575-liniowy serwer MCP, który odczytuje ten sam plik
Serwer MCP znajduje się w mcp-extension/server/index.js, dystrybuowany jako get-bananas.mcpb dla systemu rozszerzeń Claude Desktop. Otwiera ten sam plik iCloud Drive z hosta macOS:2
const ICLOUD_FILE_PATH = path.join(
os.homedir(),
"Library/Mobile Documents/iCloud~com~941apps~Banana-List/Documents/BananaList.json"
);
Pięć narzędzi: jedno czystego odczytu (get_shopping_list), trzy narzędzia odczyt-modyfikacja-zapis na poziomie pozycji (add_item, remove_item, update_item) i jedno narzędzie zbiorczej wymiany (update_shopping_list), które zapisuje bez wcześniejszego odczytu. Serwer MCP udostępnia również plik jako oddzielny Resource tylko do odczytu dla klientów preferujących API zasobu. Każdy zapis przechodzi przez blokadę pliku z odzyskiwaniem przestarzałych blokad:
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.");
}
Wzorzec blokady jest starszy niż Node.js. fs.writeFileSync z flagą 'wx' to wieloplatformowa wersja O_EXCL | O_CREAT. Jeśli plik blokady istnieje i jest starszy niż 5 sekund, serwer zakłada, że poprzedni właściciel uległ awarii, i odzyskuje blokadę. Jeśli istnieje i jest świeży, serwer czeka 50 ms i ponawia próbę. Po łącznie 5 sekundach poddaje się.8
Blokada synchronizuje wyłącznie zapisujących po stronie Node między sobą (drugie wywołanie MCP, gdy pierwsze jest w trakcie zapisu). Nie koordynuje się z aplikacją Swift, która zapisuje przez NSFileCoordinator i String.write(atomically:) i nigdy nie dotyka BananaList.json.lock. Rzeczywiste nakładanie się Swift/Node jest pozostawione dwóm słabszym mechanizmom: aplikacja Swift opóźnia zapis o 500 ms, zapisy MCP następują tylko na żądanie użytkownika, a wszelkie pozostałe kolizje rozstrzyga zasada „ostatni zapis wygrywa” w iCloud Drive, na poziomie granularności pliku.
Same zapisy używają wzorca atomowy plik tymczasowy, potem zmiana nazwy, ze sprawdzeniem parsowania JSON pomiędzy:
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 w obrębie tego samego systemu plików jest atomowa zgodnie z POSIX: czytelnik z innego procesu widzi albo stary plik, albo nowy plik, nigdy bajtów zapisanych w połowie.17 Przypięcie ścieżki tymczasowej do process.pid zapobiega temu, by dwie instancje serwera MCP (rzadkie, ale możliwe, jeśli użytkownik przeinstaluje Claude Desktop bez restartu) nadpisywały sobie nawzajem pliki tymczasowe. Sprawdzenie JSON.parse w trakcie zapisu jest krokiem ostrożnościowym: jeśli sama serializacja wytworzyła nieprawidłowy JSON, funkcja przerywa działanie przed zmianą nazwy, pozostawiając kanoniczny plik nietknięty.
Dlaczego iCloud Drive, a nie CloudKit
Wybór, który sprawia, że architektura działa, to użycie iCloud Drive (oparte na pliku) zamiast CloudKit (oparte na rekordach) jako prawdy międzyprocesowej. CloudKit jest tym, co Apple zaleca dla synchronizacji między aplikacjami. Posiada wyższego poziomu rozwiązywanie konfliktów, push po stronie serwera i partycjonowanie oparte na strefach.9 Natywne API CKContainer jest dostępne tylko na platformach Apple i wymaga uprawnień, więc podproces Claude Desktop nie może go używać tak, jak robi to moja podpisana aplikacja. Apple publikuje CloudKit Web Services dla klientów spoza platform Apple, ale jego użycie wymagałoby udostępnienia tokenu serwer-do-serwera, podpięcia go do serwera MCP i utrzymywania osobnego mostu uwierzytelniania: nie niemożliwe, ale spora ilość infrastruktury jak na listę zakupów.4
Serwer MCP działa bez sandboxu na macOS jako podproces Claude. Nie ma łańcucha podpisu Apple Developer, dopasowania identyfikatora zespołu z kontenerem CloudKit mojej aplikacji ani skonfigurowanego tokenu CloudKit Web Services.
iCloud Drive natomiast udostępnia się jako zwykła lokalizacja systemu plików. Wspierane API Apple to FileManager.url(forUbiquityContainerIdentifier:) po stronie aplikacji;14 na macOS rozwiązana lokalizacja dla Get Bananas to ~/Library/Mobile Documents/iCloud~com~941apps~Banana-List/Documents/BananaList.json. Ta ścieżka jest specyficznym dla macOS detalem implementacyjnym tego, gdzie iCloud Drive udostępnia kontener, ale dla Claude Desktop działającego na tym samym Macu jest to po prostu plik. Każdy proces z prawami odczytu katalogu domowego użytkownika może go odczytać i zapisać. Tak samo przyszły skrót, przyszła wtyczka SwiftBar, przyszły skrypt llama.cpp, który użytkownik uruchamia lokalnie. Cokolwiek potrafi odczytać plik, może się zintegrować.
Kosztem jest to, że synchronizacja iCloud Drive jest wolniejsza niż CloudKit (sekundy, nie podsekundy) i ma słabszą semantykę konfliktów (ostatni zapis wygrywa na poziomie pliku, a nie scalanie na poziomie rekordu). Dla listy zakupów z może 30 pozycjami żaden z tych kosztów nie ma znaczenia. Dla aplikacji o dużej liczbie zapisów z 10 tys. wierszy i równoczesnymi edytorami oba koszty byłyby dominujące.
Pięć warstw zapobiegania pętlom
Najtrudniejszym fragmentem kodu po stronie Swift jest zapobieganie pętlom. Bez niego: serwer MCP zapisuje JSON, iCloud Drive synchronizuje go do iOS, NSMetadataQuery aplikacji iOS zauważa zmianę, aplikacja ponownie importuje JSON do SwiftData, import wyzwala obserwatora zmian SwiftData, obserwator zmian uruchamia opóźnioną kopię zapasową, opóźniona kopia zapasowa zapisuje JSON, iCloud Drive synchronizuje go z powrotem. Wypuściłem naiwną wersję w v1.0 i obserwowałem, jak lista zakupów z 30 pozycjami rozrasta się do 4 MB w ciągu trzech minut podczas testów.10
Wypuszczona wersja używa pięciu ułożonych warstwami zabezpieczeń, a nie jednego. Każde zabezpieczenie strzeże innego krawędziowego przypadku czasowego:
// 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
}
| Warstwa | Gdzie | Co wychwytuje |
|---|---|---|
| 1. Licznik synchronizacji > 0 | Ścieżka zapisu wychodzącego | Reentrancyjne zapisy wyzwalane, gdy synchronizacja z iCloud jest aktywnie w toku |
| 2. Okno 5 s po synchronizacji | Ścieżka zapisu wychodzącego | Opóźnione wywołania zwrotne onChange w @Model, które SwiftData uruchamia po tym, jak import się ustabilizował |
| 3. Okno 2 s po kopii zapasowej | Obsługa przychodząca NSMetadataQuery |
Lokalne zdarzenia systemu plików wyzwalane zaraz po własnym zapisie aplikacji |
| 4. Dokładne dopasowanie daty modyfikacji | Obsługa przychodząca | iCloud Drive odbijający naszą własną kopię zapasową z powrotem do nas między urządzeniami |
| 5. Monotoniczna data modyfikacji | Obsługa przychodząca | NSMetadataQuery wyzwalający zarówno DidUpdate, jak i DidFinishGathering dla pojedynczej zmiany |
Pierwsze dwie warstwy bramkują ścieżkę zapisu wychodzącego: czy powinienem teraz wyeksportować do iCloud? Pozostałe trzy bramkują obsługę przychodzącą NSMetadataQuery: czy powinienem zaimportować tę zmianę do SwiftData? Każda strona z osobna jest niewystarczająca. Pojedyncze sprzężenie zwrotne synchronizacji może przejść przez zabezpieczenia po obu stronach w zależności od tego, które zdarzenie zostanie uruchomione jako pierwsze, więc każda ścieżka potrzebuje własnej obrony.
Lekcja uogólnia się na każdą architekturę „wspólny plik między dwoma piszącymi”: wykrywanie zmian oparte na czasie modyfikacji jest konieczne, ale niewystarczające. Potrzebna jest stabilna tożsamość dla „zapisów, które ja spowodowałem”, która przetrwa co najmniej jedno sprzężenie zwrotne przez warstwę synchronizacji. Najbliższą rzeczą, którą iCloud Drive daje, jest data modyfikacji pliku w momencie zapisu. Trzeba ją zachować. Porównać w drodze powrotnej.
Co zbudowałbym inaczej
Cztery lekcje z wypuszczenia tego rozwiązania.
Defensywny Codable po stronie Swift zarabia na siebie. Serwer MCP był przepisywany trzy razy. Każde przepisanie co najmniej raz zapomniało ustawić jakieś pole. Dekoder Swift wchłaniał każdy wariant i aplikacja nigdy się nie zawiesiła. Gdybym zaczynał od nowa, więcej pól umieściłbym w „dekoduj z domyślną wartością” niż w „wymagane”. Kontrakt między dwoma piszącymi jest z założenia kruchy.7
Limit czasu blokady powinien być świadomy zawartości, nie tylko mtime. Pięć sekund to mało. Jeśli Mac użytkownika jest na wolnym Wi-Fi lub urządzenie iOS wraca po długim okresie w tle, synchronizacja BananaList.json.lock przez iCloud Drive może propagować się dłużej niż 5 sekund. Serwer MCP widzi wtedy przestarzałą blokadę, która faktycznie nadal jest trzymana. Naprawą jest uzależnienie sprawdzenia przestarzałej blokady od PID zapisanego wewnątrz pliku blokady: jeśli kill(pid, 0) raportuje wciąż działający proces, nie należy łamać blokady, niezależnie od tego, jak stary wygląda mtime. Aktualny kod zapisuje PID, ale nigdy go nie odczytuje.
Narzędzie update_shopping_list było pomyłką. Zastępuje całą listę. Claude Desktop sporadycznie wywołuje je tam, gdzie wystarczyłaby operacja na pojedynczej pozycji, a wtedy znika nietrywialny fragment listy użytkownika. Powinienem był wypuścić tylko cztery narzędzia na poziomie pozycji (get, add, remove, update) i zmusić Claude do ich składania. Adnotacja destructiveHint: true w protokole MCP wprawdzie oznacza narzędzie jako destrukcyjne,11 ale Claude nie zawsze pokazuje to użytkownikowi przed wywołaniem. Narzędzie zbiorczej wymiany jest wygodne dla LLM i niebezpieczne dla użytkownika. Obecność zabezpieczenia na poziomie protokołu nie zastępuje niewypuszczania broni przeciwko sobie.
Wspólny eksport JSON potrzebuje pola wersji. ShoppingListExport dekoduje z permisywnymi domyślnymi wartościami, co działa do dnia, w którym zmienię nazwę pola, zamiast je dodać. Pole schemaVersion: 1 na początku JSON pozwoliłoby obu stronom wykryć przyszłą zmianę łamiącą i odmówić odczytu zamiast cicho produkować zniekształcony model. Migracje nadal byłyby ręczne, ale przynajmniej tryb awarii byłby głośny, a nie cichą utratą danych.
Kiedy nie używać tego wzorca
Odmowa jest częścią projektu.
Jeśli dane są regulowane (zdrowie, finanse, cokolwiek z polityką retencji zgodności), system plików kontrolowany przez użytkownika w iCloud Drive jest złym substratem. CloudKit ma logowanie i ścieżki audytu; pliki JSON czytelne dla użytkownika nie mają.
Jeśli budżet opóźnień międzyprocesowych jest podsekundowy, iCloud Drive go nie spełni. W moich testach synchronizacja iCloud Drive zwykle zajmuje sekundy, a nie poniżej sekundy przy zdrowym połączeniu; Apple nie publikuje ściślejszego SLA, a wolne sieci wydłużają to jeszcze bardziej. Dostarczanie push CloudKit jest istotnie szybsze dla aktualizacji na poziomie rekordu. Produkt do współpracy w czasie rzeczywistym potrzebuje CloudKit (lub dedykowanego serwera synchronizacji).
Jeśli schemat ewoluuje szybko, wzorzec Codable z domyślnymi wartościami narasta dług. Każde nowe pole wymaga decyzji „domyślna wartość dla starych plików”, która szybko się starzeje. Synchronizacja plików JSON jest najlepsza dla stabilnych schematów ze zmianami głównie addytywnymi.
Co to oznacza dla aplikacji, które chcą być osiągalne dla wielu ekosystemów agentów
Wzorzec jest na tyle prosty, że można go powtórzyć. Trzy elementy:
- SwiftData
@Modeldla trwałości w aplikacji. Napędza UI, szybki, natywny. - Eksport Codable JSON zapisywany do iCloud Drive na opóźnioną zmianę. Defensywny dekoder. Stabilny schemat. Wspólny plik to kontrakt.
- Mały adapter dla każdego ekosystemu agenta, który odczytuje i zapisuje ten sam plik z blokadą pliku. Node.js dla Claude Desktop. Przyszły App Intent + AppEntity dla Apple Intelligence. Przyszły skrypt powłoki dla czegokolwiek, co pojawi się następne.
Wzorzec jest przenośny, ponieważ substratem integracji jest system plików. Każde środowisko uruchomieniowe agenta, które istnieje dziś (Claude Desktop, Cursor, Goose, Cline) i większość, które pojawią się w przyszłym roku, potrafi odczytać plik.11 CloudKit nie potrafi. Natywne silniki synchronizacji nie potrafią. Najmniejszy wspólny mianownik wygrywa, gdy celem jest zasięg w ekosystemach LLM.
Anthropic i Apple nie zgadzają się co do tego, jak powinien wyglądać agent. App Intents mówią, że jest to typowana deklaracja Swift, którą Apple Intelligence rozwiązuje na urządzeniu. MCP mówi, że jest to serwer JSON-RPC z listą narzędzi, które dowolny LLM może wywołać. Oba są poprawne we własnych ekosystemach. Get Bananas nie traktuje żadnego z nich jako źródła prawdy i pozwala, by mediował system plików.12
Następnym razem, gdy będę wypuszczał aplikację, która chce mieć dwie powierzchnie agenta, zacznę od formatu pliku, zanim zajmę się modelem encji.
FAQ
Czym jest .mcpb i jak działa?
.mcpb to format pakietu rozszerzeń MCP firmy Anthropic dla Claude Desktop. Jest to archiwum zip zawierające manifest.json opisujący narzędzia, punkt wejścia serwera MCP (Node.js, Python itp.), ikonę i metadane. Claude Desktop instaluje go jak rozszerzenie przeglądarki za pomocą jednego kliknięcia i uruchamia serwer jako lokalny podproces. Serwer MCP komunikuje się przez JSON-RPC po stdio.1115 Get Bananas dostarcza swój serwer w ten sposób.
Dlaczego nie użyć nowego mostu App Intents-do-MCP?
Taki nie istnieje. App Intents (framework Apple) i MCP (protokół Anthropic) są niezależne. Apple Intelligence wywołuje App Intents przez własny resolver. Claude Desktop wywołuje serwery MCP przez własne środowisko uruchomieniowe. Aplikacja, która chce obu powierzchni, dostarcza obie; nie ma automatycznego mostu.1213
Czy można to zrobić bez iCloud Drive?
Tak, z zastrzeżeniami. Każda wspólna lokalizacja pliku z możliwością zapisu działa: folder w ~/Documents, udział sieciowy, system plików FUSE zamontowany na S3. iCloud Drive jest wygodny, ponieważ jest już na każdym Macu, na którym działa Claude Desktop, i na każdym urządzeniu iOS, które posiada użytkownik. Plik nie-iCloud zmusiłby użytkownika do skonfigurowania synchronizacji oddzielnie.
Co się dzieje, gdy występuje konflikt zapisu?
5-sekundowa blokada pliku plus 50 ms ponownych prób obsługuje współbieżnych zapisujących po stronie MCP (np. drugie wywołanie MCP przybywające, gdy pierwsze jest w trakcie zapisu). Nie koordynuje się z aplikacją Swift, która zapisuje przez własnego koordynatora. Gdy Swift i Node faktycznie się nakładają (rzadko, biorąc pod uwagę 500 ms opóźnienie Swift i to, że zapisy MCP uruchamiają się tylko na żądanie użytkownika), iCloud Drive rozstrzyga na poziomie granularności pliku: ostatni zapis wygrywa. Filtr isValid dekodera Swift następnie odrzuca cokolwiek zniekształconego.
Dlaczego nie CRDT lub transformacja operacyjna?
Przesada dla 30-pozycyjnych list zakupów. CRDT są właściwym wyborem, gdy nakładające się współbieżne edycje są częste i potrzebna jest deterministyczna semantyka scalania (edytory dokumentów do współpracy, gry wieloosobowe). Dla listy zakupów, gdzie jedna osoba dodaje pozycje przez Claude, a druga odhacza je przez aplikację iOS w drodze do sklepu, „ostatni zapis wygrywa z opóźnieniem” jest poprawne.
Dwa ekosystemy agentów, jedna lista zakupów. Mostem jest iCloud Drive plus plik JSON z wybaczającym dekoderem, i to wystarczy. Najmniejszy wspólny mianownik nie jest ograniczeniem. Jest jedyną rzeczą, co do której oba ekosystemy się zgadzają.
Bibliografia
-
Autorska aplikacja Get Bananas, aplikacja listy zakupów SwiftUI + SwiftData dla iOS, macOS, watchOS i visionOS, opublikowana przez 941 Apps. ↩
-
Get Bananas dostarcza serwer MCP (Model Context Protocol) spakowany jako
get-bananas.mcpbdla Claude Desktop. Udostępniane narzędzia:get_shopping_list,add_item,remove_item,update_item,update_shopping_list. Serwer to 575 linii Node.js wmcp-extension/server/index.js. ↩↩ -
Apple Developer, framework „SwiftData”. Dostępny od iOS 17+, macOS 14+, watchOS 10+, visionOS 1+. Tylko w czasie wykonania; brak powiązań po stronie serwera lub międzyprocesowych. ↩
-
Apple Developer, framework „CloudKit”. Natywne API
CKContainerwymaga uprawnieniacom.apple.developer.icloud-servicesi pasującego identyfikatora zespołu Apple Developer. Apple publikuje również CloudKit Web Services dla klientów spoza platform Apple, ale jego użycie wymaga osobnego mostu token/auth, którego Get Bananas nie utrzymuje. ↩↩ -
Kod produkcyjny w
Banana List/Item.swift. PolelastModifiedzostało dodane później do rozwiązywania konfliktów synchronizacji iCloud. ↩ -
Kod produkcyjny w
Banana List/iCloudBackupManager.swift. Stałe znajdują się wBanana List/Constants.swift. ↩↩ -
Kod produkcyjny w
Banana List/ShoppingListExport.swift. Niestandardowy dekoder z domyślnymi wartościamidecodeIfPresentplus filtrisValidprzy imporcie. ↩↩ -
Semantyka POSIX
O_EXCL | O_CREAT; Node.js udostępnia tę samą atomowość przezfs.writeFileSync(path, data, { flag: 'wx' }). Zobacz dokumentacja Node.js fs. ↩ -
Apple Developer, „Designing for CloudKit”. Synchronizacja oparta na push, rozwiązywanie konfliktów na poziomie rekordu, partycjonowanie stref. ↩
-
Notatki autora z debugowania. Incydent nieskończonej pętli wytworzył plik
BananaList.jsono rozmiarze 4 MB z 30-pozycyjnej listy zakupów w 3 minuty, zanim wylądowała logika licznika synchronizacji. ↩ -
Anthropic, „Model Context Protocol”. Otwarty protokół do użycia narzędzi przez LLM; wieloplatformowy (Claude Desktop, Cline, Goose itp.). ↩↩↩↩
-
Analiza autora w App Intents to nowe API Apple do Twojej aplikacji. Teza dwóch równoległych kontraktów zastosowana w powierzchniach systemowych AI (Apple) i międzyplatformowym użyciu narzędzi LLM (Anthropic). ↩↩
-
Apple Developer, „App Intents framework”. Typowana deklaratywna powierzchnia użycia narzędzi Apple dla Siri, Spotlight i Apple Intelligence. ↩
-
Apple Developer, „FileManager url(forUbiquityContainerIdentifier:)”. Wspierane API do rozwiązywania URL kontenera iCloud Drive aplikacji. Ścieżka macOS w
~/Library/Mobile Documents/to detal implementacyjny systemu hosta dotyczący tego, gdzie iCloud Drive udostępnia kontener; symboliczne API to to, co aplikacje powinny wywoływać. ↩ -
Anthropic, „Desktop Extensions”. Format
.mcpbto archiwum zip zawierającemanifest.json, punkt wejścia serwera MCP, ikonę i metadane. Instalacja jednym kliknięciem w Claude Desktop; uruchamia spakowany serwer jako lokalny podproces przez stdio JSON-RPC. ↩ -
Apple Developer, „NSFileCoordinator”. Koordynuje odczyty i zapisy do pliku między procesami, które wybierają ten sam protokół koordynacji; wymagane, gdy demon
birdiCloud Drive, obserwatorzy oparci naNSMetadataQueryoraz sama aplikacja mogą wszyscy dotykać tej samej ścieżki. ↩ -
POSIX
rename(2)musi być atomowy, gdy źródło i cel znajdują się w tym samym systemie plików. Lokalne lustro iCloud Drive w~/Library/Mobile Documents/to pojedynczy wolumin APFS, więcfs.renameSyncmiędzy rodzeństwem-plikiem tymczasowym a kanoniczną ścieżką jest atomowe z perspektywy każdego czytelnika. Zobacz specyfikację POSIX rename. ↩