Poursuivant notre parcours avec notre serveur de documentation MCP depuis notre article précédent, nous pouvons approfondir un peu l’histoire du TLS et la façon dont 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 à ChatGPT et Gemini, la nécessité du TLS est apparue. Comme nous utilisons également Let’s Encrypt partout sur DownToZero, nous avons majoritairement réutilisé le 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: Order (status = Pending)
App->>App: Créer un canal oneshot (snd, rcv)
App->>LE: Récupérer les authorizations
LE-->>App: Authorization incl. HTTP-01 challenge
App->>App: Calculer token + key_authorization (secret)
App->>Axum: Démarrer le serveur sur acme_port servant<br/>/.well-known/acme-challenge/{token} -> secret
App->>App: Pause 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: Order Ready
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 gracieux)
App-->>App: Ok(())
Comme vous pouvez le voir, le protocole ACME est assez simple. Vient maintenant la partie délicate : intégrer cela dans notre serveur MCP.
certs/{domain}.cert.pem et certs/{domain}.key.pem, ce qui signifie que nous disposons déjà de certificats valides.// loading cert and key from file
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!("listening on 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!("sse server shutdown with error, {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 démarrons le serveur MCP en HTTP simple. Cela permet des tests locaux sans avoir besoin d’un certificat ni de DNS pendant le développement.