check_ntp_status
check_ntp_status
Reports the endpoint's NTP synchronisation status and current offset from a reference time source, in milliseconds. An offset > ~300000 ms (5 minutes) breaks Kerberos, SAML, and TOTP simultaneously — this tool is the first diagnostic step when users report 'all SSO apps are broken.' Read-only; safe to run without consent.
Metadata
Name
check_ntp_status
Updated
2 months ago
Source
Risk
Low
Requires consent
false
Affected scope
user
Code
/**
* mcp/skills/checkNtpStatus.ts — check_ntp_status
*
* Reports the endpoint's NTP configuration + offset from a reference
* source. An offset > ~5 min silently breaks Kerberos, SAML, and TOTP
* simultaneously — this tool is how the identity-auth-repair skill
* detects that root cause.
*
* Platform strategy
* -----------------
* darwin `sntp -d <server>` — unauthenticated NTP query (no admin).
* Parses "offset <seconds>" from the debug output.
* win32 `w32tm /query /status` — reports Source, Phase Offset,
* Last Successful Sync Time. Also surfaces the service state
* so we can flag disabled W32Time on workgroup machines.
*
* Returns an offset in milliseconds (positive = endpoint is ahead of
* reference; negative = behind). Never throws — network / exec
* failures resolve to { offsetMs: null, status: "error", … }.
*/
import { z } from "zod";
import { execAsync, isDarwin, isWin32 } from "./_shared/platform";
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "check_ntp_status",
description:
"Reports the endpoint's NTP synchronisation status and current offset " +
"from a reference time source, in milliseconds. An offset > ~300000 ms " +
"(5 minutes) breaks Kerberos, SAML, and TOTP simultaneously — this tool " +
"is the first diagnostic step when users report 'all SSO apps are " +
"broken.' Read-only; safe to run without consent.",
riskLevel: "low",
destructive: false,
requiresConsent: false,
supportsDryRun: false,
affectedScope: ["user"],
auditRequired: false,
schema: {
server: z
.string()
.optional()
.describe(
"Reference NTP server. Defaults to 'time.apple.com' on macOS and " +
"'time.windows.com' on Windows."
),
},
outputKeys: [
"platform",
"server",
"offsetMs",
"absOffsetMs",
"lastSync",
"serviceState",
"status",
"message",
],
} as const;
// -- Types --------------------------------------------------------------------
export interface NtpStatusResult {
platform: "darwin" | "win32" | "other";
server: string;
/** Offset in milliseconds (positive = endpoint ahead, negative = behind). */
offsetMs: number | null;
/** Absolute value of offsetMs; convenient for threshold checks. */
absOffsetMs: number | null;
/** Human-readable last successful sync time (null on darwin fallback or unknown). */
lastSync: string | null;
/** W32Time / timed service state ("running" / "stopped" / "unknown"). */
serviceState: "running" | "stopped" | "unknown";
status: "ok" | "drifted" | "error";
message: string;
}
const DRIFT_THRESHOLD_MS = 5 * 60 * 1_000; // 5 min — standard Kerberos tolerance
// -- darwin implementation ----------------------------------------------------
async function checkDarwin(server: string): Promise<NtpStatusResult> {
try {
const { stdout } = await execAsync(
`sntp -d ${shellQuote(server)} 2>&1`,
{ maxBuffer: 1 * 1024 * 1024, timeout: 5_000 },
);
const match = stdout.match(/[-+]\d+(?:\.\d+)?\s+\+\/-);
if (!match) {
return {
platform: "darwin", server,
offsetMs: null, absOffsetMs: null,
lastSync: null, serviceState: "unknown",
status: "error",
message: `sntp returned no parseable offset: ${stdout.slice(0, 200)}`,
};
}
const offsetSeconds = parseFloat(match[0].replace(/\s+\+\/\-$/, "").trim());
const offsetMs = Math.round(offsetSeconds * 1_000);
const absMs = Math.abs(offsetMs);
const drifted = absMs > DRIFT_THRESHOLD_MS;
return {
platform: "darwin", server,
offsetMs, absOffsetMs: absMs,
lastSync: null,
serviceState: "unknown",
status: drifted ? "drifted" : "ok",
message: drifted
? `Endpoint clock is ${Math.round(absMs / 1000)}s ${offsetMs > 0 ? "ahead of" : "behind"} ${server} — Kerberos/SAML/TOTP will fail.`
: `Endpoint clock is within ${Math.round(absMs)}ms of ${server}.`,
};
} catch (err) {
return {
platform: "darwin", server,
offsetMs: null, absOffsetMs: null,
lastSync: null, serviceState: "unknown",
status: "error",
message: `sntp failed: ${(err as Error).message}`,
};
}
}
// -- win32 implementation -----------------------------------------------------
async function checkWin32(server: string): Promise<NtpStatusResult> {
let serviceState: "running" | "stopped" | "unknown" = "unknown";
try {
const { stdout } = await execAsync(`sc query w32time`, {
maxBuffer: 1 * 1024 * 1024, timeout: 5_000,
});
if (/STATE\s*: \s*4\s*RUNNING/i.test(stdout)) serviceState = "running";
else if (/STATE\s*: \s*1\s*STOPPED/i.test(stdout)) serviceState = "stopped";
} catch {
}
try {
const { stdout } = await execAsync(`w32tm /query /status`, {
maxBuffer: 1 * 1024 * 1024, timeout: 5_000,
});
const offMatch = stdout.match(/Phase Offset\s*:\s*([-+]\d+(?:\.\d+)?)s/i);
const lastMatch = stdout.match(/Last Successful Sync Time\s*:\s*(.+)/i);
if (!offMatch) {
return {
platform: "win32", server,
offsetMs: null, absOffsetMs: null,
lastSync: lastMatch ? lastMatch[1].trim() : null,
serviceState,
status: "error",
message: `w32tm returned no parseable Phase Offset.`,
};
}
const offsetMs = Math.round(parseFloat(offMatch[1]) * 1_000);
const absMs = Math.abs(offsetMs);
const drifted = absMs > DRIFT_THRESHOLD_MS;
return {
platform: "win32", server,
offsetMs, absOffsetMs: absMs,
lastSync: lastMatch ? lastMatch[1].trim() : null,
serviceState,
status: drifted ? "drifted" : "ok",
message: drifted
? `Endpoint clock is ${Math.round(absMs / 1000)}s ${offsetMs > 0 ? "ahead of" : "behind"} reference — Kerberos/SAML/TOTP will fail.`
: `Endpoint clock is within ${absMs}ms of reference (last sync: ${lastMatch ? lastMatch[1].trim() : "unknown"}).`,
};
} catch (err) {
return {
platform: "win32", server,
offsetMs: null, absOffsetMs: null,
lastSync: null,
serviceState,
status: "error",
message: `w32tm /query /status failed: ${(err as Error).message}`,
};
}
}
// -- Helpers ------------------------------------------------------------------
function shellQuote(s: string): string {
return `'${s.replace(/'/g, `'\\''`)}'`;
}
// Exported for unit tests.
export const __testing = { checkDarwin, checkWin32, DRIFT_THRESHOLD_MS };
// -- Exported run function ----------------------------------------------------
export async function run({
server,
}: {
server?: string;
} = {}): Promise<NtpStatusResult> {
const platform: "darwin" | "win32" | "other" =
isDarwin() ? "darwin" : isWin32() ? "win32" : "other";
const defaultServer = platform === "win32" ? "time.windows.com" : "time.apple.com";
const resolvedServer = server ?? defaultServer;
if (platform === "darwin") return checkDarwin(resolvedServer);
if (platform === "win32") return checkWin32(resolvedServer);
return {
platform: "other", server: resolvedServer,
offsetMs: null, absOffsetMs: null,
lastSync: null, serviceState: "unknown",
status: "error",
message: "Unsupported platform — cannot check NTP status.",
};
}
// -- CLI smoke test -----------------------------------------------------------
if (false) {
run()
.then((r) => console.log(JSON.stringify(r, null, 2)))
.catch((err: Error) => { console.error(err.message); process.exit(1); });
}