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 taxonomies, taxonomy terms, and menus. |
content:write | Create, 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:read | List and get media items. |
media:write | Register (create), update, and delete media metadata. |
schema:read | List collections and get collection schemas. |
schema:write | Create and delete collections and fields. |
taxonomies:manage | Create, update, and delete taxonomy terms. |
menus:manage | Create, update, and delete navigation menus and their items. |
settings:read | Read site-wide settings. |
settings:manage | Update site-wide settings. |
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.
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.
Role Requirements
Section titled “Role Requirements”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.
| Operation | Minimum role |
|---|---|
| Content read | Subscriber (10) for published items; Contributor (20) for drafts, scheduled, trash, and revisions |
| Content create | Contributor (20) |
| Content edit own / delete own | Author (30) |
| Content publish | Author (30) for own items; Editor (40) to act on others’ items |
| Schema read | Editor (40) |
| Schema write | Admin (50) |
| Taxonomies manage | Editor (40) |
| Menus manage | Editor (40) |
| Settings read | Editor (40) |
| Settings manage | Admin (50) |
Media upload (media_create) | Author (30) |
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 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.
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_unschedule
Section titled “content_unschedule”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | Yes | Collection slug |
id | string | Yes | Content item ID or slug |
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_create
Section titled “media_create”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
filename | string | Yes | Original filename (e.g. logo.png) |
mimeType | string | Yes | MIME type (e.g. image/png) |
storageKey | string | Yes | Storage path/key the file was uploaded to |
size | integer | No | File size in bytes |
width | integer | No | Image width in pixels |
height | integer | No | Image height in pixels |
contentHash | string | No | Hash of the file contents (for dedupe) |
blurhash | string | No | Blurhash for image placeholders |
dominantColor | string | No | Hex color string for the image’s dominant color |
Scope: media:write | Minimum role: Author
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. The parent’s ancestor chain must not exceed 100 levels.
| 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: taxonomies:manage | Minimum role: Editor
taxonomy_update_term
Section titled “taxonomy_update_term”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
taxonomy | string | Yes | Taxonomy name |
termSlug | string | Yes | Current slug of the term to update |
slug | string | No | New slug (must be unique in the taxonomy) |
label | string | No | New display name |
parentId | string | null | No | New parent term ID; null to detach |
description | string | No | New description |
Scope: taxonomies:manage | Minimum role: Editor
taxonomy_delete_term
Section titled “taxonomy_delete_term”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
taxonomy | string | Yes | Taxonomy name |
termSlug | string | Yes | Slug of the term to delete |
Scope: taxonomies:manage | Minimum role: Editor | Destructive: Yes
Menu Tools
Section titled “Menu Tools”menu_list
Section titled “menu_list”List navigation menus. Menus are per-locale: pass locale to return one locale’s
rows only, or omit it to list every locale variant.
| Parameter | Type | Required | Description |
|---|---|---|---|
locale | string | No | Filter by locale (omit for all locale variants) |
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. When the same menu name exists in multiple locales,
pass locale to resolve the intended translation.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Menu name (e.g. main, footer) |
locale | string | No | Locale to resolve the menu for |
Scope: content:read | Read-only: Yes
menu_create
Section titled “menu_create”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Stable identifier (/^[a-z][a-z0-9_]*$/) |
label | string | Yes | Display name for the admin |
locale | string | No | Locale for this menu (e.g. fr-fr) |
translationOf | string | No | Existing menu id to create this locale variant from |
Scope: menus:manage | Minimum role: Editor
menu_update
Section titled “menu_update”Update a menu’s label. The name (stable identifier) cannot be changed. On
multi-locale installs, pass locale so the correct translation is updated.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Menu name to update |
label | string | Yes | New display label |
locale | string | No | Locale of the menu to update |
Scope: menus:manage | Minimum role: Editor
menu_delete
Section titled “menu_delete”Delete a menu and all its items. Cannot be undone. On multi-locale installs,
pass locale so only the intended translation is removed.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Menu name to delete |
locale | string | No | Locale of the menu to delete |
Scope: menus:manage | Minimum role: Editor | Destructive: Yes
menu_set_items
Section titled “menu_set_items”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Menu name to update |
locale | string | No | Locale of the menu to rewrite |
items | MenuItem[] | Yes | Ordered list of menu items (see below) |
Each MenuItem has:
| Field | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Item display text |
type | string | Yes | One of custom, page, post, taxonomy, collection |
customUrl | string | No | URL for type: "custom" items (ignored otherwise) |
referenceCollection | string | No | Target collection slug for content references |
referenceId | string | No | Target content / term ID for references |
titleAttr | string | No | HTML title attribute |
target | string | No | HTML target attribute (e.g. _blank) |
cssClasses | string | No | Space-separated CSS classes |
parentIndex | integer | No | Array index of the parent item. Omit for top-level items. |
Scope: menus:manage | Minimum role: Editor
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
Settings Tools
Section titled “Settings Tools”Site-wide settings — title, tagline, logo, favicon, canonical URL, default page size, date and time formatting, social handles, and SEO defaults.
settings_get
Section titled “settings_get”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
settings_update
Section titled “settings_update”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | No | Site title |
tagline | string | No | Short description shown alongside the title |
logo | MediaRef | No | Logo media reference ({ mediaId, alt? }) |
favicon | MediaRef | No | Favicon media reference |
url | string | No | Canonical site URL (http or https). Empty string clears it. |
postsPerPage | integer | No | Default page size for content listings (1-100) |
dateFormat | string | No | Date format token string |
timezone | string | No | IANA timezone identifier |
social | object | No | Social handles — twitter, github, facebook, instagram, linkedin, youtube |
seo | object | No | SEO defaults (see below) |
The seo object accepts:
| Field | Type | Description |
|---|---|---|
titleSeparator | string | Separator between page title and site title (e.g. " | " for a vertical bar) |
defaultOgImage | MediaRef | Default Open Graph image when content has none |
robotsTxt | string | Custom robots.txt body. Omit to use the EmDash default. |
googleVerification | string | Google Search Console verification token |
bingVerification | string | Bing Webmaster Tools verification token |
Scope: settings:manage | Minimum role: Admin
OAuth Discovery
Section titled “OAuth Discovery”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:
Protected Resource Metadata
Section titled “Protected Resource Metadata”Request the protected resource metadata at the following endpoint:
GET /.well-known/oauth-protected-resourceThe 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"]}Authorization Server Metadata
Section titled “Authorization Server Metadata”Request the authorization server metadata at the following endpoint:
GET /.well-known/oauth-authorization-server/_emdashThe 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 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. 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.