Skip to content

MCP Server Reference

EmDash includes a built-in Model Context Protocol (MCP) server at /_emdash/api/mcp that exposes content management operations as tools for AI assistants.

This page covers the protocol details: authentication, transport, tool specifications, OAuth discovery, and error handling.

The MCP server supports three authentication methods:

MethodHow it works
OAuth 2.1 Authorization Code + PKCEStandard flow for MCP clients. User approves scopes in the browser.
Personal Access Token (PAT)Long-lived ec_pat_* tokens created in the admin panel.
Device FlowCLI-style flow where you approve a code in the browser. Used by emdash login.

Session cookies (from the admin UI) also work but aren’t practical for external MCP clients.

Tokens are scoped to limit what operations a client can perform. Scopes are requested during OAuth authorization and enforced on every tool call.

ScopeGrants access to
content:readList, get, compare, and search content. List taxonomies, taxonomy terms, and menus.
content:writeCreate, update, delete, publish, unpublish, schedule, unschedule, duplicate, and restore content. Implicitly grants taxonomies:manage and menus:manage for backwards compatibility with tokens issued before those scopes existed.
media:readList and get media items.
media:writeRegister (create), update, and delete media metadata.
schema:readList collections and get collection schemas.
schema:writeCreate and delete collections and fields.
taxonomies:manageCreate, update, and delete taxonomy terms.
menus:manageCreate, update, and delete navigation menus and their items.
settings:readRead site-wide settings.
settings:manageUpdate site-wide settings.
adminFull access to all operations.

The admin scope grants access to everything. Session-based auth (no token) also has full access based on the user’s role.

content:write implicitly grants taxonomies:manage and menus:manage so personal access tokens issued before those scopes were split out continue to work without re-issue. New tokens should request the granular scopes.

In addition to scopes, some tools require a minimum RBAC role. Both must be satisfied — a token with the right scope still fails if the calling user’s role is too low.

OperationMinimum role
Content readSubscriber (10) for published items; Contributor (20) for drafts, scheduled, trash, and revisions
Content createContributor (20)
Content edit own / delete ownAuthor (30)
Content publishAuthor (30) for own items; Editor (40) to act on others’ items
Schema readEditor (40)
Schema writeAdmin (50)
Taxonomies manageEditor (40)
Menus manageEditor (40)
Settings readEditor (40)
Settings manageAdmin (50)
Media upload (media_create)Author (30)

See the Authentication guide for role definitions.

The server uses the Streamable HTTP transport in stateless mode. Each request is independent — there are no sessions or long-lived connections.

  • POST /_emdash/api/mcp — Send JSON-RPC tool calls
  • GET /_emdash/api/mcp — Returns 405 (no SSE in stateless mode)
  • DELETE /_emdash/api/mcp — Returns 405 (no session to close)

Responses follow the JSON-RPC 2.0 format. Errors use standard JSON-RPC error codes, with MCP-specific codes for scope and permission failures.

The server exposes 45 tools across eight domains: content, schema, media, search, taxonomies, menus, revisions, and settings. Each tool returns results as JSON text content, or an error message with isError: true on failure.

List content items in a collection with optional filtering and pagination.

ParameterTypeRequiredDescription
collectionstringYesCollection slug (e.g. posts, pages)
statusstringNoFilter: draft, published, or scheduled
limitintegerNoMax items to return (1-100, default 50)
cursorstringNoPagination cursor from a previous response
orderBystringNoField to sort by (e.g. created_at, updated_at)
orderstringNoSort direction: asc or desc (default desc)
localestringNoFilter by locale (e.g. en, fr). Only relevant with i18n.

Scope: content:read | Read-only: Yes

Get a single content item by ID or slug. Returns all field values, metadata, and a _rev token for optimistic concurrency.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID (ULID) or slug
localestringNoLocale for slug lookup. IDs are globally unique.

Scope: content:read | Read-only: Yes

