Apple Notes 管理协议服务

一个基于Swift 6的JSON-RPC服务器,提供对Apple Notes账户、文件夹和笔记的全面管理功能,包括创建、读取、更新、删除和移动操作。

  • by TheInkedEngineer
  • Swift
  • 本地部署
  • 5 星标
  • 更新于
在 GitHub 查看 TheInkedEngineer/apple-notes-mcp

使用说明

苹果笔记MCP(Swift)

Apple Notes的Swift 6模型上下文协议(MCP)服务器。

它作为stdio JSON-RPC进程运行,并公开了检查帐户/文件夹以及列出、读取、创建、更新、移动和删除Apple Notes的工具。

此服务器提供什么

  • list_notes:元数据列表,包括搜索、排序、分页和可选的正文呈现格式。
  • list_accounts:Apple Notes帐户名的帐户发现。
  • list_folders:使用帐户/路径元数据进行递归文件夹发现。
  • get_note:通过注释ID进行完整注释检索,并输出纯文本、标记或原始HTML正文。
  • batch_get_notes:通过ID进行多注释检索,部分成功输出,并进行纯/markdown/原始html正文渲染。
  • batch_delete_notes:按ID删除多个音符,部分成功输出(deletedIDs, missingIDs, failed).
  • create_folder:具有嵌套路径支持和幂等性的确定性文件夹创建 mkdir -p 语义。
  • create_note:创建注释,可选择文件夹定位和纯/html/markdown正文输入。
  • update_note:注意按ID更新标题/正文突变,使用纯/html/markdown输入和可配置的输出格式。
  • rename_folder:按路径重命名文件夹,可选择帐户范围。
  • delete_folder:具有明确级联确认安全门的文件夹删除。
  • delete_note:按笔记ID删除笔记。
  • move_folder:文件夹移动到具有循环/无操作保护的目标父路径。
  • move_note:按笔记ID将笔记移动到目标文件夹路径。
  • 在工具执行之前进行Apple Notes自动化权限检查。
  • 主要参与者安全的AppleScript执行(NSAppleScript 不是线程安全的)。
  • AppleScript调用被包裹在20秒的脚本超时中,以避免Notes无响应时无限期挂起。
  • MCP工具结果的稳定JSON有效载荷输出。

需求

  • macOS 13+
  • Swift 6工具链
  • 在当前macOS用户会话中访问Apple Notes应用程序数据
  • MCP客户端(例如Claude Code)

快速开始

1) 安装

自制(推荐)

brew tap TheInkedEngineer/theinkedengineer
brew install apple-notes-mcp

卷发器安装器

最新版本:

curl -fsSL https://raw.githubusercontent.com/TheInkedEngineer/apple-notes-mcp/main/scripts/install.sh | bash

具体版本(示例):

curl -fsSL https://raw.githubusercontent.com/TheInkedEngineer/apple-notes-mcp/main/scripts/install.sh | bash -s -- 1.0.0

注:已发布的二进制工件目前针对Apple Silicon(darwin_arm64).

2) 从源代码构建(可选)

swift build -c release

二元的:

.build/release/apple-notes-mcp

3) 配置Claude代码

添加 ~/.claude.json:

{
  "mcpServers": {
    "apple-notes": {
      "command": "/absolute/path/to/apple-notes-mcp/.build/release/apple-notes-mcp",
      "args": []
    }
  }
}

更新配置后重新启动Claude Code。

4) 配置Codex(MCP兼容客户端)

对于支持MCP服务器注册的Codex客户端,请在MCP设置中添加等效的服务器条目:

{
  "mcpServers": {
    "apple-notes": {
      "command": "apple-notes-mcp",
      "args": []
    }
  }
}

如果 apple-notes-mcp 不在你的 PATH,请改用绝对二进制路径。

5) 授予macOS自动化权限

在第一次调用工具时,macOS可能会提示自动权限。

如果您没有收到提示,或者之前已被拒绝:

  1. 打开 System Settings > Privacy & Security > Automation.
  2. 找到启动MCP服务器的主机进程。
  3. 允许访问 Notes.

