← 所有文章

与 iOS 应用并存的 MCP 服务器:两个 __TERM_23__ 生态,一份清单

Get Bananas,我的 SwiftUI 购物清单应用,运行于 iOS、macOS、watchOS 和 visionOS。1 它也作为 .mcpb MCP 扩展存在于 Claude Desktop 中,暴露五个工具:get_shopping_listadd_itemremove_itemupdate_itemupdate_shopping_list2

iPhone 上的 Get Bananas 购物清单,与 MCP 服务器读写的是同一份 JSON 文件 当您让 Claude “把香蕉、牛奶和面包加到我的清单”,Claude 会调用 add_item 三次,而下次我在手机上打开应用时,这些条目就在那里。没有服务器。没有账户。没有 API 密钥。 这座桥梁是单一的 JSON 文件,加上我在发布完 v1.0 后不得不添加的五层防循环机制——那个 v1.0 在三分钟内把自己写到了一个 4MB 的文件里。

有趣的问题是怎么做到的。SwiftData 仅限于 Apple 平台运行时,无法从 Node.js 进程读取。3 原生 CloudKit 框架需要匹配的 com.apple.developer.icloud-services 权限和 Apple Developer 团队标识符;Claude Desktop 的 MCP 子进程两者都没有,因此它无法像我签名的应用那样使用 CKContainer。CloudKit Web Services 存在,但使用它需要在桌面进程和 Apple 服务器之间维护一个独立的令牌/认证桥。4 因此显而易见的路径都被堵死了。

我走的路径更古老也更奇特。Get Bananas 应用与其 MCP 服务器通过 iCloud Drive 中的一份 JSON 文件共享状态。 Swift 应用为应用内持久化保留 SwiftData 模型,并在每次更改后通过 NSFileCoordinatorBananaList.json 文件导出到其 iCloud Drive 容器。Node.js MCP 服务器使用 5 秒的独占文件锁、过期锁检测和原子性临时文件重命名写入来读写同一份文件。iCloud Drive 处理跨设备同步。如今,Claude Desktop 从 Mac 读写同一份事实来源;面向 Apple Intelligence 的 App Intents 适配器是下一个表面,针对的也是同一份文件。

这篇文章关于这种安排为什么有效、它的代价是什么、以及它会在哪里崩溃。

TL;DR

  • Get Bananas 通过随附的 MCP 服务器将其购物清单暴露给 Claude Desktop。同样的文件格式接下来将支持面向 Apple Intelligence 的 App Intents 适配器。
  • 集成基底是 iCloud Drive 加一份 JSON 文件,而不是 CloudKit、不是服务器、也不是服务。
  • Swift 应用:SwiftData @Model ShoppingItem 保证应用内速度;iCloud Drive JSON 导出保证可移植性。
  • MCP 服务器:575 行 Node.js,带过期锁检测的文件锁,运行在 Claude Desktop 的 .mcpb 包内。
  • 权衡:基于文件的 JSON 同步比 CloudKit 慢,且存在合并冲突风险,但它适用于任何能够读取文件的 LLM 生态。

Anthropic 的参考 Model Context Protocol 架构图,展示客户端/服务器连接模型

Anthropic 文档中的 Model Context Protocol 架构。一个 MCP 主机(Claude Desktop)连接到一个或多个 MCP 服务器(本文中是 get-bananas.mcpb),每个服务器都暴露主机可调用的工具、资源和提示词。来源:modelcontextprotocol.io11

一页架构图

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

两个表面。一个文件。整座桥梁就是这个文件。

Swift 端:用 SwiftData 求速度,用 JSON 求可移植性

在应用中,购物清单是一个 SwiftData @Model。真实的生产代码: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?
}

这是内存中的事实。每一次按键、每一次复选框点击、每一次分区变更都写入 SwiftData。SwiftData 驱动 SwiftUI 视图。应用感觉原生,因为它就是原生的。备份触发是基于哈希的:.onChange(of: computeItemsHash()) 监听器仅在条目的 id、名称、数量、分区、勾选或可选状态变化时触发,绝不会因纯粹的空操作编辑而触发。

诀窍在于 SwiftData 不是跨进程的事实。它是跨进程的缓存。每次更改都会去抖 500 毫秒,然后通过 Apple 的协调写入 API 把 JSON 文件写入应用的 iCloud Drive 容器: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 是写入其他进程(以及 iCloud Drive 守护进程)可能并发读取的文件的官方支持方式。16 在写入发生之前,管理器会读取现有文件,如果 JSON 逐字节匹配则完全跳过写入。这在 SwiftData 变更观察者因空操作编辑而触发时,削减了多余的 iCloud Drive 抖动。在 restore 时,管理器最多以指数退避重试三次(1 秒、2 秒、4 秒,总预算 7 秒),因为 NSMetadataQuery 在 iCloud Drive 实际下载新字节之前就报告文件变化。6

