Continuando nuestro recorrido con nuestro servidor de documentación MCP de nuestra publicación anterior, podemos profundizar un poco más en la historia del TLS y cómo estamos generando el certificado Let’s Encrypt al iniciar.
Si deseas ver el código fuente completo, el repositorio de github está enlazado al final de la publicación.
Así que observemos un poco más de cerca los detalles. Cuando intentamos integrar nuestro servidor MCP en ChatGPT y Gemini, surgió el requisito de TLS. Como también usamos Let’s Encrypt en todo DownToZero, reutilizamos principalmente el proceso en este contenedor.
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: Crear Cuenta (NewAccount)
App->>LE: Crear Nuevo Pedido (Identifier::Dns(domain))
LE-->>App: Pedido (estado = Pendiente)
App->>App: Crear canal oneshot (snd, rcv)
App->>LE: Obtener autorizaciones
LE-->>App: Autorización incl. desafío HTTP-01
App->>App: Calcular token + key_authorization (secreto)
App->>Axum: Iniciar servidor en acme_port sirviendo<br/>/.well-known/acme-challenge/{token} -> secreto
App->>App: Dormir 2s
App->>LE: challenge.set_ready()
LE->>Axum: GET /.well-known/acme-challenge/{token}
Axum-->>LE: 200 secreto (key_authorization)
App->>LE: poll_ready (con backoff)
LE-->>App: Pedido Listo
App->>LE: finalize()
LE-->>App: private_key_pem
App->>LE: poll_certificate()
LE-->>App: cert_chain_pem
App->>FS: escribir certs/{domain}.cert.pem
App->>FS: escribir certs/{domain}.key.pem
App->>Axum: snd.send() (cierre ordenado)
App-->>App: Ok(())
Como puedes ver, el protocolo ACME es bastante sencillo. Ahora viene la parte complicada de integrar esto en nuestro servidor MCP.
certs/{domain}.cert.pem y certs/{domain}.key.pem, lo que significa que ya tenemos certificados válidos.// cargando certificado y clave desde archivo
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!("escuchando en 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!("servidor sse finalizado con error, {e}");
}
});
También implementamos un mecanismo de respaldo para ejecuciones locales: si no hay una configuración de dominio presente, iniciamos el servidor MCP con HTTP simple. Esto hace posible la prueba local sin la necesidad de un certificado o DNS durante el desarrollo.