relay-companion
Version:
Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.
220 lines (208 loc) • 7.72 kB
JavaScript
import net from "node:net";
import path from "node:path";
import process from "node:process";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import {
MCP_BROKER_HELLO_MAX_BYTES,
MCP_BROKER_HELLO_TIMEOUT_MS,
MCP_BROKER_MAX_OLD_SPACE_MB,
MCP_BROKER_PROTOCOL,
MCP_BROKER_STARTUP_TIMEOUT_MS,
isMainModule,
packageRootForModule,
readMcpBrokerProvisioning,
} from "./mcp-broker-state.js";
const SESSION_ENV_KEYS = Object.freeze([
"CODEX_THREAD_ID",
"CLAUDE_CODE_SESSION_ID",
"CLAUDE_SESSION_ID",
"RELAY_CALLING_NATIVE_SESSION_ID",
]);
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function bridgeSessionContext({ argv = process.argv, env = process.env, cwd = process.cwd() } = {}) {
const args = (Array.isArray(argv) ? argv : []).map(String);
let channelEnabled = String(env.RELAY_CHANNEL_PUMP || "") === "1";
let channelSource = channelEnabled ? "relay-channel-pump-env" : "none";
for (let i = 0; !channelEnabled && i < args.length; i += 1) {
if (args[i] !== "--channels" && args[i] !== "--dangerously-load-development-channels") continue;
const value = args[i + 1] || "";
if (/(^|,)server:relay(,|$)/.test(value) || /(^|,)plugin:relay(@|,|$)/.test(value)) {
channelEnabled = true;
channelSource = "relay-channel-argv";
}
}
const sessionEnv = {};
for (const key of SESSION_ENV_KEYS) {
if (env[key]) sessionEnv[key] = String(env[key]);
}
return { cwd: path.resolve(cwd), channelEnabled, channelSource, env: sessionEnv };
}
function connect(endpoint) {
return new Promise((resolve, reject) => {
const socket = net.createConnection(endpoint);
const onError = (error) => {
socket.removeListener("connect", onConnect);
reject(error);
};
const onConnect = () => {
socket.removeListener("error", onError);
resolve(socket);
};
socket.once("error", onError);
socket.once("connect", onConnect);
});
}
function readBoundedLine(socket, { maxBytes = MCP_BROKER_HELLO_MAX_BYTES, timeoutMs = MCP_BROKER_HELLO_TIMEOUT_MS } = {}) {
return new Promise((resolve, reject) => {
let buffered = Buffer.alloc(0);
const timer = setTimeout(() => finish(new Error("Relay MCP broker handshake timed out")), timeoutMs);
timer.unref?.();
const cleanup = () => {
clearTimeout(timer);
socket.off("data", onData);
socket.off("error", onError);
socket.off("close", onClose);
};
const finish = (error, value) => {
cleanup();
if (error) reject(error);
else resolve(value);
};
const onError = (error) => finish(error);
const onClose = () => finish(new Error("Relay MCP broker closed during handshake"));
const onData = (chunk) => {
buffered = Buffer.concat([buffered, chunk]);
if (buffered.length > maxBytes) return finish(new Error("Relay MCP broker handshake exceeded its size limit"));
const newline = buffered.indexOf(0x0a);
if (newline === -1) return;
const line = buffered.subarray(0, newline).toString("utf8").replace(/\r$/, "");
const remainder = buffered.subarray(newline + 1);
socket.pause();
if (remainder.length) socket.unshift(remainder);
finish(null, line);
};
socket.on("data", onData);
socket.once("error", onError);
socket.once("close", onClose);
socket.resume();
});
}
function brokerEnvironment(env) {
const childEnv = { ...env };
for (const key of [...SESSION_ENV_KEYS, "RELAY_CHANNEL_PUMP"]) delete childEnv[key];
return childEnv;
}
function startBroker({ packageRoot, provisioning, env = process.env }) {
const entry = path.join(packageRoot, "src", "mcp-broker-entry.js");
const child = spawn(process.execPath, [
`--max-old-space-size=${MCP_BROKER_MAX_OLD_SPACE_MB}`,
entry,
`--config-scope=${provisioning.identity.configScopeId}`,
`--domain=${provisioning.identity.domainId}`,
], {
detached: true,
stdio: "ignore",
windowsHide: true,
env: brokerEnvironment(env),
});
child.once("error", () => {});
child.unref();
}
async function authenticatedConnection({ provisioning, sessionContext, deadline, packageRoot, env }) {
const delays = [25, 50, 100, 200, 400, 500];
let attempt = 0;
let started = false;
let lastError = null;
while (Date.now() < deadline) {
let socket;
try {
socket = await connect(provisioning.endpoint);
const hello = {
type: "relay-mcp-broker/hello",
protocol: MCP_BROKER_PROTOCOL,
capability: provisioning.capability.toString("base64url"),
domainId: provisioning.identity.domainId,
bridgePid: process.pid,
...sessionContext,
};
socket.write(`${JSON.stringify(hello)}\n`);
const remaining = Math.max(1, deadline - Date.now());
const line = await readBoundedLine(socket, { timeoutMs: remaining });
const response = JSON.parse(line);
if (response?.type !== "relay-mcp-broker/ready" || response?.protocol !== MCP_BROKER_PROTOCOL) {
throw new Error(String(response?.message || "Relay MCP broker rejected the bridge"));
}
socket.resume();
return socket;
} catch (error) {
lastError = error;
socket?.destroy();
const retryable = ["ENOENT", "ECONNREFUSED", "ECONNRESET", "EPIPE"].includes(String(error?.code || ""))
|| /closed during handshake|timed out/i.test(String(error?.message || ""));
if (!started && retryable) {
startBroker({ packageRoot, provisioning, env });
started = true;
} else if (!retryable) {
throw error;
}
const delay = delays[Math.min(attempt, delays.length - 1)];
attempt += 1;
if (Date.now() + delay >= deadline) break;
await sleep(delay);
}
}
throw new Error(`Relay MCP broker did not become ready before the startup deadline${lastError?.message ? `: ${lastError.message}` : ""}`);
}
export async function runMcpBridge({
argv = process.argv,
env = process.env,
cwd = process.cwd(),
input = process.stdin,
output = process.stdout,
packageRoot = packageRootForModule(import.meta.url),
deadline = Number(env.RELAY_MCP_START_DEADLINE_MS) || Date.now() + MCP_BROKER_STARTUP_TIMEOUT_MS,
} = {}) {
const provisioning = readMcpBrokerProvisioning({ env, packageRoot });
const sessionContext = bridgeSessionContext({ argv, env, cwd });
const socket = await authenticatedConnection({ provisioning, sessionContext, deadline, packageRoot, env });
return await new Promise((resolve, reject) => {
let settled = false;
let inputEnded = false;
const finish = (error, code = 0) => {
if (settled) return;
settled = true;
for (const signal of ["SIGINT", "SIGTERM"]) process.off(signal, onSignal);
input.unpipe(socket);
socket.unpipe(output);
if (error) reject(error);
else resolve(code);
};
const onSignal = () => socket.destroy();
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, onSignal);
input.once("end", () => {
inputEnded = true;
socket.end();
});
input.once("error", (error) => {
socket.destroy();
finish(error);
});
socket.once("error", (error) => finish(error));
socket.once("close", () => {
if (inputEnded) finish(null, 0);
else finish(new Error("Relay MCP broker connection closed; reload Relay MCP in this host"));
});
socket.pipe(output, { end: false });
input.pipe(socket);
input.resume();
});
}
if (isMainModule(import.meta.url)) {
runMcpBridge().catch((error) => {
process.stderr.write(`relay: ${error.message}\n`);
process.exit(1);
});
}