文件的 Codable 形态故意做得宽容。ShoppingListExport 对每个缺失字段使用默认值进行解码,并过滤掉名称为空的条目: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
        }
    }
}

防御性解码器是有意为之。下一个写入 JSON 的角色(一个 MCP 服务器、一个未来的快捷指令、一次手动粘贴)不可避免地会忘记某个字段。Swift 端会吸收这一切。共享文件格式是契约;Swift 解码器是宽容的一方。

macOS 上的 Get Bananas,MCP 服务器作为 Claude Desktop 子进程在此宿主上运行,并读取同一份 iCloud Drive 文件

Node 端:一个读取同一份文件的 575 行 MCP 服务器

MCP 服务器位于 mcp-extension/server/index.js,以 get-bananas.mcpb 形式分发给 Claude Desktop 的扩展系统。它从 macOS 宿主打开同一份 iCloud Drive 文件:2

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

五个工具:一个纯读(get_shopping_list)、三个读-改-写的条目级工具(add_itemremove_itemupdate_item),以及一个不先读取就写入的批量替换工具(update_shopping_list)。MCP 服务器还将该文件作为独立的只读 Resource 暴露,供偏好资源 API 的客户端使用。每次写入都通过带过期锁恢复的文件锁:

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

这种锁模式比 Node.js 还古老。fs.writeFileSync 配合 'wx' 标志是 O_EXCL | O_CREAT 的跨平台版本。如果锁文件存在且早于 5 秒,服务器会假定上一个持有者已崩溃并将其回收。如果锁文件存在且新鲜,服务器等待 50 毫秒并重试。总共 5 秒后,放弃。8

该锁仅在 Node 端写入者之间彼此同步(在第一次 MCP 调用写入中途到达的第二次调用)。它不与 Swift 应用协调——后者通过 NSFileCoordinatorString.write(atomically:) 写入,从不接触 BananaList.json.lock。真正的 Swift/Node 重叠交由两个较弱的机制处理:Swift 应用在写入前去抖 500 毫秒,MCP 写入仅在用户提示时发生,任何残留冲突都会落到 iCloud Drive 在文件粒度上的最后写入获胜解决方案上。

写入本身使用原子临时文件再重命名模式,中间夹一次 JSON 解析检查:

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 是 POSIX 原子的:另一进程上的读取者要么看到旧文件,要么看到新文件,绝不会看到只写一半的字节。17 把临时路径锚定到 process.pid 可防止两个 MCP 服务器实例(罕见,但用户重装 Claude Desktop 而未重启时可能发生)互相覆盖临时文件。写入中途的 JSON.parse 是一步偏执检查:如果序列化本身产生了无效 JSON,函数会在重命名之前中止,使规范文件保持原样。

为什么选 iCloud Drive,而非 CloudKit

让该架构得以工作的选择是对跨进程事实使用 iCloud Drive(基于文件)而非 CloudKit(基于记录)。 CloudKit 是 Apple 推荐用于应用到应用同步的方案。它具有更高级的冲突解决、服务器端推送和基于区域的分区。9 原生 CKContainer API 仅限 Apple 平台,且受权限门控,所以 Claude Desktop 子进程无法像我签名的应用那样使用它。Apple 确实为非 Apple 平台客户端发布了 CloudKit Web Services,但使用它需要配置一个服务器到服务器的令牌、把它接入 MCP 服务器,并维护一个独立的认证桥:并非不可能,但对于一个购物清单来说基础设施成本相当大。4

MCP 服务器在 macOS 上以 Claude 子进程的形式不带沙盒地运行。它没有 Apple Developer 签名链、没有与我应用的 CloudKit 容器匹配的团队标识符,也没有配置 CloudKit Web Services 令牌。

相比之下,iCloud Drive 把自己暴露成普通的文件系统位置。Apple 在应用端支持的 API 是 FileManager.url(forUbiquityContainerIdentifier:);14 在 macOS 上,Get Bananas 解析出的位置是 ~/Library/Mobile Documents/iCloud~com~941apps~Banana-List/Documents/BananaList.json。该路径是 iCloud Drive 在 macOS 上呈现容器位置的实现细节,但对运行在同一台 Mac 上的 Claude Desktop 来说,它就是一个文件。任何对用户主目录有读取权限的进程都能读写它。未来的快捷指令、未来的 SwiftBar 插件、用户本地运行的未来 llama.cpp 脚本同样能读写。任何能读取文件的事物都能集成。

