Connecteur (MCP + OAuth 2.1)
Obtenir un accès en lecture à une exploitation et appeler les outils : découverte, enregistrement, autorisation, jeton, appels, révocation.
Le connecteur est un serveur MCP (Model Context Protocol, transport « Streamable HTTP ») protégé par OAuth 2.1. C'est le même que celui qu'utilisent ChatGPT et Claude : votre application est un client comme eux. Il est en lecture seule, et chaque accès ne vaut que pour une exploitation, choisie par la personne qui autorise. Pour un programme qui tourne seul (script, serveur, tableur), l'API REST v1 lit les mêmes données avec une simple clé, sans OAuth.
Le parcours en bref
- Votre application découvre le serveur d'autorisation à partir de l'adresse du serveur MCP.
- Elle s'enregistre (une fois) et reçoit un
client_id. - Elle envoie la personne sur la page d'autorisation de Mon Chai : connexion, choix de l'exploitation, bouton « Autoriser ».
- Mon Chai renvoie le navigateur vers votre application avec un code à usage unique.
- Votre application échange ce code contre un jeton d'accès (1 heure) et un jeton de rafraîchissement (30 jours).
- Elle appelle les outils sur
https://tester.monchai.fr/mcpavec le jeton d'accès, et le rafraîchit quand il expire.
Les SDK MCP officiels font tout cela pour vous : voyez les
exemples complets. Les commandes curl
ci-dessous montrent chaque échange à la main ; ce sont celles que Mon Chai rejoue
après chaque mise en production pour vérifier que le connecteur fonctionne.
Les adresses
| Rôle | Adresse |
|---|---|
| Serveur MCP (ressource protégée) | https://tester.monchai.fr/mcp |
| Métadonnées de la ressource (RFC 9728) | https://tester.monchai.fr/.well-known/oauth-protected-resource/mcp |
| Métadonnées du serveur d'autorisation (RFC 8414) | https://tester.monchai.fr/.well-known/oauth-authorization-server |
| Enregistrement dynamique (RFC 7591) | https://tester.monchai.fr/oauth/register |
| Autorisation | https://tester.monchai.fr/oauth/authorize |
| Jeton | https://tester.monchai.fr/oauth/token |
| Révocation (RFC 7009) | https://tester.monchai.fr/oauth/revoke |
Portée (scope) unique : monchai:read, lecture seule.
Paramètre resource (RFC 8707) : exactement
https://tester.monchai.fr/mcp.
Pour suivre les commandes de cette page dans un terminal (bash, zsh, ou Git
Bash sous Windows ; curl et openssl requis) :
MON_CHAI="https://tester.monchai.fr"
RETOUR="http://127.0.0.1:8765/callback"
Découverte
Un appel à /mcp sans jeton répond
401 avec un en-tête WWW-Authenticate qui pointe vers
les métadonnées de la ressource ; celles-ci désignent le serveur
d'autorisation, dont les métadonnées donnent toutes les autres adresses. Les
SDK suivent ce chemin seuls. À la main :
curl -s "$MON_CHAI/.well-known/oauth-protected-resource/mcp"
curl -s "$MON_CHAI/.well-known/oauth-authorization-server"
Enregistrement de votre application
Votre application s'enregistre elle-même (enregistrement dynamique, sans
compte développeur ni validation manuelle) en client public
(token_endpoint_auth_method: none) :
curl -s -X POST "$MON_CHAI/oauth/register" \
-H 'Content-Type: application/json' \
-d '{"client_name":"Mon appli perso","redirect_uris":["http://127.0.0.1:8765/callback"],"grant_types":["authorization_code","refresh_token"],"response_types":["code"],"token_endpoint_auth_method":"none"}'
Réponse 201 : un JSON qui contient votre client_id.
Gardez-le :
CLIENT_ID="collez ici la valeur de client_id"
Règles sur l'adresse de retour (redirect_uris) :
- HTTPS, ou HTTP sur la boucle locale
(
127.0.0.1,localhost,::1) pour une application qui tourne sur votre poste ; toute autre adresse est refusée (invalid_redirect_uri) ; - sur la boucle locale, le port peut varier d'une fois à l'autre (RFC 8252) ; ailleurs, l'adresse présentée doit être identique, caractère pour caractère, à l'adresse enregistrée ;
- au plus 30 enregistrements par heure et
par adresse IP : enregistrez-vous une fois et réutilisez le
client_id.
Autre possibilité : le propriétaire ou un administrateur de
l'exploitation peut créer un identifiant client pré-enregistré
(avec ou sans secret) dans Administration ›
Connecteur ChatGPT / Claude, pour les destinations Claude, ChatGPT ou « un
outil installé sur votre ordinateur » (http://localhost/callback et
http://127.0.0.1/callback, port libre). Au plus
10 identifiants par personne.
Autorisation (PKCE)
PKCE est obligatoire, méthode S256 uniquement. Préparez un
vérificateur secret, son empreinte (le « défi ») et un état :
VERIFIEUR=$(openssl rand -base64 48 | tr -d '/+=' | head -c 64)
DEFI=$(printf '%s' "$VERIFIEUR" | openssl dgst -sha256 -binary \
| openssl base64 | tr '+/' '-_' | tr -d '=')
ETAT=$(openssl rand -hex 8)
Puis affichez l'adresse d'autorisation et ouvrez-la dans votre navigateur :
echo "$MON_CHAI/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=monchai%3Aread&state=$ETAT&code_challenge=$DEFI&code_challenge_method=S256&resource=https%3A%2F%2Ftester.monchai.fr%2Fmcp"
Mon Chai vous demande de vous connecter si besoin, affiche l'écran de
consentement (le nom de l'application, l'adresse où le code sera envoyé,
l'exploitation à choisir parmi les vôtres) et, après « Autoriser », renvoie le
navigateur vers http://127.0.0.1:8765/callback?code=…&state=…. Si rien
n'écoute sur ce port, le navigateur affiche une erreur de connexion : c'est
normal, le code est dans la barre d'adresse. Vérifiez que state
est bien le vôtre, puis :
CODE="collez ici la valeur de code lue dans la barre d'adresse"
- la demande d'autorisation reste valable 30 minutes, le code 10 minutes ;
- le code ne sert qu'une fois : présenté deux fois, il est refusé et l'accès qu'il avait ouvert est révoqué ;
- « Refuser » renvoie
error=access_deniedà la place du code.
Échange du code contre un jeton
curl -s -X POST "$MON_CHAI/oauth/token" \
-d grant_type=authorization_code \
-d "code=$CODE" \
--data-urlencode "redirect_uri=$RETOUR" \
-d "client_id=$CLIENT_ID" \
-d "code_verifier=$VERIFIEUR" \
--data-urlencode "resource=$MON_CHAI/mcp"
Réponse 200 :
{"access_token":"mca_…","token_type":"Bearer","expires_in":3600,"scope":"monchai:read","refresh_token":"mcr_…"}
ACCES="mca_…" # access_token
RAFRAICHI="mcr_…" # refresh_token
Le jeton d'accès vaut 1 heure (3600 secondes,
expires_in), le jeton de rafraîchissement 30 jours.
Les jetons sont opaques (préfixes mca_ et mcr_) : ne
cherchez pas à les décoder. Mon Chai n'en garde qu'une empreinte.
Appeler les outils
Chaque appel est un POST JSON-RPC 2.0 sur
/mcp, avec le jeton en en-tête
Authorization: Bearer. Le serveur est sans état : pas de session
à ouvrir pour un appel isolé (les SDK envoient quand même
initialize, c'est accepté). Lister les outils :
curl -s -X POST "$MON_CHAI/mcp" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer $ACCES" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Appeler un outil (ici : « combien de magnums en stock ? ») :
curl -s -X POST "$MON_CHAI/mcp" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer $ACCES" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"stock_par_format","arguments":{"format_ml":1500}}}'
Le résultat de l'outil est un objet JSON, sérialisé en texte
dans result.content[0].text : décodez ce texte
(json.loads, JSON.parse) pour obtenir l'objet.
{"jsonrpc":"2.0","id":2,"result":{
"content":[{"type":"text",
"text":"{\n \"etat\": \"ok\",\n \"format_ml\": 1500, …}"}],
"isError":false}}
Il porte toujours un champ etat : ok,
empty, unknown, ambiguous,
invalid, forbidden, unavailable ou
busy. Le détail de chaque outil est dans la
référence des outils. Les montants sont des
chaînes décimales ("125.50"), les dates au format
AAAA-MM-JJ.
Rafraîchir le jeton
curl -s -X POST "$MON_CHAI/oauth/token" \
-d grant_type=refresh_token \
-d "refresh_token=$RAFRAICHI" \
-d "client_id=$CLIENT_ID" \
--data-urlencode "resource=$MON_CHAI/mcp"
Rotation : chaque rafraîchissement rend un nouveau jeton de rafraîchissement et invalide l'ancien (et les jetons d'accès précédents). Enregistrez toujours le nouveau. Présenter un ancien jeton de rafraîchissement est traité comme un vol : l'accès entier est révoqué, et la personne doit autoriser de nouveau.
Le rafraîchissement échoue aussi si la personne a perdu son accès à l'exploitation, ou si l'accès a été révoqué : votre application doit alors relancer l'autorisation.
Révoquer l'accès
Depuis votre application (RFC 7009) — n'importe lequel des deux jetons coupe tout l'accès :
curl -s -o /dev/null -w "%{http_code}\n" -X POST "$MON_CHAI/oauth/revoke" \
-d "token=$RAFRAICHI" \
-d "client_id=$CLIENT_ID"
Réponse 200, y compris pour un jeton inconnu (la norme
l'exige). Depuis Mon Chai : la personne voit ses accès dans
Administration › Connecteur ChatGPT /
Claude et peut révoquer chacun d'eux ; l'effet est immédiat.
Erreurs
| Où | Réponse | Cause, et quoi faire |
|---|---|---|
/mcp | 401 | Jeton absent, expiré, révoqué, ou accès retiré à la personne. Rafraîchissez ; si cela échoue, relancez l'autorisation. |
/mcp | 421 | En-tête Host qui n'est pas celui de Mon Chai (protection contre le « DNS rebinding ») : appelez l'adresse publique, sans proxy qui réécrit l'hôte. |
/oauth/register | 400 invalid_redirect_uri | Adresse de retour ni HTTPS ni boucle locale, ou absente. |
/oauth/register | 400 invalid_client_metadata | Trop d'enregistrements depuis votre adresse : réutilisez votre client_id. |
/oauth/authorize | 400, ou retour avec error=… | 400 si le client_id est inconnu ou l'adresse de retour non enregistrée ; sinon retour vers votre application avec error=… (PKCE absent, portée inconnue…) ; access_denied si la personne refuse. |
/oauth/token | 400 invalid_grant | Code inconnu, déjà utilisé ou expiré ; vérificateur PKCE faux ; jeton de rafraîchissement invalide, déjà tourné ou révoqué. |
/oauth/token | 400 invalid_target | resource différent de https://tester.monchai.fr/mcp. |
/oauth/revoke | 401 unauthorized_client | client_id manquant ou inconnu. |
| toutes | 429 temporarily_unavailable | Trop de requêtes (voir les limites) : attendez la durée de l'en-tête Retry-After. |
| toutes | 503 temporarily_unavailable | Le compteur de limitation est momentanément indisponible : réessayez après Retry-After. |
| un outil | etat: busy | Mon Chai traite déjà d'autres demandes, ou votre accès a consommé son temps de calcul de la minute : réessayez après reessayer_dans_s secondes. Ce n'est pas une panne. |
| un outil | etat: forbidden | Les droits de la personne ne permettent pas cette lecture, ou l'accès a été révoqué pour abus : il faut autoriser de nouveau. |
| un outil | etat: unavailable | Le module concerné est fermé pour cette exploitation. |
Limites
| Chemin | Requêtes au plus | Par |
|---|---|---|
/oauth/register | 30 | 1 heure |
/oauth/token | 60 | 1 minute |
/oauth/authorize | 60 | 1 minute |
/mcp | 120 | 1 minute |
- Les requêtes sont comptées par jeton quand il y en a un, sinon par adresse IP.
- Chaque accès dispose de 20 secondes de calcul par
1 minute sur Mon Chai ; au-delà, les outils répondent
busy. - Une application qui insiste malgré 50 refus en 1 heure voit son accès révoqué.
- Les listes sont plafonnées et les textes longs tronqués : le connecteur sert des réponses, pas des exports (voir la référence et Exporter vos données).
Exemple complet : lire mes stocks depuis mon appli (Python)
Installation : pip install "mcp==2.1.1" — lancement :
python mes_stocks.py. Le script affiche un lien d'autorisation ;
ouvrez-le, autorisez, et le résultat s'affiche dans le terminal. Outils
appelés : contexte_connexion, stock_resume.
Fichier mes_stocks.py, à copier tel quel :
"""Lire mes stocks Mon Chai depuis mon appli — exemple complet en Python.
Ce que fait ce script :
1. il se présente à Mon Chai comme une application (enregistrement
automatique, rien à créer à la main) ;
2. il affiche un lien : ouvrez-le, connectez-vous à Mon Chai, choisissez
l'exploitation et cliquez sur « Autoriser » ;
3. Mon Chai renvoie votre navigateur vers ce script (http://127.0.0.1:8765),
qui reçoit un accès en LECTURE SEULE à cette seule exploitation ;
4. il demande au connecteur le contexte puis le résumé des stocks, et les
affiche.
Installation (Python 3.10 ou plus récent) :
pip install "mcp==2.1.1"
Lancement :
python mes_stocks.py
Autre adresse Mon Chai, ou autre port de retour :
MON_CHAI_URL=https://… MON_CHAI_PORT_RETOUR=8766 python mes_stocks.py
Les jetons restent en mémoire et disparaissent à la fin du script : chaque
lancement redemande l'autorisation. Pour les conserver, lisez la page
« Sécurité et bonnes pratiques » (jamais dans un fichier en clair).
"""
import asyncio
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse
import httpx2
from mcp.client import Client
from mcp.client.auth import AuthorizationCodeResult, OAuthClientProvider
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
MON_CHAI = os.environ.get("MON_CHAI_URL", "https://tester.monchai.fr").rstrip("/")
SERVEUR_MCP = MON_CHAI + "/mcp"
PORT_RETOUR = int(os.environ.get("MON_CHAI_PORT_RETOUR", "8765"))
ADRESSE_RETOUR = f"http://127.0.0.1:{PORT_RETOUR}/callback"
class JetonsEnMemoire:
"""Là où le SDK range l'inscription de l'application et les jetons."""
def __init__(self):
self.jetons = None
self.application = None
async def get_tokens(self) -> OAuthToken | None:
return self.jetons
async def set_tokens(self, tokens: OAuthToken) -> None:
self.jetons = tokens
async def get_client_info(self) -> OAuthClientInformationFull | None:
return self.application
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
self.application = client_info
class RetourDuNavigateur(BaseHTTPRequestHandler):
"""Reçoit le navigateur que Mon Chai renvoie après « Autoriser »."""
recu: dict = {}
def do_GET(self):
adresse = urlparse(self.path)
if adresse.path != "/callback":
self.send_response(404)
self.end_headers()
return
RetourDuNavigateur.recu = {cle: valeurs[0] for cle, valeurs in parse_qs(adresse.query).items()}
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write("C'est fait : vous pouvez fermer cet onglet.".encode("utf-8"))
def log_message(self, *args): # pas de journal HTTP dans la console
pass
ecoute = None
async def afficher_le_lien(adresse: str) -> None:
"""Le SDK a préparé l'autorisation : on écoute AVANT que la personne clique."""
global ecoute
ecoute = HTTPServer(("127.0.0.1", PORT_RETOUR), RetourDuNavigateur)
print("Ouvrez ce lien dans votre navigateur, connectez-vous et autorisez l'accès :")
print(adresse, flush=True)
async def attendre_le_retour() -> AuthorizationCodeResult:
while not RetourDuNavigateur.recu:
await asyncio.to_thread(ecoute.handle_request)
ecoute.server_close()
recu = RetourDuNavigateur.recu
if "code" not in recu:
raise SystemExit("Autorisation refusée ou annulée : " + recu.get("error", "raison inconnue"))
return AuthorizationCodeResult(code=recu["code"], state=recu.get("state"), iss=recu.get("iss"))
def donnees(resultat) -> dict:
"""Le résultat d'un outil : un dictionnaire, avec toujours un champ `etat`."""
if resultat.structured_content:
return resultat.structured_content
return json.loads(resultat.content[0].text)
async def main() -> None:
oauth = OAuthClientProvider(
server_url=SERVEUR_MCP,
client_metadata=OAuthClientMetadata(
client_name="Mon appli perso (exemple Python)",
redirect_uris=[ADRESSE_RETOUR],
grant_types=["authorization_code", "refresh_token"],
response_types=["code"],
token_endpoint_auth_method="none",
scope="monchai:read",
),
storage=JetonsEnMemoire(),
redirect_handler=afficher_le_lien,
callback_handler=attendre_le_retour,
)
async with httpx2.AsyncClient(auth=oauth, timeout=30) as http:
async with Client(streamable_http_client(SERVEUR_MCP, http_client=http)) as mon_chai:
contexte = donnees(await mon_chai.call_tool("contexte_connexion", {}))
print("Exploitation :", contexte.get("exploitation"), "— rôle :", contexte.get("role"))
stock = donnees(await mon_chai.call_tool("stock_resume", {}))
if stock.get("etat") != "ok":
print("Stocks non lisibles :", stock.get("etat"), stock.get("message", ""))
return
print("Bouteilles :", stock["bouteilles_total"], "— vrac :", stock["vrac_litres"], "L")
for ligne in stock["par_cuvee"]:
print(f" {ligne['cuvee']} : {ligne['bouteilles']} bouteilles")
if __name__ == "__main__":
asyncio.run(main())
Exemple complet : lister mes impayés (TypeScript)
Installation : npm install @modelcontextprotocol/sdk@1.32.1 tsx — lancement :
npx tsx mes_impayes.ts. Le script affiche un lien d'autorisation ;
ouvrez-le, autorisez, et le résultat s'affiche dans le terminal. Outils
appelés : factures_impayees.
Fichier mes_impayes.ts, à copier tel quel :
/**
* Lister mes impayés Mon Chai depuis mon appli — exemple complet en TypeScript.
*
* Ce que fait ce script :
* 1. il se présente à Mon Chai comme une application (enregistrement
* automatique, rien à créer à la main) ;
* 2. il affiche un lien : ouvrez-le, connectez-vous à Mon Chai, choisissez
* l'exploitation et cliquez sur « Autoriser » ;
* 3. Mon Chai renvoie votre navigateur vers ce script (http://127.0.0.1:8765),
* qui reçoit un accès en LECTURE SEULE à cette seule exploitation ;
* 4. il demande au connecteur les factures impayées et les affiche.
*
* Installation (Node.js 18 ou plus récent) :
* npm install @modelcontextprotocol/sdk@1.32.1 tsx
* Lancement :
* npx tsx mes_impayes.ts
* Autre adresse Mon Chai, ou autre port de retour :
* MON_CHAI_URL=https://… MON_CHAI_PORT_RETOUR=8766 npx tsx mes_impayes.ts
*
* Les jetons restent en mémoire et disparaissent à la fin du script : chaque
* lancement redemande l'autorisation. Pour les conserver, lisez la page
* « Sécurité et bonnes pratiques » (jamais dans un fichier en clair).
*/
import { createServer } from "node:http";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
import type {
OAuthClientInformationFull,
OAuthClientMetadata,
OAuthTokens,
} from "@modelcontextprotocol/sdk/shared/auth.js";
const MON_CHAI = (process.env.MON_CHAI_URL ?? "https://tester.monchai.fr").replace(/\/+$/, "");
const SERVEUR_MCP = new URL(MON_CHAI + "/mcp");
const PORT_RETOUR = Number(process.env.MON_CHAI_PORT_RETOUR ?? "8765");
const ADRESSE_RETOUR = `http://127.0.0.1:${PORT_RETOUR}/callback`;
/** Là où le SDK range l'inscription de l'application, les jetons et le secret PKCE. */
class AccesEnMemoire implements OAuthClientProvider {
private application?: OAuthClientInformationFull;
private jetons?: OAuthTokens;
private verifieur = "";
get redirectUrl(): string {
return ADRESSE_RETOUR;
}
get clientMetadata(): OAuthClientMetadata {
return {
client_name: "Mon appli perso (exemple TypeScript)",
redirect_uris: [ADRESSE_RETOUR],
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
token_endpoint_auth_method: "none",
scope: "monchai:read",
};
}
clientInformation() {
return this.application;
}
saveClientInformation(application: OAuthClientInformationFull) {
this.application = application;
}
tokens() {
return this.jetons;
}
saveTokens(jetons: OAuthTokens) {
this.jetons = jetons;
}
redirectToAuthorization(adresse: URL) {
console.log("Ouvrez ce lien dans votre navigateur, connectez-vous et autorisez l'accès :");
console.log(adresse.href);
}
saveCodeVerifier(verifieur: string) {
this.verifieur = verifieur;
}
codeVerifier() {
return this.verifieur;
}
}
/** Écoute le retour du navigateur ; rend le code d'autorisation. */
function attendreLeRetour(): Promise<string> {
return new Promise((resolve, reject) => {
const ecoute = createServer((requete, reponse) => {
const adresse = new URL(requete.url ?? "/", ADRESSE_RETOUR);
if (adresse.pathname !== "/callback") {
reponse.writeHead(404).end();
return;
}
reponse.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
reponse.end("C'est fait : vous pouvez fermer cet onglet.");
ecoute.close();
const code = adresse.searchParams.get("code");
if (code) resolve(code);
else reject(new Error("Autorisation refusée ou annulée : " + (adresse.searchParams.get("error") ?? "raison inconnue")));
});
ecoute.listen(PORT_RETOUR, "127.0.0.1");
});
}
/** Le résultat d'un outil : un objet, avec toujours un champ `etat`. */
function donnees(resultat: Awaited<ReturnType<Client["callTool"]>>): any {
if (resultat.structuredContent) return resultat.structuredContent;
const premier = (resultat.content as Array<{ type: string; text?: string }>)[0];
return JSON.parse(premier?.text ?? "{}");
}
async function main() {
const acces = new AccesEnMemoire();
const client = new Client({ name: "mes-impayes", version: "1.0.0" });
const retour = attendreLeRetour(); // on écoute AVANT que la personne clique
try {
await client.connect(new StreamableHTTPClientTransport(SERVEUR_MCP, { authProvider: acces }));
} catch (erreur) {
if (!(erreur instanceof UnauthorizedError)) throw erreur;
// Première fois : Mon Chai demande l'autorisation de la personne.
const transport = new StreamableHTTPClientTransport(SERVEUR_MCP, { authProvider: acces });
await transport.finishAuth(await retour);
await client.connect(new StreamableHTTPClientTransport(SERVEUR_MCP, { authProvider: acces }));
}
const impayes = donnees(await client.callTool({ name: "factures_impayees", arguments: {} }));
if (impayes.etat === "empty") {
console.log("Aucune facture impayée.");
} else if (impayes.etat !== "ok") {
console.log("Factures non lisibles :", impayes.etat, impayes.message ?? "");
} else {
console.log(`${impayes.nombre} facture(s) impayée(s), ${impayes.nombre_echues} échue(s) — total dû : ${impayes.montant_du_total} €`);
for (const facture of impayes.factures) {
console.log(` ${facture.numero} ${facture.client} reste dû ${facture.reste_du} € échéance ${facture.date_echeance ?? "—"}`);
}
}
await client.close();
process.exit(0);
}
main().catch((erreur) => {
console.error(erreur);
process.exit(1);
});
Ces deux fichiers sont ceux que Mon Chai exécute contre un connecteur réel
pour vérifier cette page. Versions testées : mcp 2.1.1 (Python) et
@modelcontextprotocol/sdk 1.32.1 (TypeScript).