EmDash expose un serveur Model Context Protocol (MCP) intégré à l’adresse /_emdash/api/mcp. Les clients MCP l’utilisent pour lire et gérer le contenu, les bylines, les schémas, les médias, les taxonomies, les menus, les révisions et les paramètres, ainsi que pour exporter ou importer l’intégralité du site.
Authentification
Le point de terminaison MCP exige un jeton Bearer. EmDash prend en charge les flux de jetons suivants :
| Méthode | Utilisation |
|---|---|
| OAuth 2.1 Authorization Code with Proof Key for Code Exchange (PKCE) | Clients MCP interactifs. L’utilisateur approuve dans un navigateur les scopes demandés. |
| Personal access token | Accès de longue durée pour un client ou une automatisation. Les jetons utilisent le préfixe ec_pat_ et sont créés dans l’admin. |
| OAuth 2.0 Device Authorization Grant | Clients en ligne de commande qui demandent à l’utilisateur d’approuver un code dans un navigateur. emdash login utilise ce flux. |
Les cookies de session n’authentifient pas le point de terminaison MCP.
Scopes
Les jetons OAuth et les jetons d’accès personnels limitent les outils qu’un client peut appeler. Le rôle de l’utilisateur est vérifié séparément : un scope n’accorde donc jamais une permission que l’utilisateur n’a pas.
| Scope | Accès |
|---|---|
content:read | Lire et rechercher le contenu, les bylines, les taxonomies, les termes, les menus et les révisions. Le contenu de type brouillon exige aussi la permission content:read_drafts de l’utilisateur. |
content:write | Créer et modifier le contenu, les bylines et les révisions. Il accorde aussi taxonomies:manage et menus:manage par compatibilité avec les jetons existants. |
media:read | Lire les enregistrements de médias. |
media:write | Téléverser, enregistrer, mettre à jour et supprimer des médias. |
schema:read | Lire les collections et les champs. |
schema:write | Créer, mettre à jour et supprimer des collections et des champs. |
taxonomies:manage | Créer, mettre à jour et supprimer des définitions de taxonomies et des termes. |
menus:manage | Créer, mettre à jour et supprimer des menus et des éléments de menu. |
settings:read | Lire les paramètres du site. |
settings:manage | Mettre à jour les paramètres du site. |
mcp:tools | Appeler les outils MCP exposés par n’importe quel plugin activé. |
mcp:tools:<pluginId> | Appeler les outils MCP exposés par un plugin activé donné. |
transfer:export | Exporter l’intégralité du site sous forme de paquet de site et le télécharger. |
transfer:analyze | Téléverser un paquet de site et l’analyser en vue de son importation. |
transfer:execute | Démarrer, faire avancer, annuler et abandonner une importation de site. |
admin | Appeler tous les outils du noyau, y compris les outils de transfert de site. Les outils de plugins exigent toujours mcp:tools ou le scope propre au plugin. |
Le scope admin inclut transfer:export, transfer:analyze et transfer:execute. Chaque scope de transfert n’accorde que ses propres actions et exige le rôle d’administrateur. Pour permettre à un client, par exemple un agent, d’analyser un paquet de site sans l’exporter ni l’importer, accordez transfer:analyze plutôt que admin.
La page de consentement du code d’autorisation permet à l’utilisateur de retirer des scopes demandés. EmDash recoupe aussi la demande avec les scopes enregistrés du client et le rôle de l’utilisateur, et refuse une autorisation vide.
Exigences de rôle
Le tableau suivant indique le rôle minimal pour chaque capacité générale. Les vérifications de propriété peuvent exiger un rôle plus élevé lorsqu’un utilisateur agit sur le contenu d’un autre utilisateur.
| Capacité | Rôle minimal |
|---|---|
| Lire le contenu publié, les médias, les taxonomies, les termes et les menus | Subscriber |
| Lire les brouillons, le contenu planifié, la corbeille, les comparaisons et les révisions | Contributor |
| Créer du contenu ou téléverser des médias | Contributor |
| Modifier ou publier son propre contenu et enregistrer des médias | Author |
| Gérer les bylines, les taxonomies, les menus ou le contenu de tous les utilisateurs | Editor |
| Lire les schémas ou les paramètres | Editor |
| Modifier les schémas ou les paramètres, supprimer définitivement du contenu ou réparer l’utilisation des médias | Admin |
| Exporter ou importer l’intégralité du site | Admin |
Consultez les rôles utilisateur pour les définitions complètes des rôles.
Transport
Le serveur utilise un HTTP Streamable sans état. Chaque requête est indépendante ; le serveur ne conserve ni session MCP ni connexion Server-Sent Events.
| Méthode | Point de terminaison | Comportement |
|---|---|---|
POST | /_emdash/api/mcp | Accepte l’initialisation JSON-RPC, le listage des outils et les appels d’outils. |
GET | /_emdash/api/mcp | Renvoie 405 Method Not Allowed. |
DELETE | /_emdash/api/mcp | Renvoie 405 Method Not Allowed. |
Les réponses utilisent JSON-RPC 2.0. Appelez tools/list pour obtenir les schémas d’entrée et les annotations MCP actuels avant de construire une requête d’outil.
Inventaire des outils
L’inventaire suivant correspond aux outils statiques renvoyés par tools/list. Le titre enregistré est indiqué, car les clients peuvent l’afficher à la place du nom de l’outil.
Outils de contenu
| Outil | Titre enregistré | Scope requis |
|---|---|---|
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 |
Outils byline
| Outil | Titre enregistré | Scope requis |
|---|---|---|
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 |
Outils de schéma
| Outil | Titre enregistré | Scope requis |
|---|---|---|
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 |
Outils média
| Outil | Titre enregistré | Scope requis |
|---|---|---|
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 |
Outil de recherche
| Outil | Titre enregistré | Scope requis |
|---|---|---|
search | Search Content | content:read |
Outils de taxonomie
| Outil | Titre enregistré | Scope requis |
|---|---|---|
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 |
Outils de menu
| Outil | Titre enregistré | Scope requis |
|---|---|---|
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 |
Outils de révision
| Outil | Titre enregistré | Scope requis |
|---|---|---|
revision_list | List Revisions | content:read |
revision_restore | Restore Revision | content:write |
Outils de paramètres
| Outil | Titre enregistré | Scope requis |
|---|---|---|
settings_get | Get Site Settings | settings:read |
settings_update | Update Site Settings | settings:manage |
Outils de transfert de site
transfer:* désigne l’un quelconque des scopes transfer:export, transfer:analyze ou transfer:execute. Le scope admin satisfait toutes les exigences de ce tableau.
| Outil | Titre enregistré | Scope requis |
|---|---|---|
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 et site_import_start acceptent aussi un jeton sans le scope lorsqu’un administrateur approuve la demande. Pour l’opération que démarre une demande approuvée, site_export_status, site_import_status, site_import_resume et site_import_receipt acceptent alors le même jeton sans le scope.
Utiliser les schémas des outils
tools/list renvoie la description, le schéma d’entrée JSON et les annotations de chaque outil. Lisez ces métadonnées avant de construire un appel afin que votre client utilise les champs, les valeurs autorisées et les limites pris en charge par la version d’EmDash installée.
Par exemple, un client qui met à jour un article appelle d’abord content_get et conserve le _rev renvoyé. Il peut ensuite envoyer cette requête 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"
}
}
}
Le résultat est renvoyé sous forme de texte JSON dans le premier bloc de contenu. Un outil doté d’un schéma de sortie peut aussi renvoyer la même valeur dans structuredContent.
Cycle de vie du contenu et bylines
La référence du cycle de vie du contenu définit le comportement des états, des révisions, des permissions, des conflits et des hooks partagé par MCP, REST, la CLI et le panneau d’administration.
content_get renvoie une valeur _rev opaque. Transmettez-la à content_update, content_publish, content_unpublish, content_schedule ou content_discard_draft. Une valeur périmée renvoie un conflit : relisez donc l’élément avant de réessayer.
content_update est une mise à jour partielle : les champs omis conservent leurs valeurs actuelles. La mise à jour d’un élément publié prépare un brouillon tandis que la version en ligne reste inchangée. Utilisez content_compare pour examiner les valeurs en ligne et celles du brouillon, puis appelez content_publish pour publier le brouillon ou content_discard_draft pour le supprimer. content_delete déplace un élément dans la corbeille ; seul content_permanent_delete supprime définitivement un élément placé dans la corbeille.
Les bylines sont des mentions réutilisables d’auteur ou de contributeur. byline_create peut créer une mention d’invité ou lier une byline à un utilisateur du CMS. Un utilisateur ne peut avoir qu’une seule byline par locale. Une traduction créée avec translationOf conserve l’utilisateur de la byline source, sauf si vous transmettez userId ; transmettez null pour laisser la traduction sans lien. Transmettez l’ID de byline renvoyé dans l’entrée bylines acceptée par content_create et content_update. Supprimer une byline retire cette mention du contenu et la supprime en tant que byline principale.
Tant qu’un autre utilisateur détient le verrou d’édition d’une entrée parce qu’il l’a ouverte dans l’admin, content_update, content_delete, content_publish, content_unpublish, content_schedule, content_unschedule, content_discard_draft et revision_restore échouent avec ENTRY_LOCKED, et l’erreur désigne le détenteur. Relire l’élément ne lève pas le refus. Transmettez overrideLock: true pour écrire malgré tout.
Traductions
Les outils de traduction du contenu, des bylines, des termes de taxonomie et des menus renvoient chaque variante de locale du groupe de traduction concerné. Utilisez l’entrée translationOf de l’outil de création lorsque son schéma en propose une ; tools/list fait foi pour les champs obligatoires.
content_translations accepte une collection et un ID ou un slug de contenu. Les outils de traduction des bylines, des termes de taxonomie et des menus acceptent soit l’ID d’un enregistrement, soit l’ID partagé du groupe de traduction. Un utilisateur sans accès aux brouillons ne voit que les traductions du contenu publié.
Schémas, médias, taxonomies et menus
Les outils de schéma modifient la structure de la base de données. Utilisez schema_get_collection avant de créer du contenu ou de modifier des champs ; il renvoie les noms de champs, les types, les contraintes et les règles de validation disponibles. La suppression de collections et de champs retire le contenu ou les valeurs de champs stockés et ne peut pas être annulée.
Utilisez media_upload pour envoyer des octets encodés en base64. Les téléversements sont soumis aux limites de taille et de type MIME configurées, et des octets identiques peuvent renvoyer un élément de média existant avec deduplicated: true.
media_create confirme un téléversement en attente créé via POST /_emdash/api/media/upload-url. Téléversez le fichier avec l’URL signée renvoyée, puis appelez media_create depuis le même compte utilisateur avec le storageKey renvoyé. L’outil vérifie que le fichier stocké existe et correspond à la taille fournie lors de la demande de l’URL de téléversement, avant de le rendre disponible dans la médiathèque.
Les définitions de taxonomie décrivent la classification et les collections auxquelles elle s’applique ; les termes sont les valeurs individuelles attribuées au contenu. Les termes hiérarchiques peuvent utiliser parentId, mais le parent doit appartenir à la même taxonomie et ne peut pas créer de cycle. La création ou la mise à jour d’un terme avec parentId dans une taxonomie non hiérarchique renvoie VALIDATION_ERROR. Un terme qui a des enfants doit voir ces enfants supprimés ou déplacés avant d’être supprimé.
menu_set_items remplace la liste complète des éléments d’un menu en une seule opération atomique. L’ordre du tableau devient l’ordre du menu. Le parentIndex d’un élément imbriqué pointe vers un élément antérieur du même tableau ; placez donc chaque parent avant ses enfants.
media_usage_repair peut traiter une collection ou toutes les collections et peut s’exécuter longtemps sur un grand site. Ses statuts complete, partial, failed et stale sont des réponses d’outil réussies. Inspectez le statut et les compteurs renvoyés plutôt que de vous fier à isError ; les échecs d’authentification, de validation et d’exécution inattendus définissent isError: true.
Transfert de site
Les outils site_* exportent un site entier sous forme de paquet de site et importent un paquet dans un site vide. Ils démarrent et pilotent les opérations et renvoient des résumés bornés. Ils ne transportent jamais les octets du paquet, les médias, le contenu des enregistrements, les adresses e-mail des principals ni les URL de téléchargement. Téléchargez un export et téléversez un paquet pour l’import avec la CLI ou l’API REST, puis désignez l’opération par son ID.
Chaque outil de transfert exige le rôle Admin. Le rôle est vérifié avant le scope : un appelant qui n’est pas administrateur reçoit donc INSUFFICIENT_PERMISSIONS et aucune demande d’approbation n’est créée.
Export
site_export_start accepte comments (true par défaut) et renvoie la nouvelle opération. site_export_status exécute une étape d’export bornée à chaque appel et indique l’opération et nextRequestInMs. Rappelez-le après ce délai jusqu’à ce que nextRequestInMs vaille null. Transmettez advance: false pour lire le statut sans exécuter d’étape. Une fois l’export terminé, le résultat inclut aussi totals : nombre d’enregistrements par type, nombre et octets de médias, et nombre et octets de fichiers du paquet.
Import
Téléversez d’abord le paquet. La commande emdash site import <file> --analyze de la CLI le téléverse, l’analyse et affiche l’ID de l’opération.
site_import_analyze exécute une étape d’analyse bornée à chaque appel. Répétez-le jusqu’à ce que nextRequestInMs vaille null ; le résultat comprend alors un résumé du plan avec packageDigest, planDigest, executable, des compteurs, des tailles, des principals, des décisions, des transformations, des avertissements et des bloqueurs. Chaque transformation est indiquée par son code, le kind de l’enregistrement lorsqu’elle en a un, et un count, sans les IDs ni les valeurs auxquels elle s’applique. Les principals, les avertissements et les bloqueurs sont listés à raison de 50 éléments au plus chacun, le total complet figurant dans total. Les principals sont listés sans adresse e-mail, avec les IDs d’utilisateur cible suggérés et ceux actuellement associés. Transmettez decisions pour associer des principals à des IDs d’utilisateur cible (ou à null) et pour choisir le titre et le slogan du paquet ou de la cible. Chaque modification produit un nouveau planDigest.
site_import_start prend l’ID de l’opération ainsi que le packageDigest et le planDigest du dernier plan. Le plan ne doit comporter aucun bloqueur. L’outil possède destructiveHint: true : une fois démarré, l’import écrit dans le site et bloque les autres écritures jusqu’à ce qu’il soit terminé ou qu’un administrateur l’abandonne. Montrez le plan à l’utilisateur et obtenez sa confirmation avant de l’appeler.
site_import_resume exécute une étape d’import bornée et indique l’opération et nextRequestInMs. Appelez-le jusqu’à ce que nextRequestInMs vaille null ; il est sûr de le répéter après une déconnexion. site_import_status indique l’opération et le nombre de fichiers téléversés sans faire avancer l’import. site_import_receipt renvoie le reçu complet, y compris receiptDigest, une fois l’import terminé.
Pendant l’exécution d’un import, et après son échec ou son annulation jusqu’à son abandon, tout autre outil capable d’écrire échoue avec TRANSFER_IMPORT_IN_PROGRESS. Cela inclut les outils de plugins. Les outils annotés readOnlyHint: true et les huit outils site_* continuent de fonctionner, et initialize et tools/list ne sont jamais bloqués. Pendant l’activation de l’utilisation des médias, les outils d’écriture échouent de la même manière avec MEDIA_USAGE_ACTIVATION_IN_PROGRESS.
Les résumés d’opération comprennent id, kind, state, stage, progress, packageDigest, planDigest, error ({ code } ou null) et des horodatages. progress est un nombre d’étapes { done, total }, auquel s’ajoutent records, les enregistrements qu’un export a écrits jusque-là, ainsi que bytesDone et bytesTotal, une fois connus. Les outils MCP ne peuvent ni annuler ni abandonner un import ; utilisez l’API REST.
Approbations
Un jeton avec admin ou avec le scope de transfert requis ne demande jamais d’approbation. Pour un jeton sans l’un ni l’autre, par exemple celui d’un agent auquel seul transfer:analyze a été accordé, site_export_start et site_import_start s’exécutent lorsqu’un administrateur approuve la demande :
- Le premier appel sans le scope crée une demande d’approbation en attente et échoue avec
TRANSFER_APPROVAL_REQUIRED. Le texte du message et_meta.detailscontiennent l’approvalIdet sonexpiresAt. Un nouvel appel avec les mêmes arguments et sansapprovalIdrenvoie la même demande ouverte. - Un administrateur approuve la demande sous Approval requests dans Settings → Transfer, ou avec le point de terminaison d’approbation réservé à la session de l’API REST. Les jetons d’API ne peuvent pas approuver de demandes.
- Le client répète l’appel avec les mêmes arguments et l’
approvalId. L’approbation est consommée lorsque cet appel démarre l’opération. Si l’opération ne parvient pas à démarrer, le client peut réessayer avec le mêmeapprovalIdjusqu’à son expiration.
Une demande est liée à l’utilisateur, au jeton, à l’action et aux arguments exacts : les options d’export, ou l’ID de l’opération d’import et les deux digests. Une demande en attente expire 15 minutes après sa création, et une demande approuvée 15 minutes après son approbation. Un appel avec des arguments différents ou un autre jeton, ou avec une approbation refusée, expirée ou déjà utilisée, échoue avec TRANSFER_APPROVAL_INVALID.
site_import_start vérifie les digests, l’état de l’opération et les bloqueurs du plan avant de créer une demande, de sorte qu’un administrateur n’est sollicité que pour approuver un import qui peut s’exécuter. Une approbation nécessite un ID de jeton ; un appelant qui n’en a pas reçoit donc INSUFFICIENT_SCOPE.
Une fois qu’un appel approuvé a démarré une opération, le même utilisateur et le même jeton peuvent appeler site_export_status, ou site_import_status, site_import_resume et site_import_receipt, pour cette opération sans le scope.
Outils de plugins
Un administrateur doit activer la surface MCP de chaque plugin. Les outils activés apparaissent dans tools/list sous la forme <pluginId>__<localName> et exigent mcp:tools ou mcp:tools:<pluginId> pour les appels authentifiés par jeton. EmDash vérifie aussi la permission déclarée par la route du plugin et consigne le plugin, l’outil, la route et l’acteur dans le journal d’audit.
Comme les outils de plugins dépendent de l’installation, ils ne font pas partie de l’inventaire statique ci-dessus.
Découverte OAuth
Les clients MCP découvrent le serveur d’autorisation à partir des métadonnées de la ressource protégée :
GET /.well-known/oauth-protected-resource
La réponse désigne /_emdash/api/mcp comme la ressource protégée et renvoie vers le serveur d’autorisation. Les clients lisent ensuite ses métadonnées à l’adresse suivante :
GET /.well-known/oauth-authorization-server/_emdash
Ce document fournit les points de terminaison actuels d’autorisation, de jeton, d’enregistrement et d’autorisation d’appareil, les scopes et types d’octroi pris en charge, ainsi que la méthode PKCE S256. Utilisez les valeurs découvertes plutôt que de coder en dur les routes du protocole OAuth.
Une requête MCP non authentifiée renvoie une réponse 401 avec l’URL de découverte :
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"
Erreurs
Un échec d’outil comporte isError: true. Le premier bloc de texte commence par un code stable, et _meta.code le répète pour les clients qui lisent les métadonnées structurées :
{
"content": [{ "type": "text", "text": "[NOT_FOUND] Collection 'articles' not found" }],
"isError": true,
"_meta": { "code": "NOT_FOUND" }
}
Un refus dû au verrou d’édition d’une entrée indique le détenteur dans _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"
}
}
}
Les échecs d’authentification utilisent des codes tels que INSUFFICIENT_SCOPE et INSUFFICIENT_PERMISSIONS. Les échecs de transport utilisent le code d’erreur interne JSON-RPC -32603 et n’exposent pas l’exception sous-jacente.