Poursuivant notre aventure avec notre serveur de documentation MCP depuis 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 de 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 est apparue. Puisque nous utilisons également Let’s Encrypt partout dans DownToZero, nous avons principalement réutilisé le processus dans ce conteneur.
sequenceDiagram
participant App as get_certificate()
participant LE as Let's Encrypt (ACME)
participant Axum as Axum HTTP Server
participant FS as Filesystem
App->>LE: Create Account (NewAccount)
App->>LE: Create New Order (Identifier::Dns(domain))
LE-->>App: Order (status = Pending)
App->>App: Create oneshot channel (snd, rcv)
App->>LE: Fetch authorizations
LE-->>App: Authorization incl. HTTP-01 challenge
App->>App: Compute token + key_authorization (secret)
App->>Axum: Start server on acme_port serving<br/>/.well-known/acme-challenge/{token} -> secret
App->>App: Sleep 2s
App->>LE: challenge.set_ready()
LE->>Axum: GET /.well-known/acme-challenge/{token}
Axum-->>LE: 200 secret (key_authorization)
App->>LE: poll_ready (with backoff)
LE-->>App: Order Ready
App->>LE: finalize()
LE-->>App: private_key_pem
App->>LE: poll_certificate()
LE-->>App: cert_chain_pem
App->>FS: write certs/{domain}.cert.pem
App->>FS: write certs/{domain}.key.pem
App->>Axum: snd.send() (graceful shutdown)
App-->>App: Ok(())
Comme vous pouvez le voir, le protocole ACME est assez simple. Vient 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 avons déjà des certificats valides.// chargement du certificat et de la clé depuis un 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é une solution de secours pour les exécutions locales : s’il n’y a pas de configuration de domaine présente, nous lançons le serveur MCP en HTTP simple. Cela rend les tests locaux possibles sans avoir besoin de certificat ni de DNS pendant le développement.