代价是 iCloud Drive 的同步比 CloudKit 慢(秒级,而非亚秒级),并且冲突语义较弱(在文件粒度上最后写入获胜,而非记录级合并)。对于大约 30 条目的购物清单,这两项代价都无关紧要。对于一个有 1 万行和并发编辑者的高写入量应用,这两项代价都会成为主导。

五层防循环

Swift 端最棘手的代码是循环防护。没有它:MCP 服务器写入 JSON,iCloud Drive 同步到 iOS,iOS 应用的 NSMetadataQuery 注意到变化,应用把 JSON 重新导入 SwiftData,导入触发 SwiftData 变更观察者,变更观察者触发去抖备份,去抖备份写入 JSON,iCloud Drive 同步回去。我在 v1.0 发布了天真的版本,亲眼看着一个 30 条目的购物清单在测试中三分钟内膨胀到 4MB。10

发布版使用了五层叠加防护,而非一层。每一层防护一个不同的时序边界情况:

// 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
}
层级 位置 它捕获的内容
1. 同步计数器 > 0 出站写入路径 在来自 iCloud 的同步正在进行中触发的可重入写入
2. 同步后 5 秒窗口 出站写入路径 SwiftData 在导入安顿之后触发的延迟 @Model onChange 回调
3. 备份后 2 秒窗口 入站 NSMetadataQuery 处理器 应用自身写入紧随其后触发的本地文件系统事件
4. 修改日期完全匹配 入站处理器 iCloud Drive 跨设备把我们自己的备份回放给我们
5. 单调修改日期 入站处理器 NSMetadataQuery 对单次更改同时触发 DidUpdateDidFinishGathering

前两层守住出站写入路径:我现在该不该导出到 iCloud? 余下三层守住入站 NSMetadataQuery 处理器:我该不该把这个变化导入到 SwiftData? 任何一侧单独都不够。一次同步往返可能因哪个事件先触发而穿过任一侧的防护,因此每条路径都需要自己的防御。

这条经验适用于任何”两个写入者共享文件”的架构:基于修改时间的变化检测必要但不充分。你需要一种’我引发的写入’的稳定身份,该身份至少要在同步层经历一次往返后仍然存活。 iCloud Drive 给你的最接近的东西就是文件在你写入那一刻的修改日期。守住它。回程时比对它。

我会以何种方式重建

发布这套系统的四点教训。

Swift 端的防御性 Codable 物有所值。 MCP 服务器已被重写过三次。每次重写都至少漏设过一个字段。Swift 解码器吸收了每个变体,应用从未崩溃。如果让我重新开始,我会把更多字段推向”按默认解码”而非”必填”。两个写入者之间的契约在设计上就是脆弱的。7

锁超时应当感知内容,而不仅看修改时间。 5 秒太短。如果用户的 Mac 在慢速 Wi-Fi 上,或 iOS 设备在长时间后台后正在恢复,BananaList.json.lock 的 iCloud Drive 同步传播可能超过 5 秒。MCP 服务器随后会把仍在持有的锁误认为是过期锁。修复办法是将过期锁检查闸住于锁文件内写入的 PID:如果 kill(pid, 0) 报告进程仍在运行,无论 mtime 看起来多老都不要破坏锁。当前代码写入了 PID 但从不读回。

update_shopping_list 工具是个错误。 它替换整张清单。Claude Desktop 偶尔会在单条目操作就够用时调用它,然后用户清单中相当一块就消失了。我本应只发布四个条目级工具(getaddremoveupdate),并强制 Claude 组合它们。MCP 协议的 destructiveHint: true 注解确实将该工具标记为破坏性,11 但 Claude 在调用前并不总是把这一点告知用户。批量替换工具对 LLM 来说便利,对用户来说危险。在协议层有护栏并不能替代不发布走火工具。

共享 JSON 导出需要一个版本字段。 ShoppingListExport 以宽容默认值解码,这能撑到我重命名某字段而非新增字段的那一天。JSON 顶部的 schemaVersion: 1 能让任一侧检测到未来的破坏性变更并拒绝读取,而不是默默生成畸形模型。迁移仍需手动,但至少失败模式会响亮,而不是默默丢失数据。

何时不要使用此模式

拒绝是设计的一部分。

