---
title: MCP Server Reference
description: Protocol details, tool specifications, and OAuth configuration for the MCP server.
---

import { Aside } from "@astrojs/starlight/components";

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

<Aside>
	Looking to connect Claude, ChatGPT, or another AI tool to your site? See the [AI Tools guide](/guides/ai-tools) for setup instructions and usage tips.
</Aside>

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

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

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

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](/guides/authentication#user-roles) for role definitions.

## 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 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](https://www.jsonrpc.org/specification) format. Errors use standard JSON-RPC error codes, with MCP-specific codes for scope and permission failures.

## Tools

The server exposes 33 tools across seven domains. Each tool returns results as JSON text content, or an error message with `isError: true` on failure.

### Content Tools

#### `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`

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

<Aside>
	The `_rev` token in the response is used for conflict detection. Pass it back when updating to ensure no one else has modified the item since you read it.
</Aside>

#### `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`

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`

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`

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`

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`

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`

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`

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`

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`

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`

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`

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`

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

<Aside type="caution">
	Schema tools modify the database structure. Creating or deleting collections and fields changes the underlying tables. These operations require Admin role.
</Aside>

#### `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`

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`

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`

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`

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`

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

#### `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`

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`

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`

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

#### `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

#### `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`

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`

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

#### `menu_list`

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

**No parameters.**

**Scope:** `content:read` | **Read-only:** Yes

#### `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

#### `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`

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

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

### Protected Resource Metadata

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

```json
{
  "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

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

```json
{
  "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
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.

## Error Handling

Tool errors are returned as text content with `isError: true`:

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

Scope and permission errors throw MCP protocol errors:

```json
{
  "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.