重要主机进程细节:

  • 如果Claude Code在iTerm内部运行,则iTerm是需要权限的进程。
  • 如果Claude Code从另一个终端/应用程序运行,则该主机需要权限。

工具

list_accounts

列出所有Apple Notes帐户名。

输入参数:

  • 无

退货:

  • 帐户对象的JSON数组:
    • name

行为说明:

  • 结果按帐户名不区分大小写排序。

list_folders

递归列出Apple Notes文件夹。

输入参数(可选):

  • account (string):范围到一个帐户(例如 iCloud, On My Mac).

退货:

  • JSON文件夹对象数组:
    • account:拥有帐户名
    • path:完整文件夹路径(Parent/Child)
    • name:叶文件夹名称
    • parentPath:父路径(顶级文件夹为空字符串)
    • depth:嵌套深度(0 对于顶级)

行为说明:

  • 没有 account,将返回所有帐户中的文件夹。
  • 随着 account,只返回该帐户中的文件夹。
  • 未知帐户返回描述性错误。
  • 结果按不敏感的情况进行排序 account那么 path.

list_notes

列出注释元数据。默认情况下省略正文内容(body: null)为了提高可扩展性,可以使用 includeBody=true.

输入参数(全部可选):

  • limit (integer >= 0):最大退回钞票数。 0 意味着无限。
  • offset (integer >= 0):跳过前N个匹配的音符。需要 limit > 0.
  • searchText (string):非空修剪文本查询。
  • searchIn ("title" | "body" | "all"):默认为 title 当 searchText 已设置。
  • account (string):可选帐户范围(例如 iCloud, On My Mac).
  • folder (string):可选嵌套文件夹路径范围(例如 Jokes/IT).
  • includeBody (boolean):包括退回笔记的正文内容。
  • bodyFormat ("plain" | "markdown" | "html"):主体输出格式 includeBody=true当被忽视时 includeBody=false.
  • orderBy ("modified" | "created"):排序字段。
  • orderDirection ("recent" | "oldest"):订购方向。
  • createdAfter (string):ISO8601创建日期下限(含)。
  • createdBefore (string):创建日期的ISO8601上限(含)。仅限日期的输入被视为一天的结束。
  • modifiedAfter (string):修改日期为ISO8601下限(含)。
  • modifiedBefore (string):修改日期为ISO8601上限(含)。仅限日期的输入被视为一天的结束。

行为说明:

  • 快速路径优化用于简单的仅元数据限制查询:
    • limit > 0
    • 不 searchText
    • 不 orderBy/orderDirection
    • includeBody=false
    • 不 folder 作用域(仅支持帐户作用域)
    • 在此模式下,AppleScript仅获取所请求的元数据窗口(offset + limit)同时返回相同的元数据形状。
  • 没有 orderBy,使用本机Notes遍历顺序。
  • orderDirection=oldest 没有 orderBy 在内部遍历反向原生顺序。
  • searchIn=body 和 searchIn=all 执行批量身体查找。
  • 对于 searchIn=all,在正文查找之前接受标题匹配,以减少AppleScript调用。
  • includeBody=true 重用在正文搜索期间已提取的正文,只提取缺失的选定音符ID。
  • includeBody=true 不进行搜索,只获取最终选定笔记的正文。
  • bodyFormat="html" 返回原始存储的Notes HTML(无标题剥离或转换)。
  • 当元数据标题为空时, list_notes 从正文标题中解析标题,以实现一致的标题行为 get_note 和 batch_get_notes.
  • 正文搜索始终匹配从Notes HTML转换而来的纯文本,而不是原始标记。
  • 如果单个笔记的正文查找失败,这些笔记仍将返回 body: null (不是查询错误)。
  • 文件夹过滤被推送到AppleScript元数据获取中,因此管道的其余部分仅处理作用域注释中的内容。

文件夹目标格式:

  • account 省略+ folder 提供:解析所有帐户的文件夹路径(必须是唯一的)。
  • account 提供+ folder 省略:列出该帐户中的所有注释。
  • account + folder:解析该帐户内的文件夹路径。
  • folder 支持嵌套 / (例如 Parent/Child/Leaf).
  • 文件夹匹配区分大小写。

