EmDash 在 /_emdash/api/mcp 提供内置的 Model Context Protocol(MCP)服务器。MCP 客户端通过它读取和管理内容、Byline、模式、媒体、分类法、菜单、修订和设置,并导出或导入整个站点。
认证
MCP 端点需要 Bearer 令牌。EmDash 支持以下令牌流程:
| 方法 | 用途 |
|---|---|
| OAuth 2.1 Authorization Code with Proof Key for Code Exchange (PKCE) | 交互式 MCP 客户端。用户在浏览器中批准所请求的 scope。 |
| Personal access token | 供客户端或自动化使用的长期访问。令牌使用 ec_pat_ 前缀,在管理后台创建。 |
| OAuth 2.0 Device Authorization Grant | 要求用户在浏览器中批准代码的命令行客户端。emdash login 使用此流程。 |
会话 Cookie 不能用于认证 MCP 端点。
Scopes
OAuth 令牌和个人访问令牌会限制客户端可以调用哪些工具。用户的角色会单独检查,因此 scope 永远不会授予用户本身没有的权限。
| Scope | 访问权限 |
|---|---|
content:read | 读取和搜索内容、Byline、分类法、术语、菜单和修订。类草稿内容还需要用户具有 content:read_drafts 权限。 |
content:write | 创建和更改内容、Byline 和修订。为了兼容现有令牌,它还授予 taxonomies:manage 和 menus:manage。 |
media:read | 读取媒体记录。 |
media:write | 上传、注册、更新和删除媒体。 |
schema:read | 读取集合和字段。 |
schema:write | 创建、更新和删除集合和字段。 |
taxonomies:manage | 创建、更新和删除分类法定义和术语。 |
menus:manage | 创建、更新和删除菜单及菜单项。 |
settings:read | 读取站点设置。 |
settings:manage | 更新站点设置。 |
mcp:tools | 调用任何已启用插件公开的 MCP 工具。 |
mcp:tools:<pluginId> | 调用某一个已启用插件公开的 MCP 工具。 |
transfer:export | 将整个站点导出为站点包并下载。 |
transfer:analyze | 上传站点包并分析其是否可以导入。 |
transfer:execute | 开始、推进、取消和放弃站点导入。 |
admin | 调用所有核心工具,包括站点迁移工具。插件工具仍然需要 mcp:tools 或插件专用的 scope。 |
admin scope 包含 transfer:export、transfer:analyze 和 transfer:execute。每个迁移 scope 只授予其自身的操作,并且要求管理员角色。若要让客户端(例如智能体)只分析站点包而不导出或导入,请授予 transfer:analyze 而不是 admin。
授权码同意页面允许用户移除所请求的 scope。EmDash 还会将请求与客户端已注册的 scope 以及用户的角色取交集,并拒绝空授权。
角色要求
下表显示了各项宽泛能力所需的最低角色。当用户操作其他用户的内容时,所有权检查可能要求更高的角色。
| 能力 | 最低角色 |
|---|---|
| 读取已发布的内容、媒体、分类法、术语和菜单 | Subscriber |
| 读取草稿、定时内容、回收站、对比和修订 | Contributor |
| 创建内容或上传媒体 | Contributor |
| 编辑或发布自己的内容并注册媒体 | Author |
| 管理 Byline、分类法、菜单或所有用户的内容 | Editor |
| 读取模式或设置 | Editor |
| 更改模式或设置、永久删除内容或修复媒体使用情况 | Admin |
| 导出或导入整个站点 | Admin |
完整的角色定义请参阅用户角色。
传输
服务器使用无状态的 Streamable HTTP。每个请求都是独立的;服务器不会保留 MCP 会话或 Server-Sent Events 连接。
| 方法 | 端点 | 行为 |
|---|---|---|
POST | /_emdash/api/mcp | 接受 JSON-RPC 的初始化、工具列表和工具调用。 |
GET | /_emdash/api/mcp | 返回 405 Method Not Allowed。 |
DELETE | /_emdash/api/mcp | 返回 405 Method Not Allowed。 |
响应使用 JSON-RPC 2.0。构造工具请求之前,请调用 tools/list 获取当前的输入 schema 和 MCP 注解。
工具清单
下面的清单与 tools/list 返回的静态工具一致。其中包含已注册的标题,因为客户端可能会显示标题而不是工具名称。
内容工具
| 工具 | 已注册标题 | 所需 scope |
|---|---|---|
content_list | List Content | content:read |
content_get | Get Content | content:read |
content_create | Create Content | content:write |
content_update | Update Content | content:write |
content_delete | Delete Content (Trash) | content:write |
content_restore | Restore Content | content:write |
content_permanent_delete | Permanently Delete Content | content:write |
content_publish | Publish Content | content:write |
content_unpublish | Unpublish Content | content:write |
content_schedule | Schedule Content | content:write |
content_unschedule | Cancel Scheduled Publication | content:write |
content_compare | Compare Live vs Draft | content:read |
content_discard_draft | Discard Draft | content:write |
content_list_trashed | List Trashed Content | content:read |
content_duplicate | Duplicate Content | content:write |
content_translations | Get Content Translations | content:read |
Byline 工具
| 工具 | 已注册标题 | 所需 scope |
|---|---|---|
byline_list | List Bylines | content:read |
byline_get | Get Byline | content:read |
byline_create | Create Byline | content:write |
byline_update | Update Byline | content:write |
byline_delete | Delete Byline | content:write |
byline_translations | List Byline Translations | content:read |
模式工具
| 工具 | 已注册标题 | 所需 scope |
|---|---|---|
schema_list_collections | List Collections | schema:read |
schema_get_collection | Get Collection Schema | schema:read |
schema_list_block_types | List Block Types | schema:read |
schema_get_block_type | Get Block Type | schema:read |
schema_create_block_type | Create Block Type | schema:write |
schema_update_block_type | Update Block Type | schema:write |
schema_activate_block_type_version | Activate Block Type Version | schema:write |
schema_create_collection | Create Collection | schema:write |
schema_delete_collection | Delete Collection | schema:write |
schema_update_collection | Update Collection | schema:write |
schema_create_field | Add Field to Collection | schema:write |
schema_delete_field | Remove Field from Collection | schema:write |
schema_update_field | Update Field | schema:write |
媒体工具
| 工具 | 已注册标题 | 所需 scope |
|---|---|---|
media_list | List Media | media:read |
media_create | Confirm Signed Media Upload | media:write |
media_upload | Upload Media | media:write |
media_get | Get Media Item | media:read |
media_update | Update Media Metadata | media:write |
media_delete | Delete Media | media:write |
media_usage_repair | Repair Media Usage Index | admin |
搜索工具
| 工具 | 已注册标题 | 所需 scope |
|---|---|---|
search | Search Content | content:read |
分类法工具
| 工具 | 已注册标题 | 所需 scope |
|---|---|---|
taxonomy_list | List Taxonomies | content:read |
taxonomy_get | Get Taxonomy Definition | content:read |
taxonomy_create | Create Taxonomy Definition | taxonomies:manage |
taxonomy_update | Update Taxonomy Definition | taxonomies:manage |
taxonomy_delete | Delete Taxonomy Definition | taxonomies:manage |
taxonomy_list_terms | List Taxonomy Terms | content:read |
taxonomy_create_term | Create Taxonomy Term | taxonomies:manage |
taxonomy_update_term | Update Taxonomy Term | taxonomies:manage |
taxonomy_delete_term | Delete Taxonomy Term | taxonomies:manage |
taxonomy_term_translations | List Term Translations | content:read |
菜单工具
| 工具 | 已注册标题 | 所需 scope |
|---|---|---|
menu_list | List Menus | content:read |
menu_get | Get Menu with Items | content:read |
menu_translations | List Menu Translations | content:read |
menu_create | Create Menu | menus:manage |
menu_update | Update Menu | menus:manage |
menu_delete | Delete Menu | menus:manage |
menu_set_items | Set Menu Items | menus:manage |
修订工具
| 工具 | 已注册标题 | 所需 scope |
|---|---|---|
revision_list | List Revisions | content:read |
revision_restore | Restore Revision | content:write |
设置工具
| 工具 | 已注册标题 | 所需 scope |
|---|---|---|
settings_get | Get Site Settings | settings:read |
settings_update | Update Site Settings | settings:manage |
站点迁移工具
transfer:* 表示 transfer:export、transfer:analyze 或 transfer:execute 中的任意一个。admin scope 满足此表中的每一项要求。
| 工具 | 已注册标题 | 所需 scope |
|---|---|---|
site_transfer_capabilities | Get Site Transfer Capabilities | transfer:* |
site_export_start | Start Site Export | transfer:export |
site_export_status | Get Site Export Status | transfer:export |
site_import_analyze | Analyze Site Import | transfer:analyze |
site_import_start | Start Site Import | transfer:execute |
site_import_status | Get Site Import Status | transfer:* |
site_import_resume | Resume Site Import | transfer:execute |
site_import_receipt | Get Site Import Receipt | transfer:* |
site_export_start 和 site_import_start 在管理员批准请求时,也接受不带该 scope 的令牌。对于已批准的请求所启动的操作,site_export_status、site_import_status、site_import_resume 和 site_import_receipt 随后也接受同一个不带该 scope 的令牌。
使用工具 schema
tools/list 返回每个工具的描述、JSON 输入 schema 和注解。构造调用之前请先读取这些元数据,这样客户端使用的字段、允许的值和限制才与已安装的 EmDash 版本所支持的一致。
例如,更新文章的客户端会先调用 content_get 并保留返回的 _rev。然后它可以发送如下 JSON-RPC 请求:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "content_update",
"arguments": {
"collection": "articles",
"id": "01JARTICLE0000000000000000",
"data": { "title": "Updated title" },
"_rev": "opaque-revision-token"
}
}
}
结果会以 JSON 文本的形式返回在第一个内容块中。带有输出 schema 的工具还可能在 structuredContent 中返回相同的值。
内容生命周期与 Byline
内容生命周期参考定义了 MCP、REST、CLI 和管理面板共享的状态、修订、权限、冲突和钩子行为。
content_get 返回一个不透明的 _rev 值。把它传给 content_update、content_publish、content_unpublish、content_schedule 或 content_discard_draft。过期的值会返回冲突,因此请重新读取该条目后再重试。
content_update 是部分更新:省略的字段保留当前值。更新已发布的条目会暂存一份草稿,而线上版本保持不变。使用 content_compare 查看线上值和草稿值,然后调用 content_publish 让草稿上线,或调用 content_discard_draft 移除草稿。content_delete 会把条目移入回收站;只有 content_permanent_delete 才会永久删除回收站中的条目。
Byline 是可复用的作者或贡献者署名。byline_create 可以创建访客署名,也可以把 Byline 关联到 CMS 用户。每个用户在每个区域设置下只能有一个 Byline。使用 translationOf 创建的翻译会保留源 Byline 的用户,除非你传入 userId;传入 null 则使该翻译不关联任何用户。把返回的 Byline ID 传入 content_create 和 content_update 接受的 bylines 输入。删除 Byline 会从内容中移除该署名,并清除其作为主 Byline 的设置。
当另一位用户因在管理后台中打开某个条目而持有它的编辑锁时,content_update、content_delete、content_publish、content_unpublish、content_schedule、content_unschedule、content_discard_draft 和 revision_restore 会以 ENTRY_LOCKED 失败,错误信息会指明持有者。重新读取该条目并不会消除这一拒绝。传入 overrideLock: true 可强制写入。
翻译
内容、Byline、分类法术语和菜单的翻译工具会返回相应翻译组中的每一个区域设置变体。当创建工具的 schema 提供 translationOf 输入时,请使用它;必填字段以 tools/list 为准。
content_translations 接受集合以及内容 ID 或 slug。Byline、分类法术语和菜单的翻译工具接受单条记录的 ID 或共享的翻译组 ID。没有草稿访问权限的用户只能看到已发布的内容翻译。
模式、媒体、分类法与菜单
模式工具会更改数据库结构。在创建内容或更改字段之前,请先使用 schema_get_collection;它会返回可用的字段名称、类型、约束和校验规则。删除集合和字段会移除已存储的内容或字段值,且无法撤销。
使用 media_upload 发送 base64 编码的字节。上传受已配置的大小和 MIME 类型限制,相同的字节可能会返回已有的媒体项,并带有 deduplicated: true。
media_create 用于确认通过 POST /_emdash/api/media/upload-url 创建的待处理上传。使用返回的签名 URL 上传文件,然后用同一个用户账户调用 media_create,并传入返回的 storageKey。该工具会检查已存储的文件是否存在,并且大小与请求上传 URL 时提供的大小一致,然后才会让它在媒体库中可用。
分类法定义描述分类方式及其适用的集合;术语是分配给内容的各个值。层级术语可以使用 parentId,但父项必须属于同一个分类法,且不能形成环。在非层级分类法中使用 parentId 创建或更新术语会返回 VALIDATION_ERROR。带有子项的术语必须先移除或移动其子项,才能删除。
menu_set_items 在一次原子操作中替换菜单的完整项目列表。数组顺序即菜单顺序。嵌套项目的 parentIndex 指向同一数组中位于它之前的项目,因此请把每个父项放在其子项之前。
media_usage_repair 可以处理一个集合或所有集合,在大型站点上可能运行很长时间。其 complete、partial、failed 和 stale 状态都是成功的工具响应。请检查返回的状态和计数,而不要依赖 isError;认证、校验和意外的执行失败会设置 isError: true。
站点迁移
site_* 工具把整个站点导出为站点包,并把站点包导入到空站点中。它们负责启动并驱动各项操作,返回有限的摘要。它们从不携带站点包字节、媒体、记录内容、主体的电子邮件地址或下载 URL。请使用 CLI 或 REST API 下载导出结果、上传用于导入的站点包,然后通过 ID 引用该操作。
每个迁移工具都需要 Admin 角色。角色会在 scope 之前检查,因此非管理员调用者会收到 INSUFFICIENT_PERMISSIONS,并且不会创建批准请求。
导出
site_export_start 接受 comments(默认为 true)并返回新建的操作。site_export_status 每次调用运行一个有限的导出步骤,并报告该操作和 nextRequestInMs。在该延迟之后再次调用,直到 nextRequestInMs 为 null。传入 advance: false 可以只读取状态而不运行步骤。导出完成后,结果还会包含 totals:按类型统计的记录数、媒体数量和字节数,以及站点包的文件数量和字节数。
导入
请先上传站点包。CLI 的 emdash site import <file> --analyze 会上传并分析它,并打印操作 ID。
site_import_analyze 每次调用运行一个有限的分析步骤。重复调用,直到 nextRequestInMs 为 null;结果随后会包含一份计划摘要,其中有 packageDigest、planDigest、executable、计数、大小、主体、决策、转换、警告和阻塞项。每个转换以其 code、记录 kind(如果有)和 count 的形式列出,不含它所应用的 ID 或值。主体、警告和阻塞项各最多列出 50 项,完整数量在 total 中。列出主体时不含电子邮件地址,但包含建议的和当前已映射的目标用户 ID。传入 decisions 可以把主体映射到目标用户 ID(或 null),并选择站点包或目标站点的标题和标语。每次更改都会生成新的 planDigest。
site_import_start 接受操作 ID 以及最新计划的 packageDigest 和 planDigest。该计划不能有阻塞项。此工具带有 destructiveHint: true:一旦启动,导入就会写入站点,并阻止其他写入,直到导入完成或管理员放弃它。调用之前,请向用户展示计划并获得确认。
site_import_resume 运行一个有限的导入步骤,并报告该操作和 nextRequestInMs。调用它直到 nextRequestInMs 为 null;断开连接后重复调用是安全的。site_import_status 报告操作和已上传的文件数量,而不会推进导入。导入完成后,site_import_receipt 返回完整的回执,包括 receiptDigest。
导入执行期间,以及导入失败或被取消之后、被放弃之前,所有其他可以写入的工具都会以 TRANSFER_IMPORT_IN_PROGRESS 失败。这包括插件工具。带有 readOnlyHint: true 注解的工具和八个 site_* 工具仍然可用,initialize 和 tools/list 永远不会被阻止。在媒体使用情况激活期间,写入工具会以同样的方式因 MEDIA_USAGE_ACTIVATION_IN_PROGRESS 而失败。
操作摘要包括 id、kind、state、stage、progress、packageDigest、planDigest、error({ code } 或 null)和时间戳。progress 是 { done, total } 步骤数,另外在已知时还包括 records(导出至今已写入的记录数)以及 bytesDone 和 bytesTotal。MCP 工具无法取消或放弃导入;请使用 REST API。
批准
带有 admin 或所需迁移 scope 的令牌从不需要批准。对于两者都没有的令牌(例如只被授予 transfer:analyze 的智能体),site_export_start 和 site_import_start 会在管理员批准请求后运行:
- 不带该 scope 的首次调用会创建一个待处理的批准请求,并以
TRANSFER_APPROVAL_REQUIRED失败。消息文本和_meta.details中带有approvalId及其expiresAt。使用相同的参数且不带approvalId再次调用,会返回同一个未关闭的请求。 - 管理员在 Settings → Transfer 的 Approval requests 下批准该请求,或使用 REST API 中仅限会话的批准端点。API 令牌不能批准请求。
- 客户端使用相同的参数和
approvalId重复调用。该调用启动操作时,这次批准即被用掉。如果操作启动失败,客户端可以使用同一个approvalId重试,直到它过期。
请求与用户、令牌、操作和确切的参数绑定:导出选项,或导入操作 ID 与两个摘要。待处理的请求在创建 15 分钟后过期,已批准的请求在批准 15 分钟后过期。使用不同参数或另一个令牌的调用,或使用已拒绝、已过期或已用掉的批准的调用,会以 TRANSFER_APPROVAL_INVALID 失败。
site_import_start 会先检查摘要、操作状态和计划的阻塞项,然后才创建请求,因此只会请求管理员批准可以运行的导入。批准需要令牌 ID,因此没有令牌 ID 的调用者会收到 INSUFFICIENT_SCOPE。
已批准的调用启动某个操作之后,同一用户和令牌可以针对该操作调用 site_export_status,或 site_import_status、site_import_resume 和 site_import_receipt,而无需该 scope。
插件工具
管理员必须启用每个插件的 MCP 接口。已启用的工具会以 <pluginId>__<localName> 的形式出现在 tools/list 中,并且对于使用令牌认证的调用,需要 mcp:tools 或 mcp:tools:<pluginId>。EmDash 还会检查插件路由声明的权限,并在审计日志中记录插件、工具、路由和操作者。
由于插件工具取决于具体的安装,因此它们不属于上面的静态清单。
OAuth 发现
MCP 客户端从受保护资源元数据中发现授权服务器:
GET /.well-known/oauth-protected-resource
响应将 /_emdash/api/mcp 标识为受保护资源,并链接到授权服务器。客户端随后读取其元数据:
GET /.well-known/oauth-authorization-server/_emdash
该文档提供当前的授权、令牌、注册和设备授权端点,以及支持的 scope、授权类型和 S256 PKCE 方法。请使用发现到的值,而不要硬编码 OAuth 协议路由。
未认证的 MCP 请求会返回 401 响应,其中包含发现 URL:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"
错误
工具失败时带有 isError: true。第一个文本块以稳定的错误码开头,_meta.code 会为读取结构化元数据的客户端重复该错误码:
{
"content": [{ "type": "text", "text": "[NOT_FOUND] Collection 'articles' not found" }],
"isError": true,
"_meta": { "code": "NOT_FOUND" }
}
来自条目编辑锁的拒绝会在 _meta.details 中携带持有者信息:
{
"content": [{ "type": "text", "text": "[ENTRY_LOCKED] Ada is holding this entry" }],
"isError": true,
"_meta": {
"code": "ENTRY_LOCKED",
"details": {
"userId": "01JB...",
"userName": "Ada",
"acquiredAt": "2026-05-01T09:12:04.117Z",
"expiresAt": "2026-05-01T09:19:04.117Z"
}
}
}
认证失败使用 INSUFFICIENT_SCOPE 和 INSUFFICIENT_PERMISSIONS 等错误码。传输失败使用 JSON-RPC 内部错误码 -32603,并且不会暴露底层异常。