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 taxonomy terms and menus.
content:writeCreate, update, delete, publish, unpublish, schedule, duplicate, and restore content. Create taxonomy terms.
media:readList and get media items.
media:writeUpdate and delete media metadata.
schema:readList collections and get collection schemas.
schema:writeCreate and delete collections and fields.
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.

In addition to scopes, some tools require a minimum RBAC role:

OperationMinimum role
Content operationsNo minimum (scopes control access)
Schema readEditor (40)
Schema writeAdmin (50)

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 28 tools across seven domains. 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

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

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.

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

Scope: content:write

List all navigation menus. Returns name, label, and timestamps.

No parameters.

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.

ParameterTypeRequiredDescription
namestringYesMenu name (e.g. main, footer)

Scope: content:read | Read-only: Yes

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

MCP clients that support OAuth 2.1 can automatically discover how to authenticate. The server publishes two metadata documents:

GET /.well-known/oauth-protected-resource
{
"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",
"admin"
],
"bearer_methods_supported": ["header"]
}
GET /_emdash/.well-known/oauth-authorization-server
{
"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:

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

Scope and permission errors throw MCP protocol errors:

{
"jsonrpc": "2.0",
"error": {
"code": -32600,
"message": "Insufficient scope: requires content:write"
},
"id": 1
}

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