Create a new content item. The data object should contain field values matching the collection’s schema — use schema_get_collection to check what fields are available. Items are created as draft by default.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
dataobjectYesField values as key-value pairs
slugstringNoURL slug (auto-generated from title if omitted)
statusstringNoInitial status: draft or published (default draft)
localestringNoLocale for this content (defaults to site default)
translationOfstringNoID of the item this is a translation of

Scope: content:write

Update an existing content item. Only include fields you want to change — unspecified fields are left unchanged.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID or slug
dataobjectNoField values to update
slugstringNoNew URL slug
statusstringNoNew status: draft or published
_revstringNoRevision token from content_get for conflict detection

Scope: content:write

Soft-delete a content item by moving it to the trash. Use content_restore to undo, or content_permanent_delete to remove it forever.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID or slug

Scope: content:write | Destructive: Yes

Restore a soft-deleted content item from the trash.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID or slug

Scope: content:write

Permanently and irreversibly delete a trashed content item. The item must be in the trash first.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID or slug

Scope: content:write | Destructive: Yes

Publish a content item, making it live on the site. Creates a published revision from the current draft. Further edits create a new draft without affecting the live version until re-published.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID or slug

Scope: content:write

Revert a published item to draft status. It will no longer be visible on the live site but its content is preserved.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID or slug

Scope: content:write

Schedule a content item for future publication. It will be automatically published at the specified date/time.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID or slug
scheduledAtstringYesISO 8601 datetime (e.g. 2026-06-01T09:00:00Z)

Scope: content:write

Cancel a previously scheduled publication. The item keeps its current status; only the scheduledAt timestamp is cleared. Idempotent — calling on an item that isn’t scheduled is a no-op.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID or slug

Scope: content:write

Compare the published (live) version of a content item with its current draft. Returns both versions and a flag indicating whether there are changes.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID or slug

Scope: content:read | Read-only: Yes

Discard the current draft and revert to the last published version. Only works on items that have been published at least once.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID or slug

Scope: content:write | Destructive: Yes

List soft-deleted content items in a collection’s trash.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
limitintegerNoMax items (1-100, default 50)
cursorstringNoPagination cursor

Scope: content:read | Read-only: Yes

Create a copy of an existing content item. The duplicate is created as a draft with “(Copy)” appended to the title and an auto-generated slug.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID or slug to duplicate

Scope: content:write

Get all locale variants of a content item. Returns the translation group and a summary of each locale version. Only relevant when i18n is enabled.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID or slug

Scope: content:read | Read-only: Yes

List all content collections defined in the CMS. Returns slug, label, supported features, and timestamps.

No parameters.

Scope: schema:read | Minimum role: Editor | Read-only: Yes

Get detailed info about a collection including all field definitions. Fields describe the data model: name, type, constraints, and validation rules. Use this to understand what content_create and content_update expect.

ParameterTypeRequiredDescription
slugstringYesCollection slug (e.g. posts)

Scope: schema:read | Minimum role: Editor | Read-only: Yes

Create a new content collection. This creates a database table and schema definition. The slug must be lowercase alphanumeric with underscores, starting with a letter.

ParameterTypeRequiredDescription
slugstringYesUnique identifier (/^[a-z][a-z0-9_]*$/)
labelstringYesDisplay name (plural, e.g. “Blog Posts”)
labelSingularstringNoSingular display name
descriptionstringNoDescription of this collection
iconstringNoIcon name for the admin UI
supportsstring[]NoFeatures: drafts, revisions, preview, scheduling, search (default: ['drafts', 'revisions'])

Scope: schema:write | Minimum role: Admin

Delete a collection and its database table. This is irreversible and deletes all content in the collection.

ParameterTypeRequiredDescription
slugstringYesCollection slug to delete
forcebooleanNoForce deletion even if the collection has content

Scope: schema:write | Minimum role: Admin | Destructive: Yes

