Skip to content

MCP Server Reference

EmDash exposes a built-in Model Context Protocol (MCP) server at /_emdash/api/mcp. MCP clients use it to read and manage content, bylines, schemas, media, taxonomies, menus, revisions, and settings, and to export or import the whole site.

The MCP endpoint requires a Bearer token. EmDash supports these token flows:

Method Use
OAuth 2.1 Authorization Code with Proof Key for Code Exchange (PKCE) Interactive MCP clients. The user approves the requested scopes in a browser.
Personal access token Long-lived access for a client or automation. Tokens use the ec_pat_ prefix and are created in the admin.
OAuth 2.0 Device Authorization Grant Command-line clients that ask the user to approve a code in a browser. emdash login uses this flow.

Session cookies do not authenticate the MCP endpoint.

OAuth and personal access tokens limit which tools a client can call. The user’s role is checked separately, so a scope never grants a permission the user does not have.

Scope Access
content:read Read and search content, bylines, taxonomies, terms, menus, and revisions. Draft-like content also requires the user’s content:read_drafts permission.
content:write Create and change content, bylines, and revisions. It also grants taxonomies:manage and menus:manage for compatibility with existing tokens.
media:read Read media records.
media:write Upload, register, update, and delete media.
schema:read Read collections and fields.
schema:write Create, update, and delete collections and fields.
taxonomies:manage Create, update, and delete taxonomy definitions and terms.
menus:manage Create, update, and delete menus and menu items.
settings:read Read site settings.
settings:manage Update site settings.
mcp:tools Call MCP tools exposed by any enabled plugin.
mcp:tools:<pluginId> Call MCP tools exposed by one enabled plugin.
transfer:export Export the whole site as a site package and download it.
transfer:analyze Upload a site package and analyze it for import.
transfer:execute Start, advance, cancel, and abandon a site import.
admin Call every core tool, including the site transfer tools. Plugin tools still require mcp:tools or the plugin-specific scope.

The admin scope includes transfer:export, transfer:analyze, and transfer:execute. Each transfer scope grants only its own actions and requires the administrator role. To let a client, such as an agent, analyze a site package without exporting or importing, grant transfer:analyze instead of admin.

The authorization-code consent page lets the user remove requested scopes. EmDash also intersects the request with the client’s registered scopes and the user’s role, and refuses an empty grant.

The following table shows the minimum role for the broad capability. Ownership checks can require a higher role when a user acts on another user’s content.

Capability Minimum role
Read published content, media, taxonomies, terms, and menus Subscriber
Read drafts, scheduled content, trash, comparisons, and revisions Contributor
Create content or upload media Contributor
Edit or publish owned content and register media Author
Manage bylines, taxonomies, menus, or all users’ content Editor
Read schemas or settings Editor
Change schemas or settings, permanently delete content, or repair media usage Admin
Export or import the whole site Admin

See user roles for the complete role definitions.

The server uses stateless Streamable HTTP. Each request is independent; the server does not keep an MCP session or a Server-Sent Events connection.

Method Endpoint Behavior
POST /_emdash/api/mcp Accepts JSON-RPC initialization, tool listing, and tool calls.
GET /_emdash/api/mcp Returns 405 Method Not Allowed.
DELETE /_emdash/api/mcp Returns 405 Method Not Allowed.

Responses use JSON-RPC 2.0. Call tools/list to obtain the current input schemas and MCP annotations before constructing a tool request.

The following inventory matches the static tools returned by tools/list. The registered title is included because clients may display it instead of the tool name.

Tool Registered title Required 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
Tool Registered title Required 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
Tool Registered title Required 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
Tool Registered title Required 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
Tool Registered title Required scope
search Search Content content:read
Tool Registered title Required 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
Tool Registered title Required 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
Tool Registered title Required scope
revision_list List Revisions content:read
revision_restore Restore Revision content:write
Tool Registered title Required scope
settings_get Get Site Settings settings:read
settings_update Update Site Settings settings:manage

transfer:* means any one of transfer:export, transfer:analyze, or transfer:execute. The admin scope satisfies every requirement in this table.

Tool Registered title Required 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 and site_import_start also accept a token without the scope when an admin approves the request. For the operation an approved request starts, site_export_status, site_import_status, site_import_resume, and site_import_receipt then accept the same token without the scope.

tools/list returns each tool’s description, JSON input schema, and annotations. Read that metadata before constructing a call so your client uses the fields, allowed values, and limits supported by the installed EmDash version.

