与 iOS 应用并存的 MCP 服务器:两个 __TERM_23__ 生态,一份清单
Get Bananas,我的 SwiftUI 购物清单应用,运行于 iOS、macOS、watchOS 和 visionOS。1 它也作为 .mcpb MCP 扩展存在于 Claude Desktop 中,暴露五个工具:get_shopping_list、add_item、remove_item、update_item、update_shopping_list。2
当您让 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 模型,并在每次更改后通过 NSFileCoordinator 将 BananaList.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 架构。一个 MCP 主机(Claude Desktop)连接到一个或多个 MCP 服务器(本文中是 get-bananas.mcpb),每个服务器都暴露主机可调用的工具、资源和提示词。来源:modelcontextprotocol.io。11
一页架构图
┌─────────────────────────────────────────────────────────┐
│ 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 解码器是宽容的一方。

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_item、remove_item、update_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 应用协调——后者通过 NSFileCoordinator 和 String.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 对单次更改同时触发 DidUpdate 和 DidFinishGathering |
前两层守住出站写入路径:我现在该不该导出到 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 偶尔会在单条目操作就够用时调用它,然后用户清单中相当一块就消失了。我本应只发布四个条目级工具(get、add、remove、update),并强制 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 生态触达的应用意味着什么
该模式简单到可重复。三块拼图:
- 一个用于应用内持久化的 SwiftData
@Model。 驱动 UI、快、原生。 - 在去抖变更时写入 iCloud Drive 的 Codable JSON 导出。 防御性解码器。稳定模式。共享文件就是契约。
- 针对每个 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 文件,这就够了。最低公分母不是限制。它是两个生态唯一都能认同的东西。
参考文献
-
作者的 Get Bananas,由 941 Apps 发布的 SwiftUI + SwiftData 购物清单应用,适用于 iOS、macOS、watchOS 和 visionOS。 ↩
-
Get Bananas 随附一个 MCP(Model Context Protocol)服务器,以
get-bananas.mcpb形式打包给 Claude Desktop。暴露的工具:get_shopping_list、add_item、remove_item、update_item、update_shopping_list。该服务器是mcp-extension/server/index.js中 575 行的 Node.js。 ↩↩ -
Apple Developer,《SwiftData》框架。可用于 iOS 17+、macOS 14+、watchOS 10+、visionOS 1+。仅限运行时;无服务器端或跨进程绑定。 ↩
-
Apple Developer,《CloudKit》框架。原生
CKContainerAPI 需要com.apple.developer.icloud-services权限和匹配的 Apple Developer 团队标识符。Apple 还为非 Apple 平台客户端发布了 CloudKit Web Services,但使用它需要一个 Get Bananas 不维护的独立令牌/认证桥。 ↩↩ -
生产代码位于
Banana List/Item.swift。lastModified字段是后来为 iCloud 同步冲突解决而添加的。 ↩ -
生产代码位于
Banana List/iCloudBackupManager.swift。常量位于Banana List/Constants.swift。 ↩↩ -
生产代码位于
Banana List/ShoppingListExport.swift。带decodeIfPresent默认值的自定义解码器,以及导入时的isValid过滤器。 ↩↩ -
POSIX
O_EXCL | O_CREAT语义;Node.js 通过fs.writeFileSync(path, data, { flag: 'wx' })暴露相同的原子性。参见 Node.js fs 文档。 ↩ -
Apple Developer,《为 CloudKit 设计》。基于推送的同步、记录级冲突解决、区域分区。 ↩
-
作者的调试笔记。无限循环事件在同步计数器逻辑落地之前,使一个 30 条目的购物清单在 3 分钟内产生了 4MB 的
BananaList.json。 ↩ -
Anthropic,《Model Context Protocol》。面向 LLM 工具使用的开放协议;多运行时(Claude Desktop、Cline、Goose 等)。 ↩↩↩↩
-
作者在 《App Intents 是 Apple 通往你应用的新 API》 中的分析。两条平行契约论题适用于系统 AI 表面(Apple)和跨 LLM 工具使用(Anthropic)。 ↩↩
-
Apple Developer,《App Intents 框架》。Apple 面向 Siri、Spotlight 和 Apple Intelligence 的有类型声明式工具使用表面。 ↩
-
Apple Developer,《FileManager url(forUbiquityContainerIdentifier:)》。用于解析应用 iCloud Drive 容器 URL 的官方支持 API。
~/Library/Mobile Documents/下的 macOS 路径是 iCloud Drive 在宿主操作系统上呈现容器位置的实现细节;符号 API 才是应用应当调用的。 ↩ -
Anthropic,《Desktop Extensions》。
.mcpb格式是包含manifest.json、MCP 服务器入口点、图标和元数据的 zip 归档。在 Claude Desktop 中单击安装;通过 stdio JSON-RPC 把打包的服务器作为本地子进程运行。 ↩ -
Apple Developer,《NSFileCoordinator》。在选择加入同一协调协议的进程之间协调对文件的读写;当 iCloud Drive 的
bird守护进程、NSMetadataQuery驱动的观察者以及应用本身都可能触碰同一路径时,这是必需的。 ↩ -
当源和目标位于同一文件系统时,POSIX
rename(2)必须是原子的。iCloud Drive 在~/Library/Mobile Documents/下的本地镜像是单一 APFS 卷,因此fs.renameSync在同级临时文件与规范路径之间从任何读取者的视角看都是原子的。参见 POSIX rename 规范。 ↩