如果数据受监管(健康、金融、任何具有合规留存策略的内容),iCloud Drive 用户可控的文件系统就是错误的基底。CloudKit 有日志和审计跟踪;用户可读的 JSON 文件没有。

如果跨进程延迟预算是亚秒级,iCloud Drive 无法满足。在我的测试中,健康连接下 iCloud Drive 同步通常需要数秒而非亚秒;Apple 没有发布更紧的 SLA,慢速网络会让它更长。CloudKit 基于推送的投递对记录级更新明显更快。实时协作产品需要 CloudKit(或专用同步服务器)。

如果模式快速演进,Codable 加默认值的模式会复利累积技术债。每个新字段都需要做”对老文件用什么默认值”的决策,而这种决策老化得很快。JSON 文件同步最适合稳定的、变化主要为加法的模式。

这对希望被多个 LLM 生态触达的应用意味着什么

该模式简单到可重复。三块拼图:

  1. 一个用于应用内持久化的 SwiftData @Model 驱动 UI、快、原生。
  2. 在去抖变更时写入 iCloud Drive 的 Codable JSON 导出。 防御性解码器。稳定模式。共享文件就是契约。
  3. 针对每个 LLM 生态的小适配器,以文件锁读写同一份文件。Node.js 用于 Claude Desktop。未来的 App Intent + AppEntity 用于 Apple Intelligence。未来的 shell 脚本用于下一个登场的东西。

该模式可移植,因为集成基底是文件系统。今天存在的每个 LLM 运行时(Claude Desktop、Cursor、Goose、Cline)以及大多数将在明年发布的运行时都能读取文件。11 CloudKit 不行。原生同步引擎不行。当目标是横跨 LLM 生态触达时,最低公分母获胜。

Anthropic 与 Apple 对 LLM 应当是什么样子有不同看法。 App Intents 说它是一个由 Apple Intelligence 在设备上解析的有类型 Swift 声明。MCP 说它是一个带工具列表、任何 LLM 都可调用的 JSON-RPC 服务器。两者在各自的生态里都正确。Get Bananas 谁也不当作事实来源,而让文件系统居中调停。12

下次我发布一个想要两个 LLM 表面的应用时,我会先从文件格式开始,而非实体模型。

常见问答

.mcpb 是什么,它如何运作?

.mcpb 是 Anthropic 为 Claude Desktop 设计的 MCP 扩展打包格式。它是一个 zip 归档,包含描述工具的 manifest.json、MCP 服务器入口点(Node.js、Python 等)、一个图标和元数据。Claude Desktop 通过单击像浏览器扩展那样安装它,并把服务器作为本地子进程运行。MCP 服务器通过 stdio 讲 JSON-RPC。1115 Get Bananas 以这种方式打包并发布其服务器。

为什么不使用新的 App Intents 到 MCP 桥?

没有这种桥。App Intents(Apple 的框架)和 MCP(Anthropic 的协议)彼此独立。Apple Intelligence 通过自己的解析器调用 App Intents。Claude Desktop 通过自己的运行时调用 MCP 服务器。一个想要两个表面的应用就发布两套;不存在自动桥。1213

没有 iCloud Drive 能这么做吗?

可以,但有代价。任何共享可写文件位置都行:~/Documents 中的某个文件夹、网络共享、挂载为 FUSE 文件系统的 S3。iCloud Drive 方便,因为它已经在每台运行 Claude Desktop 的 Mac 和用户拥有的每台 iOS 设备上。非 iCloud 文件会迫使用户单独配置同步。

写入冲突时会发生什么?

5 秒文件锁加 50 毫秒重试处理并发的 MCP 端写入者(例如,在第一次写入中途到达的第二次 MCP 调用)。它不与 Swift 应用协调,后者通过自己的协调器写入。当 Swift 与 Node 真正重叠时(罕见,鉴于 Swift 500 毫秒去抖,且 MCP 写入仅在用户提示时触发),iCloud Drive 在文件粒度解决:最后写入获胜。Swift 解码器的 isValid 过滤器随后会丢掉任何畸形条目。

为什么不用 CRDT 或操作变换?

对于 30 条目的购物清单是杀鸡用牛刀。CRDT 的正确应用场景是并发重叠编辑常见且需要确定性合并语义之处(协作文档编辑器、多用户游戏)。对于一个购物清单——一个人通过 Claude 加条目,另一个人去店里的路上通过 iOS 应用勾掉它们——带去抖的最后写入获胜是正确的。


两个 LLM 生态,一份购物清单。这座桥梁是 iCloud Drive 加一份带宽容解码器的 JSON 文件,这就够了。最低公分母不是限制。它是两个生态唯一都能认同的东西。