验证规则:

  • 负面的 limit 或 offset 无效。
  • offset 需要一个积极的 limit.
  • searchIn 需要 searchText.
  • 格式错误的文件夹路径(例如 /Work, Work/, Work//Sub)被拒绝。
  • 文件夹查找错误是显式的(account not found, folder not found, ambiguous folder).

get_note

通过ID获取一张完整的钞票。

输入参数:

  • id (必填,非空 string)
  • bodyFormat (可选 "plain" | "markdown" | "html",默认为 "plain")

退货:

  • 一个JSON数组,成功时有一个note对象。
  • MCP错误结果(isError: true)如果验证失败,则找不到注释,或者脚本执行失败。

batch_get_notes

在一次通话中按ID获取多张钞票。

输入参数:

  • ids (非空字符串的必需数组)
  • bodyFormat (可选 "plain" | "markdown" | "html",默认为 "plain")

退货:

  • JSON对象:
    • notes:已找到的笔记有效载荷数组
    • missingIDs:找不到ID数组

行为说明:

  • 设计上部分成功:缺少ID不会导致整个请求失败。
  • 重复的ID会在第一次出现时进行重复数据消除。
  • 重复数据删除后,返回的笔记遵循输入ID顺序。
  • ID在内部分批处理(默认批量大小为100)。
  • 在批次之间检查取消情况。

batch_delete_notes

在一次通话中按ID删除多个笔记。

输入参数:

  • ids (非空字符串的必需数组)

退货:

  • JSON对象:
    • deletedIDs:已成功删除已解析的笔记ID数组
    • missingIDs:找不到ID数组
    • failed:数组 { id, reason } 对于已找到但不可删除的ID

行为说明:

  • 设计上部分成功:缺少/失败的ID不会使整个请求失败。
  • 重复的ID会在第一次出现时进行重复数据消除。
  • deletedIDs 包含删除前由Notes解析的ID(与 delete_note).
  • ID在内部分批处理(默认批量大小为100)。
  • 在批次之间检查取消情况。

create_note

创建新笔记并返回创建的笔记有效负载。

输入参数:

  • title (必填,非空 string)
  • body (可选 string,默认为空)
  • bodyFormat (可选 "plain" | "html" | "markdown",默认为 "plain")
  • account (可选 string)
  • folder (可选 string)

放置行为:

  • account 省略+ folder 省略:默认注释位置。
  • account 提供+ folder 省略:该帐户中的默认位置。
  • account 省略+ folder 提供:解析所有帐户的文件夹路径(必须是唯一的)。
  • account + folder:解析该帐户内的文件夹路径。
  • folder 支持嵌套 / (例如 Parent/Child).

文件夹匹配区分大小写。

身体语义:

  • bodyFormat="plain":
    • 输入正文为纯文本。
    • &, <, > HTML在创建之前被转义。
    • 新线路得以保留(\n, \r\n, \r 归一化为换行符)。
  • bodyFormat="markdown":
    • 输入体已标记。
    • Markdown在保存前转换为Notes友好的富HTML。
    • 如果你想按原样存储文字标记文本,请使用 bodyFormat="plain".
  • bodyFormat="html":
    • 输入正文被视为富HTML,并按照提供的方式发送到Notes。
    • 使用此模式可以保留标题、列表、表格、链接和内联样式。

标题存储语义:

  • create_note 首先将可见标题存储为正文HTML中的前导富标题块。
  • 然后设置Apple Notes元数据标题(name)创建后,避免正文中的标题行重复。
  • 读取原始HTML时(bodyFormat="html"),苹果可能会将该标题序列化为基于样式的HTML(例如粗体+字体大小),而不是文字 <h1> 标签。

错误行为:

  • 格式错误的文件夹路径(例如 /Work, Work/, Work//Sub)返回验证错误。
  • 跨帐户的文件夹名称不明确会返回描述性不明确错误。
  • 缺少帐户/文件夹返回说明找不到错误。

create_folder

创建文件夹路径并返回解析的文件夹元数据。

输入参数:

  • folder (必填 string,支持嵌套路径,如 Parent/Child)
  • account (可选 string,将创建范围限定到特定帐户)

行为说明:

  • 自动创建缺失的中间段(mkdir -p 语义)。
  • alreadyExisted=true 只有当每个路径段都已经存在时。
  • 随着 account + folder,创建仅限于该帐户。
  • 随着 folder 仅限于:
    • 第一个路径段必须已经存在于一个帐户中
    • 如果它存在于多个帐户中,则返回歧义错误
    • 如果它不存在于任何帐户中,则会出现一个显式错误,要求 account
  • 文件夹创建没有隐式默认帐户回退。
  • 路径匹配区分大小写。

update_note

按ID更新现有笔记,并返回更新后的笔记有效载荷。

输入参数:

  • id (必填,非空 string)
  • title (可选非空 string)
  • body (可选 string)
  • bodyFormat (可选 "plain" | "html" | "markdown",默认为 "plain")
  • bodyMode (可选 "replace" | "append" | "prepend",默认为 "replace")
  • outputBodyFormat (可选 "plain" | "markdown" | "html",默认为 "plain")

行为说明:

  • 必须至少提供一个可变字段(title 和 body).
  • bodyMode 仅在以下情况下有效 body 提供。
  • 支持标题/正文更新;重新定位不在此工具的范围内。
  • 使用 move_note 用于文件夹/帐户移动。
  • 在写入时,Notes正文存储始终保持前导H1标题与有效标题同步。
  • bodyFormat="plain" 商店转义了纯文本。
  • bodyFormat="markdown" 存储从markdown输入中呈现的富文本HTML。
  • bodyFormat="html" 商店按原样提供丰富的HTML。
  • outputBodyFormat="html" 返回原始存储的Notes HTML。
  • outputBodyFormat="plain" 和 "markdown" 在格式化响应内容之前,删除注入的前导标题。

错误行为:

  • 缺少/无效/为空的验证错误 id,格式枚举无效,更新请求为空。
  • 当注释不存在时,找不到结构化映射。
  • 脚本/运行时失败表现为MCP isError: true.

delete_note

按ID删除一个注释。

输入参数:

  • id (必填,非空 string)

退货:

  • JSON对象:
    • id:删除前由Notes解析的已删除笔记ID
    • deleted: true

错误行为:

  • 缺少/无效/为空的验证错误 id.
  • 当注释不存在时,找不到结构化映射。
  • 脚本/运行时失败表现为MCP isError: true.

delete_folder

删除一个文件夹路径。

这始终是一个级联删除操作:

  • 目标文件夹和所有子文件夹都将被删除。
  • 里面的笔记被移动到 Recently Deleted 作为注释(不保留文件夹上下文)。
  • 注意事项 Recently Deleted 约30天后,Notes将永久删除。

输入参数:

  • folder (必填 string,支持嵌套路径,如 Parent/Child)
  • confirmCascadeDelete (必填 boolean,必须 true)
  • account (可选 string,将文件夹解析范围限定到特定帐户)

安全行为:

  • confirmCascadeDelete 是必需的,并且必须是文字布尔值 true.
  • 没有非级联模式。
  • 省略国旗,传递 false,或者传递非布尔值会返回验证错误。

分辨率行为:

  • 随着 account + folder,目标路径在该帐户内解析。
  • 随着 folder 仅在所有帐户中解析目标路径:
    • 不匹配:找不到文件夹错误
    • 多个匹配项:文件夹不明确错误
  • 仅限帐户的呼叫无效(folder 是必需的)。

系统文件夹保护:

  • 顶级系统文件夹,如 Notes 和 Recently Deleted 无法删除。

退货:

  • JSON对象:
    • account:已解析的帐户名
    • path:已解析完整文件夹路径
    • folder:已解析叶子文件夹名称
  • deleted: true

rename_folder

重命名一个文件夹路径。

输入参数:

  • folder (必填 string,支持嵌套路径,如 Parent/Child)
  • newName (必填非空 string,不得包含 /)
  • account (可选 string,将文件夹解析范围限定到特定帐户)

行为说明:

  • 随着 account + folder,目标路径在该帐户中已解析。
  • 随着 folder 仅在所有帐户中解析目标路径:
    • 不匹配:找不到文件夹错误
    • 多个匹配项:文件夹不明确错误
  • 如果 newName 等于当前叶名,重命名是不可行的(renamed=false).
  • 顶级系统文件夹(Notes, Recently Deleted)无法重命名。
  • 同一父路径中的名称冲突会返回描述性错误。

退货:

  • JSON对象:
    • account
    • oldPath
    • newPath
    • name
    • parentPath
    • depth
    • renamed

move_folder

将一个文件夹路径移动到目标父文件夹路径中。实现为编排:创建目标文件夹树,按ID移动笔记,然后删除源。

输入参数:

  • folder (必填 string,源路径)
  • destinationFolder (必填 string,目标父路径--源文件夹的名称会自动附加,因此不要包含它。要将“path/To/Example”移动到“NewPath”中,请使用 NewPath",不 "NewPath/Example")
  • account (可选 string,源帐户范围)
  • destinationAccount (可选 string,目标账户范围;默认为已解析的源帐户)

行为说明:

  • 源路径和目标路径支持嵌套 / 并且区分大小写。
  • 如果 destinationAccount 省略,目标解析使用源帐户。
  • 钞票ID被保留;文件夹对象标识不是。
  • 如果缺少目标,则自动创建目标(mkdir -p 语义)。
  • 目标位置的同名文件夹会导致合并错误,而不是冲突错误。
  • 只有在成功移动所有笔记后,才会删除源文件夹。
  • 部分故障会使源保持未删除状态,并返回详细的故障信息。
  • 循环保护具有帐户意识:
    • 循环检查仅在源帐户和目标帐户匹配时运行。
    • 进入自我/后代是被拒绝的。
  • 无操作保护:
    • 移动到同一帐户中的当前父级被拒绝。
  • 顶级系统文件夹(Notes, Recently Deleted)无法移动。

退货:

  • JSON对象:
    • sourceAccount
    • sourcePath
    • destinationAccount
    • destinationPath
    • folder
    • moved
    • movedNoteCount
    • failedNoteMoves (数组 {noteID, noteTitle, sourceFolder, destinationFolder, reason})
    • createdFolders (新创建的文件夹路径数组)
    • sourceDeleted
    • partial

move_note

按ID将一个笔记移动到目标文件夹路径。

输入参数:

  • id (必填,非空 string)
  • folder (必填 string,支持嵌套路径,如 Parent/Child)
  • account (可选 string,将文件夹解析范围限定到特定帐户)

退货:

  • JSON对象:
    • id:注释ID
    • title:尽力注释元数据标题(可以为空)
    • account:目标帐户名
    • path:目标完整文件夹路径
    • folder:目标叶文件夹名称
    • modifiedAt:ISO8601时间戳

行为说明:

  • folder 总是需要的。
  • account 没有 folder 被拒绝。
  • 随着 account + folder,在该帐户中解析目标路径。
  • 随着 folder 仅在所有帐户中解析目标路径:
    • 不匹配:找不到文件夹错误
    • 多个匹配项:文件夹不明确错误
  • 相同帐户移动使用 move n to targetFolder 并保留原始钞票ID和时间戳。
  • 跨帐户移动在目的地重新创建笔记(创建+删除):笔记获得新的ID,时间戳重置,附件可能无法传输。
  • 跨账户范围的移动(account + folder)自动创建丢失的目标文件夹。
  • title 仅供参考。对待 id, account, path, folder,以及 modifiedAt 作为权威的移动确认字段。

打破变更通知

文件夹定位不再使用 AccountName/FolderName 里面 folder 价值。

请改用单独的参数:

  • account: "iCloud", folder: "Work"
  • folder: "Jokes/IT" (搜索所有帐户,必须是唯一的)

输出形状

工具响应是JSON文本内容。

注意有效载荷(list_notes, get_note, create_note, update_note)包括:

  • id
  • title
  • body (null 当没有请求或不可用时;纯html、markdown或原始html,具体取决于 bodyFormat)
  • folder
  • createdAt (ISO8601,分数秒)
  • modifiedAt (ISO8601,分数秒)

时间戳注释:

  • 有效载荷时间戳为UTC(Z)ISO8601字符串。Notes.app UI可能会显示当地时间。

删除有效载荷(delete_note)包括:

  • id
  • deleted

删除文件夹负载(delete_folder)包括:

  • account
  • path
  • folder
  • deleted

重命名文件夹负载(rename_folder)包括:

  • account
  • oldPath
  • newPath
  • name
  • parentPath
  • depth
  • renamed

移动文件夹负载(move_folder)包括:

  • sourceAccount
  • sourcePath
  • destinationAccount
  • destinationPath
  • folder
  • moved

创建文件夹负载(create_folder)包括:

  • account
  • path
  • name
  • parentPath
  • depth
  • alreadyExisted

批量获取有效载荷(batch_get_notes)包括:

  • notes (钞票有效载荷数组)
  • missingIDs (未找到字符串ID数组)

批量删除有效载荷(batch_delete_notes)包括:

  • deletedIDs (已解析的钞票ID数组已删除)
  • missingIDs (未找到字符串ID数组)
  • failed (数组 { id, reason } 行)

移动有效载荷(move_note)包括:

  • id
  • title (尽最大努力;可能为空)
  • account
  • path
  • folder
  • modifiedAt

帐户有效负载(list_accounts)包括:

  • name

文件夹负载(list_folders)包括:

  • account
  • path
  • name
  • parentPath (根文件夹为空字符串)
  • depth (基于0的嵌套级别)

运行和测试

运行完整测试:

swift test

仅运行单位目标:

swift test --filter AppleNotesMCPTests

仅运行集成烟雾目标:

swift test --filter AppleNotesMCPIntegrationTests

项目布局

  • Package.swift:包装和目标。
  • Sources/AppleNotesMCP/AppleNotesMCP.swift:进程入口点。
  • Sources/AppleNotesMCP/Server/:生命周期和关机编排。
  • Sources/AppleNotesMCP/Tools/:工具合同和实施。
  • Sources/AppleNotesMCP/Helpers/:AppleScript、解析、规范化、日志记录、权限。
  • Sources/AppleNotesMCP/Errors/:集中式错误命名空间(Error.*).
  • Sources/AppleNotesMCP/Models/:有效载荷和应用程序配置模型。
  • Tests/AppleNotesMCPTests/:确定性单元测试。
  • Tests/AppleNotesMCPIntegrationTests/:MCP接线烟雾测试。

故障排除

过程开始,但“什么也不做”

预期用于stdio MCP服务器。该进程等待来自客户端的JSON-RPC消息。

未出现macOS权限提示

  • 在自动化设置中验证正确的主机应用程序(终端、iTerm、Claude Code主机进程)。
  • 确保该工具已被实际调用;提示出现在工具执行时,而不是进程启动时。

“尚未授予Apple Notes的自动化权限”

在自动化设置中为主机进程授予权限,然后再次调用该工具。

工具调用似乎挂起

  • AppleScript调用被包裹在20秒的超时中。
  • 如果Notes忙/无响应,则调用应失败,并出现与超时相关的AppleScript错误,而不是无限期挂起。

日志中断协议通信

不要将诊断打印到stdout。MCP JSON-RPC使用stdout。此项目通过以下方式登录到stderr Logger.

开发者文档

  • docs/DEVELOPMENT.md:架构、数据流、错误模型、扩展工作流。
  • docs/TOOLS.md:详细的工具契约和算法语义。
  • docs/TESTING.md:套件设计、接缝以及如何添加确定性测试。
  • docs/RICH_TEXT.md:注意HTML指南和markdown呈现行为。

许可证

MIT。看 LICENSE.