Add a new field to a collection’s schema. This adds a column to the database table.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
slugstringYesField identifier (/^[a-z][a-z0-9_]*$/)
labelstringYesDisplay name
typestringYesData type (see below)
requiredbooleanNoWhether the field is required
uniquebooleanNoWhether values must be unique
defaultValueanyNoDefault value for new items
validationobjectNoConstraints: min, max, minLength, maxLength, pattern, options
optionsobjectNoWidget config: collection (for references), rows (for textarea)
searchablebooleanNoInclude in full-text search index
translatablebooleanNoWhether this field is translatable (default true)

Field types: string, text, number, integer, boolean, datetime, select, multiSelect, portableText, image, file, reference, json, slug.

For select and multiSelect types, provide allowed values in validation.options.

Scope: schema:write | Minimum role: Admin

Remove a field from a collection. This drops the column and deletes all data in that field. Irreversible.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
fieldSlugstringYesField slug to remove

Scope: schema:write | Minimum role: Admin | Destructive: Yes

List uploaded media files with optional MIME type filtering and pagination.

ParameterTypeRequiredDescription
mimeTypestringNoFilter by MIME type prefix (e.g. image/, application/pdf)
limitintegerNoMax items (1-100, default 50)
cursorstringNoPagination cursor

Scope: media:read | Read-only: Yes

Register a media file that has already been uploaded to storage. The caller is responsible for placing the file at storageKey (typically using a signed upload URL from the admin UI or a separate API). This tool persists the metadata record so the file is discoverable via media_list / media_get and can be referenced by content.

ParameterTypeRequiredDescription
filenamestringYesOriginal filename (e.g. logo.png)
mimeTypestringYesMIME type (e.g. image/png)
storageKeystringYesStorage path/key the file was uploaded to
sizeintegerNoFile size in bytes
widthintegerNoImage width in pixels
heightintegerNoImage height in pixels
contentHashstringNoHash of the file contents (for dedupe)
blurhashstringNoBlurhash for image placeholders
dominantColorstringNoHex color string for the image’s dominant color

Scope: media:write | Minimum role: Author

Get details of a single media file by ID. Returns metadata including filename, MIME type, size, dimensions, alt text, and URL.

ParameterTypeRequiredDescription
idstringYesMedia item ID

Scope: media:read | Read-only: Yes

Update metadata of an uploaded media file. The file itself cannot be changed.

ParameterTypeRequiredDescription
idstringYesMedia item ID
altstringNoAlt text for accessibility
captionstringNoCaption text
widthintegerNoImage width in pixels
heightintegerNoImage height in pixels

Scope: media:write

Permanently delete a media file. Removes the database record and the file from storage. Content referencing this media will have broken references.

ParameterTypeRequiredDescription
idstringYesMedia item ID

Scope: media:write | Destructive: Yes

Full-text search across content collections. Collections must have search in their supports list and fields must be marked as searchable.

ParameterTypeRequiredDescription
querystringYesSearch query text
collectionsstring[]NoLimit search to specific collection slugs
localestringNoFilter results by locale
limitintegerNoMax results (1-50, default 20)

Scope: content:read | Read-only: Yes

List all taxonomy definitions (e.g. categories, tags). Returns name, label, whether hierarchical, and associated collections.

No parameters.

Scope: content:read | Read-only: Yes

List terms in a taxonomy with pagination.

ParameterTypeRequiredDescription
taxonomystringYesTaxonomy name (e.g. categories, tags)
limitintegerNoMax items (1-100, default 50)
cursorstringNoPagination cursor

Scope: content:read | Read-only: Yes

Create a new term in a taxonomy. For hierarchical taxonomies, specify a parentId to create a child term. The parent’s ancestor chain must not exceed 100 levels.

ParameterTypeRequiredDescription
taxonomystringYesTaxonomy name
slugstringYesURL-safe identifier
labelstringYesDisplay name
parentIdstringNoParent term ID (for hierarchical taxonomies)
descriptionstringNoDescription of the term

Scope: taxonomies:manage | Minimum role: Editor

