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.
Authentication
Section titled “Authentication”The MCP server supports three authentication methods:
| Method | How it works |
|---|---|
| OAuth 2.1 Authorization Code + PKCE | Standard 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 Flow | CLI-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.
Scopes
Section titled “Scopes”Tokens are scoped to limit what operations a client can perform. Scopes are requested during OAuth authorization and enforced on every tool call.
| Scope | Grants access to |
|---|---|
content:read | List, get, compare, and search content. List taxonomy terms and menus. |
content:write | Create, update, delete, publish, unpublish, schedule, duplicate, and restore content. Create taxonomy terms. |
media:read | List and get media items. |
media:write | Update and delete media metadata. |
schema:read | List collections and get collection schemas. |
schema:write | Create and delete collections and fields. |
admin | Full 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.
Role Requirements
Section titled “Role Requirements”In addition to scopes, some tools require a minimum RBAC role:
| Operation | Minimum role |
|---|---|
| Content operations | No minimum (scopes control access) |
| Schema read | Editor (40) |
| Schema write | Admin (50) |
See the Authentication guide for role definitions.
Transport
Section titled “Transport”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 callsGET /_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.
Content Tools
Section titled “Content Tools”content_list
Section titled “content_list”List content items in a collection with optional filtering and pagination.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug (e.g. posts, pages) |
status | string | No | Filter: draft, published, or scheduled |
limit | integer | No | Max items to return (1-100, default 50) |
cursor | string | No | Pagination cursor from a previous response |
orderBy | string | No | Field to sort by (e.g. created_at, updated_at) |
order | string | No | Sort direction: asc or desc (default desc) |
locale | string | No | Filter by locale (e.g. en, fr). Only relevant with i18n. |
Scope: content:read | Read-only: Yes
content_get
Section titled “content_get”Get a single content item by ID or slug. Returns all field values, metadata, and a _rev token for optimistic concurrency.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID (ULID) or slug |
locale | string | No | Locale for slug lookup. IDs are globally unique. |
Scope: content:read | Read-only: Yes
content_create
Section titled “content_create”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
data | object | Yes | Field values as key-value pairs |
slug | string | No | URL slug (auto-generated from title if omitted) |
status | string | No | Initial status: draft or published (default draft) |
locale | string | No | Locale for this content (defaults to site default) |
translationOf | string | No | ID of the item this is a translation of |
Scope: content:write
content_update
Section titled “content_update”Update an existing content item. Only include fields you want to change — unspecified fields are left unchanged.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID or slug |
data | object | No | Field values to update |
slug | string | No | New URL slug |
status | string | No | New status: draft or published |
_rev | string | No | Revision token from content_get for conflict detection |
Scope: content:write
content_delete
Section titled “content_delete”Soft-delete a content item by moving it to the trash. Use content_restore to undo, or content_permanent_delete to remove it forever.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID or slug |
Scope: content:write | Destructive: Yes
content_restore
Section titled “content_restore”Restore a soft-deleted content item from the trash.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID or slug |
Scope: content:write
content_permanent_delete
Section titled “content_permanent_delete”Permanently and irreversibly delete a trashed content item. The item must be in the trash first.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID or slug |
Scope: content:write | Destructive: Yes
content_publish
Section titled “content_publish”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID or slug |
Scope: content:write
content_unpublish
Section titled “content_unpublish”Revert a published item to draft status. It will no longer be visible on the live site but its content is preserved.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID or slug |
Scope: content:write
content_schedule
Section titled “content_schedule”Schedule a content item for future publication. It will be automatically published at the specified date/time.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID or slug |
scheduledAt | string | Yes | ISO 8601 datetime (e.g. 2026-06-01T09:00:00Z) |
Scope: content:write
content_compare
Section titled “content_compare”Compare the published (live) version of a content item with its current draft. Returns both versions and a flag indicating whether there are changes.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID or slug |
Scope: content:read | Read-only: Yes
content_discard_draft
Section titled “content_discard_draft”Discard the current draft and revert to the last published version. Only works on items that have been published at least once.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID or slug |
Scope: content:write | Destructive: Yes
content_list_trashed
Section titled “content_list_trashed”List soft-deleted content items in a collection’s trash.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
limit | integer | No | Max items (1-100, default 50) |
cursor | string | No | Pagination cursor |
Scope: content:read | Read-only: Yes
content_duplicate
Section titled “content_duplicate”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID or slug to duplicate |
Scope: content:write
content_translations
Section titled “content_translations”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID or slug |
Scope: content:read | Read-only: Yes
Schema Tools
Section titled “Schema Tools”schema_list_collections
Section titled “schema_list_collections”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
schema_get_collection
Section titled “schema_get_collection”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | Collection slug (e.g. posts) |
Scope: schema:read | Minimum role: Editor | Read-only: Yes
schema_create_collection
Section titled “schema_create_collection”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | Unique identifier (/^[a-z][a-z0-9_]*$/) |
label | string | Yes | Display name (plural, e.g. “Blog Posts”) |
labelSingular | string | No | Singular display name |
description | string | No | Description of this collection |
icon | string | No | Icon name for the admin UI |
supports | string[] | No | Features: drafts, revisions, preview, scheduling, search (default: ['drafts', 'revisions']) |
Scope: schema:write | Minimum role: Admin
schema_delete_collection
Section titled “schema_delete_collection”Delete a collection and its database table. This is irreversible and deletes all content in the collection.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | Collection slug to delete |
force | boolean | No | Force deletion even if the collection has content |
Scope: schema:write | Minimum role: Admin | Destructive: Yes
schema_create_field
Section titled “schema_create_field”Add a new field to a collection’s schema. This adds a column to the database table.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
slug | string | Yes | Field identifier (/^[a-z][a-z0-9_]*$/) |
label | string | Yes | Display name |
type | string | Yes | Data type (see below) |
required | boolean | No | Whether the field is required |
unique | boolean | No | Whether values must be unique |
defaultValue | any | No | Default value for new items |
validation | object | No | Constraints: min, max, minLength, maxLength, pattern, options |
options | object | No | Widget config: collection (for references), rows (for textarea) |
searchable | boolean | No | Include in full-text search index |
translatable | boolean | No | Whether 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
schema_delete_field
Section titled “schema_delete_field”Remove a field from a collection. This drops the column and deletes all data in that field. Irreversible.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
fieldSlug | string | Yes | Field slug to remove |
Scope: schema:write | Minimum role: Admin | Destructive: Yes
Media Tools
Section titled “Media Tools”media_list
Section titled “media_list”List uploaded media files with optional MIME type filtering and pagination.
| Parameter | Type | Required | Description |
|---|---|---|---|
mimeType | string | No | Filter by MIME type prefix (e.g. image/, application/pdf) |
limit | integer | No | Max items (1-100, default 50) |
cursor | string | No | Pagination cursor |
Scope: media:read | Read-only: Yes
media_get
Section titled “media_get”Get details of a single media file by ID. Returns metadata including filename, MIME type, size, dimensions, alt text, and URL.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Media item ID |
Scope: media:read | Read-only: Yes
media_update
Section titled “media_update”Update metadata of an uploaded media file. The file itself cannot be changed.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Media item ID |
alt | string | No | Alt text for accessibility |
caption | string | No | Caption text |
width | integer | No | Image width in pixels |
height | integer | No | Image height in pixels |
Scope: media:write
media_delete
Section titled “media_delete”Permanently delete a media file. Removes the database record and the file from storage. Content referencing this media will have broken references.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Media item ID |
Scope: media:write | Destructive: Yes
Search Tool
Section titled “Search Tool”search
Section titled “search”Full-text search across content collections. Collections must have search in their supports list and fields must be marked as searchable.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | Yes | Search query text |
collections | string[] | No | Limit search to specific collection slugs |
locale | string | No | Filter results by locale |
limit | integer | No | Max results (1-50, default 20) |
Scope: content:read | Read-only: Yes
Taxonomy Tools
Section titled “Taxonomy Tools”taxonomy_list
Section titled “taxonomy_list”List all taxonomy definitions (e.g. categories, tags). Returns name, label, whether hierarchical, and associated collections.
No parameters.
Scope: content:read | Read-only: Yes
taxonomy_list_terms
Section titled “taxonomy_list_terms”List terms in a taxonomy with pagination.
| Parameter | Type | Required | Description |
|---|---|---|---|
taxonomy | string | Yes | Taxonomy name (e.g. categories, tags) |
limit | integer | No | Max items (1-100, default 50) |
cursor | string | No | Pagination cursor |
Scope: content:read | Read-only: Yes
taxonomy_create_term
Section titled “taxonomy_create_term”Create a new term in a taxonomy. For hierarchical taxonomies, specify a parentId to create a child term.
| Parameter | Type | Required | Description |
|---|---|---|---|
taxonomy | string | Yes | Taxonomy name |
slug | string | Yes | URL-safe identifier |
label | string | Yes | Display name |
parentId | string | No | Parent term ID (for hierarchical taxonomies) |
description | string | No | Description of the term |
Scope: content:write
Menu Tools
Section titled “Menu Tools”menu_list
Section titled “menu_list”List all navigation menus. Returns name, label, and timestamps.
No parameters.
Scope: content:read | Read-only: Yes
menu_get
Section titled “menu_get”Get a menu by name including all its items in order. Items have a label, URL, type, and optional parent for nesting.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Menu name (e.g. main, footer) |
Scope: content:read | Read-only: Yes
Revision Tools
Section titled “Revision Tools”revision_list
Section titled “revision_list”List revision history for a content item, newest first. Requires the collection to support revisions.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID or slug |
limit | integer | No | Max revisions (1-50, default 20) |
Scope: content:read | Read-only: Yes
revision_restore
Section titled “revision_restore”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
revisionId | string | Yes | Revision ID to restore |
Scope: content:write
OAuth Discovery
Section titled “OAuth Discovery”MCP clients that support OAuth 2.1 can automatically discover how to authenticate. The server publishes two metadata documents:
Protected Resource Metadata
Section titled “Protected Resource Metadata”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"]}Authorization Server Metadata
Section titled “Authorization Server Metadata”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 UnauthorizedWWW-Authenticate: Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"This triggers the standard MCP client discovery flow.
Error Handling
Section titled “Error Handling”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.