UNPKG

comfyui-mcp

Version:

Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),

622 lines • 34.2 kB
#!/usr/bin/env node import { randomBytes } from "node:crypto"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { collectToolCatalog, registerFullTools } from "./tools/index.js"; import { registerCompactTools } from "./tools/compact.js"; import { tryInstallRetiredNameRedirect } from "./tools/retired-redirect.js"; import { configurePersistentLogFile, logger } from "./utils/logger.js"; import { readPackageVersion } from "./utils/package-version.js"; import { JobWatcher } from "./services/job-watcher.js"; import { parseCliArgs, validateConnectUrl, exportExplicitToolMode } from "./transport/cli.js"; import { startHttpServer } from "./transport/http.js"; import { resolveToolSurfacePolicy } from "./tools/tool-surface-filter.js"; import { isLocalMode } from "./config.js"; import { ensurePanelInstalled } from "./services/panel-installer.js"; import { adoptOrphanedDownloadJobs } from "./services/download-jobs.js"; import { checkAndSelfUpdate } from "./services/self-update.js"; import { tr } from "./i18n/index.js"; import { resolveBridgePort } from "./services/bridge-ports.js"; import { advertisedPublicOrigin, configuredPublicOrigin, } from "./services/advertised-origin.js"; import { banner, labelRows, numberedSteps } from "./i18n/terminal-layout.js"; import { STDIO_HANDSHAKE_INSTRUCTIONS } from "./handshake-instructions.js"; import { orchestratorLogPath } from "./services/orchestrator-log.js"; import { probeSharp } from "./services/sharp-loader.js"; /** A RunPod proxy can only reach a listener exposed beyond loopback. */ function advertisedOriginForBind(host, port) { // An explicit public URL is the operator's reverse-proxy declaration; it is // valid precisely when the local listener is intentionally loopback-bound. const configured = configuredPublicOrigin(); if (configured) return configured; const normalized = host.trim().toLowerCase(); if (normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost") { return undefined; } return advertisedPublicOrigin(port); } /** * Fire-and-forget: ensure the ComfyUI sidebar panel is installed (install-if- * missing) on MCP load. LOCAL-only, hard-timed-out, and never throws — it must * never block or crash startup. Opt out with COMFYUI_MCP_PANEL_AUTOINSTALL=0. * The explicit `install_comfyui(action:'panel', panel_action:'update')` tool refreshes nightly on demand. */ /** * #2411 — SAY IT AT STARTUP, NOT ON THE FIRST RENDER OF A LONG SESSION. * * Before this, a `sharp` whose native library would not load crashed the process * during module evaluation; now it degrades, which means it can also go unnoticed * until someone asks for a preview an hour in. One warning at boot is what turns * that into a fact the user already has. * * Deliberately after the transport is up and deliberately non-blocking: this is * diagnostics, and a diagnostic that can delay or fail startup is a worse defect * than the one it reports. */ function probeSharpOnLoad() { void probeSharp() .then((warning) => { if (warning) logger.warn(warning); }) .catch(() => { // probeSharp() already swallows the load failure and returns it as text; // reaching here means the probe ITSELF broke, which is not worth a second // warning about a feature the user may never touch. }); } function ensurePanelOnLoad() { if (!isLocalMode()) return; void ensurePanelInstalled() .then((res) => { switch (res.action) { case "installed": logger.info("Panel auto-install: installed the sidebar panel (nightly). RESTART ComfyUI to load it.", res); break; // #806 — "already present" is the whole claim. The line used to be paired // with `action: "up-to-date"`, and users read the pair as "you are on the // newest panel"; nothing on this path compares versions. case "present": logger.info("Panel auto-install: panel already present — NOT a version check. " + "Run install_comfyui(action:'panel', panel_action:'status') to compare it against what this build needs.", res); break; case "skipped-dev": logger.info("Panel auto-install: skipped — dev install (symlink), managed manually.", res); break; case "skipped": logger.debug("Panel auto-install: disabled via COMFYUI_MCP_PANEL_AUTOINSTALL.", res); break; case "shadowed": logger.warn("Panel auto-install: a SHADOW copy in custom_nodes may be served instead of the real panel (#641). " + "Move the backup dir OUT of custom_nodes and hard-refresh the ComfyUI tab.", res); break; default: logger.debug("Panel auto-install: unavailable.", res); } }) .catch(() => { }); } /** * #1567 (item 3) — resume the downloads the PREVIOUS session's death orphaned. * * A tool-session respawn (or a crash) kills every transfer that session owned and * leaves their records reading "downloading" forever; recovery used to be cancel * × N + re-issue × N by hand. The new session has everything a resume needs — the * job records and the staged `.partial` files — so it adopts them itself. What gets * adopted and what is deliberately left alone is decided inside * adoptOrphanedDownloadJobs (proven-dead owners only, local route only, resumable * partial required, redacted URLs refused). * * Fire-and-forget, hard-guarded, and never throws — it must never block or crash * startup, mirrors the ensure-panel/self-update pattern. Silent when there is * nothing to adopt: an ordinary boot has no orphans, and a boot line about zero * downloads would be noise on every start. */ function adoptOrphanedDownloadsOnLoad() { void adoptOrphanedDownloadJobs() .then((adopted) => { for (const a of adopted) { logger.info(`Adopted an orphaned download from a dead session (#1567): ` + `${a.filename ?? a.id} — resuming from ${(a.resumedBytes / 1024 / 1024).toFixed(1)} MB ` + `already staged on disk (id ${a.id}, tray ${a.trayId}).` + (a.staleRecordLeft ? ` The dead session's record file could not be deleted (a transient ` + `filesystem error), so action:"status" may keep showing its stale row ` + `for a while — it expires on its own.` : "")); } }) .catch((err) => { logger.warn(`Orphaned-download adoption failed (#1567): ${err instanceof Error ? err.message : String(err)}. ` + `Any downloads a previous session left behind stay on the manual path: ` + `download_model action:"status", then cancel + re-issue each one.`); }); } /** * Fire-and-forget: on MCP load, check the npm registry and (for global/local * installs) auto-update the package on disk, then surface a "reconnect to load * vX" note — the running process can't hot-swap its own code. NEVER updates a * dev (npm link) install, hard-timed-out, and never throws. Opt out with * COMFYUI_MCP_AUTOUPDATE=0. Mirrors the panel auto-install ensure pattern. */ function selfUpdateOnLoad() { void checkAndSelfUpdate() .then((res) => { switch (res.action) { case "updated": logger.info(`Self-update: updated comfyui-mcp ${res.from} → ${res.to} (${res.mode}). ${res.note ?? ""}`, res); break; case "notify": logger.info(`Self-update: ${res.note ?? `v${res.to} available.`}`, res); break; case "up-to-date": logger.debug("Self-update: already on the latest version.", res); break; case "skipped-dev": logger.info("Self-update: skipped — dev install (npm link / checkout).", res); break; case "skipped-disabled": logger.debug("Self-update: disabled via COMFYUI_MCP_AUTOUPDATE.", res); break; default: logger.debug("Self-update: unavailable.", res); } }) .catch(() => { }); } /** * #1447 — THE VERSION WE ADVERTISE MUST BE THE VERSION WE ARE. * * This was hardcoded `0.1.0`, so every `initialize` response told the client a version * this package has never shipped. The reporter noticed while filing a bug about something * else, and that is exactly the cost: `serverInfo.version` is what a client shows and what * a bug report quotes, so a constant here makes every report ambiguous about which build * produced it — including the reports we ask people to send us. * * Resolution lives in `readPackageVersion` (utils/package-version.ts) — cheap enough to sit * on the handshake path, and testable by execution rather than by reading this file's text. * * Read ONCE at module load rather than per call: a running process cannot hot-swap its own * code, so a later read could only differ if the files changed underneath it — which would * report a version this process is not executing. */ const SERVER_VERSION = readPackageVersion(); async function createConfiguredServer(toolMode = "compact") { const server = new McpServer({ name: "comfyui-mcp", version: SERVER_VERSION, }, { // We declare `resources` and `prompts` (with noop list handlers below) // so federating clients like LiteLLM's MCP gateway, which probe every // standard list endpoint on initialize fan-out, get a fast empty list // instead of a per-server timeout from "Method not found". We don't // expose resources or prompts today; advertising them is spec-correct // when paired with a list handler that returns the empty set. // Reported by @ductiletoaster in #29. capabilities: { tools: {}, resources: {}, prompts: {}, }, // #1747 — cross-cutting flows (call order, async handles, pre-flight, // trust posture). Tool schemas stay on the tools. Compact mode cannot // see the catalog until it asks, so this is the only place those flows // exist at connect. instructions: STDIO_HANDSHAKE_INSTRUCTIONS, }); if (toolMode === "compact") { // Compact tool mode (DEFAULT since #667): capture the whole tool surface // into a catalog and expose only the list_tools/describe_tool/call_tool // meta-tools, keeping the client's context cost near-zero until a tool is // actually needed. Built for small/local LLMs (Hermes Agent, Ollama — // issue #97), and the DEFAULT from #667 until 0.50.0, when it became the // OPT-IN: the reason it was default was that the direct surface cost // ~200KB (~50k tokens) per tools/list at 154 tools, and consolidation took // that to 37. Reach for `--compact` when the client's context is the // binding constraint — a small local model, or a harness that re-injects // tools/list on every read. const catalog = await collectToolCatalog(); registerCompactTools(server, catalog); logger.info(`Compact tool mode: ${catalog.tools.size} tools available via list_tools/describe_tool/call_tool`); } else { // Full direct surface (opt-in via --full) PLUS the compact facade // (list_tools/describe_tool/call_tool) as a stable reconnect escape hatch — // see registerFullTools and issue #616. One atomic registration pass, // completed before the transport connects below. Opt out of the facade // with COMFYUI_MCP_NO_FACADE=1. await registerFullTools(server); } // Serve the retirement ledger on the DIRECT tools/call path too, not only through // the call_tool facade — see src/tools/retired-redirect.ts. Installed here, after // BOTH branches, because both need it and for different reasons: // // - full mode is the one the 0.50.0 flip (#726) makes the default, where 121 // retired names would otherwise answer with a bare "Tool X not found"; // - compact mode advertises only the 3 meta-tools, so a client calling a retired // name DIRECTLY is exactly the stale-snapshot client the facade exists for (#616) // — it holds a cached binding for a name this surface no longer lists, and the // ledger is the only thing that can tell it where the capability went. // // Additive: a registered name, and any name the ledger does not know, dispatches // through the SDK unchanged. Deliberately BEFORE server.connect() (the callers // below connect the returned server), so no request can arrive mid-swap. // // The result is REPORTED rather than discarded. If it is false the server still // boots (see tryInstallRetiredNameRedirect for why bricking startup is the worse // failure), but "boots normally while silently answering 121 retired names with a // bare 404" is exactly the shape of failure this whole change exists to remove — so // it is stated on the same startup line an operator already reads to confirm the // surface came up, not left as one error line among registration noise. // // Residual risk, stated plainly rather than papered over: nothing here can make the // degraded state visible to the CALLER, because the wrapper is the only thing that // could have changed what the caller sees. What actually holds the guarantee is the // `~1.29.0` cap on `@modelcontextprotocol/sdk` in package.json — this is normally run // as `npx comfyui-mcp`, which resolves fresh, so an open range would have let a user // silently pick up internals nothing ever ran against. const redirects = await tryInstallRetiredNameRedirect(server); logger.info(`Tool mode: ${toolMode}. Retired-name redirects on direct tools/call: ` + (redirects ? "active" : "INACTIVE — a call to a name removed in an earlier release will 404 without naming its replacement")); server.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [], })); server.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [] })); server.server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [], })); return server; } /** * Open a cloudflared quick tunnel to the local HTTP MCP port and print a clear, * ready-to-paste Claude Desktop Custom Connector block (public https://…/mcp * URL + token + headless X-API-Key usage). Resilient: a missing cloudflared * binary (or any tunnel error) surfaces install guidance and leaves the local * server running instead of crashing. */ async function openTunnelAndAnnounce(host, port, token) { const { startQuickTunnel } = await import("./services/tunnel.js"); logger.info("[tunnel] starting cloudflared quick tunnel…"); let publicUrl; try { const tunnel = await startQuickTunnel(port, host); publicUrl = tunnel.url; } catch (err) { const message = err instanceof Error ? err.message : String(err); const advertised = advertisedOriginForBind(host, port); const fallback = advertised ? `${advertised}/mcp` : host === "0.0.0.0" || host === "::" ? `http://<this-machine-address>:${port}/mcp` : `http://${host}:${port}/mcp`; logger.error(`[tunnel] could not start cloudflared: ${message}\n` + ` Install it, then re-run with --tunnel:\n` + ` npm install -g cloudflared # or: brew install cloudflared / winget install cloudflare.cloudflared\n` + ` The local server is still running at ${fallback} (token auth active).`); return; } const mcpUrl = `${publicUrl}/mcp`; const snippet = JSON.stringify({ mcpServers: { comfyui: { url: mcpUrl, headers: { "X-API-Key": token }, }, }, }, null, 2); // Single multi-line block to stderr so it survives MCP stdio framing and is // easy to copy from the terminal. process.stderr.write(banner(`ComfyUI MCP — ${tr("cli.tunnel_title", "Remote / Hosted Connector is LIVE")}`, [ ...labelRows(" ", [ [tr("cli.tunnel_label_public_url", "Public MCP URL"), mcpUrl], [tr("cli.tunnel_label_token", "Auth token"), token], ]), "", // Names CLAUDE DESKTOP's own menu path, which is localised by Claude Desktop's // language setting and not by ours. The English fallback must keep matching that app's // English UI exactly; a catalog should only translate it to the wording Claude Desktop // itself uses in that language, never to a literal translation of these words. ` ${tr("cli.tunnel_desktop_connector_path", "Claude Desktop → Settings → Connectors → Add custom connector:")}`, // The header NAMES are wire protocol, not prose — only the row labels translate. ...labelRows(" • ", [ [tr("cli.tunnel_field_name", "Name"), "ComfyUI"], [tr("cli.tunnel_field_url", "URL"), mcpUrl], [ tr("cli.tunnel_field_header", "Header"), // The alternative is one phrase, not the word "or" plus a header name: a lone // conjunction is untranslatable without the sentence it sits in, and both header // spellings have to survive into every language byte-for-byte anyway. `X-API-Key: ${token} ${tr("cli.tunnel_header_alt", "(or Authorization: Bearer {token})", { token })}`, ], ]), "", ` ${tr("cli.tunnel_headless_snippet", "Headless / programmatic config snippet:")}`, snippet, "", ` ${tr("cli.tunnel_keep_open", "Keep this terminal open — the tunnel closes when the process exits.")}`, ])); } /** A remote (non-loopback) ComfyUI served over https — the case where the pod's * browser panel needs the secure wss:// bridge instead of plain ws://. */ function isRemoteHttpsPod(u) { try { const url = new URL(u); const h = url.hostname.toLowerCase(); return (url.protocol === "https:" && !["127.0.0.1", "localhost", "::1", "0.0.0.0", ""].includes(h)); } catch { return false; } } async function main() { const cli = parseCliArgs(process.argv); // `--help` / `-h`: print usage and exit BEFORE anything else. It must precede // every other branch — including `setup` and `connect` — because a user who // asks for help has by definition not decided what to run yet, and starting a // server or writing a harness config in response to a help request is doing // something they did not ask for. Exits 0: asking for help is not an error. // // stdout, not stderr: this is the requested output, and a user piping it to a // pager or grep should get it on the stream that carries results. if (cli.help) { const { renderCliHelp } = await import("./transport/cli.js"); process.stdout.write(renderCliHelp()); return; } // The panel companion is an explicit, out-of-band install. Handle it before // every server/orchestrator branch so launcher management can never start the // MCP stdio server by accident. if (cli.launcherAction !== undefined) { const { installPanelLauncher, queryPanelLauncher, startPanelLauncherBroker, uninstallPanelLauncher, } = await import("./services/panel-launcher.js"); const usage = "comfyui-mcp launcher <install|status|uninstall>"; if (!cli.launcherAction) { process.stderr.write(`\n${tr("cli.launcher_usage", "Usage: {command}", { command: usage })}\n\n`); process.exitCode = 1; return; } try { if (cli.launcherAction === "install") { await installPanelLauncher(); process.stdout.write(`\n${tr("cli.launcher_installed", "✓ ComfyUI MCP panel launcher installed for this user.")}\n` + ` ${tr("cli.launcher_installed_note", "MCP itself still starts only when the Agent panel asks for it.")}\n\n`); } else if (cli.launcherAction === "uninstall") { uninstallPanelLauncher(); process.stdout.write(`\n${tr("cli.launcher_uninstalled", "✓ ComfyUI MCP panel launcher uninstalled.")}\n\n`); } else if (cli.launcherAction === "status") { process.stdout.write(`${JSON.stringify(await queryPanelLauncher(), null, 2)}\n`); } else { await startPanelLauncherBroker(); } } catch (err) { process.stderr.write(`\n${tr("cli.launcher_failed", "ComfyUI MCP launcher {action} failed: {error}", { action: cli.launcherAction, error: err instanceof Error ? err.message : String(err), })}\n\n`); process.exitCode = 1; } return; } // `setup <agent>`: write the comfyui MCP entry into a non-Claude harness's // config (Hermes Agent / OpenClaw / Copilot CLI — issue #97), print next // steps, and exit. Never starts the MCP server. if (cli.setupAgent !== undefined) { const { setupAgent, AGENT_NAMES } = await import("./services/agent-setup.js"); const agent = cli.setupAgent; if (!AGENT_NAMES.includes(agent)) { // Only the prose translates. The command itself is passed in as a variable rather than // spliced around a translated "Usage" label, so what the user has to type back stays // verbatim in every language — including the agent names, which are the literal values // the parser matches on. const usage = `comfyui-mcp setup <${AGENT_NAMES.join("|")}> [--compact|--full] [--comfyui-url <url>] [--dry-run]`; process.stderr.write(`\n${tr("cli.setup_usage", "Usage: {command}", { command: usage })}\n` + (cli.setupAgent ? `\n${tr("cli.setup_unknown_agent", 'Unknown agent "{agent}".', { agent: cli.setupAgent })}\n` : "") + `\n${tr("cli.setup_what_it_does", "Writes the comfyui MCP server entry into the agent's own config file.")}\n`); process.exit(1); } try { const result = await setupAgent({ agent, compact: cli.toolModeExplicit ? cli.toolMode === "compact" : undefined, comfyuiUrl: cli.comfyuiUrl, dryRun: cli.setupDryRun, }); const lines = [ "", result.wrote ? tr("cli.setup_wrote", '✓ Added the "comfyui" MCP server to {path}', { path: result.configPath, }) : tr("cli.setup_dry_run", "— dry run: would write {path} as —", { path: result.configPath, }), ...(result.wrote ? [] : ["", result.content.trimEnd()]), "", tr("cli.setup_next_steps", "Next steps:"), ...result.nextSteps.map((s) => ` • ${s}`), "", ]; process.stdout.write(lines.join("\n")); process.exit(0); } catch (err) { process.stderr.write(`\n${tr("cli.setup_failed", "comfyui-mcp setup failed: {error}", { error: err instanceof Error ? err.message : String(err), })}\n`); process.exit(1); } } // #873 — VALIDATE THE OPERATOR'S TOOL POLICY BEFORE ANY SERVER STARTS, of any kind. // // resolveToolSurfacePolicy() throws on a misconfiguration (an unknown preset, a variable // set but empty) with a message that says "Refusing to start". Making that true took two // corrections, both the same mistake: // // - On --transport http the server is built LAZILY inside the request handler, so the // socket bound, "running on http://…" printed, and every initialize came back 500 // with the reason buried in a log line. // - Moving the call above the http branch left --panel-orchestrator, which returns // BELOW me and never came back. A typo'd preset there started the bridge, the // loopback panel MCP and the console, printed the ready banner, and only threw later // at first use — inside a request handler, per session. // // Both times the fix was placed at the first spot that satisfied the case in front of me // rather than at the point every path passes through. This is that point: above every // branch that starts anything. resolveToolSurfacePolicy(); // Standalone background orchestrator: owns the UI bridge and drives the panel // with autonomous Agent SDK sessions. Not an MCP server — it never returns. if (cli.panelOrchestrator) { // The panel orchestrator is commonly launched from a disposable terminal (and // the panel broker intentionally detaches that terminal). Mirror its existing // stderr logger to a bounded file before URL validation or dynamic imports, so // every connect-mode lifecycle and exit path leaves evidence on disk. const logPath = orchestratorLogPath(); if (configurePersistentLogFile(logPath)) { logger.info(`[panel-orchestrator] persistent log enabled at ${logPath}`); } else { logger.warn(`[panel-orchestrator] could not open persistent log at ${logPath}`); } // `connect <comfyui-url>`: drive a (possibly REMOTE) ComfyUI from an agent on // THIS machine. Export the URL as COMFYUI_URL so the orchestrator and the // comfyui MCP it spawns target that server — the same remote-URL mechanism the // panel's "Remote ComfyUI URL" setting uses, just from the CLI. For a REMOTE // https pod the orchestrator auto-opens a secure wss:// Cloudflare tunnel so // the pod's HTTPS panel can reach the bridge (a plain ws:// from https is // browser-blocked); --insecure-bridge forces the plain loopback bridge. if (cli.insecureBridge) process.env.COMFYUI_MCP_INSECURE_BRIDGE = "1"; // #667: an explicit --full/--compact must reach the orchestrator's spawned // MCP children, which read the mode from the ENV — the flag alone never // made it downstream, silently running compact when full was requested. exportExplicitToolMode(cli); if (cli.comfyuiUrl) { // Hard-fail on a bad `connect <url>` instead of silently falling back to the // local ComfyUI (which would make the banner below lie about what it drives). const urlError = validateConnectUrl(cli.comfyuiUrl); if (urlError) { process.stderr.write(`\n${tr("cli.cannot_start", "ComfyUI MCP — cannot start: {error}", { error: urlError })}\n\n`); process.exit(1); } process.env.COMFYUI_URL = cli.comfyuiUrl; // One port shared by all providers; COMFYUI_MCP_BRIDGE_PORT is a pin. // An unpinned self-restart stays on 9180 so a live session is not stranded. const bridgePort = resolveBridgePort(); // Remote https pod → secure wss:// tunnel (auto); local/http or // --insecure-bridge → plain loopback ws://. Informational only here. const secureBridge = !cli.insecureBridge && isRemoteHttpsPod(cli.comfyuiUrl); const bridgeValue = secureBridge ? tr("cli.connect_bridge_secure", "wss:// secure Cloudflare tunnel (auto — URL is printed when ready)") : `ws://127.0.0.1:${bridgePort}`; // Every string below that NAMES A PANEL CONTROL keeps its English fallback exactly as // the panel renders it in English today, and says so in its key. The panel's own labels // are being translated separately; until a catalog carries both, an English fallback is // the only wording that still matches the button the user has to find. A catalog entry // for one of these keys is only correct if it uses the panel's translation of that // control, not a fresh translation of the sentence. // There used to be a step here telling the user to turn ON "Use external/local // orchestrator (advanced)" in the panel's Settings → General. That control does not // exist. The panel declares the setting id and never registers a row for it, and // `externalOrchestratorMode()` returns true unconditionally — its own comment calls the // setting "a back-compat no-op", because a Registry-compliant pure-frontend pack can no // longer spawn the orchestrator, so external/local is the only mode there is. The step // therefore sent people hunting for a toggle that isn't there, to enable something that // cannot be turned off. It had just been translated into eleven languages, which is how // it surfaced: faithfully wrong in twelve. const steps = [ tr("cli.connect_step_open_comfyui", "Open that ComfyUI in your browser: {url}", { url: cli.comfyuiUrl, }), tr("cli.connect_step_click_connect", "Click Connect in the panel (the Agent panel's Connect dropdown)."), ]; process.stderr.write(banner(`ComfyUI MCP — ${tr("cli.connect_title", "local agent bridge is starting")}`, [ ...labelRows(" ", [ [tr("cli.connect_label_bridge", "Agent bridge"), bridgeValue], [tr("cli.connect_label_driving", "Driving"), cli.comfyuiUrl], ...(secureBridge ? [ [ tr("cli.connect_label_secure", "Secure"), tr("cli.connect_secure_note", "the pod's HTTPS panel connects automatically over an\nencrypted tunnel — the URL is printed when ready."), ], ] : []), ]), "", ` ${tr("cli.connect_next_steps", "Next steps:")}`, ...numberedSteps(steps), "", ...tr("cli.connect_quiet_note", "Until you click Connect this window stays quiet — that's expected, not\n" + "a hang. The agent runs HERE on your Claude/Codex login; nothing is\n" + "installed on the ComfyUI box. Keep this terminal open.") .split("\n") .map((l) => ` ${l}`), ])); } const { runPanelOrchestrator } = await import("./orchestrator/index.js"); await runPanelOrchestrator(); return; } await JobWatcher.cleanupOldFiles(); if (cli.transport === "http") { // In tunnel mode, require a token even if none was configured: the endpoint // is about to be exposed publicly, so generate a strong one on the fly. const token = cli.token ?? (cli.tunnel ? randomBytes(24).toString("hex") : undefined); await startHttpServer({ host: cli.host, port: cli.port, token, allowUnauthenticated: cli.allowUnauthenticated, createServer: () => createConfiguredServer(cli.toolMode), }); const advertised = advertisedOriginForBind(cli.host, cli.port); const runningOrigin = advertised ? advertised : cli.host === "0.0.0.0" || cli.host === "::" ? `http://<bind-address>:${cli.port}` : `http://${cli.host}:${cli.port}`; logger.info(`ComfyUI MCP server running on ${runningOrigin}/mcp`); if (token) { logger.info(`HTTP MCP auth ENABLED — send 'Authorization: Bearer <token>' or 'X-API-Key: <token>'.`); } if (cli.tunnel) { await openTunnelAndAnnounce(cli.host, cli.port, token); } } else { const server = await createConfiguredServer(cli.toolMode); const transport = new StdioServerTransport(); await server.connect(transport); logger.info("ComfyUI MCP server running on stdio"); } // After the server is up: auto-ensure the sidebar panel (non-blocking). ensurePanelOnLoad(); // ...and self-check the npm registry for a newer published version (non-blocking). selfUpdateOnLoad(); // ...and resume any downloads the previous session's death orphaned (non-blocking). // After the server is up deliberately: adoption re-issues real transfers, so it // must not delay or endanger the transport coming up. adoptOrphanedDownloadsOnLoad(); // ...and say ONCE, at startup, if sharp's native library will not load (#2411). probeSharpOnLoad(); } main().catch((err) => { // Error instances JSON-serialize to {} — log the message/stack explicitly so // a startup failure (e.g. the unauthenticated-non-loopback guard in Docker) // is actually debuggable from the console output. logger.error(`Fatal error: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`); process.exit(1); }); //# sourceMappingURL=boot.js.map