# MCP Server Reference

> Authentication, protocol behavior, and the complete tool inventory for the MCP server.

Source: https://emdashcmseverything.com/official-docs/reference/mcp-server/

EmDash exposes a built-in [Model Context Protocol](https://modelcontextprotocol.io) (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.

> **Note**
>
> To connect Claude, ChatGPT, or another AI client, follow the [AI tools guide](https://emdashcmseverything.com/official-docs/guides/ai-tools/).

## Authentication

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

| Method | Use |
| --- | --- |
| 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 token | Long-lived access for a client or automation. Tokens use the `ec_pat_` prefix and are created in the admin. |
| OAuth 2.0 Device Authorization Grant | Command-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.

| Scope | Access |
| --- | --- |
| `content:read` | Read and search content, bylines, taxonomies, terms, menus, and revisions. Draft-like content also requires the user's `content:read_drafts` permission. |
| `content:write` | Create and change content, bylines, and revisions. It also grants `taxonomies:manage` and `menus:manage` for compatibility with existing tokens. |
| `media:read` | Read media records. |
| `media:write` | Upload, register, update, and delete media. |
| `schema:read` | Read collections and fields. |
| `schema:write` | Create, update, and delete collections and fields. |
| `taxonomies:manage` | Create, update, and delete taxonomy definitions and terms. |
| `menus:manage` | Create, update, and delete menus and menu items. |
| `settings:read` | Read site settings. |
| `settings:manage` | Update site settings. |
| `mcp:tools` | Call MCP tools exposed by any enabled plugin. |
| `mcp:tools:<pluginId>` | Call MCP tools exposed by one enabled plugin. |
| `transfer:export` | Export the whole site as a [site package](https://emdashcmseverything.com/official-docs/guides/site-transfer/) and download it. |
| `transfer:analyze` | Upload a site package and analyze it for import. |
| `transfer:execute` | Start, advance, cancel, and abandon a site import. |
| `admin` | Call 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.

| Capability | Minimum role |
| --- | --- |
| Read published content, media, taxonomies, terms, and menus | Subscriber |
| Read drafts, scheduled content, trash, comparisons, and revisions | Contributor |
| Create content, edit owned drafts, or upload media | Contributor |
| Edit owned published or scheduled content, publish owned content, or register media | Author |
| Manage bylines, taxonomies, menus, or all users' content | Editor |
| Read schemas or settings | Editor |
| Change schemas or settings, permanently delete content, or repair media usage | Admin |
| Export or import the whole site | Admin |

See [user roles](https://emdashcmseverything.com/official-docs/guides/authentication/#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.

| Method | Endpoint | Behavior |
| --- | --- | --- |
| `POST` | `/_emdash/api/mcp` | Accepts JSON-RPC initialization, tool listing, and tool calls. |
| `GET` | `/_emdash/api/mcp` | Returns `405 Method Not Allowed`. |
| `DELETE` | `/_emdash/api/mcp` | Returns `405 Method Not Allowed`. |

Responses use [JSON-RPC 2.0](https://www.jsonrpc.org/specification). 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.

{/* mcp-tool-inventory:start */}

### Content tools

| Tool | Registered title | Required scope |
| --- | --- | --- |
| `content_list` | List Content | `content:read` |
| `content_get` | Get Content | `content:read` |
| `content_create` | Create Content | `content:write` |
| `content_update` | Update Content | `content:write` |
| `content_delete` | Delete Content (Trash) | `content:write` |
| `content_restore` | Restore Content | `content:write` |
| `content_permanent_delete` | Permanently Delete Content | `content:write` |
| `content_publish` | Publish Content | `content:write` |
| `content_unpublish` | Unpublish Content | `content:write` |
| `content_schedule` | Schedule Content | `content:write` |
| `content_unschedule` | Cancel Scheduled Publication | `content:write` |
| `content_compare` | Compare Live vs Draft | `content:read` |
| `content_discard_draft` | Discard Draft | `content:write` |
| `content_list_trashed` | List Trashed Content | `content:read` |
| `content_duplicate` | Duplicate Content | `content:write` |
| `content_translations` | Get Content Translations | `content:read` |

### Byline tools

| Tool | Registered title | Required scope |
| --- | --- | --- |
| `byline_list` | List Bylines | `content:read` |
| `byline_get` | Get Byline | `content:read` |
| `byline_create` | Create Byline | `content:write` |
| `byline_update` | Update Byline | `content:write` |
| `byline_delete` | Delete Byline | `content:write` |
| `byline_translations` | List Byline Translations | `content:read` |

### Schema tools

| Tool | Registered title | Required scope |
| --- | --- | --- |
| `schema_list_collections` | List Collections | `schema:read` |
| `schema_get_collection` | Get Collection Schema | `schema:read` |
| `schema_list_block_types` | List Block Types | `schema:read` |
| `schema_get_block_type` | Get Block Type | `schema:read` |
| `schema_create_block_type` | Create Block Type | `schema:write` |
| `schema_update_block_type` | Update Block Type | `schema:write` |
| `schema_activate_block_type_version` | Activate Block Type Version | `schema:write` |
| `schema_create_collection` | Create Collection | `schema:write` |
| `schema_delete_collection` | Delete Collection | `schema:write` |
| `schema_update_collection` | Update Collection | `schema:write` |
| `schema_create_field` | Add Field to Collection | `schema:write` |
| `schema_delete_field` | Remove Field from Collection | `schema:write` |
| `schema_update_field` | Update Field | `schema:write` |

### Media tools

| Tool | Registered title | Required scope |
| --- | --- | --- |
| `media_list` | List Media | `media:read` |
| `media_create` | Confirm Signed Media Upload | `media:write` |
| `media_upload` | Upload Media | `media:write` |
| `media_get` | Get Media Item | `media:read` |
| `media_update` | Update Media Metadata | `media:write` |
| `media_delete` | Delete Media | `media:write` |
| `media_usage_repair` | Repair Media Usage Index | `admin` |

### Search tool

| Tool | Registered title | Required scope |
| --- | --- | --- |
| `search` | Search Content | `content:read` |

### Taxonomy tools

| Tool | Registered title | Required scope |
| --- | --- | --- |
| `taxonomy_list` | List Taxonomies | `content:read` |
| `taxonomy_get` | Get Taxonomy Definition | `content:read` |
| `taxonomy_create` | Create Taxonomy Definition | `taxonomies:manage` |
| `taxonomy_update` | Update Taxonomy Definition | `taxonomies:manage` |
| `taxonomy_delete` | Delete Taxonomy Definition | `taxonomies:manage` |
| `taxonomy_list_terms` | List Taxonomy Terms | `content:read` |
| `taxonomy_create_term` | Create Taxonomy Term | `taxonomies:manage` |
| `taxonomy_update_term` | Update Taxonomy Term | `taxonomies:manage` |
| `taxonomy_delete_term` | Delete Taxonomy Term | `taxonomies:manage` |
| `taxonomy_term_translations` | List Term Translations | `content:read` |

### Menu tools

| Tool | Registered title | Required scope |
| --- | --- | --- |
| `menu_list` | List Menus | `content:read` |
| `menu_get` | Get Menu with Items | `content:read` |
| `menu_translations` | List Menu Translations | `content:read` |
| `menu_create` | Create Menu | `menus:manage` |
| `menu_update` | Update Menu | `menus:manage` |
| `menu_delete` | Delete Menu | `menus:manage` |
| `menu_set_items` | Set Menu Items | `menus:manage` |

### Revision tools

| Tool | Registered title | Required scope |
| --- | --- | --- |
| `revision_list` | List Revisions | `content:read` |
| `revision_restore` | Restore Revision | `content:write` |

### Settings tools

| Tool | Registered title | Required scope |
| --- | --- | --- |
| `settings_get` | Get Site Settings | `settings:read` |
| `settings_update` | Update Site Settings | `settings: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.

| Tool | Registered title | Required scope |
| --- | --- | --- |
| `site_transfer_capabilities` | Get Site Transfer Capabilities | `transfer:*` |
| `site_export_start` | Start Site Export | `transfer:export` |
| `site_export_status` | Get Site Export Status | `transfer:export` |
| `site_import_analyze` | Analyze Site Import | `transfer:analyze` |
| `site_import_start` | Start Site Import | `transfer:execute` |
| `site_import_status` | Get Site Import Status | `transfer:*` |
| `site_import_resume` | Resume Site Import | `transfer:execute` |
| `site_import_receipt` | Get Site Import Receipt | `transfer:*` |

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

{/* mcp-tool-inventory:end */}

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

```json
{
	"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](https://emdashcmseverything.com/official-docs/reference/content-lifecycle/) 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. A user can have only one byline per locale. A translation created with `translationOf` keeps the source byline's user unless you pass `userId`; pass `null` to leave the translation unlinked. 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](https://emdashcmseverything.com/official-docs/reference/rest-api/#entry-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`.

> **Caution**
>
> Tools whose registered metadata has `destructiveHint: true` can delete stored data. Confirm the target and its current state before calling them.

## Site transfer

The `site_*` tools export a whole site as a [site package](https://emdashcmseverything.com/official-docs/guides/site-transfer/) 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](https://emdashcmseverything.com/official-docs/reference/cli/#emdash-site) or the [REST API](https://emdashcmseverything.com/official-docs/guides/site-transfer/#import-with-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](https://emdashcmseverything.com/official-docs/guides/site-transfer/#cancel-an-import).

### 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](https://emdashcmseverything.com/official-docs/reference/rest-api/#site-transfer). 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` with an MCP-safe plugin prefix followed by `__<localName>` (for example, `@acme/calendar` and `createEvent` become `acme__calendar__createEvent`) 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.

Scoped IDs with ambiguous separators include a short hash before the local tool name. Use `tools/list` to discover generated names. If two tools generate the same name, EmDash keeps the first and logs a warning for the skipped tool.

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:

```http
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:

```http
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
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:

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

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