Update an existing term in a taxonomy. Any field can be omitted to leave it unchanged. Renaming a slug must not collide with another term in the same taxonomy. Set parentId to null to detach from a parent. The new parent must exist, belong to the same taxonomy, and not introduce a cycle.

ParameterTypeRequiredDescription
taxonomystringYesTaxonomy name
termSlugstringYesCurrent slug of the term to update
slugstringNoNew slug (must be unique in the taxonomy)
labelstringNoNew display name
parentIdstring | nullNoNew parent term ID; null to detach
descriptionstringNoNew description

Scope: taxonomies:manage | Minimum role: Editor

Permanently delete a term from a taxonomy. Any content tagged with the term loses the association. Cannot delete a term that has children — delete children first.

ParameterTypeRequiredDescription
taxonomystringYesTaxonomy name
termSlugstringYesSlug of the term to delete

Scope: taxonomies:manage | Minimum role: Editor | Destructive: Yes

List navigation menus. Menus are per-locale: pass locale to return one locale’s rows only, or omit it to list every locale variant.

ParameterTypeRequiredDescription
localestringNoFilter by locale (omit for all locale variants)

Scope: content:read | Read-only: Yes

Get a menu by name including all its items in order. Items have a label, URL, type, and optional parent for nesting. When the same menu name exists in multiple locales, pass locale to resolve the intended translation.

ParameterTypeRequiredDescription
namestringYesMenu name (e.g. main, footer)
localestringNoLocale to resolve the menu for

Scope: content:read | Read-only: Yes

Create a new navigation menu. The name is the stable identifier used by site templates; label is the human-readable name shown in the admin. Menus are per-locale, so pass locale when the same menu name exists in multiple translations. Add items afterwards with menu_set_items. If translationOf is set, locale must also be set.

ParameterTypeRequiredDescription
namestringYesStable identifier (/^[a-z][a-z0-9_]*$/)
labelstringYesDisplay name for the admin
localestringNoLocale for this menu (e.g. fr-fr)
translationOfstringNoExisting menu id to create this locale variant from

Scope: menus:manage | Minimum role: Editor

Update a menu’s label. The name (stable identifier) cannot be changed. On multi-locale installs, pass locale so the correct translation is updated.

ParameterTypeRequiredDescription
namestringYesMenu name to update
labelstringYesNew display label
localestringNoLocale of the menu to update

Scope: menus:manage | Minimum role: Editor

Delete a menu and all its items. Cannot be undone. On multi-locale installs, pass locale so only the intended translation is removed.

ParameterTypeRequiredDescription
namestringYesMenu name to delete
localestringNoLocale of the menu to delete

Scope: menus:manage | Minimum role: Editor | Destructive: Yes

Replace the entire item list of a menu in one call. Atomic: existing items are deleted and the new list is inserted in the order provided. Use this rather than per-item add/remove operations so the resulting order and parent links are unambiguous. On multi-locale installs, pass locale so only the intended translation is rewritten.

Items are positioned by array index. Nesting is expressed via parentIndex — an item with parentIndex: 0 is nested under the item at index 0. The parent must appear earlier in the list. Items without parentIndex are top-level.

ParameterTypeRequiredDescription
namestringYesMenu name to update
localestringNoLocale of the menu to rewrite
itemsMenuItem[]YesOrdered list of menu items (see below)

Each MenuItem has:

FieldTypeRequiredDescription
labelstringYesItem display text
typestringYesOne of custom, page, post, taxonomy, collection
customUrlstringNoURL for type: "custom" items (ignored otherwise)
referenceCollectionstringNoTarget collection slug for content references
referenceIdstringNoTarget content / term ID for references
titleAttrstringNoHTML title attribute
targetstringNoHTML target attribute (e.g. _blank)
cssClassesstringNoSpace-separated CSS classes
parentIndexintegerNoArray index of the parent item. Omit for top-level items.

Scope: menus:manage | Minimum role: Editor

List revision history for a content item, newest first. Requires the collection to support revisions.

