mcpField
A
Apple Mail

Apple Mail

本地服务
cupertino.mgcrea.io通信协作5今天更新

关于 Apple Mail

Apple Mail 是用于访问已同步到 macOS Mail 应用的邮件的 MCP 服务,无需邮件凭据,也不进行网络调用。它结合索引搜索、邮件读取和操作;默认不注册写入工具。

主要功能

  1. 01列出账户、邮箱和邮件
  2. 02搜索邮件及正文
  3. 03读取邮件会话和附件
  4. 04统计邮件总数与未读数
  5. 05默认禁用写入操作

适用场景

  1. 01按筛选条件或正文查找邮件
  2. 02阅读邮件会话及其附件
  3. 03查看邮箱和未读邮件数量
  4. 04启用写入前保持邮件操作不可用

MCP 相关

向 MCP 客户端提供 Apple Mail 账户、邮件、搜索、读取和操作工具。只有启用 `APPLE_MAIL_ALLOW_WRITES` 后才会注册修改类工具。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"mgcrea-mcp-apple-mail": {
"command": "npx",
"args": [
"-y",
"@mgcrea/mcp-apple-mail"
],
"env": {
"APPLE_MAIL_ALLOW_WRITES": "<APPLE_MAIL_ALLOW_WRITES>",
"APPLE_MAIL_ACCOUNTS": "<APPLE_MAIL_ACCOUNTS>",
"APPLE_MAIL_ALLOW_FILE_SEND": "<APPLE_MAIL_ALLOW_FILE_SEND>"
}
}
}
}
启动命令npx -y @mgcrea/mcp-apple-mail
需要配置的环境变量
APPLE_MAIL_ALLOW_WRITESRegister the mutating tools. Off by default, and the gate decides whether the write tools exist at all rather than merely refusing — an agent with writes off cannot see them.
APPLE_MAIL_ACCOUNTSRestrict every read and write to these Mail accounts, by name. Bounds reading, not just writing.
APPLE_MAIL_ALLOW_FILE_SENDLet the compose tools attach a local file and send in the same call. Attaching to a draft needs only writes; this is off by default because an immediate send puts the file in someone's inbox with nobody reviewing it.

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
npm@mgcrea/mcp-apple-mail1.31.0

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

@mgcrea/mcp-apple-mail

适用于 macOS Apple Mail 应用的 Model Context Protocol 服务。读取、搜索和操作已同步到 Mac 的邮件——无需 IMAP 凭据、OAuth,也不会有邮件离开本机。

非官方。 与 Apple 无关。它通过 Mac 上已有的 Mail 应用执行操作。

状态

搜索、列表、计数、会话串联、正文读取、正文_搜索_和所有修改操作均已实现。

工作原理

Apple Mail 提供了两种截然不同的接口,本服务会各取所长。采用这种划分并非出于偏好,而是基于对一个包含 29,617 封邮件的真实邮箱进行测量得出的结果:

操作Apple Events (AppleScript/JXA)结论
列出 4 个账户和所有邮箱0.6 s足够快
邮箱总数 / 未读数295 ms / 76 ms足够快
在包含 29 k 封邮件的邮箱中按 id 查找一封邮件0.10 s足够快
获取 N 封邮件的各个字段~130 ms + 每封邮件 42 ms约 50 封以内可用
循环读取每封邮件的一个属性每封约 250 ms绝不要这样做
messages whose read status is false74 秒无法使用

因此,搜索完全不能通过 Apple Events 实现。它必须读取 Mail 自己的 SQLite 索引(~/Library/Mail/V10/MailData/Envelope Index),也就是 Mail 自身进行搜索所用的索引。这形成了三条路径:

  • AppleScript 路径 — 账户、邮箱、计数、按 id 查找邮件,以及所有修改操作。这也是_权威_路径:写入后,结果以 Mail 重新读取的内容为准,而非索引中的内容。
  • 索引路径 — 只读 SQLite,用于搜索和筛选。需要完全磁盘访问权限。
  • 正文路径 — 从磁盘上的 .emlx 文件读取邮件正文和附件。需要完全磁盘访问权限,并会在必要时回退到逐封邮件的 Apple Event。