参考文献


  1. 作者的 Get Bananas,由 941 Apps 发布的 SwiftUI + SwiftData 购物清单应用,适用于 iOS、macOS、watchOS 和 visionOS。 

  2. Get Bananas 随附一个 MCP(Model Context Protocol)服务器,以 get-bananas.mcpb 形式打包给 Claude Desktop。暴露的工具:get_shopping_listadd_itemremove_itemupdate_itemupdate_shopping_list。该服务器是 mcp-extension/server/index.js 中 575 行的 Node.js。 

  3. Apple Developer,《SwiftData》框架。可用于 iOS 17+、macOS 14+、watchOS 10+、visionOS 1+。仅限运行时;无服务器端或跨进程绑定。 

  4. Apple Developer,《CloudKit》框架。原生 CKContainer API 需要 com.apple.developer.icloud-services 权限和匹配的 Apple Developer 团队标识符。Apple 还为非 Apple 平台客户端发布了 CloudKit Web Services,但使用它需要一个 Get Bananas 不维护的独立令牌/认证桥。 

  5. 生产代码位于 Banana List/Item.swiftlastModified 字段是后来为 iCloud 同步冲突解决而添加的。 

  6. 生产代码位于 Banana List/iCloudBackupManager.swift。常量位于 Banana List/Constants.swift。 

  7. 生产代码位于 Banana List/ShoppingListExport.swift。带 decodeIfPresent 默认值的自定义解码器,以及导入时的 isValid 过滤器。 

  8. POSIX O_EXCL | O_CREAT 语义;Node.js 通过 fs.writeFileSync(path, data, { flag: 'wx' }) 暴露相同的原子性。参见 Node.js fs 文档。 

  9. Apple Developer,《为 CloudKit 设计》。基于推送的同步、记录级冲突解决、区域分区。 

  10. 作者的调试笔记。无限循环事件在同步计数器逻辑落地之前,使一个 30 条目的购物清单在 3 分钟内产生了 4MB 的 BananaList.json。 

  11. Anthropic,《Model Context Protocol》。面向 LLM 工具使用的开放协议;多运行时(Claude Desktop、Cline、Goose 等)。 

  12. 作者在 《App Intents 是 Apple 通往你应用的新 API》 中的分析。两条平行契约论题适用于系统 AI 表面(Apple)和跨 LLM 工具使用(Anthropic)。 

  13. Apple Developer,《App Intents 框架》。Apple 面向 Siri、Spotlight 和 Apple Intelligence 的有类型声明式工具使用表面。 

  14. Apple Developer,《FileManager url(forUbiquityContainerIdentifier:)》。用于解析应用 iCloud Drive 容器 URL 的官方支持 API。~/Library/Mobile Documents/ 下的 macOS 路径是 iCloud Drive 在宿主操作系统上呈现容器位置的实现细节;符号 API 才是应用应当调用的。 

  15. Anthropic,《Desktop Extensions》.mcpb 格式是包含 manifest.json、MCP 服务器入口点、图标和元数据的 zip 归档。在 Claude Desktop 中单击安装;通过 stdio JSON-RPC 把打包的服务器作为本地子进程运行。 

  16. Apple Developer,《NSFileCoordinator》。在选择加入同一协调协议的进程之间协调对文件的读写;当 iCloud Drive 的 bird 守护进程、NSMetadataQuery 驱动的观察者以及应用本身都可能触碰同一路径时,这是必需的。 

  17. 当源和目标位于同一文件系统时,POSIX rename(2) 必须是原子的。iCloud Drive 在 ~/Library/Mobile Documents/ 下的本地镜像是单一 APFS 卷,因此 fs.renameSync 在同级临时文件与规范路径之间从任何读取者的视角看都是原子的。参见 POSIX rename 规范。 

相关文章

单一可信源:SwiftData、MCP、iCloud

三个调用方可以写入同一份购物清单:人类、Apple Intelligence 和外部代理。真相必须存放在某处。请选择基础载体。

3 分钟阅读

App Intents 与 MCP:路由问题

两个协议,一个应用。App Intents 将您的应用暴露给 Apple Intelligence。MCP 将同一领域暴露给 Claude、ChatGPT 等其他工具。这是路由问题。

3 分钟阅读

你的Agent中间商你从未审查过

研究人员测试了28个LLM API路由器。17个接触了AWS金丝雀凭证。一个从私钥中抽走了ETH。路由器层是新的攻击面。

1 分钟阅读