Aller au contenu principal
MCP

Piloter un workflow ComfyUI JSON depuis Claude Code

Tutoriel pour exposer un workflow ComfyUI exporté en JSON via un outil MCP custom et le faire éditer dynamiquement par Claude Code (sampler, ControlNet).

  • Référence
  • Outils
Publié le Mis à jour le

Anatomie d'un workflow ComfyUI JSON

Un workflow ComfyUI est un graphe orienté stocké en JSON. Chaque entrée du dictionnaire racine est un noeud identifié par un id numérique (sous forme de chaîne : "1", "2", etc.). Un noeud a deux champs obligatoires : class_type (le type de noeud, qui détermine son comportement) et inputs (les paramètres du noeud).

Les connexions entre noeuds sont exprimées à l'intérieur des inputs : quand un paramètre attend la sortie d'un autre noeud, sa valeur est un tableau [node_id, output_index] au lieu d'une valeur scalaire. output_index correspond à la position de la sortie dans l'ordre de déclaration du noeud.

Exemple : CheckpointLoaderSimple déclare trois sorties dans l'ordre MODEL (indice 0), CLIP (indice 1), VAE (indice 2). Pour brancher le CLIP vers un CLIPTextEncode, on écrit "clip": ["2", 1] si le loader est le noeud "2".

Voici un workflow minimal représentant la chaîne classique de génération texte vers image :

{
"1": {
"class_type": "CheckpointLoaderSimple",
"inputs": {
"ckpt_name": "flux1-schnell-fp8.safetensors"
}
},
"2": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "a robot astronaut on the moon",
"clip": ["1", 1]
}
},
"3": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "",
"clip": ["1", 1]
}
},
"4": {
"class_type": "EmptyLatentImage",
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 1
}
},
"5": {
"class_type": "KSampler",
"inputs": {
"seed": 42,
"steps": 4,
"cfg": 1.0,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 1.0,
"model": ["1", 0],
"positive": ["2", 0],
"negative": ["3", 0],
"latent_image": ["4", 0]
}
},
"6": {
"class_type": "VAEDecode",
"inputs": {
"samples": ["5", 0],
"vae": ["1", 2]
}
},
"7": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "claude_run",
"images": ["6", 0]
}
}
}

Les quatre types de noeuds présents ici sont suffisants pour comprendre 90 % des workflows :

  • Loaders (CheckpointLoaderSimple, VAELoader, UNETLoader) : chargent les fichiers modèle depuis le disque
  • Samplers (KSampler, KSamplerAdvanced) : pilotent le processus de débruitage itératif
  • Encodeurs de texte (CLIPTextEncode) : transforment le prompt en embedding latent
  • Noeuds de sortie (VAEDecode, SaveImage) : convertissent le résultat en pixels et l'écrivent sur disque

Exposer le workflow via un outil MCP custom

L'architecture est directe : Claude Code appelle un tool MCP, le serveur MCP charge le fichier JSON du workflow, applique les paramètres dynamiques reçus, puis POST sur http://127.0.0.1:8188/prompt. ComfyUI exécute le workflow et écrit l'image dans son dossier output/.

Le wrapper ci-dessous utilise le même pattern McpServer + server.tool() que les autres exemples du site, présenté dans Créer un MCP en TypeScript.

1

Créer le projet et installer les dépendances

mkdir comfyui-workflow-mcp && cd comfyui-workflow-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node
2

Préparer le fichier de workflow JSON

Exporte ton workflow depuis l'interface ComfyUI via le bouton "Save (API Format)" (pas le bouton "Save" classique : celui-ci produit un format étendu non compatible avec l'API). Place le fichier dans workflow.json à la racine du projet.

Tu peux partir du workflow minimal de la section précédente.

3

Écrire le wrapper MCP