绝不会写入索引。索引由 Mail 管理并保持打开,且会与 IMAP 同步;写入索引会造成延迟显现的损坏。

权限

macOS 中有两种独立权限,各司其职。两种权限都不需要授予 Mail.app——需要权限的是_读取器_,而不是 Mail。

权限用途未授予时
自动化 → Mail所有操作服务无法执行任何操作;你会收到 -1743 错误
完全磁盘访问权限搜索、邮件正文、附件服务仍可运行:账户、邮箱、计数和数量受限的列表功能都可正常使用

自动化权限需要授予启动服务的应用(Terminal、iTerm、VS Code、Claude 等)。macOS 会在首次触发 Apple Event 时提示,通常只需点击“允许”。

完全磁盘访问权限比较棘手。

为什么不应直接授予编辑器此权限

macOS 会将进程的文件访问权限归属到其_负责进程_——也就是启动链顶端的应用。当 Claude 启动此服务时,启动链如下:

launchd → Visual Studio Code.app → Code Helper → claude → node dist/cli.js

因此必须将权限授予 VS Code;随之而来的是,该编辑器运行的所有扩展、任务和终端命令都将获得读取整个磁盘的权限:Messages、Safari 历史记录、SSH 密钥,以及其他应用的容器。这远远超出了“可以搜索我的邮件”的权限范围,也会使 APPLE_MAIL_ACCOUNTS 失去作用,而该设置正是为了限制访问范围。

你无法通过将权限授予 .mcp.json(它是数据,不是代码)或 node(它不是负责进程,而且机器上的所有 node 程序共用它)来绕过这一点。

替代方案

安装 Cupertino.app,这个带签名的菜单栏应用正是本软件包的开发环境。它持有一项“完全磁盘访问”授权,并在其下运行此服务,因此权限授予的是经过公证的二进制文件,而不是你的编辑器:

brew install --cask mgcrea/tap/cupertino

实测结果:授予带签名 .app 的“完全磁盘访问”和“自动化”权限会向下继承两层,传递给它启动的进程。因此,作为孙进程的 node 可以读取 Envelope Index,而 tccd 会将孙进程 osascript 解析为该应用,而不是启动它的程序。

此软件包的早期版本曾介绍一个小型编译启动器,它通过私有 API 让自身成为自己的责任进程。现在已不再构建或分发该启动器——已经没有需要逃离的东西了——不过 native/launcher.c 仍保留在仓库中,作为对这个问题最清晰的说明。

如果你不想安装该应用,向编辑器授予“完全磁盘访问”权限也确实有效。操作前,请先阅读上面的段落,了解该授权实际涵盖的内容。

授予权限后,请重启 MCP 主机。apple_mail_diagnostics 会报告缺少哪项权限,以及它会造成什么阻碍。

还有一点值得了解:对受 TCC 保护的文件调用 stat() 会成功——即使没有“完全磁盘访问”权限,你也能查看索引的大小和修改时间,只有读取操作会被拒绝。因此,“文件存在”并不能证明已授予权限;access(R_OK) 才能。

这种安装方式只是权宜之计。docs/distribution.md 介绍了后续计划——发布经过签名和公证的应用,让授权在更新后仍然有效,也无需任何人安装编译器——以及为什么 App Store 无法上架这些内容。

快速开始

# A. from npm
npx -y @mgcrea/mcp-apple-mail

# B. from source
pnpm install && pnpm build && node dist/cli.js

将其接入 Claude Code(.mcp.json)或 Claude Desktop:

{
  "mcpServers": {
    "apple-mail": {
      "command": "npx",
      "args": ["-y", "@mgcrea/mcp-apple-mail"],
      "env": {
        // Off by default. With it off the write tools are not registered at all.
        "APPLE_MAIL_ALLOW_WRITES": "0",
      },
    },
  },
}

直接检查这些工具:

npx @modelcontextprotocol/inspector node dist/cli.js

工具

工具功能
apple_mail_diagnostics查看服务当前能做什么以及原因。遇到任何异常时,请先调用此工具——它会指出要打开的具体“系统设置”面板。
apple_mail_list_accounts列出账户 UUID、地址和邮箱名称。从这里开始。
apple_mail_list_mailboxes列出邮箱,可选包含邮件数量——一次从索引读取;不使用索引时每个约需 0.3 秒。
apple_mail_list_messages列出某个邮箱中最新的 N 封邮件,每封邮件都有一个 ref。
apple_mail_search_messages支持任意组合的筛选条件。body 可搜索邮件正文——参见正文搜索。
apple_mail_count_messages按数据来源标注总数和未读数。
apple_mail_list_open_drafts列出当前在 Mail 中打开的草稿;每封草稿都有一个 composer 句柄、收件人和独立于引用内容的正文,正文由应用的 apple_desktop_mail_composers 动词读取。

