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),
151 lines • 6.29 kB
JavaScript
import { getObjectInfo, backfillObjectInfo, } from "../comfyui/client.js";
import { isUiFormat, convertUiToApi, collectNodeTypes, } from "../services/workflow-converter.js";
import { validateWorkflow } from "../services/workflow-validator.js";
import { enqueueWorkflow } from "../services/workflow-executor.js";
import { applyOverrides } from "../services/asset-registry.js";
import { fetchWorkflowFromUrl } from "../services/workflow-url.js";
import { ValidationError } from "../utils/errors.js";
/**
* Detect a ComfyUI API-format prompt graph: a plain object whose values all look
* like nodes ({ class_type, inputs }). Some shares wrap the graph as
* `{ prompt: {...} }` (a raw /prompt request body) — `unwrapApiWorkflow` handles
* that before this check.
*/
function isApiFormat(obj) {
if (typeof obj !== "object" || obj === null || Array.isArray(obj))
return false;
const values = Object.values(obj);
if (values.length === 0)
return false;
return values.every((v) => v != null &&
typeof v === "object" &&
!Array.isArray(v) &&
"class_type" in v);
}
/** Unwrap `{ prompt: {...} }` /prompt-request bodies to the bare node graph. */
function unwrapApiWorkflow(json) {
if (json != null &&
typeof json === "object" &&
!Array.isArray(json) &&
"prompt" in json &&
isApiFormat(json.prompt)) {
return json.prompt;
}
return json;
}
/** Class-type fragments that identify the structurally important nodes. */
const KEY_NODE_PATTERNS = /(checkpoint|unet|diffusionmodel|sampler|saveimage|savevideo|vaedecode|cliptextencode|loraloader|controlnet)/i;
function summarizeWorkflow(workflow) {
const histogram = {};
const keyNodes = [];
for (const [id, node] of Object.entries(workflow)) {
const t = node.class_type ?? "?";
histogram[t] = (histogram[t] ?? 0) + 1;
if (KEY_NODE_PATTERNS.test(t))
keyNodes.push(`${id}:${t}`);
}
const types = Object.entries(histogram)
.sort((a, b) => b[1] - a[1])
.map(([t, c]) => `${c}× ${t}`)
.join(", ");
const lines = [
`Node count: ${Object.keys(workflow).length}`,
`Node types: ${types}`,
];
if (keyNodes.length > 0) {
lines.push(`Key nodes: ${keyNodes.join(", ")}`);
}
return lines.join("\n");
}
/**
* `enqueue_workflow (action:"run_url")` — the handler the standalone
* run-from-URL tool used to carry (0.50.0 slice 16), unchanged apart from losing
* its own registration.
*
* The body is byte-for-byte the old handler's: same fetch, same UI→API
* conversion, same validation, same two content blocks in read-only mode and
* the same enqueued JSON when `run` is true. The try/catch moved OUT to the
* dispatcher in workflow-execute.ts, which wraps every action in the identical
* `errorToToolResult`, so a thrown ValidationError still comes back as the same
* error result it always did.
*/
export async function runWorkflowUrlAction(args) {
const { url, run, inputs } = args;
const { json, finalUrl } = await fetchWorkflowFromUrl(url);
// Resolve to an API-format graph: convert UI exports, unwrap /prompt bodies.
let workflow;
const warnings = [];
if (isUiFormat(json)) {
const bulk = await getObjectInfo();
const objectInfo = await backfillObjectInfo(bulk, collectNodeTypes(json));
const converted = convertUiToApi(json, objectInfo);
workflow = converted.workflow;
warnings.push(...converted.warnings);
}
else {
const candidate = unwrapApiWorkflow(json);
if (!isApiFormat(candidate)) {
throw new ValidationError("The URL returned JSON that is not a recognized ComfyUI workflow " +
"(neither an API-format prompt graph of {class_type, inputs} nodes, " +
"nor a UI-format export with nodes[]/links[]).");
}
workflow = candidate;
}
if (Object.keys(workflow).length === 0) {
throw new ValidationError("The workflow contains no nodes.");
}
// Validate (best-effort — never throws; reports 'cannot reach ComfyUI' as an issue).
const validation = await validateWorkflow(workflow);
if (!run) {
const lines = [];
lines.push(`# Workflow loaded from ${finalUrl}`);
if (warnings.length > 0) {
lines.push("");
lines.push(`**Conversion warnings (${warnings.length}):**`);
lines.push(...warnings.map((w) => `- ${w}`));
}
lines.push("");
lines.push(summarizeWorkflow(workflow));
lines.push("");
lines.push(`Validation: ${validation.summary}`);
for (const issue of validation.issues) {
const loc = issue.node_id
? `${issue.node_id} (${issue.node_type})`
: "workflow";
lines.push(`- [${issue.severity}] ${loc}: ${issue.message}`);
}
lines.push("");
lines.push("Re-call with run=true to enqueue this workflow.");
return {
content: [
{ type: "text", text: lines.join("\n") },
{ type: "text", text: JSON.stringify(workflow, null, 2) },
],
};
}
// run=true → apply overrides and enqueue.
const next = applyOverrides(workflow, inputs);
const result = await enqueueWorkflow(next);
return {
content: [
{
type: "text",
text: JSON.stringify({
status: "enqueued",
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 }
: {}),
source_url: finalUrl,
overrides_applied: inputs ?? {},
conversion_warnings: warnings,
validation: validation.summary,
}, null, 2),
},
],
};
}
//# sourceMappingURL=workflow-url.js.map