← Files Meetings (Beta)ARCHIVED FILE
scripts/start_meetings_mcp.mjs
40.9 KB · Oct 8, 2026 · 12:02 UTC
import {
createWriteStream,
lstatSync,
readFileSync,
readdirSync,
realpathSync,
statSync,
} from "node:fs";
import {
basename,
delimiter,
dirname,
isAbsolute,
join,
resolve,
} from "node:path";
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { StringDecoder } from "node:string_decoder";
import { Transform } from "node:stream";
import { fileURLToPath } from "node:url";
import { TextDecoder } from "node:util";
const scriptRoot = dirname(fileURLToPath(import.meta.url));
const script = join(scriptRoot, "meetings_mcp_entrypoint.py");
const isWindows = process.platform === "win32";
const pythonNames = isWindows
? ["python.exe", "python3.exe"]
: ["python3", "python"];
// Match the MCP environment without running its entrypoint or touching client
// stdin. Disable ambient oaipkg missing-import repair in both processes.
const pythonEnvironment = { ...process.env, OAIPKG_DISABLE_META_MISSING: "1" };
const pythonProbe = [
"import sys",
"sys.exit(1) if sys.version_info < (3, 10) else None",
"import ctypes, hashlib, ssl, urllib.request, xml.parsers.expat",
].join("; ");
const pythonProbeTimeoutMs = 2000;
// Admit candidates for three seconds, then let the last probe finish its full
// two-second window. With reaping, selection takes at most 5.1 seconds, leaving
// the MCP's 12-second deadline and two seven-second cleanup periods inside the
// host's 35-second startup budget.
const pythonSelectionAdmissionMs = 3000;
const pythonProbeCleanupMs = 100;
const cacheVersion = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
const maximumIdentityFileBytes = 64 * 1024;
const maximumRuntimeConfigBytes = 16 * 1024;
const runtimeConfigKeys = [
"schemaVersion",
"flavor",
"distribution",
"pluginName",
"marketplaceName",
"developmentUpdatePolicy",
"serverName",
"defaultMcpProfile",
"appName",
"bundleIdentifier",
"appSupportDirectory",
"controlTarget",
"teamIdentifier",
"toolNames",
"frameworkSymlinks",
"officialCachePublishers",
];
const runtimeComponent = /^[A-Za-z0-9][A-Za-z0-9._() -]{0,127}$/u;
const runtimeToolName = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
const distributionMarketplaces = Object.freeze({
internal: "openai-internal-testing",
external: "openai-curated-remote",
});
const trustedPublishers = Object.values(distributionMarketplaces);
const semanticVersion =
/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
function samePath(first, second) {
return isWindows
? first.toLowerCase() === second.toLowerCase()
: first === second;
}
function sameDirectoryIdentity(first, second) {
if (!second.isDirectory() || second.isSymbolicLink()) return false;
// Some Windows filesystems do not expose stable inode identifiers. A
// disappearing cache entry remains observable without trusting zeroes.
return first.ino === 0 || second.ino === 0
? first.dev === second.dev
: first.dev === second.dev && first.ino === second.ino;
}
function scanJSONObjects(source, visit) {
const objects = [];
for (let index = 0; index < source.length; index += 1) {
const character = source[index];
if (character === "{") {
objects.push(new Set());
} else if (character === "}") {
objects.pop();
} else if (character === '"') {
const start = index;
for (index += 1; source[index] !== '"'; index += 1) {
if (source[index] === "\\") index += 1;
}
let next = index + 1;
while (/\s/u.test(source[next] ?? "")) next += 1;
if (source[next] === ":") {
const key = JSON.parse(source.slice(start, index + 1));
const keys = objects.at(-1);
if (
!keys ||
visit(keys, key, source, next + 1, objects.length) === false
)
return false;
}
}
}
return true;
}
function parseUniqueJSONObject(bytes) {
const source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
const value = JSON.parse(source);
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
// JSON.parse discards duplicate keys, while the Python runtime rejects
// them. Scan parsed JSON strings so escaped spellings compare identically.
if (
!scanJSONObjects(source, (keys, key) => {
if (keys.has(key)) return false;
keys.add(key);
return true;
})
) {
return null;
}
return value;
}
function safeRuntimePath(value) {
return (
typeof value === "string" &&
value.length > 0 &&
!value.includes("\\") &&
!value.includes("\0") &&
value
.split("/")
.every((part) => part !== "" && part !== "." && part !== "..")
);
}
function validRuntimeConfig(value) {
if (
!value ||
runtimeConfigKeys.some(
(key) => !Object.prototype.hasOwnProperty.call(value, key),
) ||
value.schemaVersion !== 1 ||
[
"flavor",
"serverName",
"defaultMcpProfile",
"appName",
"appSupportDirectory",
"controlTarget",
].some(
(key) =>
typeof value[key] !== "string" || !runtimeComponent.test(value[key]),
) ||
typeof value.bundleIdentifier !== "string" ||
!/^[A-Za-z0-9][A-Za-z0-9.-]{0,127}$/u.test(value.bundleIdentifier ?? "") ||
typeof value.teamIdentifier !== "string" ||
!/^[A-Z0-9]{10}$/u.test(value.teamIdentifier ?? "") ||
!value.appName.endsWith(".app")
) {
return false;
}
if (
typeof value.distribution !== "string" ||
!Object.prototype.hasOwnProperty.call(
distributionMarketplaces,
value.distribution,
) ||
value.pluginName !== "chatgpt-meetings" ||
value.marketplaceName !== distributionMarketplaces[value.distribution] ||
!["production", "development"].includes(value.flavor) ||
!["disabled", "local-to-internal"].includes(
value.developmentUpdatePolicy,
) ||
(value.developmentUpdatePolicy === "local-to-internal" &&
(value.flavor !== "development" || value.distribution !== "internal")) ||
(value.flavor === "development" && value.distribution !== "internal")
) {
return false;
}
const tools = value.toolNames;
if (
!Array.isArray(tools) ||
tools.length < 1 ||
tools.length > 32 ||
new Set(tools).size !== tools.length ||
tools.some(
(tool) => typeof tool !== "string" || !runtimeToolName.test(tool),
)
) {
return false;
}
const links = value.frameworkSymlinks;
if (!Array.isArray(links) || links.length > 16) return false;
const paths = new Set();
for (const link of links) {
if (
!link ||
typeof link !== "object" ||
Array.isArray(link) ||
Object.keys(link).length !== 2 ||
!Object.prototype.hasOwnProperty.call(link, "path") ||
!Object.prototype.hasOwnProperty.call(link, "target") ||
!safeRuntimePath(link.path) ||
!safeRuntimePath(link.target) ||
!link.path.startsWith("Contents/Frameworks/Sentry.framework/") ||
paths.has(link.path)
) {
return false;
}
paths.add(link.path);
}
const publishers = value.officialCachePublishers;
return (
Array.isArray(publishers) &&
publishers.length ===
(value.flavor === "production" ? trustedPublishers.length : 0) &&
new Set(publishers).size === publishers.length &&
publishers.every((publisher) => trustedPublishers.includes(publisher))
);
}
function readIdentity(path, { runtimeConfig = false } = {}) {
try {
const metadata = lstatSync(path);
if (
!metadata.isFile() ||
metadata.isSymbolicLink() ||
metadata.size <= 0 ||
metadata.size >
(runtimeConfig
? maximumRuntimeConfigBytes
: maximumIdentityFileBytes) ||
(!isWindows && (metadata.mode & 0o022) !== 0) ||
(typeof process.getuid === "function" &&
metadata.uid !== process.getuid())
) {
return null;
}
const bytes = readFileSync(path);
if (bytes.length !== metadata.size) return null;
const value = parseUniqueJSONObject(bytes);
if (!value || (runtimeConfig && !validRuntimeConfig(value))) return null;
return runtimeConfig
? Object.fromEntries(runtimeConfigKeys.map((key) => [key, value[key]]))
: value;
} catch {
return null;
}
}
function newerPluginVersion(current, candidate) {
const previous = semanticVersion.exec(current);
const next = semanticVersion.exec(candidate);
if (
!previous ||
!next ||
[previous[4], next[4]].some(
(prerelease) =>
prerelease &&
prerelease
.split(".")
.some(
(identifier) =>
/^[0-9]+$/u.test(identifier) &&
identifier.length > 1 &&
identifier.startsWith("0"),
),
)
) {
return false;
}
for (let index = 1; index <= 3; index += 1) {
const before = BigInt(previous[index]);
const after = BigInt(next[index]);
if (before !== after) return after > before;
}
if (!previous[4] || !next[4]) return Boolean(previous[4]) && !next[4];
const before = previous[4].split(".");
const after = next[4].split(".");
for (
let index = 0;
index < Math.max(before.length, after.length);
index += 1
) {
if (before[index] === undefined) return true;
if (after[index] === undefined) return false;
if (before[index] === after[index]) continue;
const previousNumeric = /^[0-9]+$/u.test(before[index]);
const nextNumeric = /^[0-9]+$/u.test(after[index]);
if (previousNumeric !== nextNumeric) return previousNumeric;
return previousNumeric
? BigInt(after[index]) > BigInt(before[index])
: after[index] > before[index];
}
return false;
}
function officialPluginInstallation() {
try {
const configuredHome = (process.env.CODEX_HOME || "").trim();
const userHome = process.env.USERPROFILE || process.env.HOME;
const codexHome =
configuredHome || (userHome ? join(userHome, ".codex") : "");
if (!codexHome || !isAbsolute(codexHome) || codexHome.includes("\0"))
return null;
const root = realpathSync(dirname(scriptRoot));
const family = dirname(root);
const publisherRoot = dirname(family);
const cache = dirname(publisherRoot);
if (!samePath(cache, realpathSync(join(codexHome, "plugins", "cache"))))
return null;
const config = readIdentity(join(root, "scripts", "runtime-config.json"), {
runtimeConfig: true,
});
const manifest = readIdentity(join(root, ".codex-plugin", "plugin.json"));
const publisher = basename(publisherRoot);
if (
config?.schemaVersion !== 1 ||
config.flavor !== "production" ||
config.pluginName !== basename(family) ||
config.marketplaceName !== publisher ||
config.serverName !== basename(family) ||
!Array.isArray(config.officialCachePublishers) ||
!config.officialCachePublishers.includes(publisher) ||
manifest?.name !== config.serverName ||
manifest.version !== basename(root) ||
!cacheVersion.test(basename(root))
) {
return null;
}
const rootIdentity = lstatSync(root);
const familyIdentity = lstatSync(family);
if (
[rootIdentity, familyIdentity].some(
(value) => !value.isDirectory() || value.isSymbolicLink(),
)
) {
return null;
}
return { config, family, familyIdentity, publisher, root, rootIdentity };
} catch {
// Source checkouts, private publishers, and incomplete installations do
// not have installer-owned update authority and must remain untouched.
return null;
}
}
function officialPluginWasReplaced(installation) {
if (!installation) return false;
try {
if (
!sameDirectoryIdentity(
installation.familyIdentity,
lstatSync(installation.family),
) ||
!samePath(realpathSync(installation.family), installation.family)
) {
return false;
}
try {
const current = lstatSync(installation.root);
if (sameDirectoryIdentity(installation.rootIdentity, current))
return false;
if (!current.isDirectory() || current.isSymbolicLink()) return false;
} catch (error) {
if (error?.code !== "ENOENT" && error?.code !== "ENOTDIR") return false;
}
const candidates = [];
for (const entry of readdirSync(installation.family, {
withFileTypes: true,
})) {
if (!cacheVersion.test(entry.name)) continue;
if (entry.isSymbolicLink() || !entry.isDirectory()) return false;
const candidate = join(installation.family, entry.name);
const metadata = lstatSync(candidate, { bigint: true });
if (!metadata.isDirectory() || metadata.isSymbolicLink()) return false;
if (!samePath(dirname(realpathSync(candidate)), installation.family))
return false;
const empty = readdirSync(candidate).length === 0;
if (empty) {
const rechecked = lstatSync(candidate, { bigint: true });
if (
!rechecked.isDirectory() ||
rechecked.isSymbolicLink() ||
rechecked.dev !== metadata.dev ||
rechecked.ino !== metadata.ino ||
rechecked.ctimeNs !== metadata.ctimeNs ||
rechecked.mtimeNs !== metadata.mtimeNs ||
!samePath(dirname(realpathSync(candidate)), installation.family) ||
readdirSync(candidate).length !== 0
) {
return false;
}
}
candidates.push({ path: candidate, metadata, empty });
}
// An old, unchanged empty cache tombstone is not an installed successor.
// New empty contenders may still be staging and must remain fail-closed.
const installed = candidates.filter((candidate) => !candidate.empty);
if (installed.length !== 1) return false;
const selected = installed[0];
const selectedCreated =
selected.metadata.birthtimeNs > 0n
? selected.metadata.birthtimeNs
: selected.metadata.ctimeNs;
if (
candidates.some((candidate) => {
if (!candidate.empty) return false;
const created =
candidate.metadata.birthtimeNs > 0n
? candidate.metadata.birthtimeNs
: candidate.metadata.ctimeNs;
return created >= selectedCreated;
})
) {
return false;
}
const successor = selected.path;
const successorScripts = join(successor, "scripts");
const successorPluginMetadata = join(successor, ".codex-plugin");
for (const directory of [successorScripts, successorPluginMetadata]) {
const metadata = lstatSync(directory);
if (!metadata.isDirectory() || metadata.isSymbolicLink()) return false;
}
for (const name of [
"start_meetings_mcp.mjs",
"meetings_mcp_entrypoint.py",
"meetings_mcp.py",
]) {
const metadata = lstatSync(join(successorScripts, name));
if (!metadata.isFile() || metadata.isSymbolicLink()) return false;
}
const manifest = readIdentity(join(successorPluginMetadata, "plugin.json"));
const config = readIdentity(join(successorScripts, "runtime-config.json"), {
runtimeConfig: true,
});
if (
manifest?.name !== installation.config.serverName ||
manifest.version !== basename(successor) ||
!newerPluginVersion(basename(installation.root), manifest.version) ||
!Array.isArray(config?.officialCachePublishers) ||
!config.officialCachePublishers.includes(installation.publisher)
) {
return false;
}
return [
"schemaVersion",
"flavor",
"distribution",
"pluginName",
"marketplaceName",
"developmentUpdatePolicy",
"serverName",
"defaultMcpProfile",
"appName",
"bundleIdentifier",
"teamIdentifier",
"appSupportDirectory",
"controlTarget",
].every((key) => config[key] === installation.config[key]);
} catch {
return false;
}
}
function usable(path) {
if (!path) return null;
try {
const canonical = realpathSync(path);
if (/(?:^|[\\/])WindowsApps(?:[\\/]|$)/i.test(canonical)) return null;
return statSync(canonical).isFile() ? canonical : null;
} catch {
return null;
}
}
function* commandPaths(command) {
if (!command) return;
if (/[\\/]/.test(command)) {
yield command;
return;
}
const extensions = isWindows ? ["", ".exe"] : [""];
for (const directory of (process.env.PATH || "").split(delimiter)) {
if (!directory) continue;
for (const extension of extensions) {
yield join(directory, `${command}${extension}`);
}
}
}
function dependencyCandidates(root) {
return isWindows
? [
join(root, "python", "python.exe"),
join(root, "dependencies", "python", "python.exe"),
join(root, "python.exe"),
]
: [
join(root, "python", "bin", "python3"),
join(root, "dependencies", "python", "bin", "python3"),
join(root, "bin", "python3"),
];
}
function* pythonCandidatePaths() {
const roots = [
process.env.CODEX_RUNTIME_DEPENDENCIES,
process.env.CODEX_WORKSPACE_DEPENDENCIES,
process.env.CODEX_DEPENDENCIES,
].filter(Boolean);
const home = process.env.USERPROFILE || process.env.HOME;
if (home)
roots.push(
join(
home,
".cache",
"codex-runtimes",
"codex-primary-runtime",
"dependencies",
),
);
for (const root of roots) {
yield* dependencyCandidates(root);
}
for (const name of pythonNames) {
yield* commandPaths(name);
}
}
function* usableCandidates(paths) {
for (const path of paths) {
const candidate = usable(path);
if (candidate) yield candidate;
}
}
function probePython(candidate, timeoutMs, signal, cwd) {
return new Promise((resolveProbe, rejectProbe) => {
let probe;
let timeout;
let cleanup;
let stopped = false;
let finished = false;
const finish = (failure) => {
if (finished) return;
finished = true;
clearTimeout(timeout);
clearTimeout(cleanup);
signal.removeEventListener("abort", stop);
resolveProbe(failure);
};
const stop = () => {
if (finished || stopped) return;
stopped = true;
try {
probe.kill("SIGKILL");
} catch {
// The owned probe can exit concurrently with cancellation or timeout.
}
cleanup = setTimeout(() => {
if (finished) return;
finished = true;
clearTimeout(timeout);
signal.removeEventListener("abort", stop);
rejectProbe(new Error("Python runtime probe could not be stopped"));
}, pythonProbeCleanupMs);
};
try {
probe = spawn(candidate, ["-B", "-c", pythonProbe], {
cwd,
env: pythonEnvironment,
stdio: "ignore",
windowsHide: true,
});
} catch {
finish("could not be started");
return;
}
probe.once("error", () => {
// A failed kill must still wait for exit or fail the bounded cleanup;
// it cannot admit another probe while this child might remain alive.
if (!stopped) finish("could not be started");
});
probe.once("exit", (code, exitSignal) => {
finish(
stopped
? "probe timed out"
: exitSignal
? `signal ${exitSignal}`
: code === 0
? null
: `exit ${code}`,
);
});
timeout = setTimeout(stop, timeoutMs);
signal.addEventListener("abort", stop, { once: true });
if (signal.aborted) stop();
});
}
async function findPython(signal, cwd, candidatesAtLaunch) {
signal.throwIfAborted();
const override = (process.env.CHATGPT_MEETINGS_PYTHON || "").trim();
const paths =
candidatesAtLaunch ??
(override ? commandPaths(override) : pythonCandidatePaths());
const candidates = usableCandidates(paths);
if (override) {
const resolved = candidates.next().value;
if (!resolved)
throw new Error("CHATGPT_MEETINGS_PYTHON is not a usable Python runtime");
// An explicit interpreter keeps the existing MCP startup allowance. Its
// single startup remains fail-closed, with no automatic fallback or retry.
return resolved;
}
const deadline = performance.now() + pythonSelectionAdmissionMs;
const seen = new Set();
while (performance.now() < deadline) {
const { value: candidate, done } = candidates.next();
if (done) break;
const identity = isWindows ? candidate.toLowerCase() : candidate;
if (seen.has(identity)) continue;
seen.add(identity);
signal.throwIfAborted();
if (performance.now() >= deadline) break;
const failure = await probePython(
candidate,
pythonProbeTimeoutMs,
signal,
cwd,
);
signal.throwIfAborted();
if (!failure) return candidate;
// A rejected binary can emit private paths or arbitrary diagnostics. Only
// the process outcome is safe here; stdout remains exclusively MCP traffic.
console.error(
`ChatGPT Meetings skipped an unusable Python runtime (${failure})`,
);
}
throw new Error("no usable Python runtime was found for ChatGPT Meetings");
}
function windowsWorkingDirectory() {
const candidates = [
process.env.CODEX_HOME,
process.env.LOCALAPPDATA,
process.env.USERPROFILE,
process.env.HOME,
process.env.TEMP,
];
for (const value of candidates) {
if (typeof value !== "string" || value.length === 0 || value.length > 4096)
continue;
if (value.includes("\0") || !isAbsolute(value)) continue;
try {
const absolute = resolve(value);
const canonical = realpathSync(absolute);
// A junction/symlink anywhere in the candidate changes its real path.
// Never chdir through one: the long-lived launcher and child must not
// pin an attacker-controlled directory or a replaceable cache entry.
if (canonical.toLowerCase() !== absolute.toLowerCase()) continue;
const metadata = lstatSync(absolute);
if (!metadata.isDirectory() || metadata.isSymbolicLink()) continue;
if (/(?:^|[\\/])plugins[\\/]cache(?:[\\/]|$)/i.test(canonical)) continue;
return canonical;
} catch {
continue;
}
}
throw new Error(
"no safe Windows working directory was found for ChatGPT Meetings",
);
}
function redactPythonStderr(runtime) {
const privatePath = Buffer.from(runtime);
let pending = Buffer.alloc(0);
return new Transform({
transform(chunk, _encoding, done) {
const output = Buffer.concat([pending, chunk]);
let offset = 0;
for (
let match = output.indexOf(privatePath);
match !== -1;
match = output.indexOf(privatePath, offset)
) {
this.push(output.subarray(offset, match));
this.push("Python runtime could not be started");
offset = match + privatePath.length;
}
const safeEnd = Math.max(offset, output.length - privatePath.length + 1);
this.push(output.subarray(offset, safeEnd));
pending = output.subarray(safeEnd);
done();
},
flush(done) {
this.push(pending);
done();
},
});
}
let python;
let workingDirectory;
let installedPlugin;
const selectionAbort = new AbortController();
const cancelSelection = () => selectionAbort.abort();
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, cancelSelection);
process.stdin.once("close", cancelSelection);
process.stdin.once("end", cancelSelection);
process.stdout.once("close", cancelSelection);
// Detect an empty, orderly EOF during selection without consuming protocol
// bytes. Nonempty input stays buffered until it can be piped to the MCP.
process.stdin.read(0);
try {
let candidatesAtLaunch;
if (isWindows) {
// Resolve relative and drive-relative paths before changing either cwd.
// Keep filesystem checks lazy so a healthy preferred runtime never waits
// on an unused PATH directory, which can be an unavailable network share.
const override = (process.env.CHATGPT_MEETINGS_PYTHON || "").trim();
candidatesAtLaunch = Array.from(
override ? commandPaths(override) : pythonCandidatePaths(),
(path) => resolve(path),
);
workingDirectory = windowsWorkingDirectory();
// .mcp.json initially starts Node in the installed plugin directory.
// Releasing that cwd before the session becomes long lived lets Codex
// atomically refresh its Windows plugin cache while the MCP is active.
process.chdir(workingDirectory);
}
installedPlugin = officialPluginInstallation();
python = await findPython(
selectionAbort.signal,
workingDirectory,
candidatesAtLaunch,
);
} catch (error) {
if (selectionAbort.signal.aborted) process.exit(0);
console.error(`ChatGPT Meetings MCP launch failed: ${error.message}`);
process.exit(1);
} finally {
for (const signal of ["SIGINT", "SIGTERM"])
process.removeListener(signal, cancelSelection);
process.stdin.removeListener("close", cancelSelection);
process.stdin.removeListener("end", cancelSelection);
process.stdout.removeListener("close", cancelSelection);
}
// Windows process.stdout/stderr use synchronous pipe writes. Keep host output
// off the event loop so an abandoned reader cannot block owned-child cleanup.
const hostStdout = isWindows
? createWriteStream(null, { fd: 1, autoClose: false })
: process.stdout;
const hostStderr = isWindows
? createWriteStream(null, { fd: 2, autoClose: false })
: process.stderr;
let launchFailed = false;
async function failLaunch() {
launchFailed = true;
process.exitCode = 1;
// A failed spawn owns no Python process. Give a healthy diagnostic reader
// its complete message, but bound a closed or permanently full host pipe.
const deadline = setTimeout(() => forceLauncherExit(1), 1000);
await new Promise((done) => {
hostStderr.once("error", done);
hostStderr.write(
"ChatGPT Meetings MCP launch failed: Python runtime could not be started\n",
done,
);
});
clearTimeout(deadline);
process.exit(1);
}
let child;
try {
child = spawn(python, ["-u", script, ...process.argv.slice(2)], {
cwd: workingDirectory,
env: pythonEnvironment,
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
});
} catch {
await failLaunch();
}
const redactedStderr = redactPythonStderr(python);
child.stderr.pipe(redactedStderr).pipe(hostStderr, { end: false });
const maximumObservedRequestFrameBytes = 1 * 1024 * 1024;
const maximumObservedResponseFrameBytes = 32 * 1024 * 1024;
const maximumRPCIDBytes = 4 * 1024;
const maximumPendingRequests = 1024;
const startupTimeoutMs = boundedDuration(
process.env.CHATGPT_MEETINGS_MCP_STARTUP_TIMEOUT_MS,
// Leave both owned-child cleanup grace periods inside the host's 35-second
// MCP startup deadline, even when it keeps an abandoned stdio session open.
12 * 1000,
);
const requestTimeoutMs = boundedDuration(
process.env.CHATGPT_MEETINGS_MCP_REQUEST_TIMEOUT_MS,
4 * 60 * 1000,
);
const terminationGraceMs = boundedDuration(
process.env.CHATGPT_MEETINGS_MCP_TERMINATION_GRACE_MS,
// Cover the RPC drain, the auth manager's three-second worker join, and
// its owned app-server's bounded TERM/KILL and reader cleanup budgets.
7 * 1000,
);
const pendingRequests = new Map();
let terminating = false;
let terminationExitCode;
let pendingStdioDisconnect;
let pendingClientDisconnect;
let pendingTermination;
let pendingForcedTermination;
let pendingLauncherExit;
let pendingDiagnosticRelease;
let startupRequestID;
let startupResponseObserved = false;
let startupResponsePending = false;
let startupResponseDelivered = false;
let startupClientAcknowledged = false;
let startupComplete = false;
function boundedDuration(value, fallback) {
if (value === undefined || value === "") return fallback;
const parsed = Number(value);
return Number.isSafeInteger(parsed) &&
parsed >= 50 &&
parsed <= 24 * 60 * 60 * 1000
? parsed
: fallback;
}
function rpcID(value, frame) {
if (typeof value === "string") {
return Buffer.byteLength(value) <= maximumRPCIDBytes
? `string:${value}`
: `long-string:${createHash("sha256").update(value).digest("hex")}`;
}
if (typeof value === "number" && Number.isFinite(value)) {
if (!Number.isInteger(value)) return `number:${value}`;
if (Number.isSafeInteger(value)) return `integer:${value}`;
let exact = null;
scanJSONObjects(frame, (_keys, key, source, start, depth) => {
if (depth === 1 && key === "id") {
exact =
/^\s*(-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)/u.exec(
source.slice(start),
)?.[1];
}
return true;
});
// Python compares integral floats and integers by their exact numeric
// value. Preserve arbitrary integer spelling before JavaScript rounds it.
return `integer:${BigInt(/^-?(?:0|[1-9][0-9]*)$/u.test(exact ?? "") ? exact : value)}`;
}
return null;
}
function observeFrames(stream, maximumFrameBytes, onFrame) {
const decoder = new StringDecoder("utf8");
let buffered = "";
let bufferedBytes = 0;
let droppingOversized = false;
stream.on("data", (chunk) => {
const decoded = decoder.write(chunk);
buffered += decoded;
bufferedBytes += Buffer.byteLength(decoded);
while (true) {
const newline = buffered.indexOf("\n");
if (newline < 0) {
if (bufferedBytes > maximumFrameBytes) {
buffered = "";
bufferedBytes = 0;
droppingOversized = true;
}
return;
}
const line = buffered.slice(0, newline);
buffered = buffered.slice(newline + 1);
const lineBytes = Buffer.byteLength(line);
bufferedBytes -= lineBytes + 1;
if (droppingOversized) {
droppingOversized = false;
continue;
}
if (lineBytes > maximumFrameBytes) continue;
try {
onFrame(JSON.parse(line), line);
} catch {
// The Python server owns parse errors; the watchdog must remain a
// transparent observer and never turn an invalid frame into a crash.
}
}
});
}
// Start at MCP child launch, not receipt of initialize: an abandoned host can
// retain both stdio endpoints without ever sending its first protocol frame.
const startupDeadline = setTimeout(
() => terminate("startup deadline"),
startupTimeoutMs,
);
startupDeadline.unref();
function completeStartupHandshake() {
if (
terminating ||
startupComplete ||
!startupResponseDelivered ||
!startupClientAcknowledged
) {
return;
}
startupComplete = true;
clearTimeout(startupDeadline);
}
observeFrames(
process.stdin,
maximumObservedRequestFrameBytes,
(message, frame) => {
if (!message || typeof message !== "object") return;
const id = rpcID(message.id, frame);
if (
startupResponseObserved &&
!startupComplete &&
message.jsonrpc === "2.0" &&
!Object.prototype.hasOwnProperty.call(message, "result") &&
!Object.prototype.hasOwnProperty.call(message, "error") &&
((message.method === "notifications/initialized" &&
!Object.prototype.hasOwnProperty.call(message, "id")) ||
(typeof message.method === "string" &&
message.method !== "initialize" &&
!message.method.startsWith("notifications/") &&
message.method !== "$/cancelRequest" &&
id !== null))
) {
// Codex sends initialized only after consuming the initialize reply.
// Accept a later real request as the equivalent legacy-client proof.
startupClientAcknowledged = true;
completeStartupHandshake();
}
if (typeof message.method === "string" && id !== null) {
if (
!pendingRequests.has(id) &&
pendingRequests.size >= maximumPendingRequests
) {
terminate("request capacity");
return;
}
// A duplicate in-flight id is invalid JSON-RPC, but it must not be able
// to keep a wedged child alive by continually resetting its deadline.
if (!pendingRequests.has(id)) pendingRequests.set(id, Date.now());
if (message.method === "initialize" && startupRequestID === undefined) {
startupRequestID = id;
}
return;
}
// A cancellation is advisory. Keep its original deadline until the child
// actually responds so an ignored cancellation plus heartbeat traffic
// cannot retain a wedged session indefinitely.
},
);
process.stdin.pipe(child.stdin);
// Register the forwarding listener first so its real write is queued before
// the zero-byte delivery checkpoint in the response observer below.
child.stdout.pipe(hostStdout, { end: false });
observeFrames(
child.stdout,
maximumObservedResponseFrameBytes,
(message, frame) => {
if (
!message ||
typeof message !== "object" ||
typeof message.method === "string"
)
return;
const id = rpcID(message.id, frame);
if (id === null) return;
if (id === startupRequestID && !startupComplete) {
if (
terminating ||
startupResponsePending ||
message.jsonrpc !== "2.0" ||
!Object.prototype.hasOwnProperty.call(message, "result") ||
Object.prototype.hasOwnProperty.call(message, "error") ||
typeof message.result !== "object" ||
message.result === null ||
Array.isArray(message.result)
) {
return;
}
startupResponseObserved = true;
startupResponsePending = true;
hostStdout.write("", (error) => {
startupResponsePending = false;
if (terminating) return;
if (error || hostStdout.destroyed || hostStdout.writableEnded) {
terminate("stdio disconnect");
return;
}
pendingRequests.delete(id);
startupResponseDelivered = true;
completeStartupHandshake();
});
return;
}
pendingRequests.delete(id);
},
);
const observeClientDisconnect = () => {
if (terminating || pendingClientDisconnect) return;
clearTimeout(startupDeadline);
// Once the host disconnects, outstanding requests can no longer outlive a
// client. Their watchdog must not interrupt Python's owned-child cleanup.
clearInterval(watchdog);
// The ordinary pipe already forwards EOF to Python. Preserve its natural
// exit and final response; intervene only if that owned session wedges.
pendingClientDisconnect = setTimeout(
() => terminate("client disconnect", { expected: true }),
terminationGraceMs,
);
pendingClientDisconnect.unref();
};
process.stdin.once("end", observeClientDisconnect);
process.stdin.once("close", observeClientDisconnect);
child.stdin.once("error", (error) => {
if (terminating) return;
if (error.code !== "EPIPE") {
terminate("stdio disconnect");
return;
}
if (child.exitCode !== null || child.signalCode !== null) return;
// A normally exiting child can close stdin before its final reply drains.
pendingStdioDisconnect = setTimeout(
() => {
if (child.exitCode === null && child.signalCode === null) {
terminate("stdio disconnect");
}
},
Math.min(terminationGraceMs, 1000),
);
pendingStdioDisconnect.unref();
});
function signalChild(signal) {
if (child.exitCode !== null || child.signalCode !== null) return;
try {
child.kill(signal);
} catch {
// Child exit can race its exact owned-process signal.
}
}
function scheduleRetiredLauncherExit() {
if (!terminating || pendingLauncherExit) return;
// A full host pipe can prevent ChildProcess "close" indefinitely even after
// the exact owned Python child exits. Never apply this bound to healthy
// sessions: their final multi-megabyte replies must drain completely.
pendingLauncherExit = setTimeout(
() => forceLauncherExit(terminationExitCode),
Math.min(terminationGraceMs, 1000),
);
pendingLauncherExit.unref();
}
function forceLauncherExit(code) {
// Called only after Python exits or fails to spawn. A Windows async fd write
// can block in the thread pool and prevent process.exit from finishing.
if (isWindows) process.kill(process.pid, "SIGKILL");
else process.exit(code);
}
function releaseStalledDiagnostics() {
if (!terminating || !redactedStderr.isPaused() || pendingDiagnosticRelease)
return;
pendingDiagnosticRelease = setTimeout(
() => {
pendingDiagnosticRelease = undefined;
if (!redactedStderr.isPaused()) return;
// A retired session's blocked host must not stop Python from reaping its
// helper. Keep the redactor flowing without retaining additional bytes.
redactedStderr.unpipe(hostStderr);
redactedStderr.resume();
},
Math.min(terminationGraceMs / 2, 1000),
);
pendingDiagnosticRelease.unref();
}
redactedStderr.on("pause", releaseStalledDiagnostics);
redactedStderr.on("resume", () => {
clearTimeout(pendingDiagnosticRelease);
pendingDiagnosticRelease = undefined;
});
function terminate(reason, { expected = false } = {}) {
if (terminating || launchFailed) return;
terminating = true;
terminationExitCode = expected ? 0 : 1;
// An errored host stream can let Node exit before either drain callback.
process.exitCode = terminationExitCode;
clearTimeout(startupDeadline);
clearTimeout(pendingStdioDisconnect);
clearTimeout(pendingClientDisconnect);
hostStderr.write(`ChatGPT Meetings MCP session closed after ${reason}\n`);
// Stop forwarding a live client's heartbeats into a child that is already
// being reaped. In particular, a Windows pipe can otherwise keep the
// launcher/child stdio pair alive across forceful process termination.
process.stdin.unpipe(child.stdin);
process.stdin.pause();
// Retirement also covers expected shutdowns whose host stopped reading.
// Release stdout so Python can finish a blocked write and reap
// its auth helper. Ordinary EOF gets its final-response grace before here.
child.stdout.unpipe(hostStdout);
child.stdout.resume();
// Preserve the redactor's tail and cleanup diagnostics while the host reads.
// Release a persistently blocked destination inside the child cleanup grace.
releaseStalledDiagnostics();
// Every retirement reason must first give Python's independent stdin reader
// an opportunity to reap its exact owned Codex app-server. Signals cannot
// otherwise reach a detached POSIX grandchild or a Windows child tree.
child.stdin.end();
if (child.exitCode !== null || child.signalCode !== null) {
scheduleRetiredLauncherExit();
return;
}
pendingTermination = setTimeout(() => {
signalChild("SIGTERM");
pendingForcedTermination = setTimeout(
() => signalChild("SIGKILL"),
terminationGraceMs,
);
pendingForcedTermination.unref();
}, terminationGraceMs);
pendingTermination.unref();
}
const watchdog = setInterval(
() => {
const now = Date.now();
let oldestRequest = now;
for (const startedAt of pendingRequests.values()) {
oldestRequest = Math.min(oldestRequest, startedAt);
}
if (pendingRequests.size > 0 && now - oldestRequest >= requestTimeoutMs) {
terminate("request deadline");
} else if (
pendingRequests.size === 0 &&
hostStdout.writableLength === 0 &&
officialPluginWasReplaced(installedPlugin)
) {
// A complete response can still be queued for a connected slow reader.
// Updating the plugin does not authorize discarding that host output.
// Retire this launcher and its own Python MCP child only. An active
// native recorder belongs to the authenticated update/handoff flow and
// must never be stopped by stale-session cleanup.
terminate("plugin update", { expected: true });
}
},
Math.min(1000, requestTimeoutMs),
).unref();
for (const signal of ["SIGINT", "SIGTERM"]) {
process.on(signal, () => terminate("client shutdown", { expected: true }));
}
for (const output of [hostStdout, hostStderr]) {
output.on("error", () => terminate("stdio disconnect"));
}
child.once("error", failLaunch);
child.once("exit", () => {
scheduleRetiredLauncherExit();
finishAfterChildExit();
});
child.stdout.once("end", finishAfterChildExit);
function finishAfterChildExit() {
if (
launchFailed ||
(child.exitCode === null && child.signalCode === null) ||
!child.stdout.readableEnded
) {
return;
}
// ChildProcess close also waits for stderr. Finish once Python has exited
// and stdout has ended, so blocked diagnostics cannot prevent this drain.
clearTimeout(pendingStdioDisconnect);
clearTimeout(pendingTermination);
clearTimeout(pendingForcedTermination);
clearInterval(watchdog);
// Expected client/update shutdowns are not reconnect-worthy failures, while
// deadline, capacity, and unhealthy-session watchdog exits remain visible.
// Keep the EOF deadline until the host actually drains its final output.
hostStdout.write("", (error) => {
if (error) terminate("stdio disconnect");
clearTimeout(pendingClientDisconnect);
const exitCode = terminating ? terminationExitCode : child.exitCode ?? 1;
// Stdout has drained and Python is gone. Preserve ordinary stderr, but do
// not let an abandoned diagnostic pipe retain the launcher indefinitely.
const stderrDeadline = setTimeout(
() => forceLauncherExit(exitCode),
Math.min(terminationGraceMs, 1000),
);
stderrDeadline.unref();
const finishStderr = () => {
hostStderr.write("", () => {
clearTimeout(stderrDeadline);
clearTimeout(pendingLauncherExit);
process.exit(exitCode);
});
};
// The child's diagnostic pipe can still contain bytes after its process
// exits. Checkpoint only once the redactor has forwarded its final tail.
if (redactedStderr.readableEnded) finishStderr();
else redactedStderr.once("end", finishStderr);
});
}
SHA-256: e7c9408c0e2a91e72cac1664edef9b8a3a8e7231ab132e2d5a310ec77e77fe53