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),
157 lines • 7.45 kB
JavaScript
import { z } from "zod";
import { errorToToolResult } from "../utils/errors.js";
import { applyKitchenRecommendation, assessKitchen, assessKitchenGraph, extractLoaders, gatherKitchenStatus, kitchenProactiveHint, } from "../services/kitchen.js";
import { isApiFormat, isUiFormat } from "../services/workflow-converter.js";
const ACTIONS = ["status", "assess", "apply"];
function json(value) {
return {
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
};
}
function parseGraph(raw) {
if (raw == null)
return undefined;
if (typeof raw === "string") {
const trimmed = raw.trim();
if (!trimmed)
return undefined;
return JSON.parse(trimmed);
}
if (typeof raw === "object")
return raw;
return undefined;
}
function graphKind(graph) {
if (isApiFormat(graph))
return "api";
if (isUiFormat(graph))
return "ui";
if (graph && typeof graph === "object" && Array.isArray(graph.nodes)) {
return "nodes";
}
return "unknown";
}
export async function kitchenStatusAction() {
const status = await gatherKitchenStatus();
const hint = kitchenProactiveHint(status, assessKitchen(status, {}));
return hint ? { status, hint } : { status };
}
export async function kitchenAssessAction(args) {
const status = await gatherKitchenStatus();
const graph = parseGraph(args.workflow);
if (graph === undefined) {
return {
status,
loaders: [],
recommendations: [],
graph_kind: "none",
note: 'assess needs a workflow JSON (`workflow`). For the open canvas use panel_kitchen with action:"assess".',
};
}
const recommendations = await assessKitchenGraph(status, graph);
const hint = kitchenProactiveHint(status, recommendations);
return {
status,
loaders: extractLoaders(graph),
recommendations,
graph_kind: graphKind(graph),
...(hint ? { hint } : {}),
};
}
export async function kitchenApplyAction(args) {
if (!args.recommendation_id) {
throw new Error('action:"apply" needs recommendation_id from a prior assess.');
}
const status = await gatherKitchenStatus();
const graph = parseGraph(args.workflow);
const recs = graph !== undefined ? await assessKitchenGraph(status, graph) : [];
const rec = recs.find((r) => r.id === args.recommendation_id);
if (!rec) {
return {
applied: false,
error: `No recommendation "${args.recommendation_id}" in this assess. Call kitchen action:"assess" with the same workflow first.`,
recommendations: recs.map((r) => r.id),
};
}
if (rec.change.type === "widget" && rec.node_id && rec.change.widget && rec.change.value !== undefined && graph) {
const result = await applyKitchenRecommendation({
rec,
confirm: args.confirm,
applyWidget: async (nodeId, widget, value) => {
applyWidgetOnGraph(graph, nodeId, widget, value);
return { node_id: nodeId, widget, value };
},
});
return { ...result, workflow: graph };
}
return applyKitchenRecommendation({ rec, confirm: args.confirm });
}
function applyWidgetOnGraph(graph, nodeId, widget, value) {
if (isApiFormat(graph) && graph[nodeId]) {
graph[nodeId].inputs[widget] = value;
return;
}
const nodes = isUiFormat(graph)
? graph.nodes
: Array.isArray(graph?.nodes)
? (graph.nodes)
: undefined;
if (!nodes)
return;
const node = nodes.find((n) => String(n.id) === nodeId);
if (!node)
return;
const captured = node.capturedWidgetValues;
if (captured && typeof captured === "object" && !Array.isArray(captured)) {
captured[widget] = value;
}
const inputs = node.inputs;
if (inputs && typeof inputs === "object" && !Array.isArray(inputs)) {
inputs[widget] = value;
}
}
export function registerKitchenTools(server) {
server.tool("kitchen", "See what comfy-kitchen can do on this GPU, find where a graph is leaving it on the table, and apply the faster path. Driven by `action`:\n" +
'- action:"status" — kitchen version, backends (hip/cuda/triton/eager), INT8 attention, GPU fp8/NVFP4/MXFP8, launch flags (--use-ck-attention, --enable-triton-backend, --fast fp8_matrix_mult). Local gets log + /system_stats + an import probe when COMFYUI_PATH is set; remote gets log + /system_stats only and reports model.quant / the probe as unknown. A failed probe is unknown, never a no.\n' +
'- action:"assess" — walk the workflow JSON\'s UNETLoaders and emit a recommendation only when every fact it needs is known: (1) weight_dtype default on a bf16 UNETLoader + GPU fp8 + kitchen present → fp8_e4m3fn_fast (widget, no restart); (2) no --use-sage-attention, sageattention not installed, kitchen INT8 available → --use-ck-attention (restart, confirm); (3) Blackwell + local NVFP4 sibling → model swap; (4) ROCm + triton ≥ 3.7 + kitchen, triton backend off → --enable-triton-backend. Pass `workflow` (API or UI JSON). For the open canvas use panel_kitchen.\n' +
'- action:"apply" — apply one recommendation_id from assess. Widget edits are reversible and do not need confirm. Flags and downloads need `confirm: true`. Flag apply names the launch flag; restart_comfyui replays the previous argv and does not inject a new one. Proof (before/after s/it, peak VRAM, output not black) is the panel_kitchen apply path.', {
action: z
.enum(ACTIONS)
.describe('Which kitchen operation. "status" takes no other parameters; "assess" takes `workflow`; "apply" takes `recommendation_id` and `confirm` for restarts/downloads.'),
workflow: z
.union([z.string(), z.record(z.string(), z.unknown())])
.optional()
.describe('action:"assess" / "apply" — workflow JSON (API-format {id:{class_type,inputs}} or UI-format {nodes,links}), as a string or object.'),
recommendation_id: z
.string()
.optional()
.describe('action:"apply" — id from assess (e.g. "fp8_unet_fast:12" or "ck_attention").'),
confirm: z
.boolean()
.optional()
.describe('action:"apply" — required true for anything that restarts or downloads. Widget edits do not need it.'),
}, async (args) => {
try {
switch (args.action) {
case "status":
return json(await kitchenStatusAction());
case "assess":
return json(await kitchenAssessAction({ workflow: args.workflow }));
case "apply":
return json(await kitchenApplyAction({
recommendation_id: args.recommendation_id,
workflow: args.workflow,
confirm: args.confirm,
}));
default: {
const exhaustive = args.action;
throw new Error(`Unknown kitchen action "${String(exhaustive)}". Expected one of: ${ACTIONS.join(", ")}.`);
}
}
}
catch (err) {
return errorToToolResult(err);
}
});
}
//# sourceMappingURL=kitchen.js.map