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),

141 lines • 6.24 kB
import { promises as fs } from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { parseDocument, Document } from "yaml"; import { tr } from "../i18n/index.js"; export const AGENT_NAMES = ["hermes", "openclaw", "copilot"]; export function defaultConfigPath(agent) { switch (agent) { case "hermes": return join(homedir(), ".hermes", "config.yaml"); case "openclaw": return join(homedir(), ".openclaw", "openclaw.json"); case "copilot": return join(homedir(), ".copilot", "mcp-config.json"); } } export function defaultCompact(agent) { return agent !== "copilot"; } function serverArgs(compact) { // Explicit on BOTH sides, which is what made this survive the 0.50.0 flip // untouched. Bare `npx comfyui-mcp` meant compact from #667 and means FULL // from 0.50.0; because every generated entry pins its mode, no config written // before the flip changed meaning after it. `defaultCompact` below is a // PER-AGENT judgement (hermes/openclaw run small local models, copilot does // not) and is deliberately independent of whatever the server default is. return compact ? ["-y", "comfyui-mcp", "--compact"] : ["-y", "comfyui-mcp", "--full"]; } function buildEntry(agent, compact, comfyuiUrl) { const env = comfyuiUrl ? { COMFYUI_URL: comfyuiUrl } : undefined; switch (agent) { case "hermes": return { command: "npx", args: serverArgs(compact), ...(env ? { env } : {}) }; case "openclaw": return { command: "npx", args: serverArgs(compact), transport: "stdio", ...(env ? { env } : {}) }; case "copilot": return { type: "stdio", command: "npx", args: serverArgs(compact), env: env ?? {}, tools: ["*"], }; } } /** Merge into Hermes' YAML via the document API so user comments survive. */ function mergeHermesYaml(existing, entry) { const doc = existing.trim() ? parseDocument(existing) : new Document({}); if (doc.errors?.length) { throw new Error(`~/.hermes/config.yaml did not parse as YAML (${doc.errors[0].message}); fix it or pass --dry-run and merge manually.`); } doc.setIn(["mcp_servers", "comfyui"], doc.createNode(entry)); return doc.toString(); } function mergeJson(existing, entry, file) { let root = {}; if (existing.trim()) { let parsed; try { parsed = JSON.parse(existing); } catch (err) { throw new Error(`${file} did not parse as JSON (${err instanceof Error ? err.message : err}); fix it or pass --dry-run and merge manually.`); } if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error(`${file} is not a JSON object; fix it or pass --dry-run and merge manually.`); } root = parsed; } const servers = root.mcpServers && typeof root.mcpServers === "object" && !Array.isArray(root.mcpServers) ? root.mcpServers : {}; servers.comfyui = entry; root.mcpServers = servers; return `${JSON.stringify(root, null, 2)}\n`; } /** * Post-install instructions, printed to the terminal by `comfyui-mcp setup <agent>`. * * Translated (unlike almost everything else this package emits) because a person reads these * and then does them by hand — see the scope note in src/i18n/index.ts. What stays verbatim * in every language is everything the user has to TYPE or will SEE spelled that way in * another program: the `/reload-mcp` and `/mcp show` slash commands, the `mcp_comfyui_*` * tool names, the `--full` / `--compact` flags, and the model names. */ function nextSteps(agent, compact) { const compactNote = compact ? tr("cli.setup_note_compact", "Compact tool mode is ON: the model gets list_tools/describe_tool/call_tool instead of ~200 schemas (rerun with --full for the complete surface).") : tr("cli.setup_note_full", "Full tool mode: ~200 tool schemas — right for frontier models (rerun with --compact for small/local models)."); // One key, three call sites: both local-model harnesses print the same sentence, and a // translator seeing it twice would be asked the same question twice. const localModelsNote = tr("cli.setup_note_local_models", "Local models: gemma4 (e4b or larger), qwen3, llama3.1+ have native tool calling; gemma3 does not."); switch (agent) { case "hermes": return [ tr("cli.setup_hermes_reload", "In a running Hermes session type /reload-mcp (or restart Hermes)."), tr("cli.setup_hermes_tool_names", "Tools appear as mcp_comfyui_list_tools / _describe_tool / _call_tool."), localModelsNote, compactNote, ]; case "openclaw": return [ tr("cli.setup_openclaw_restart", "Restart the OpenClaw gateway to pick up the new server."), localModelsNote, compactNote, ]; case "copilot": return [ tr("cli.setup_copilot_check", "Run `copilot` and check the server with /mcp show."), compactNote, ]; } } export async function setupAgent(opts) { const compact = opts.compact ?? defaultCompact(opts.agent); const configPath = opts.configPath ?? defaultConfigPath(opts.agent); const entry = buildEntry(opts.agent, compact, opts.comfyuiUrl); let existing = ""; try { existing = await fs.readFile(configPath, "utf8"); } catch { // no config yet — we create it } const content = opts.agent === "hermes" ? mergeHermesYaml(existing, entry) : mergeJson(existing, entry, configPath); if (!opts.dryRun) { await fs.mkdir(dirname(configPath), { recursive: true }); await fs.writeFile(configPath, content, "utf8"); } return { agent: opts.agent, configPath, compact, content, wrote: !opts.dryRun, nextSteps: nextSteps(opts.agent, compact), }; } //# sourceMappingURL=agent-setup.js.map