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
JavaScript
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