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),
101 lines • 4.95 kB
JavaScript
import { basename } from "node:path";
import { discoverWorkflows, buildToolSchema, applyParams, getWorkflowsDir, } from "../services/workflow-autoload.js";
import { enqueueWorkflow } from "../services/workflow-executor.js";
import { errorToToolResult } from "../utils/errors.js";
import { logger } from "../utils/logger.js";
import { DEAD_NAMES, TOOL_NAMES } from "./vocabulary.js";
/**
* Discover workflow JSON files in COMFYUI_WORKFLOWS_DIR (default
* `~/.comfyui-mcp/workflows`) and register each as its own MCP tool with a
* schema derived from its PARAM_* placeholders. Failures are logged and do
* not abort startup.
*/
export async function registerAutoloadedWorkflows(server) {
const dir = getWorkflowsDir();
let discovered;
try {
discovered = await discoverWorkflows(dir);
}
catch (err) {
logger.warn("Workflow autoload discovery failed", {
dir,
error: err instanceof Error ? err.message : err,
});
return;
}
if (discovered.length === 0) {
logger.info("No autoloaded workflows discovered", { dir });
return;
}
// A tool name here comes from a FILENAME (slugify, src/services/workflow-autoload.ts),
// so it is user-controlled and can collide. Before this guard, saving a workflow as
// `get_history.json` made the SDK throw "Tool get_history is already registered" and the
// MCP server FAILED TO START — a user bricking their own install by naming a file,
// with an error that names a tool rather than the file to rename.
//
// Skip and warn instead. `taken` additionally catches two saved files that
// slugify to the same name (`My Flow.json` and `my-flow.json` both become
// `my_flow`).
//
// RETIRED names are reserved too, not just live ones. Otherwise the consolidation
// creates a worse failure than the one it removes: once a name is retired, a
// pre-existing <name>.json stops being "skipped, name taken" and becomes a
// REGISTERED tool, so a model still holding the old name silently ENQUEUES the
// user's workflow instead of calling the replacement. A 404 is recoverable; a wrong
// action that looks like it worked is not.
const taken = new Set([...TOOL_NAMES, ...DEAD_NAMES.map((d) => d.name)]);
for (const wf of discovered) {
if (taken.has(wf.toolName)) {
logger.warn("Skipping autoloaded workflow: tool name already taken", {
file: basename(wf.filePath),
toolName: wf.toolName,
hint: `Rename the file so it does not slugify to "${wf.toolName}".`,
});
continue;
}
taken.add(wf.toolName);
const shape = buildToolSchema(wf.placeholders);
const paramSummary = wf.placeholders
.map((p) => `${p.name}:${p.type}`)
.filter((s, i, a) => a.indexOf(s) === i)
.join(", ");
const description = `Enqueue the autoloaded ComfyUI workflow "${wf.toolName}" ` +
`(loaded from ${basename(wf.filePath)}) for execution and return immediately ` +
`with its prompt_id and queue position — it does NOT wait for the result; ` +
`poll queue (action:"status") or get_history afterwards. Requires a running ComfyUI server. ` +
`Each parameter below substitutes a PARAM_* placeholder in the saved workflow, and ` +
`all parameters are required. Params: ${paramSummary || "(none)"}`;
server.tool(wf.toolName, description, shape, async (args) => {
try {
const built = applyParams(wf.workflow, wf.placeholders, args);
const result = await enqueueWorkflow(built);
return {
content: [
{
type: "text",
text: JSON.stringify({
status: "enqueued",
tool: wf.toolName,
prompt_id: result.prompt_id,
queue_remaining: result.queue_remaining,
// #1037 — a 200 from /prompt does not mean every output was accepted;
// ComfyUI queues the branches that validate and reports the rest.
...(result.rejectedOutputs
? { rejected_outputs: result.rejectedOutputs }
: {}),
}, null, 2),
},
],
};
}
catch (err) {
return errorToToolResult(err);
}
});
logger.info("Registered autoloaded workflow tool", {
tool: wf.toolName,
params: wf.placeholders.length,
});
}
}
//# sourceMappingURL=workflow-autoload.js.map