UNPKG

@essentialai/cogent-bridge

Version:

Cogent Bridge — cross-agent comms for Claude Code, OpenAI Codex and Slack. Codex users: install with `curl -fsSL https://cogent.tools/install.sh | sh`, then start with `cogent-codex` for real-time peer wake.

177 lines • 9.64 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { SERVER_NAME, SERVER_VERSION } from "./constants.js"; import { startUpdateCheckLoop } from "./backend/npm-update-check.js"; import { runStartup, runPreflights, resolvePendingLabel, isCloudMode, setInboxNotifier, cloudWsClient, cloudInbox, autoRelay, restorePeerPresence } from "./startup.js"; import { logger } from "./logger.js"; import { registerRegisterPeerTool } from "./tools/register-peer.js"; import { registerSendMessageTool } from "./tools/send-message.js"; import { registerListPeersTool } from "./tools/list-peers.js"; import { registerGetHistoryTool } from "./tools/get-history.js"; import { registerDeregisterPeerTool } from "./tools/deregister-peer.js"; import { registerHealthCheckTool } from "./tools/health-check.js"; import { registerWakeLogTool } from "./tools/wake-log.js"; import { registerCreateSessionTool } from "./tools/create-session.js"; import { registerJoinSessionTool } from "./tools/join-session.js"; import { registerSetupMailTool } from "./tools/setup-mail.js"; import { registerSendMailTool } from "./tools/send-mail.js"; import { registerFetchMailTool } from "./tools/fetch-mail.js"; import { registerRotateMailCredsTool } from "./tools/rotate-mail-creds.js"; import { heartbeat } from "./services/heartbeat.js"; // --------------------------------------------------------------------------- // Global error handlers -- registered FIRST before anything else // --------------------------------------------------------------------------- process.on("uncaughtException", (err) => { // EPIPE means the parent process disconnected -- exit cleanly if (err.code === "EPIPE") { logger.close(); process.exit(0); return; } logger.error(`Uncaught exception: ${err.message}`, { stack: err.stack }); // Do NOT exit -- MCP server should try to continue }); process.on("unhandledRejection", (reason) => { const msg = reason instanceof Error ? reason.message : String(reason); logger.error(`Unhandled rejection: ${msg}`); }); // --------------------------------------------------------------------------- // Graceful shutdown handlers // --------------------------------------------------------------------------- process.on("SIGTERM", () => { logger.info("SIGTERM received, shutting down"); autoRelay.stop(); heartbeat.stop(); if (cloudWsClient) { cloudWsClient.disconnect(); } logger.close(); process.exit(0); }); process.on("SIGINT", () => { logger.info("SIGINT received, shutting down"); autoRelay.stop(); heartbeat.stop(); if (cloudWsClient) { cloudWsClient.disconnect(); } logger.close(); process.exit(0); }); // --------------------------------------------------------------------------- // Main entry point // --------------------------------------------------------------------------- async function main() { // Startup MUST complete before MCP transport starts // (handles first-run prompt, config loading, logger init, validation) await runStartup(); const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }); // Register all 8 tools (6 existing + create-session + join-session) registerRegisterPeerTool(server); registerDeregisterPeerTool(server); registerSendMessageTool(server); registerListPeersTool(server); registerGetHistoryTool(server); registerHealthCheckTool(server); registerWakeLogTool(server); registerCreateSessionTool(server); registerJoinSessionTool(server); registerSetupMailTool(server); registerSendMailTool(server); registerFetchMailTool(server); registerRotateMailCredsTool(server); // Register the MCP inbox resource for cloud mode. // // 🔴 This used to be gated on `cloudInbox` EXISTING AT STARTUP, which quietly // excluded every deferred agent — i.e. anyone who joins with // cogent_join_session rather than pre-supplying credentials. Their inbox is // created later by reinitCloudBackend(), long after this registration ran, so // `cogent://inbox` was never advertised and the sendResourceUpdated() below // fired for a resource that did not exist. Gate on cloud MODE and read the // inbox LAZILY inside the handler, so deferred and pre-credentialed agents get // the same surface. if (isCloudMode()) { server.registerResource("cogent_inbox", "cogent://inbox", { description: "Incoming messages received via cloud bridge WebSocket. Read this resource to see new messages from other peers.", mimeType: "application/json", }, // Read the inbox LAZILY: in deferred mode it does not exist yet and is // created later by reinitCloudBackend(). An empty inbox is the honest // answer for "connected to cloud, no session joined yet". async (uri) => ({ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ messages: cloudInbox ? cloudInbox.getUnread() : [], lastMessageId: cloudInbox ? cloudInbox.getLastMessageId() : null, }), }], })); // When WS messages arrive, notify the MCP host that new messages are available. // Registered through startup so it also binds to an inbox created LATER // (deferred join) — attaching directly here would only ever see the // startup-time inbox, i.e. never in deferred mode. setInboxNotifier(() => { try { server.server.sendResourceUpdated({ uri: "cogent://inbox" }); } catch (err) { // sendResourceUpdated may fail if transport is not connected yet logger.warn(`Failed to send resource update notification: ${err}`); } }); } // Connect stdio transport const transport = new StdioServerTransport(); await server.connect(transport); // Startup banner: always prints regardless of log level process.stderr.write(`${SERVER_NAME} v${SERVER_VERSION} running on stdio\n`); // A COGENT_SESSION_ID *label* needs a relay round-trip to become a session UUID. // It runs HERE, after connect, for exactly the same reason preflight does: // awaiting it beforehand added a reproducible +2437 ms to `initialize` against an // unreachable relay (measured by scripts/startup-nonblocking-test.mjs). Bounding // that fetch with a 2500 ms AbortSignal — what the code used to do — caps the // stall but does not remove it. Fire-and-forget; on success it upgrades the // deferred backend via reinitCloudBackend(). void resolvePendingLabel().catch((err) => { logger.warn(`Label resolve error (non-fatal): ${err instanceof Error ? err.message : String(err)}`); }); // Platform CLI + sandbox preflight runs AFTER the transport connects: it SPAWNS // the vendor CLI (`codex/claude --help`, capped at 10s) and must never precede // the `initialize` handshake — a slow spawn there closed the MCP connection on // cold hosts. Fire-and-forget: warn-only diagnostics + the sandbox-fallback // override, both consulted only later at wake time. Never throws, but guard anyway. void runPreflights().catch((err) => { logger.warn(`Preflight error (non-fatal): ${err instanceof Error ? err.message : String(err)}`); }); // PRESENCE. Work out whether this agent is actually IN its channel, and if it owns a // peer there but has not registered it, put it back. An agent whose bridge is running // while its peer is absent is invisible to everyone messaging it, and every repair in // this client used to sit behind a successful register — so when registration was the // thing that did not happen, nothing recovered and nothing cleaned up. Measured: 18 // live bridges on one checkout, peer absent from a healthy channel for five days. // States and the full explanation: services/peer-presence.ts. // Fire-and-forget for the same reason as the two above: it does network I/O and must // never precede `initialize`. Never throws; guarded anyway. void restorePeerPresence().catch((err) => { logger.warn(`Presence restore error (non-fatal): ${err instanceof Error ? err.message : String(err)}`); }); // Best-effort proactive update nudge. Fire-and-forget: never blocks the server // becoming ready, never throws, and writes only to STDERR (stdout is the MCP // protocol). Stays silent unless a newer cogent-bridge is published on npm. // Re-checks on an interval, not just at boot: this process runs for days, and a one-shot check // could never see a version published after startup (3.21.4→3.21.8 all shipped inside one day). // The timer is unref'd, so it never keeps the process alive at shutdown. startUpdateCheckLoop(SERVER_VERSION); logger.info("Server ready. Tools: cogent_register_peer, cogent_deregister_peer, cogent_send_message, cogent_list_peers, cogent_get_history, cogent_health_check, cogent_create_session, cogent_join_session"); } main().catch((err) => { const msg = err instanceof Error ? err.message : String(err); logger.error(`Fatal startup error: ${msg}`); // Fallback to stderr in case logger failed process.stderr.write(`Fatal error: ${msg}\n`); logger.close(); process.exit(1); }); //# sourceMappingURL=index.js.map