For example, a client updating an article first calls content_get and keeps the returned _rev. It can then send this JSON-RPC request:

{
"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"
}
}
}

The result is returned as JSON text in the first content block. A tool with an output schema may also return the same value in structuredContent.

The content lifecycle reference defines the state, revision, permission, conflict, and hook behavior shared by MCP, REST, the CLI, and the admin panel.

content_get returns an opaque _rev value. Pass it to content_update, content_publish, content_unpublish, content_schedule, or content_discard_draft. A stale value returns a conflict, so read the item again before retrying.

content_update is a partial update: omitted fields keep their current values. Updating a published item stages a draft while the live version remains unchanged. Use content_compare to review the live and draft values, then call content_publish to make the draft live or content_discard_draft to remove it. content_delete moves an item to trash; only content_permanent_delete removes a trashed item permanently.

Bylines are reusable author or contributor credits. byline_create can create a guest credit or link a byline to a CMS user. A user can have only one byline per locale. A translation created with translationOf keeps the source byline’s user unless you pass userId; pass null to leave the translation unlinked. Pass the returned byline ID in the bylines input accepted by content_create and content_update. Deleting a byline removes that credit from content and clears it as the primary byline.

While another user holds an entry’s edit lock because they have it open in the admin, content_update, content_delete, content_publish, content_unpublish, content_schedule, content_unschedule, content_discard_draft and revision_restore fail with ENTRY_LOCKED, and the error names the holder. Reading the item again does not clear the refusal. Pass overrideLock: true to write anyway.

Content, byline, taxonomy term, and menu translation tools return every locale variant in the relevant translation group. Use the creation tool’s translationOf input when its schema provides one; tools/list is authoritative for the required fields.

content_translations accepts a collection and content ID or slug. The byline, taxonomy-term, and menu translation tools accept either one record’s ID or the shared translation-group ID. A user without draft access sees only published content translations.

Schema tools change the database structure. Use schema_get_collection before creating content or changing fields; it returns the available field names, types, constraints, and validation rules. Collection and field deletion remove stored content or field values and cannot be undone.

Use media_upload to send base64-encoded bytes. Uploads are subject to the configured size and MIME-type limits, and identical bytes may return an existing media item with deduplicated: true.

media_create confirms a pending upload created through POST /_emdash/api/media/upload-url. Upload the file with the returned signed URL, then call media_create from the same user account with the returned storageKey. The tool checks that the stored file exists and matches the size supplied when the upload URL was requested before making it available in the media library.

Taxonomy definitions describe the classification and the collections it applies to; terms are the individual values assigned to content. Hierarchical terms can use parentId, but a parent must belong to the same taxonomy and cannot create a cycle. Creating or updating a term with parentId in a non-hierarchical taxonomy returns VALIDATION_ERROR. A term with children must have those children removed or moved before deletion.

menu_set_items replaces a menu’s complete item list in one atomic operation. The array order becomes the menu order. A nested item’s parentIndex points to an earlier item in the same array, so place every parent before its children.

media_usage_repair can process one collection or every collection and may run for a long time on a large site. Its complete, partial, failed, and stale statuses are successful tool responses. Inspect the returned status and counts instead of relying on isError; authentication, validation, and unexpected execution failures set isError: true.

The site_* tools export a whole site as a site package and import a package into an empty site. They start and drive operations and return bounded summaries. They never carry package bytes, media, record contents, principal email addresses, or download URLs. Download an export, and upload a package for import, with the CLI or the REST API, then refer to the operation by its ID.

Every transfer tool requires the Admin role. The role is checked before the scope, so a non-admin caller receives INSUFFICIENT_PERMISSIONS and no approval request is created.

site_export_start accepts comments (default true) and returns the new operation. site_export_status runs one bounded export step on each call and reports the operation and nextRequestInMs. Call it again after that delay until nextRequestInMs is null. Pass advance: false to read the status without running a step. Once the export is complete, the result also includes totals: record counts by kind, media count and bytes, and package file count and bytes.

Upload the package first. The CLI’s emdash site import <file> --analyze uploads and analyzes it and prints the operation ID.

