Poursuivant notre aventure avec notre serveur de documentation MCP de notre article précédent, nous pouvons approfondir un peu plus l’histoire du TLS, et comment nous générons le certificat Let’s Encrypt au démarrage.
Si vous souhaitez voir le code source complet, le dépôt github est lié en bas de l’article.
Regardons donc un peu plus près les détails. Lorsque nous avons essayé d’intégrer notre serveur MCP dans ChatGPT et Gemini, la nécessité du TLS s’est imposée. Comme nous utilisons également Let’s Encrypt partout sur DownToZero, nous avons principalement réutilisé ce processus dans ce conteneur.
sequenceDiagram
participant App as get_certificate()
participant LE as Let's Encrypt (ACME)
participant Axum as Serveur HTTP Axum
participant FS as Système de fichiers
App->>LE: Créer un compte (NewAccount)
App->>LE: Créer une nouvelle commande (Identifier::Dns(domain))
LE-->>App: Commande (statut = Pending)
App->>App: Créer un canal oneshot (snd, rcv)
App->>LE: Récupérer les autorisations
LE-->>App: Autorisation incl. défi HTTP-01
App->>App: Calculer le token + key_authorization (secret)
App->>Axum: Démarrer le serveur sur acme_port servant<br/>/.well-known/acme-challenge/{token} -> secret
App->>App: Dormir 2s
App->>LE: challenge.set_ready()
LE->>Axum: GET /.well-known/acme-challenge/{token}
Axum-->>LE: 200 secret (key_authorization)
App->>LE: poll_ready (avec backoff)
LE-->>App: Commande prête
App->>LE: finalize()
LE-->>App: private_key_pem
App->>LE: poll_certificate()
LE-->>App: cert_chain_pem
App->>FS: écrire certs/{domain}.cert.pem
App->>FS: écrire certs/{domain}.key.pem
App->>Axum: snd.send() (arrêt en douceur)
App-->>App: Ok(())
Comme vous pouvez le voir, le protocole ACME est assez simple. Voici maintenant la partie délicate de l’intégration dans notre serveur MCP.
certs/{domain}.cert.pem et certs/{domain}.key.pem, ce qui signifie que nous disposons déjà de certificats valides.// chargement du certificat et de la clé depuis le fichier
let cert_file = format!("certs/{}.cert.pem", domain);
let key_file = format!("certs/{}.key.pem", domain);
let tls_config = RustlsConfig::from_pem_file(cert_file, key_file)
.await
.unwrap();
log::info!("écoute sur https://[::]:{}", config.port);
let sse_config = SseServerConfig {
bind: format!("[::]:{}", config.port).parse().unwrap(),
sse_path: "/sse".to_string(),
post_path: "/message".to_string(),
ct: tokio_util::sync::CancellationToken::new(),
sse_keep_alive: None,
};
let (sse_server, router) = SseServer::new(sse_config);
let addr = sse_server.config.bind;
let ct = sse_server.with_service(DowntozeroTool::new);
let server = axum_server::bind_rustls(addr, tls_config).serve(router.into_make_service());
tokio::spawn(async move {
if let Err(e) = server.await {
log::error!("arrêt du serveur sse avec erreur, {e}");
}
});
Nous avons également implémenté un fallback pour les exécutions locales : s’il n’y a pas de configuration de domaine présente, nous démarrons le serveur MCP en HTTP simple. Cela permet de tester localement sans besoin de certificat ni DNS durant le développement.