EmDash expone un servidor integrado de Model Context Protocol (MCP) en /_emdash/api/mcp. Los clientes MCP lo usan para leer y gestionar contenido, bylines, esquemas, medios, taxonomías, menús, revisiones y ajustes, y para exportar o importar todo el sitio.
Autenticación
El endpoint MCP requiere un token Bearer. EmDash admite estos flujos de token:
| Método | Uso |
|---|---|
| OAuth 2.1 Authorization Code with Proof Key for Code Exchange (PKCE) | Clientes MCP interactivos. El usuario aprueba en un navegador los scopes solicitados. |
| Personal access token | Acceso de larga duración para un cliente o una automatización. Los tokens usan el prefijo ec_pat_ y se crean en el admin. |
| OAuth 2.0 Device Authorization Grant | Clientes de línea de comandos que piden al usuario que apruebe un código en un navegador. emdash login usa este flujo. |
Las cookies de sesión no autentican el endpoint MCP.
Scopes
Los tokens de OAuth y los tokens de acceso personal limitan qué herramientas puede llamar un cliente. El rol del usuario se comprueba por separado, de modo que un scope nunca concede un permiso que el usuario no tiene.
| Scope | Acceso |
|---|---|
content:read | Leer y buscar contenido, bylines, taxonomías, términos, menús y revisiones. El contenido de tipo borrador también requiere el permiso content:read_drafts del usuario. |
content:write | Crear y modificar contenido, bylines y revisiones. También concede taxonomies:manage y menus:manage por compatibilidad con los tokens existentes. |
media:read | Leer registros de medios. |
media:write | Subir, registrar, actualizar y eliminar medios. |
schema:read | Leer colecciones y campos. |
schema:write | Crear, actualizar y eliminar colecciones y campos. |
taxonomies:manage | Crear, actualizar y eliminar definiciones de taxonomías y términos. |
menus:manage | Crear, actualizar y eliminar menús y elementos de menú. |
settings:read | Leer los ajustes del sitio. |
settings:manage | Actualizar los ajustes del sitio. |
mcp:tools | Llamar a las herramientas MCP que exponga cualquier plugin habilitado. |
mcp:tools:<pluginId> | Llamar a las herramientas MCP que exponga un plugin habilitado concreto. |
transfer:export | Exportar todo el sitio como un paquete del sitio y descargarlo. |
transfer:analyze | Subir un paquete del sitio y analizarlo para su importación. |
transfer:execute | Iniciar, avanzar, cancelar y abandonar una importación del sitio. |
admin | Llamar a todas las herramientas del núcleo, incluidas las de transferencia de sitio. Las herramientas de plugins siguen requiriendo mcp:tools o el scope específico del plugin. |
El scope admin incluye transfer:export, transfer:analyze y transfer:execute. Cada scope de transferencia concede solo sus propias acciones y requiere el rol de administrador. Para que un cliente, como un agente, pueda analizar un paquete del sitio sin exportar ni importar, concede transfer:analyze en lugar de admin.
La página de consentimiento del código de autorización permite al usuario quitar scopes de los solicitados. EmDash también cruza la solicitud con los scopes registrados del cliente y con el rol del usuario, y rechaza una concesión vacía.
Requisitos de rol
La siguiente tabla muestra el rol mínimo para cada capacidad general. Las comprobaciones de propiedad pueden exigir un rol superior cuando un usuario actúa sobre el contenido de otro usuario.
| Capacidad | Rol mínimo |
|---|---|
| Leer contenido publicado, medios, taxonomías, términos y menús | Subscriber |
| Leer borradores, contenido programado, papelera, comparaciones y revisiones | Contributor |
| Crear contenido o subir medios | Contributor |
| Editar o publicar contenido propio y registrar medios | Author |
| Gestionar bylines, taxonomías, menús o el contenido de todos los usuarios | Editor |
| Leer esquemas o ajustes | Editor |
| Cambiar esquemas o ajustes, eliminar contenido de forma permanente o reparar el uso de medios | Admin |
| Exportar o importar todo el sitio | Admin |
Consulta los roles de usuario para ver las definiciones completas de los roles.
Transporte
El servidor usa HTTP Streamable sin estado. Cada solicitud es independiente; el servidor no mantiene una sesión MCP ni una conexión de Server-Sent Events.
| Método | Endpoint | Comportamiento |
|---|---|---|
POST | /_emdash/api/mcp | Acepta la inicialización de JSON-RPC, el listado de herramientas y las llamadas a herramientas. |
GET | /_emdash/api/mcp | Devuelve 405 Method Not Allowed. |
DELETE | /_emdash/api/mcp | Devuelve 405 Method Not Allowed. |
Las respuestas usan JSON-RPC 2.0. Llama a tools/list para obtener los esquemas de entrada y las anotaciones MCP actuales antes de construir una solicitud de herramienta.
Inventario de herramientas
El siguiente inventario coincide con las herramientas estáticas que devuelve tools/list. Se incluye el título registrado porque los clientes pueden mostrarlo en lugar del nombre de la herramienta.
Herramientas de contenido
| Herramienta | Título registrado | Scope requerido |
|---|---|---|
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 |
Herramientas de byline
| Herramienta | Título registrado | Scope requerido |
|---|---|---|
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 |
Herramientas de esquema
| Herramienta | Título registrado | Scope requerido |
|---|---|---|
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 |
Herramientas de medios
| Herramienta | Título registrado | Scope requerido |
|---|---|---|
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 |
Herramienta de búsqueda
| Herramienta | Título registrado | Scope requerido |
|---|---|---|
search | Search Content | content:read |
Herramientas de taxonomía
| Herramienta | Título registrado | Scope requerido |
|---|---|---|
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 |
Herramientas de menú
| Herramienta | Título registrado | Scope requerido |
|---|---|---|
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 |
Herramientas de revisión
| Herramienta | Título registrado | Scope requerido |
|---|---|---|
revision_list | List Revisions | content:read |
revision_restore | Restore Revision | content:write |
Herramientas de ajustes
| Herramienta | Título registrado | Scope requerido |
|---|---|---|
settings_get | Get Site Settings | settings:read |
settings_update | Update Site Settings | settings:manage |
Herramientas de transferencia de sitio
transfer:* significa cualquiera de transfer:export, transfer:analyze o transfer:execute. El scope admin satisface todos los requisitos de esta tabla.
| Herramienta | Título registrado | Scope requerido |
|---|---|---|
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 y site_import_start también aceptan un token sin el scope cuando un administrador aprueba la solicitud. Para la operación que inicia una solicitud aprobada, site_export_status, site_import_status, site_import_resume y site_import_receipt aceptan entonces el mismo token sin el scope.
Usar los esquemas de las herramientas
tools/list devuelve la descripción, el esquema JSON de entrada y las anotaciones de cada herramienta. Lee esos metadatos antes de construir una llamada para que tu cliente use los campos, los valores permitidos y los límites que admite la versión de EmDash instalada.
Por ejemplo, un cliente que actualiza un artículo llama primero a content_get y conserva el _rev devuelto. Después puede enviar esta solicitud JSON-RPC:
{
"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"
}
}
}
El resultado se devuelve como texto JSON en el primer bloque de contenido. Una herramienta con un esquema de salida también puede devolver el mismo valor en structuredContent.
Ciclo de vida del contenido y bylines
La referencia del ciclo de vida del contenido define el comportamiento de estados, revisiones, permisos, conflictos y hooks que comparten MCP, REST, la CLI y el panel de administración.
content_get devuelve un valor _rev opaco. Pásalo a content_update, content_publish, content_unpublish, content_schedule o content_discard_draft. Un valor obsoleto devuelve un conflicto, así que vuelve a leer el elemento antes de reintentar.
content_update es una actualización parcial: los campos omitidos conservan sus valores actuales. Actualizar un elemento publicado prepara un borrador mientras la versión en vivo permanece sin cambios. Usa content_compare para revisar los valores en vivo y del borrador, y luego llama a content_publish para publicar el borrador o a content_discard_draft para eliminarlo. content_delete mueve un elemento a la papelera; solo content_permanent_delete elimina de forma permanente un elemento que está en la papelera.
Los bylines son créditos reutilizables de autor o colaborador. byline_create puede crear un crédito de invitado o vincular un byline a un usuario del CMS. Un usuario solo puede tener un byline por configuración regional. Una traducción creada con translationOf conserva el usuario del byline de origen a menos que pases userId; pasa null para dejar la traducción sin vincular. Pasa el ID de byline devuelto en la entrada bylines que aceptan content_create y content_update. Eliminar un byline quita ese crédito del contenido y lo borra como byline principal.
Mientras otro usuario mantiene el bloqueo de edición de una entrada porque la tiene abierta en el admin, content_update, content_delete, content_publish, content_unpublish, content_schedule, content_unschedule, content_discard_draft y revision_restore fallan con ENTRY_LOCKED, y el error indica quién lo mantiene. Volver a leer el elemento no elimina el rechazo. Pasa overrideLock: true para escribir de todos modos.
Traducciones
Las herramientas de traducción de contenido, bylines, términos de taxonomía y menús devuelven todas las variantes de configuración regional del grupo de traducción correspondiente. Usa la entrada translationOf de la herramienta de creación cuando su esquema la ofrezca; tools/list es la fuente autorizada para los campos obligatorios.
content_translations acepta una colección y un ID o slug de contenido. Las herramientas de traducción de bylines, términos de taxonomía y menús aceptan el ID de un registro o el ID compartido del grupo de traducción. Un usuario sin acceso a borradores solo ve las traducciones de contenido publicado.
Esquemas, medios, taxonomías y menús
Las herramientas de esquema cambian la estructura de la base de datos. Usa schema_get_collection antes de crear contenido o cambiar campos; devuelve los nombres de campo, tipos, restricciones y reglas de validación disponibles. Eliminar colecciones y campos borra el contenido o los valores de campo almacenados y no se puede deshacer.
Usa media_upload para enviar bytes codificados en base64. Las subidas están sujetas a los límites de tamaño y de tipo MIME configurados, y unos bytes idénticos pueden devolver un elemento de medios existente con deduplicated: true.
media_create confirma una subida pendiente creada mediante POST /_emdash/api/media/upload-url. Sube el archivo con la URL firmada devuelta y luego llama a media_create desde la misma cuenta de usuario con el storageKey devuelto. La herramienta comprueba que el archivo almacenado existe y que coincide con el tamaño indicado al solicitar la URL de subida antes de ponerlo a disposición en la biblioteca de medios.
Las definiciones de taxonomía describen la clasificación y las colecciones a las que se aplica; los términos son los valores individuales asignados al contenido. Los términos jerárquicos pueden usar parentId, pero el padre debe pertenecer a la misma taxonomía y no puede crear un ciclo. Crear o actualizar un término con parentId en una taxonomía no jerárquica devuelve VALIDATION_ERROR. Un término con hijos debe tener esos hijos eliminados o movidos antes de borrarse.
menu_set_items reemplaza la lista completa de elementos de un menú en una sola operación atómica. El orden del array se convierte en el orden del menú. El parentIndex de un elemento anidado apunta a un elemento anterior del mismo array, así que coloca cada padre antes de sus hijos.
media_usage_repair puede procesar una colección o todas, y puede tardar mucho en un sitio grande. Sus estados complete, partial, failed y stale son respuestas correctas de la herramienta. Inspecciona el estado y los recuentos devueltos en lugar de confiar en isError; los fallos de autenticación, validación y ejecución inesperados establecen isError: true.
Transferencia de sitio
Las herramientas site_* exportan un sitio completo como paquete del sitio e importan un paquete en un sitio vacío. Inician y dirigen las operaciones y devuelven resúmenes acotados. Nunca transportan bytes del paquete, medios, contenido de registros, direcciones de correo de los principales ni URL de descarga. Descarga una exportación y sube un paquete para importarlo con la CLI o la API REST, y luego haz referencia a la operación por su ID.
Todas las herramientas de transferencia requieren el rol Admin. El rol se comprueba antes que el scope, de modo que un llamador que no es administrador recibe INSUFFICIENT_PERMISSIONS y no se crea ninguna solicitud de aprobación.
Exportación
site_export_start acepta comments (por defecto true) y devuelve la nueva operación. site_export_status ejecuta un paso de exportación acotado en cada llamada e informa de la operación y de nextRequestInMs. Vuelve a llamarla después de ese retraso hasta que nextRequestInMs sea null. Pasa advance: false para leer el estado sin ejecutar un paso. Cuando la exportación termina, el resultado también incluye totals: recuentos de registros por tipo, número y bytes de medios, y número y bytes de archivos del paquete.
Importación
Sube primero el paquete. El comando emdash site import <file> --analyze de la CLI lo sube, lo analiza e imprime el ID de la operación.
site_import_analyze ejecuta un paso de análisis acotado en cada llamada. Repítelo hasta que nextRequestInMs sea null; el resultado incluye entonces un resumen del plan con packageDigest, planDigest, executable, recuentos, tamaños, principales, decisiones, transformaciones, advertencias y bloqueos. Cada transformación aparece con su code, el kind del registro cuando lo tiene y un count, sin los IDs ni los valores a los que se aplica. Los principales, las advertencias y los bloqueos se listan con un máximo de 50 elementos cada uno, con el recuento completo en total. Los principales se listan sin direcciones de correo, con los IDs de usuario de destino sugeridos y los asignados actualmente. Pasa decisions para asignar principales a IDs de usuario de destino (o null) y para elegir el título y el eslogan del paquete o del destino. Cada cambio produce un nuevo planDigest.
site_import_start recibe el ID de la operación y el packageDigest y el planDigest del plan más reciente. El plan no debe tener bloqueos. La herramienta tiene destructiveHint: true: una vez iniciada, la importación escribe en el sitio y bloquea otras escrituras hasta que termina o un administrador la abandona. Muestra el plan al usuario y obtén su confirmación antes de llamarla.
site_import_resume ejecuta un paso de importación acotado e informa de la operación y de nextRequestInMs. Llámala hasta que nextRequestInMs sea null; es seguro repetirla después de una desconexión. site_import_status informa de la operación y del número de archivos subidos sin avanzar la importación. site_import_receipt devuelve el recibo completo, incluido receiptDigest, cuando la importación ha terminado.
Mientras se ejecuta una importación, y después de que falle o se cancele hasta que se abandone, cualquier otra herramienta que pueda escribir falla con TRANSFER_IMPORT_IN_PROGRESS. Esto incluye las herramientas de plugins. Las herramientas anotadas con readOnlyHint: true y las ocho herramientas site_* siguen funcionando, y initialize y tools/list nunca se bloquean. Durante la activación del uso de medios, las herramientas de escritura fallan de la misma manera con MEDIA_USAGE_ACTIVATION_IN_PROGRESS.
Los resúmenes de operación incluyen id, kind, state, stage, progress, packageDigest, planDigest, error ({ code } o null) y marcas de tiempo. progress es { done, total } pasos, más records, los registros que una exportación ha escrito hasta el momento, y bytesDone y bytesTotal, una vez conocidos. Las herramientas MCP no pueden cancelar ni abandonar una importación; usa la API REST.
Aprobaciones
Un token con admin o con el scope de transferencia necesario nunca pide aprobación. Para un token sin ninguno de los dos, como un agente al que solo se le concedió transfer:analyze, site_export_start y site_import_start se ejecutan cuando un administrador aprueba la solicitud:
- La primera llamada sin el scope crea una solicitud de aprobación pendiente y falla con
TRANSFER_APPROVAL_REQUIRED. El texto del mensaje y_meta.detailsincluyen elapprovalIdy suexpiresAt. Volver a llamar con los mismos argumentos y sinapprovalIddevuelve la misma solicitud abierta. - Un administrador aprueba la solicitud en Approval requests, dentro de Settings → Transfer, o con el endpoint de aprobación solo de sesión de la API REST. Los tokens de API no pueden aprobar solicitudes.
- El cliente repite la llamada con los mismos argumentos y el
approvalId. La aprobación se consume cuando esa llamada inicia la operación. Si la operación no llega a iniciarse, el cliente puede reintentar con el mismoapprovalIdhasta que caduque.
Una solicitud está vinculada al usuario, al token, a la acción y a los argumentos exactos: las opciones de exportación, o el ID de la operación de importación y ambos digests. Una solicitud pendiente caduca 15 minutos después de crearse, y una aprobada, 15 minutos después de la aprobación. Una llamada con argumentos distintos o con otro token, o con una aprobación denegada, caducada o ya usada, falla con TRANSFER_APPROVAL_INVALID.
site_import_start comprueba los digests, el estado de la operación y los bloqueos del plan antes de crear una solicitud, de modo que solo se pide a un administrador que apruebe una importación que se puede ejecutar. Una aprobación necesita un ID de token, así que un llamador sin él recibe INSUFFICIENT_SCOPE.
Después de que una llamada aprobada inicie una operación, el mismo usuario y el mismo token pueden llamar a site_export_status, o a site_import_status, site_import_resume y site_import_receipt, para esa operación sin el scope.
Herramientas de plugins
Un administrador debe habilitar la superficie MCP de cada plugin. Las herramientas habilitadas aparecen en tools/list con un prefijo de plugin seguro para MCP seguido de __<localName> (por ejemplo, @acme/calendar y createEvent se convierten en acme__calendar__createEvent) y requieren mcp:tools o mcp:tools:<pluginId> en las llamadas autenticadas con token. EmDash también comprueba el permiso que declara la ruta del plugin y registra el plugin, la herramienta, la ruta y el actor en el registro de auditoría.
Los ID con ámbito (scoped) y separadores ambiguos incluyen un hash corto antes del nombre local de la herramienta. Usa tools/list para descubrir los nombres generados. Si dos herramientas generan el mismo nombre, EmDash conserva la primera y registra una advertencia para la herramienta omitida.
Como las herramientas de plugins dependen de cada instalación, no forman parte del inventario estático anterior.
Descubrimiento de OAuth
Los clientes MCP descubren el servidor de autorización a partir de los metadatos del recurso protegido:
GET /.well-known/oauth-protected-resource
La respuesta identifica /_emdash/api/mcp como el recurso protegido y enlaza con el servidor de autorización. Después, los clientes leen sus metadatos en:
GET /.well-known/oauth-authorization-server/_emdash
Ese documento proporciona los endpoints actuales de autorización, token, registro y autorización de dispositivo, los scopes y tipos de concesión admitidos y el método PKCE S256. Usa los valores descubiertos en lugar de codificar de forma fija las rutas del protocolo OAuth.
Una solicitud MCP sin autenticar devuelve una respuesta 401 con la URL de descubrimiento:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"
Errores
Un fallo de herramienta tiene isError: true. El primer bloque de texto empieza con un código estable, y _meta.code lo repite para los clientes que leen metadatos estructurados:
{
"content": [{ "type": "text", "text": "[NOT_FOUND] Collection 'articles' not found" }],
"isError": true,
"_meta": { "code": "NOT_FOUND" }
}
Un rechazo por el bloqueo de edición de una entrada incluye quién lo mantiene en _meta.details:
{
"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"
}
}
}
Los fallos de autenticación usan códigos como INSUFFICIENT_SCOPE e INSUFFICIENT_PERMISSIONS. Los fallos de transporte usan el código de error interno de JSON-RPC -32603 y no exponen la excepción subyacente.