site_import_analyze runs one bounded analysis step per call. Repeat it until nextRequestInMs is null; the result then includes a plan summary with packageDigest, planDigest, executable, counts, sizes, principals, decisions, transformations, warnings, and blockers. Each transformation is listed as its code, the record kind when it has one, and a count, without the IDs or values it applies to. Principals, warnings, and blockers list at most 50 items each, with the full count in total. Principals are listed without email addresses, with the suggested and currently mapped target user IDs. Pass decisions to map principals to target user IDs (or null) and to choose the package or target title and tagline. Each change produces a new planDigest.

site_import_start takes the operation ID and the packageDigest and planDigest of the latest plan. The plan must have no blockers. The tool has destructiveHint: true: once started, the import writes to the site and blocks other writes until it completes or an administrator abandons it. Show the user the plan and get their confirmation before calling it.

site_import_resume runs one bounded import step and reports the operation and nextRequestInMs. Call it until nextRequestInMs is null; it is safe to repeat after a disconnect. site_import_status reports the operation and uploaded file counts without advancing the import. site_import_receipt returns the complete receipt, including receiptDigest, once the import is complete.

While an import is executing, and after it fails or is cancelled until it is abandoned, every other tool that can write fails with TRANSFER_IMPORT_IN_PROGRESS. This includes plugin tools. Tools annotated readOnlyHint: true and the eight site_* tools keep working, and initialize and tools/list are never blocked. During media usage activation, write tools fail with MEDIA_USAGE_ACTIVATION_IN_PROGRESS in the same way.

Operation summaries include id, kind, state, stage, progress, packageDigest, planDigest, error ({ code } or null), and timestamps. progress is { done, total } steps, plus records, the records an export has written so far, and bytesDone and bytesTotal, once they are known. The MCP tools cannot cancel or abandon an import; use the REST API.

A token with admin or the needed transfer scope never asks for approval. For a token without either, such as an agent granted only transfer:analyze, site_export_start and site_import_start run when an administrator approves the request:

  1. The first call without the scope creates a pending approval request and fails with TRANSFER_APPROVAL_REQUIRED. The message text and _meta.details carry the approvalId and its expiresAt. Calling again with the same arguments and no approvalId returns the same open request.
  2. An administrator approves the request under Approval requests in Settings → Transfer, or with the session-only approval endpoint of the REST API. API tokens cannot approve requests.
  3. The client repeats the call with the same arguments and the approvalId. The approval is used up when that call starts the operation. If the operation fails to start, the client can retry with the same approvalId until it expires.

A request is bound to the user, the token, the action, and the exact arguments: the export options, or the import operation ID and both digests. A pending request expires 15 minutes after it is made, and an approved one 15 minutes after approval. A call with different arguments or another token, or with a denied, expired, or used approval, fails with TRANSFER_APPROVAL_INVALID.

site_import_start checks the digests, the operation state, and the plan’s blockers before it creates a request, so an administrator is only asked to approve an import that can run. An approval needs a token ID, so a caller without one receives INSUFFICIENT_SCOPE.

After an approved call starts an operation, the same user and token can call site_export_status, or site_import_status, site_import_resume, and site_import_receipt, for that operation without the scope.

An administrator must enable each plugin’s MCP surface. Enabled tools appear in tools/list as <pluginId>__<localName> and require mcp:tools or mcp:tools:<pluginId> for token-authenticated calls. EmDash also checks the permission declared by the plugin route and records the plugin, tool, route, and actor in the audit log.

Because plugin tools are installation-specific, they are not part of the static inventory above.

MCP clients discover the authorization server from the protected-resource metadata:

GET /.well-known/oauth-protected-resource

The response identifies /_emdash/api/mcp as the protected resource and links to the authorization server. Clients then read its metadata at:

GET /.well-known/oauth-authorization-server/_emdash

That document supplies the current authorization, token, registration, and device-authorization endpoints, supported scopes, grant types, and the S256 PKCE method. Use the discovered values instead of hard-coding the OAuth protocol routes.

An unauthenticated MCP request returns a 401 response with the discovery URL:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"

A tool failure has isError: true. The first text block starts with a stable code, and _meta.code repeats it for clients that read structured metadata:

{
"content": [{ "type": "text", "text": "[NOT_FOUND] Collection 'articles' not found" }],
"isError": true,
"_meta": { "code": "NOT_FOUND" }
}

A refusal from an entry’s edit lock carries the holder in _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"
}
}
}

Authentication failures use codes such as INSUFFICIENT_SCOPE and INSUFFICIENT_PERMISSIONS. Transport failures use the JSON-RPC internal-error code -32603 and do not expose the underlying exception.