MCP Server Reference

On this page

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.

Authentication

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

MethodUse
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 tokenLong-lived access for a client or automation. Tokens use the ec_pat_ prefix and are created in the admin.
OAuth 2.0 Device Authorization GrantCommand-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.

Scopes

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.

ScopeAccess
content:readRead and search content, bylines, taxonomies, terms, menus, and revisions. Draft-like content also requires the user’s content:read_drafts permission.
content:writeCreate and change content, bylines, and revisions. It also grants taxonomies:manage and menus:manage for compatibility with existing tokens.
media:readRead media records.
media:writeUpload, register, update, and delete media.
schema:readRead collections and fields.
schema:writeCreate, update, and delete collections and fields.
taxonomies:manageCreate, update, and delete taxonomy definitions and terms.
menus:manageCreate, update, and delete menus and menu items.
settings:readRead site settings.
settings:manageUpdate site settings.
mcp:toolsCall MCP tools exposed by any enabled plugin.
mcp:tools:<pluginId>Call MCP tools exposed by one enabled plugin.
transfer:exportExport the whole site as a site package and download it.
transfer:analyzeUpload a site package and analyze it for import.
transfer:executeStart, advance, cancel, and abandon a site import.
adminCall 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.

Role requirements

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.

CapabilityMinimum role
Read published content, media, taxonomies, terms, and menusSubscriber
Read drafts, scheduled content, trash, comparisons, and revisionsContributor
Create content or upload mediaContributor
Edit or publish owned content and register mediaAuthor
Manage bylines, taxonomies, menus, or all users’ contentEditor
Read schemas or settingsEditor
Change schemas or settings, permanently delete content, or repair media usageAdmin
Export or import the whole siteAdmin

See user roles for the complete role definitions.

Transport

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

MethodEndpointBehavior
POST/_emdash/api/mcpAccepts JSON-RPC initialization, tool listing, and tool calls.
GET/_emdash/api/mcpReturns 405 Method Not Allowed.
DELETE/_emdash/api/mcpReturns 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.

Tool inventory

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.

Content tools

ToolRegistered titleRequired scope
content_listList Contentcontent:read
content_getGet Contentcontent:read
content_createCreate Contentcontent:write
content_updateUpdate Contentcontent:write
content_deleteDelete Content (Trash)content:write
content_restoreRestore Contentcontent:write
content_permanent_deletePermanently Delete Contentcontent:write
content_publishPublish Contentcontent:write
content_unpublishUnpublish Contentcontent:write
content_scheduleSchedule Contentcontent:write
content_unscheduleCancel Scheduled Publicationcontent:write
content_compareCompare Live vs Draftcontent:read
content_discard_draftDiscard Draftcontent:write
content_list_trashedList Trashed Contentcontent:read
content_duplicateDuplicate Contentcontent:write
content_translationsGet Content Translationscontent:read

Byline tools

ToolRegistered titleRequired scope
byline_listList Bylinescontent:read
byline_getGet Bylinecontent:read
byline_createCreate Bylinecontent:write
byline_updateUpdate Bylinecontent:write
byline_deleteDelete Bylinecontent:write
byline_translationsList Byline Translationscontent:read

Schema tools

ToolRegistered titleRequired scope
schema_list_collectionsList Collectionsschema:read
schema_get_collectionGet Collection Schemaschema:read
schema_list_block_typesList Block Typesschema:read
schema_get_block_typeGet Block Typeschema:read
schema_create_block_typeCreate Block Typeschema:write
schema_update_block_typeUpdate Block Typeschema:write
schema_activate_block_type_versionActivate Block Type Versionschema:write
schema_create_collectionCreate Collectionschema:write
schema_delete_collectionDelete Collectionschema:write
schema_update_collectionUpdate Collectionschema:write
schema_create_fieldAdd Field to Collectionschema:write
schema_delete_fieldRemove Field from Collectionschema:write
schema_update_fieldUpdate Fieldschema:write

Media tools

ToolRegistered titleRequired scope
media_listList Mediamedia:read
media_createConfirm Signed Media Uploadmedia:write
media_uploadUpload Mediamedia:write
media_getGet Media Itemmedia:read
media_updateUpdate Media Metadatamedia:write
media_deleteDelete Mediamedia:write
media_usage_repairRepair Media Usage Indexadmin

Search tool

ToolRegistered titleRequired scope
searchSearch Contentcontent:read

Taxonomy tools

ToolRegistered titleRequired scope
taxonomy_listList Taxonomiescontent:read
taxonomy_getGet Taxonomy Definitioncontent:read
taxonomy_createCreate Taxonomy Definitiontaxonomies:manage
taxonomy_updateUpdate Taxonomy Definitiontaxonomies:manage
taxonomy_deleteDelete Taxonomy Definitiontaxonomies:manage
taxonomy_list_termsList Taxonomy Termscontent:read
taxonomy_create_termCreate Taxonomy Termtaxonomies:manage
taxonomy_update_termUpdate Taxonomy Termtaxonomies:manage
taxonomy_delete_termDelete Taxonomy Termtaxonomies:manage
taxonomy_term_translationsList Term Translationscontent:read
ToolRegistered titleRequired scope
menu_listList Menuscontent:read
menu_getGet Menu with Itemscontent:read
menu_translationsList Menu Translationscontent:read
menu_createCreate Menumenus:manage
menu_updateUpdate Menumenus:manage
menu_deleteDelete Menumenus:manage
menu_set_itemsSet Menu Itemsmenus:manage

Revision tools

ToolRegistered titleRequired scope
revision_listList Revisionscontent:read
revision_restoreRestore Revisioncontent:write

Settings tools

ToolRegistered titleRequired scope
settings_getGet Site Settingssettings:read
settings_updateUpdate Site Settingssettings:manage

Site transfer tools

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

ToolRegistered titleRequired scope
site_transfer_capabilitiesGet Site Transfer Capabilitiestransfer:*
site_export_startStart Site Exporttransfer:export
site_export_statusGet Site Export Statustransfer:export
site_import_analyzeAnalyze Site Importtransfer:analyze
site_import_startStart Site Importtransfer:execute
site_import_statusGet Site Import Statustransfer:*
site_import_resumeResume Site Importtransfer:execute
site_import_receiptGet Site Import Receipttransfer:*

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.

Use the tool schemas

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.

Content lifecycle and bylines

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. 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.

Translations

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.

Schemas, media, taxonomies, and menus

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.

Site transfer

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.

Export

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.

Import

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.

Approvals

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.

Plugin tools

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.

OAuth discovery

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"

Errors

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.