Crée src/index.ts :

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { readFileSync } from "fs";
import { join } from "path";
const COMFYUI_URL = "http://127.0.0.1:8188";
const server = new McpServer({
name: "comfyui-workflow-pilot",
version: "0.1.0",
});
server.tool(
"run_workflow",
"Exécute un workflow ComfyUI JSON avec des paramètres dynamiques",
{
positive_prompt: z.string().describe("Prompt positif (description de l'image)"),
negative_prompt: z.string().default("").describe("Prompt négatif (éléments à éviter)"),
seed: z.number().int().default(-1).describe("Seed (-1 = aléatoire)"),
sampler_name: z
.enum(["euler", "dpmpp_2m", "dpmpp_3m_sde", "ddim", "uni_pc"])
.default("euler")
.describe("Algorithme de sampling"),
scheduler: z
.enum(["simple", "karras", "exponential", "sgm_uniform", "beta"])
.default("simple")
.describe("Stratégie de scheduling du bruit"),
steps: z.number().int().min(1).max(150).default(4).describe("Nombre d'étapes"),
},
async ({ positive_prompt, negative_prompt, seed, sampler_name, scheduler, steps }) => {
const workflowPath = join(process.cwd(), "workflow.json");
const workflow = JSON.parse(readFileSync(workflowPath, "utf-8")) as Record<
string,
{ class_type: string; inputs: Record<string, unknown> }
>;
const resolvedSeed = seed === -1 ? Math.floor(Math.random() * 1e9) : seed;
for (const node of Object.values(workflow)) {
if (node.class_type === "CLIPTextEncode") {
const isNegative =
typeof node.inputs["text"] === "string" && node.inputs["text"] === "";
node.inputs["text"] = isNegative ? negative_prompt : positive_prompt;
}
if (node.class_type === "KSampler") {
node.inputs["seed"] = resolvedSeed;
node.inputs["sampler_name"] = sampler_name;
node.inputs["scheduler"] = scheduler;
node.inputs["steps"] = steps;
}
}
const res = await fetch(`${COMFYUI_URL}/prompt`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: workflow }),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`ComfyUI /prompt a répondu ${res.status}: ${text}`);
}
const data = (await res.json()) as { prompt_id: string; number: number };
return {
content: [
{
type: "text" as const,
text: `Workflow en file. prompt_id: ${data.prompt_id}, position: ${data.number}`,
},
],
};
}
);
async function main(): Promise<void> {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((err: unknown) => {
console.error("Erreur MCP:", err);
process.exit(1);
});
4

Configurer Claude Code pour utiliser le MCP

Ajoute (ou crée) .mcp.json à la racine de ton projet Claude Code :

{
"mcpServers": {
"comfyui-workflow": {
"command": "npx",
"args": ["tsx", "/chemin/absolu/comfyui-workflow-mcp/src/index.ts"]
}
}
}

Redémarre Claude Code pour qu'il charge la nouvelle config.


Faire éditer le workflow par Claude

Une fois le MCP connecté, Claude Code peut modifier les paramètres du workflow à la demande. Il n'a pas besoin de voir le JSON brut : il reçoit une description du tool et de ses paramètres via le protocole MCP.

Exemple de prompt système

Tu as accès à l'outil run_workflow qui lance des générations d'images via ComfyUI.
Les paramètres disponibles sont : positive_prompt, negative_prompt, seed,
sampler_name (euler | dpmpp_2m | dpmpp_3m_sde | ddim | uni_pc),
scheduler (simple | karras | exponential | sgm_uniform | beta), steps.
Pour les générations rapides (tests, aperçu), utilise sampler_name=euler avec
scheduler=simple et steps=4.
Pour les générations de qualité, utilise sampler_name=dpmpp_2m avec
scheduler=karras et steps=20.

Exemple d'interaction

Message utilisateur :

Génère une illustration d'une ville futuriste sous la pluie, style cyberpunk,
teintes bleues et orange. Utilise un sampler de qualité supérieure.

Appel de tool que Claude va effectuer :

{
"tool": "run_workflow",
"arguments": {
"positive_prompt": "futuristic city in the rain, cyberpunk style, blue and orange tones, neon reflections on wet streets, highly detailed",
"negative_prompt": "blurry, low quality, watermark, text",
"seed": -1,
"sampler_name": "dpmpp_2m",
"scheduler": "karras",
"steps": 20
}
}

Réponse retournée au modèle :

Workflow en file. prompt_id: a3f7c2d1-..., position: 1

Cas d'usage

Changer le sampler pour ajuster qualité vs vitesse

Le KSampler est le noeud le plus influent sur le rendu final. Passer de euler/simple/4 steps à dpmpp_2m/karras/20 steps change significativement la cohérence et la précision des détails, au prix d'un temps de génération 4 à 5 fois plus long. C'est utile en deux temps : générer une dizaine d'aperçus rapides pour valider la direction, puis lancer une génération longue sur le meilleur candidat. Le wrapper MCP le rend trivial depuis Claude Code : un seul message pour chaque phase.

Ajouter un ControlNet à un workflow existant

Un ControlNet s'insère dans le graphe entre le loader de modèle et le KSampler. Tu charges une image de référence (contours Canny, pose OpenPose ou carte de profondeur), tu la passes à un node ControlNetApply, et tu branches la sortie conditioning sur l'entrée positive du KSampler à la place du CLIPTextEncode direct.

Pour piloter ça via MCP, ajoute au tool un paramètre controlnet_image_path et controlnet_strength. Le wrapper modifie dynamiquement le workflow en injectant les noeuds ControlNetLoader et ControlNetApply si le paramètre est fourni, sinon il passe directement le CLIPTextEncode. Claude peut ainsi activer ou désactiver le ControlNet sur une génération sans toucher au fichier JSON.

A/B test sur N prompts en boucle

Le cas le plus productif : tu veux comparer 10 formulations de prompt sur le même seed et les mêmes paramètres de sampling. Donne à Claude une liste de prompts et demande-lui de les lancer séquentiellement avec seed fixe. Il appelle run_workflow dix fois, chacune avec un positive_prompt différent et le même seed, ce qui garantit des variations contrôlées. Tu récupères les dix images dans ComfyUI/output/ et tu choisis la meilleure formulation avant de lancer une génération finale en haute qualité.


Prochaines étapes