DANS CET ARTICLE7 SECTIONS
Le serveur MCP de Cord connecte des assistants comme Claude ou Cursor aux données réelles d’une entreprise : devis, factures, créances échues. Avec une clé en écriture, il envoie aussi des devis et enregistre des paiements. Autrement dit : un modèle de langage qui agit avec les identifiants d’une entreprise. Nous l’avons audité de bout en bout, et voici ce qui en est ressorti.
1. Une clé en lecture pouvait écrire
Cord expose MCP sur deux transports : HTTP sans état (POST /api/mcp) et l’ancien transport SSE (GET /api/mcp/sse pour ouvrir la session et POST /api/mcp/message pour envoyer des messages). La session SSE conserve l’organisation, la clé et le droit avec lesquels elle a été ouverte.
Le problème se trouvait dans /api/mcp/message. La route vérifiait que la clé qui envoie le message appartenait à la même organisation que la session, puis exécutait l’outil avec le droit enregistré dans la session. Une clé en lecture seule de la même entreprise qui connaissait un sessionId pouvait exécuter des outils d’écriture avec la session d’une autre clé.
La correction tient en une ligne, mais c’est la bonne : une session appartient à une clé, pas à une organisation.
if (session.orgId !== auth.orgId || session.keyId !== auth.keyId) {
return rpcErrRes(null, -32001, 'Esta API key no corresponde a la sesión.', 403);
}
// et l’outil s’exécute avec auth.scope, le droit de l’appelant, pas celui de la session
Un test la couvre : une clé en lecture munie du sessionId d’une clé en écriture reçoit un 403, et l’outil ne s’exécute pas.
2. Un contrat de sortie non respecté
Les 19 outils déclaraient un outputSchema. Dans la version 2025-06-18 du protocole, le déclarer oblige à renvoyer un structuredContent conforme à ce schéma. Cord ne renvoyait que du texte. De plus, les schémas avaient additionalProperties: false et un next_cursor typé comme chaîne alors qu’il peut en réalité valoir null : un client qui valide aurait rejeté des réponses correctes.
Désormais, chaque outil répond avec les deux : structuredContent pour le client qui valide, et le même JSON compact sous forme de texte pour celui qui ne valide pas. Les schémas de sortie sont ouverts : un nouveau champ ne casse rien chez personne. Et le texte n’est plus indenté, ce qui économise des tokens à chaque appel.
3. Des montants sans devise
Cord a une règle : un montant sans devise est un nombre, pas de l’argent. Les outils de devis, de produits et de clients renvoyaient total: 12500 sans préciser en quelle devise. Un agent qui suppose des pesos fournit des chiffres erronés à une entreprise qui vend en dollars. Aujourd’hui, chaque montant voyage avec sa moneda.
4. Des erreurs qui n’atteignaient pas le modèle
Un outil de recouvrement levait une erreur générique lorsque l’offre ne l’incluait pas. Le serveur la traitait comme une panne interne et le modèle recevait « Erreur interne du serveur », sans aucun moyen d’expliquer à l’utilisateur ce qui se passait. En plus, le message citait la mauvaise offre. Désormais, les limites d’offre sont signalées comme des erreurs métier, avec le nom de l’offre réellement nécessaire, lu dans le même contrat que celui qu’utilise le reste de Cord.
Au passage, nous avons découvert que l’index de la documentation renvoyait la trace de pile complète en cas d’échec. Ce n’est plus le cas.
5. Sept nouveaux outils
Nous sommes passés de 19 à 26 outils. Les nouveaux sont pensés pour qu’un agent puisse intégrer Cord, et pas seulement l’utiliser :
| Outil | À quoi il sert |
|---|---|
contexto_cuenta |
Entreprise, pays, devise, offre, et si la clé est une clé de test ou une clé live. Le premier appel. |
proponer_configuracion |
Propose le profil, les taxes et le catalogue à partir du site de l’entreprise. Renvoie un lien ; une personne valide. |
estado_configuracion |
Si cette proposition a déjà été validée, et ce qui a été appliqué. |
validar_datos_fiscales |
RFC, régime et usage du CFDI, NIF ou EIN, avec les mêmes règles que lors de la facturation. |
validar_apariencia |
Vérifie un appearance de Cord Elements avant de le mettre dans le code. |
simular_evento |
Déclenche un webhook de test via le moteur réel. Uniquement avec une clé de test. |
buscar_documentacion |
Cherche dans la documentation et renvoie les pages avec un extrait et leur URL. |
Aucun ne modifie directement les paramètres du compte. proponer_configuracion laisse une proposition qu’une personne valide dans Cord, comme celle proposée à la création du compte.
6. Une API qui se décrit elle-même
Un agent qui écrit du code pour une API lit sa spécification. Sur les 343 champs de notre spécification OpenAPI, 279 n’avaient pas de description : l’agent devait deviner ce qu’était terminos, si total incluait les taxes ou dans quelle unité était exprimé rate.
Désormais, un glossaire, FIELD_DOCS, décrit chaque nom de champ de l’API, et le générateur l’applique à chaque propriété dépourvue de description propre. L’étape de CI qui compare la spécification au code échoue si une propriété ou un paramètre reste sans description. Ajouter un champ sans l’expliquer ne passe plus.
Ce qui n’a pas changé, volontairement
- Aucun outil ne timbre de factures. Créer un brouillon, oui ; l’émettre auprès de l’autorité fiscale est irréversible, et c’est une personne qui le déclenche.
- Le texte que rédige le client final sur le lien public parvient au modèle marqué comme
cliente_externoet délimité, pour qu’il le signale au lieu de le suivre comme une instruction. - Les actions qui atteignent le client (envoyer, approuver, refuser, enregistrer un paiement) portent
destructiveHint: un client MCP bien configuré demande donc une confirmation avant de les exécuter.
Si vous connectez Cord à votre assistant, demandez-lui d’abord contexto_cuenta. Tout le reste en découle.
Le serveur MCP de Cord connecte des assistants comme Claude ou Cursor aux données réelles d’une entreprise : devis, factures, créances échues. Avec une clé en écriture, il envoie aussi des devis et enregistre des paiements. Autrement dit : un modèle de langage qui agit avec les identifiants d’une entreprise. Nous l’avons audité de bout en bout, et voici ce qui en est ressorti.
## 1. Une clé en lecture pouvait écrire
Cord expose MCP sur deux transports : HTTP sans état (`POST /api/mcp`) et l’ancien transport SSE (`GET /api/mcp/sse` pour ouvrir la session et `POST /api/mcp/message` pour envoyer des messages). La session SSE conserve l’organisation, la clé et le droit avec lesquels elle a été ouverte.
Le problème se trouvait dans `/api/mcp/message`. La route vérifiait que la clé qui envoie le message appartenait **à la même organisation** que la session, puis exécutait l’outil avec le **droit enregistré dans la session**. Une clé en lecture seule de la même entreprise qui connaissait un sessionId pouvait exécuter des outils d’écriture avec la session d’une autre clé.
La correction tient en une ligne, mais c’est la bonne : une session appartient à une clé, pas à une organisation.
```ts
if (session.orgId !== auth.orgId || session.keyId !== auth.keyId) {
return rpcErrRes(null, -32001, 'Esta API key no corresponde a la sesión.', 403);
}
// et l’outil s’exécute avec auth.scope, le droit de l’appelant, pas celui de la session
```
Un test la couvre : une clé en lecture munie du sessionId d’une clé en écriture reçoit un 403, et l’outil ne s’exécute pas.
## 2. Un contrat de sortie non respecté
Les 19 outils déclaraient un `outputSchema`. Dans la version 2025-06-18 du protocole, le déclarer oblige à renvoyer un `structuredContent` conforme à ce schéma. Cord ne renvoyait que du texte. De plus, les schémas avaient `additionalProperties: false` et un `next_cursor` typé comme chaîne alors qu’il peut en réalité valoir `null` : un client qui valide aurait rejeté des réponses correctes.
Désormais, chaque outil répond avec les deux : `structuredContent` pour le client qui valide, et le même JSON compact sous forme de texte pour celui qui ne valide pas. Les schémas de sortie sont ouverts : un nouveau champ ne casse rien chez personne. Et le texte n’est plus indenté, ce qui économise des tokens à chaque appel.
## 3. Des montants sans devise
Cord a une règle : un montant sans devise est un nombre, pas de l’argent. Les outils de devis, de produits et de clients renvoyaient `total: 12500` sans préciser en quelle devise. Un agent qui suppose des pesos fournit des chiffres erronés à une entreprise qui vend en dollars. Aujourd’hui, chaque montant voyage avec sa `moneda`.
## 4. Des erreurs qui n’atteignaient pas le modèle
Un outil de recouvrement levait une erreur générique lorsque l’offre ne l’incluait pas. Le serveur la traitait comme une panne interne et le modèle recevait « Erreur interne du serveur », sans aucun moyen d’expliquer à l’utilisateur ce qui se passait. En plus, le message citait la mauvaise offre. Désormais, les limites d’offre sont signalées comme des erreurs métier, avec le nom de l’offre réellement nécessaire, lu dans le même contrat que celui qu’utilise le reste de Cord.
Au passage, nous avons découvert que l’index de la documentation renvoyait la trace de pile complète en cas d’échec. Ce n’est plus le cas.
## 5. Sept nouveaux outils
Nous sommes passés de 19 à 26 outils. Les nouveaux sont pensés pour qu’un agent puisse intégrer Cord, et pas seulement l’utiliser :
| Outil | À quoi il sert |
|---|---|
| `contexto_cuenta` | Entreprise, pays, devise, offre, et si la clé est une clé de test ou une clé live. Le premier appel. |
| `proponer_configuracion` | Propose le profil, les taxes et le catalogue à partir du site de l’entreprise. Renvoie un lien ; une personne valide. |
| `estado_configuracion` | Si cette proposition a déjà été validée, et ce qui a été appliqué. |
| `validar_datos_fiscales` | RFC, régime et usage du CFDI, NIF ou EIN, avec les mêmes règles que lors de la facturation. |
| `validar_apariencia` | Vérifie un `appearance` de Cord Elements avant de le mettre dans le code. |
| `simular_evento` | Déclenche un webhook de test via le moteur réel. Uniquement avec une clé de test. |
| `buscar_documentacion` | Cherche dans la documentation et renvoie les pages avec un extrait et leur URL. |
Aucun ne modifie directement les paramètres du compte. `proponer_configuracion` laisse une proposition qu’une personne valide dans Cord, comme celle proposée à la création du compte.
## 6. Une API qui se décrit elle-même
Un agent qui écrit du code pour une API lit sa spécification. Sur les 343 champs de notre spécification OpenAPI, 279 n’avaient pas de description : l’agent devait deviner ce qu’était `terminos`, si `total` incluait les taxes ou dans quelle unité était exprimé `rate`.
Désormais, un glossaire, `FIELD_DOCS`, décrit chaque nom de champ de l’API, et le générateur l’applique à chaque propriété dépourvue de description propre. L’étape de CI qui compare la spécification au code échoue si une propriété ou un paramètre reste sans description. Ajouter un champ sans l’expliquer ne passe plus.
## Ce qui n’a pas changé, volontairement
- **Aucun outil ne timbre de factures.** Créer un brouillon, oui ; l’émettre auprès de l’autorité fiscale est irréversible, et c’est une personne qui le déclenche.
- **Le texte que rédige le client final** sur le lien public parvient au modèle marqué comme `cliente_externo` et délimité, pour qu’il le signale au lieu de le suivre comme une instruction.
- **Les actions qui atteignent le client** (envoyer, approuver, refuser, enregistrer un paiement) portent `destructiveHint` : un client MCP bien configuré demande donc une confirmation avant de les exécuter.
Si vous connectez Cord à votre assistant, demandez-lui d’abord `contexto_cuenta`. Tout le reste en découle.