Checks the TLS certificate expiry date for a given hostname. Use when diagnosing HTTPS connection failures, email server issues, or VPN authentication problems caused by expired certificates.
/** * mcp/skills/checkCertificateExpiry.ts — check_certificate_expiry skill * * Checks the TLS certificate expiry date for a given hostname using Node.js * tls.connect() — no child_process needed. * * Platform strategy * ----------------- * darwin & win32 Pure Node.js tls module — cross-platform * * Smoke test * npx tsx -r dotenv/config mcp/skills/checkCertificateExpiry.ts google.com */import * as os from "os";import * as tls from "tls";import { z } from "zod";// -- Meta ---------------------------------------------------------------------export const meta = { name: "check_certificate_expiry", description: "Checks the TLS certificate expiry date for a given hostname. " + "Use when diagnosing HTTPS connection failures, email server issues, or " + "VPN authentication problems caused by expired certificates.", riskLevel: "low", destructive: false, requiresConsent: false, supportsDryRun: false, affectedScope: ["user"], auditRequired: false, // See docs/proactivesupport/PROACTIVE-ARCHITECTURE.md §6. Wave 2 Track B Trigger 2 // (`certificate-expiring`) references `daysUntilExpiry` and `isExpired`. outputKeys: [ "platform", "host", "port", "subject", "issuer", "validFrom", "validTo", "daysUntilExpiry", "isExpired", "isExpiringSoon", "error", ], schema: { host: z .string() .describe("Hostname to check (e.g. 'mail.company.com', 'vpn.example.com'). Hostname ONLY — never include a port."), port: z .number() .optional() .describe("Primary TLS port to try first. Default: 443. Must be a TLS-listening port — never the VPN's own connection port (e.g. WireGuard 51821, IKEv2 500/4500), which has no TLS listener."), fallbackPorts: z .array(z.number()) .optional() .describe("Additional TLS ports to try, in order, if `port` yields no cert (connection refused / handshake failure). E.g. [8443] for SSL VPNs. The result reports the port that actually returned a cert."), },} as const;// -- Types --------------------------------------------------------------------interface CertResult { host: string; port: number; subject: string; issuer: string; validFrom: string; validTo: string; daysUntilExpiry: number; isExpired: boolean; isExpiringSoon: boolean; error?: string;}// -- Shared implementation (darwin + win32) -----------------------------------function tlsConnect(host: string, port: number): Promise<tls.TLSSocket> { return new Promise((resolve, reject) => { const socket = tls.connect( { host, port, rejectUnauthorized: false, servername: host }, () => resolve(socket), ); socket.setTimeout(10_000); socket.on("timeout", () => { socket.destroy(); reject(new Error(`Connection to ${host}:${port} timed out`)); }); socket.on("error", reject); });}function formatDN(dn: Record<string, string> | undefined): string { if (!dn) return "unknown"; return Object.entries(dn) .map(([k, v]) => `${k}=${v}`) .join(", ");}async function checkCertificate(host: string, port: number): Promise<CertResult> { const socket = await tlsConnect(host, port); try { const cert = socket.getPeerCertificate(); if (!cert || !cert.valid_to) { return { host, port, subject: "unknown", issuer: "unknown", validFrom: "unknown", validTo: "unknown", daysUntilExpiry: -1, // No cert read — we cannot know expiry. Do NOT report isExpired:true, // which would misdiagnose an unreadable cert as an expired one. // Consumers must branch on `error` before trusting the expiry fields. isExpired: false, isExpiringSoon: false, error: "No certificate returned", }; } const validTo = new Date(cert.valid_to); const validFrom = new Date(cert.valid_from); const now = new Date(); const msPerDay = 1000 * 60 * 60 * 24; const daysUntilExpiry = Math.floor((validTo.getTime() - now.getTime()) / msPerDay); return { host, port, subject: formatDN(cert.subject as unknown as Record<string, string>), issuer: formatDN(cert.issuer as unknown as Record<string, string>), validFrom: validFrom.toISOString(), validTo: validTo.toISOString(), daysUntilExpiry, isExpired: daysUntilExpiry < 0, isExpiringSoon: daysUntilExpiry >= 0 && daysUntilExpiry < 30, }; } finally { socket.destroy(); }}// -- Exported run function ----------------------------------------------------export async function run({ host, port = 443, fallbackPorts,}: { host: string; port?: number; fallbackPorts?: number[];}) { // Validate host — reject anything that looks like shell injection if (!/^[a-zA-Z0-9.\-]+$/.test(host)) { throw new Error(`[check_certificate_expiry] Invalid hostname: ${host}`); } // Ordered, de-duplicated list of ports to try. The primary `port` first, then // any `fallbackPorts` — so an SSL VPN whose cert lives on 8443 is still found // when 443 refuses, in a single tool call. const ports = [port, ...(fallbackPorts ?? [])].filter( (p, i, arr) => arr.indexOf(p) === i, ); for (const p of ports) { if (p < 1 || p > 65535) { throw new Error(`[check_certificate_expiry] Invalid port: ${p}`); } } const platform = os.platform(); let last: CertResult & { error: string } = { host, port, subject: "unknown", issuer: "unknown", validFrom: "unknown", validTo: "unknown", daysUntilExpiry: -1, isExpired: false, isExpiringSoon: false, error: "No ports tried", }; for (const p of ports) { try { const result = await checkCertificate(host, p); // First port that returns a readable cert wins. A cert-less response // (result.error set, e.g. "No certificate returned") falls through to the // next fallback port. if (!result.error) return { platform, ...result }; last = { ...result, error: result.error }; } catch (err) { // Connection/handshake failure on THIS port — the host was unreachable or // did not speak implicit TLS here. That is NOT an expired certificate; // surfacing isExpired:true caused a false "cert expired" diagnosis (e.g. // an SMTP host probed on 443). Record it and try the next fallback port. last = { host, port: p, subject: "unknown", issuer: "unknown", validFrom: "unknown", validTo: "unknown", daysUntilExpiry: -1, isExpired: false, isExpiringSoon: false, error: (err as Error).message, }; } } // Every port failed — return the last failure. Callers branch on `error`. return { platform, ...last };}// -- CLI smoke test -----------------------------------------------------------if (false) { run({ host: process.argv[2] ?? "google.com" }) .then((r) => console.log(JSON.stringify(r, null, 2))) .catch((err: Error) => { console.error(err.message); process.exit(1); });}