使用说明
苹果笔记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可能会提示自动权限。
如果您没有收到提示,或者之前已被拒绝:
- 打开
System Settings > Privacy & Security > Automation. - 找到启动MCP服务器的主机进程。
- 允许访问
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解析的已删除笔记IDdeleted: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对象:
accountoldPathnewPathnameparentPathdepthrenamed
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对象:
sourceAccountsourcePathdestinationAccountdestinationPathfoldermovedmovedNoteCountfailedNoteMoves(数组{noteID, noteTitle, sourceFolder, destinationFolder, reason})createdFolders(新创建的文件夹路径数组)sourceDeletedpartial
move_note
按ID将一个笔记移动到目标文件夹路径。
输入参数:
id(必填,非空string)folder(必填string,支持嵌套路径,如Parent/Child)account(可选string,将文件夹解析范围限定到特定帐户)
退货:
- JSON对象:
id:注释IDtitle:尽力注释元数据标题(可以为空)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)包括:
idtitlebody(null当没有请求或不可用时;纯html、markdown或原始html,具体取决于bodyFormat)foldercreatedAt(ISO8601,分数秒)modifiedAt(ISO8601,分数秒)
时间戳注释:
- 有效载荷时间戳为UTC(
Z)ISO8601字符串。Notes.app UI可能会显示当地时间。
删除有效载荷(delete_note)包括:
iddeleted
删除文件夹负载(delete_folder)包括:
accountpathfolderdeleted
重命名文件夹负载(rename_folder)包括:
accountoldPathnewPathnameparentPathdepthrenamed
移动文件夹负载(move_folder)包括:
sourceAccountsourcePathdestinationAccountdestinationPathfoldermoved
创建文件夹负载(create_folder)包括:
accountpathnameparentPathdepthalreadyExisted
批量获取有效载荷(batch_get_notes)包括:
notes(钞票有效载荷数组)missingIDs(未找到字符串ID数组)
批量删除有效载荷(batch_delete_notes)包括:
deletedIDs(已解析的钞票ID数组已删除)missingIDs(未找到字符串ID数组)failed(数组{ id, reason }行)
移动有效载荷(move_note)包括:
idtitle(尽最大努力;可能为空)accountpathfoldermodifiedAt
帐户有效负载(list_accounts)包括:
name
文件夹负载(list_folders)包括:
accountpathnameparentPath(根文件夹为空字符串)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.