每封邮件都有一个不透明的 ref(m1:<accountUuid>/<mailbox>#<id>),读取和操作工具都使用它。该标识带有版本信息并包含邮箱,因此行 ID 永远不会被用于错误的邮箱。请勿手动构造。

附件

邮件几乎总是将附件正文存储在邮件文件之外的伴随目录树中——在真实的邮件存储中,抽样检查的附件没有一个是内嵌的。因此,仅解析邮件无法确定附件的大小,也无法判断附件是否可获取,而两个看似合理的捷径都不正确:

  • MIME 部分非空不代表字节数据存在。剥离内容后会留下一字节的分隔符空白,看起来就像存在一个 1 字节的附件。
  • X-Apple-Content-Length 不是字节大小。它记录的是 base64 编码后的长度,所以一个 164,156 字节的 PDF 显示为 224,634。

因此,list_attachments 会报告磁盘上文件的 sizeBytes,并提供一个 retrievable 标志,说明 save_attachment 是否确实能够成功,而不是根据邮件进行猜测。

安全性

影响范围。 此服务可以读取你的整个邮件归档;启用写入后,还可以用你的身份发送邮件。这里有两项相互独立的控制措施:

  • APPLE_MAIL_ALLOW_WRITES(默认关闭)控制所有变更操作。关闭时,写入工具不会注册——对模型不可见,而不仅仅是拒绝执行。
  • APPLE_MAIL_ACCOUNTS 限制哪些帐户可见。这是读取侧的控制,ALLOW_WRITES 无法覆盖它。该限制集中在一处强制执行,因此任何查询路径都无法绕过。

实现后,发送操作默认会保留一封草稿供你审核;要实际发送,必须同时启用 ALLOW_WRITES 并明确指定 confirm: true。

不含机密信息。 此服务不持有任何凭据——它通过你授予宿主应用的 macOS 权限进行访问。这里没有任何信息可泄露,也不会向任何地方发送任何内容:完全不会进行网络调用。

附件。 save_attachment 只能写入 APPLE_MAIL_ATTACHMENT_DIR 目录中,并且会先将文件名转换为基本文件名——即使发件人将附件命名为 ../../../.ssh/authorized_keys,你下载文件夹中也只会得到一个名为 authorized_keys 的文件,仅此而已。除非你明确要求,否则绝不会覆盖现有文件。

反过来,send_message、reply_to_message 和 forward_message 接受 attachments,即 Mail 会读取并附加的绝对路径。默认情况下,邮件会先保存为草稿,你可以在发送前查看文件。除非设置了 APPLE_MAIL_ALLOW_FILE_SEND,否则不允许在一次调用中附加并发送(sendNow),因为这样无人审核,而路径可能让此服务能够读取的任何内容都被邮寄出去。

不使用 shell。 此软件包中唯一会生成进程的地方使用 execFile,绝不使用 exec,也绝不会将调用方输入插入脚本文本。脚本是通过 stdin 传给 osascript 的静态常量;每个值都以 JSON 参数的形式传递。名为 "; do shell script "touch /tmp/pwned"; // 的邮箱名称只是数据,不是语法——对此有专门的测试,并且还有一道防护机制,会拒绝运行任何包含 ${ 的脚本。

依赖项。 两项:MCP SDK 和 zod。SQLite 来自 Node 24 内置的 node:sqlite。

配置

变量默认值说明
APPLE_MAIL_ALLOW_WRITES0注册会修改数据的工具。
APPLE_MAIL_ACCOUNTSall以逗号分隔的账户名称或 UUID。
APPLE_MAIL_DEBUGoff将详细日志输出到 stderr。
APPLE_MAIL_INDEX_MODEautoauto | ro | immutable | off。off 会完全禁用索引通道。
APPLE_MAIL_ROOTauto覆盖 Mail 的数据根目录。通常会从 Mail 本身发现。
APPLE_MAIL_ENVELOPE_INDEXauto显式指定索引路径,用于测试和取证副本。
APPLE_MAIL_OSASCRIPT_TIMEOUT_MS30000为首次运行时会阻塞的权限提示设置的超时时间。
APPLE_MAIL_DEGRADED_MAX_MESSAGES50Apple Events 列表通道的上限。
APPLE_MAIL_MAX_RESULTS200搜索结果上限。
APPLE_MAIL_BODY_MAX_BYTES262144正文截断限制,用于保护上下文窗口。
APPLE_MAIL_ATTACHMENT_DIR~/Downloads附件只能保存到此目录。
APPLE_MAIL_ALLOW_FILE_SENDoff允许撰写工具在同一次调用中附加本地文件并发送。
APPLE_MAIL_ALLOW_WATCHESoff注册 watch 和 unwatch:供 Agent 读取、供主机订阅的资源。
APPLE_MAIL_ALLOW_EVENTSoff提供实验性的 mail.received 事件(仅限 2026-07-28 的 MCP Events 扩展草案)。
APPLE_MAIL_MAILBOX_CACHE_TTL_MS60000账户/邮箱映射的缓存时长。

注意事项

这些问题很容易让人踩坑,因此在此记录,免得别人再重新发现:

  • Mail 的 unread count 可能完全不准确。 在这台机器上,一个邮箱报告的 unreadCount = 0,但 messages whose read status is false 的计数是 1618,而 Mail 自己的徽标 显示 37——同一个邮箱出现了三个数字。这是一个缓存值。因此,报告计数时会注明其来源,而不会合并。
  • Gmail 账户会将所有内容保存在 [Gmail]/All Mail 中。 INBOX 成员资格是一个 标签,因此 直观的索引查询(WHERE mailbox = ?)会返回空收件箱。邮箱名称会通过逐级剥离 [Gmail]/ 前缀来解析。
  • 磁盘上的目录布局也有嵌套,而 ref 无法识别这些嵌套。 All Mail 并不在 <account>/All Mail.mbox 中,而是在 <account>/[Gmail].mbox/All Mail.mbox 中;像 Work/Projects 这样的标签会嵌套两层。索引记录了完整路径,但上面的逐级剥离会在生成 ref 之前将其 简化为叶节点,因此文件通道只能获取 All Mail,而没有其他信息。因此,它会遍历账户根目录下的 *.mbox 目录(绝不进入 Data/),并在叶节点名称不唯一时,选择实际包含该 rowid 的容器。此处 使用扁平连接会悄无声息地禁用 Gmail 账户上的所有邮件文件功能。
  • 在受 TCC 保护的路径上,existsSync 也会成功。 它基于 stat,因此回答的是“是否存在”,而不是“是否 可以读取”——返回 EPERM 的是 readdirSync 和 readFileSync。是否存在与是否可读是两个不同的问题, 因此 apple_mail_diagnostics 会读取真实邮件文件的一个字节,而不是对其执行 stat;查找操作也能区分 “路径错误”和“没有权限”,而不会将两者都归咎于“完全磁盘访问权限”。
  • 用 immutable=1 打开索引是错误的做法,尽管这是常见建议。它会告诉 SQLite 忽略 -wal 文件,而 Mail 运行在 WAL 模式下,因此读取时可能会漏掉尚未检查点写入的内容——恰好是 Agent 通常会被询问的最新邮件。 请注意,这是一个竞态,而非必然如此:在一个正在运行的 437 MB 索引上,同时探测两种模式,且存在一个 1 MB 的 -wal 文件时,返回了相同的 MAX(ROWID),因为最新邮件恰好已经完成检查点写入。默认使用 mode=ro 是为了避免这个问题,而不是因为已经观察到数据过时。
  • 读取邮件不会启动 Mail。 如果 Mail 未运行,读取工具会正常报错,而不会启动它,因为启动 Mail 会抢占 焦点并开始同步。

正文搜索

search_messages 接受一个 body 词条,这是唯一没有索引支持的筛选条件。

没有可供索引的内容。Envelope Index 不包含 FTS 表,而 Spotlight 卷索引完全排除了 ~/Library——无论查询如何措辞,mdfind 都不会返回该路径下的任何结果。这是 Spotlight 的覆盖范围所致,与邮件无关。Mail 自身的正文搜索依赖 CoreSpotlight 提供的数据,由提供数据的应用通过 CSSearchQuery 查询,我们无法在此访问。测量数据见 docs/mail-body.md。

因此,索引会缩小范围,扫描只读取剩余候选项。耗时与候选项数量呈线性关系,在一个包含 181,734 封邮件的存储中测得,每封邮件耗时 0.48 ms:

其他筛选条件之后的候选项耗时
100 — 筛选范围较小48 ms
500242 ms
1,932 — 一个邮箱的 90 天邮件0.9 s
6,566 — 所有邮箱的 90 天邮件3.2 s
182,329 — 未设置任何筛选条件88 s,无法使用

因此,限制会明确告知,而非暗中执行。超过 APPLE_MAIL_BODY_SCAN_MAX(2,000)时,搜索会返回 degraded,指出候选项数量和上限,并且不执行扫描。如果静默地只检查最新的 N 封邮件,那么对于较旧邮件中的内容,返回“未找到”就无法与确实不存在区分开来;模型无法判断两者的区别,而它可以读取拒绝信息并据此采取行动。

需要了解的两个影响:

  • 将 body 与缩小范围的筛选条件结合使用。 mailbox、sender、dateFrom——任选其一即可。对数百个候选项进行正文搜索几乎是即时完成的。
  • 扫描会读取每个文件开头的 APPLE_MAIL_BODY_SCAN_BYTES(64 KB)。 实际存储中 79% 的字节是 base64 编码内容,文本搜索不会匹配;而 MIME 会将文本部分放在附件之前——因此,这种做法舍弃了没人需要的文件尾部内容,换取每个候选项都能降低开销。若词条藏在读取上限之后,就会被漏掉;如果这比延迟更重要,可以调高上限。

开发

pnpm install
pnpm dev            # tsdown --watch
pnpm test           # vitest, fully offline
pnpm typecheck
pnpm lint && pnpm format:check
pnpm probe          # the phase 0 spike (needs Full Disk Access)

测试离线运行且完全隔离:进程边界是注入的,因此队列、注入探针和信封处理都能真实运行,而无需启动 osascript 或触碰 Mail。

许可证

MIT

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。