ParameterTypeRequiredDescription
collectionstringYesCollection slug
idstringYesContent item ID or slug
limitintegerNoMax revisions (1-50, default 20)

Scope: content:read | Read-only: Yes

Restore a content item to a previous revision. Replaces the current draft with the specified revision’s data. Not automatically published — use content_publish afterward if needed.

ParameterTypeRequiredDescription
revisionIdstringYesRevision ID to restore

Scope: content:write

Site-wide settings — title, tagline, logo, favicon, canonical URL, default page size, date and time formatting, social handles, and SEO defaults.

Get all site-wide settings. Media references (logo, favicon, seo.defaultOgImage) include resolved URLs alongside the underlying mediaId. Unset values are omitted from the response.

No parameters.

Scope: settings:read | Minimum role: Editor | Read-only: Yes

Update one or more site-wide settings. Partial update: only the fields provided are changed; omitted fields are left as-is. Returns the full settings object after the update.

To set a media reference (logo, favicon, seo.defaultOgImage), pass an object with mediaId (and optional alt). The media item must already exist — use media_create first.

ParameterTypeRequiredDescription
titlestringNoSite title
taglinestringNoShort description shown alongside the title
logoMediaRefNoLogo media reference ({ mediaId, alt? })
faviconMediaRefNoFavicon media reference
urlstringNoCanonical site URL (http or https). Empty string clears it.
postsPerPageintegerNoDefault page size for content listings (1-100)
dateFormatstringNoDate format token string
timezonestringNoIANA timezone identifier
socialobjectNoSocial handles — twitter, github, facebook, instagram, linkedin, youtube
seoobjectNoSEO defaults (see below)

The seo object accepts:

FieldTypeDescription
titleSeparatorstringSeparator between page title and site title (e.g. " | " for a vertical bar)
defaultOgImageMediaRefDefault Open Graph image when content has none
robotsTxtstringCustom robots.txt body. Omit to use the EmDash default.
googleVerificationstringGoogle Search Console verification token
bingVerificationstringBing Webmaster Tools verification token

Scope: settings:manage | Minimum role: Admin

Most MCP clients handle this for you; this section is for building an MCP client against EmDash directly. Clients that support OAuth 2.1 discover how to authenticate from two metadata documents the server publishes:

Request the protected resource metadata at the following endpoint:

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

The server responds with the resource identifier, its authorization server, and supported scopes:

{
"resource": "https://example.com/_emdash/api/mcp",
"authorization_servers": ["https://example.com/_emdash"],
"scopes_supported": [
"content:read", "content:write",
"media:read", "media:write",
"schema:read", "schema:write",
"taxonomies:manage", "menus:manage",
"settings:read", "settings:manage",
"admin"
],
"bearer_methods_supported": ["header"]
}

Request the authorization server metadata at the following endpoint:

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

The server responds with the endpoints, scopes, and grant types it supports:

{
"issuer": "https://example.com/_emdash",
"authorization_endpoint": "https://example.com/_emdash/oauth/authorize",
"token_endpoint": "https://example.com/_emdash/api/oauth/token",
"scopes_supported": ["content:read", "content:write", "..."],
"response_types_supported": ["code"],
"grant_types_supported": [
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code"
],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"device_authorization_endpoint": "https://example.com/_emdash/api/oauth/device/code"
}

When an unauthenticated request hits the MCP endpoint, the server returns:

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

This triggers the standard MCP client discovery flow.

Tool errors are returned as text content with isError: true. The message is prefixed with a stable [CODE], and the same code is repeated in _meta.code:

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

Scope and permission errors use the same tool error envelope:

{
"content": [
{ "type": "text", "text": "[INSUFFICIENT_SCOPE] Insufficient scope: requires content:write" }
],
"isError": true,
"_meta": { "code": "INSUFFICIENT_SCOPE" }
}

Transport-level errors (server misconfiguration, unhandled exceptions) return JSON-RPC error code -32603 (Internal error) without leaking implementation details.