@reaatech/media-pipeline-mcp-comfyui 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/index.cjs +662 -0
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +628 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
// src/comfyui-provider.ts
|
|
2
|
+
import {
|
|
3
|
+
InvalidInputError,
|
|
4
|
+
WorkflowExpiredError,
|
|
5
|
+
WorkflowNotFoundError
|
|
6
|
+
} from "@reaatech/media-pipeline-mcp-core";
|
|
7
|
+
import { MediaProvider } from "@reaatech/media-pipeline-mcp-provider-core";
|
|
8
|
+
|
|
9
|
+
// src/workflows/flux-text2img.json
|
|
10
|
+
var flux_text2img_default = {
|
|
11
|
+
"3": {
|
|
12
|
+
class_type: "KSampler",
|
|
13
|
+
inputs: {
|
|
14
|
+
seed: -1,
|
|
15
|
+
steps: 20,
|
|
16
|
+
cfg: 1,
|
|
17
|
+
sampler_name: "euler",
|
|
18
|
+
scheduler: "simple",
|
|
19
|
+
denoise: 1,
|
|
20
|
+
model: ["4", 0],
|
|
21
|
+
positive: ["6", 0],
|
|
22
|
+
negative: ["7", 0],
|
|
23
|
+
latent_image: ["5", 0]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"4": {
|
|
27
|
+
class_type: "CheckpointLoaderSimple",
|
|
28
|
+
inputs: {
|
|
29
|
+
ckpt_name: "flux1-dev.safetensors"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"5": {
|
|
33
|
+
class_type: "EmptyLatentImage",
|
|
34
|
+
inputs: {
|
|
35
|
+
width: 1024,
|
|
36
|
+
height: 1024,
|
|
37
|
+
batch_size: 1
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"6": {
|
|
41
|
+
class_type: "CLIPTextEncode",
|
|
42
|
+
inputs: {
|
|
43
|
+
text: "",
|
|
44
|
+
clip: ["4", 1]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"7": {
|
|
48
|
+
class_type: "CLIPTextEncode",
|
|
49
|
+
inputs: {
|
|
50
|
+
text: "",
|
|
51
|
+
clip: ["4", 1]
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"8": {
|
|
55
|
+
class_type: "VAEDecode",
|
|
56
|
+
inputs: {
|
|
57
|
+
samples: ["3", 0],
|
|
58
|
+
vae: ["4", 2]
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"9": {
|
|
62
|
+
class_type: "SaveImage",
|
|
63
|
+
inputs: {
|
|
64
|
+
filename_prefix: "ComfyUI",
|
|
65
|
+
images: ["8", 0]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/workflows/sdxl-img2img.json
|
|
71
|
+
var sdxl_img2img_default = {
|
|
72
|
+
"3": {
|
|
73
|
+
class_type: "KSampler",
|
|
74
|
+
inputs: {
|
|
75
|
+
seed: -1,
|
|
76
|
+
steps: 20,
|
|
77
|
+
cfg: 7,
|
|
78
|
+
sampler_name: "euler",
|
|
79
|
+
scheduler: "normal",
|
|
80
|
+
denoise: 0.75,
|
|
81
|
+
model: ["4", 0],
|
|
82
|
+
positive: ["6", 0],
|
|
83
|
+
negative: ["7", 0],
|
|
84
|
+
latent_image: ["11", 0]
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"4": {
|
|
88
|
+
class_type: "CheckpointLoaderSimple",
|
|
89
|
+
inputs: {
|
|
90
|
+
ckpt_name: "sd_xl_base_1.0.safetensors"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"6": {
|
|
94
|
+
class_type: "CLIPTextEncode",
|
|
95
|
+
inputs: {
|
|
96
|
+
text: "",
|
|
97
|
+
clip: ["4", 1]
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"7": {
|
|
101
|
+
class_type: "CLIPTextEncode",
|
|
102
|
+
inputs: {
|
|
103
|
+
text: "",
|
|
104
|
+
clip: ["4", 1]
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"8": {
|
|
108
|
+
class_type: "VAEDecode",
|
|
109
|
+
inputs: {
|
|
110
|
+
samples: ["3", 0],
|
|
111
|
+
vae: ["4", 2]
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"9": {
|
|
115
|
+
class_type: "SaveImage",
|
|
116
|
+
inputs: {
|
|
117
|
+
filename_prefix: "ComfyUI",
|
|
118
|
+
images: ["8", 0]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"10": {
|
|
122
|
+
class_type: "LoadImage",
|
|
123
|
+
inputs: {
|
|
124
|
+
image: "input.png"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"11": {
|
|
128
|
+
class_type: "VAEEncode",
|
|
129
|
+
inputs: {
|
|
130
|
+
pixels: ["10", 0],
|
|
131
|
+
vae: ["4", 2]
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// src/workflows/sdxl-text2img.json
|
|
137
|
+
var sdxl_text2img_default = {
|
|
138
|
+
"3": {
|
|
139
|
+
class_type: "KSampler",
|
|
140
|
+
inputs: {
|
|
141
|
+
seed: -1,
|
|
142
|
+
steps: 20,
|
|
143
|
+
cfg: 7,
|
|
144
|
+
sampler_name: "euler",
|
|
145
|
+
scheduler: "normal",
|
|
146
|
+
denoise: 1,
|
|
147
|
+
model: ["4", 0],
|
|
148
|
+
positive: ["6", 0],
|
|
149
|
+
negative: ["7", 0],
|
|
150
|
+
latent_image: ["5", 0]
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"4": {
|
|
154
|
+
class_type: "CheckpointLoaderSimple",
|
|
155
|
+
inputs: {
|
|
156
|
+
ckpt_name: "sd_xl_base_1.0.safetensors"
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"5": {
|
|
160
|
+
class_type: "EmptyLatentImage",
|
|
161
|
+
inputs: {
|
|
162
|
+
width: 1024,
|
|
163
|
+
height: 1024,
|
|
164
|
+
batch_size: 1
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"6": {
|
|
168
|
+
class_type: "CLIPTextEncode",
|
|
169
|
+
inputs: {
|
|
170
|
+
text: "",
|
|
171
|
+
clip: ["4", 1]
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"7": {
|
|
175
|
+
class_type: "CLIPTextEncode",
|
|
176
|
+
inputs: {
|
|
177
|
+
text: "",
|
|
178
|
+
clip: ["4", 1]
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
"8": {
|
|
182
|
+
class_type: "VAEDecode",
|
|
183
|
+
inputs: {
|
|
184
|
+
samples: ["3", 0],
|
|
185
|
+
vae: ["4", 2]
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"9": {
|
|
189
|
+
class_type: "SaveImage",
|
|
190
|
+
inputs: {
|
|
191
|
+
filename_prefix: "ComfyUI",
|
|
192
|
+
images: ["8", 0]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// src/workflows/svd-img2vid.json
|
|
198
|
+
var svd_img2vid_default = {
|
|
199
|
+
"3": {
|
|
200
|
+
class_type: "KSampler",
|
|
201
|
+
inputs: {
|
|
202
|
+
seed: -1,
|
|
203
|
+
steps: 20,
|
|
204
|
+
cfg: 2.5,
|
|
205
|
+
sampler_name: "euler",
|
|
206
|
+
scheduler: "karras",
|
|
207
|
+
denoise: 1,
|
|
208
|
+
model: ["15", 0],
|
|
209
|
+
positive: ["12", 0],
|
|
210
|
+
negative: ["12", 1],
|
|
211
|
+
latent_image: ["12", 2]
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"8": {
|
|
215
|
+
class_type: "VAEDecode",
|
|
216
|
+
inputs: {
|
|
217
|
+
samples: ["3", 0],
|
|
218
|
+
vae: ["15", 2]
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
"10": {
|
|
222
|
+
class_type: "LoadImage",
|
|
223
|
+
inputs: {
|
|
224
|
+
image: "input.png"
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
"12": {
|
|
228
|
+
class_type: "SVD_img2vid_Conditioning",
|
|
229
|
+
inputs: {
|
|
230
|
+
width: 1024,
|
|
231
|
+
height: 576,
|
|
232
|
+
video_frames: 14,
|
|
233
|
+
motion_bucket_id: 127,
|
|
234
|
+
fps: 6,
|
|
235
|
+
augmentation_level: 0,
|
|
236
|
+
clip_vision: ["15", 1],
|
|
237
|
+
init_image: ["10", 0],
|
|
238
|
+
vae: ["15", 2]
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
"15": {
|
|
242
|
+
class_type: "ImageOnlyCheckpointLoader",
|
|
243
|
+
inputs: {
|
|
244
|
+
ckpt_name: "svd_xt.safetensors"
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
"20": {
|
|
248
|
+
class_type: "SaveAnimatedWEBP",
|
|
249
|
+
inputs: {
|
|
250
|
+
filename_prefix: "SVD",
|
|
251
|
+
fps: 6,
|
|
252
|
+
lossless: false,
|
|
253
|
+
quality: 90,
|
|
254
|
+
method: "default",
|
|
255
|
+
images: ["8", 0]
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// src/comfyui-provider.ts
|
|
261
|
+
var BUILT_IN_WORKFLOWS = {
|
|
262
|
+
"sdxl-text2img": {
|
|
263
|
+
name: "SDXL Text-to-Image",
|
|
264
|
+
apiFormat: sdxl_text2img_default,
|
|
265
|
+
inputs: {
|
|
266
|
+
prompt: { path: "6.inputs.text", type: "string", required: true },
|
|
267
|
+
negative_prompt: { path: "7.inputs.text", type: "string", default: "" },
|
|
268
|
+
seed: { path: "3.inputs.seed", type: "number", default: -1 },
|
|
269
|
+
steps: { path: "3.inputs.steps", type: "number", default: 20 },
|
|
270
|
+
cfg: { path: "3.inputs.cfg", type: "number", default: 7 },
|
|
271
|
+
width: { path: "5.inputs.width", type: "number", default: 1024 },
|
|
272
|
+
height: { path: "5.inputs.height", type: "number", default: 1024 }
|
|
273
|
+
},
|
|
274
|
+
outputs: {
|
|
275
|
+
"9": "image"
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
"sdxl-img2img": {
|
|
279
|
+
name: "SDXL Image-to-Image",
|
|
280
|
+
apiFormat: sdxl_img2img_default,
|
|
281
|
+
inputs: {
|
|
282
|
+
prompt: { path: "6.inputs.text", type: "string", required: true },
|
|
283
|
+
negative_prompt: { path: "7.inputs.text", type: "string", default: "" },
|
|
284
|
+
seed: { path: "3.inputs.seed", type: "number", default: -1 },
|
|
285
|
+
steps: { path: "3.inputs.steps", type: "number", default: 20 },
|
|
286
|
+
cfg: { path: "3.inputs.cfg", type: "number", default: 7 },
|
|
287
|
+
denoise: { path: "3.inputs.denoise", type: "number", default: 0.75 },
|
|
288
|
+
image: { path: "10.inputs.image", type: "string", required: true }
|
|
289
|
+
},
|
|
290
|
+
outputs: {
|
|
291
|
+
"9": "image"
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
"flux-text2img": {
|
|
295
|
+
name: "Flux.1-dev Text-to-Image",
|
|
296
|
+
apiFormat: flux_text2img_default,
|
|
297
|
+
inputs: {
|
|
298
|
+
prompt: { path: "6.inputs.text", type: "string", required: true },
|
|
299
|
+
negative_prompt: { path: "7.inputs.text", type: "string", default: "" },
|
|
300
|
+
seed: { path: "3.inputs.seed", type: "number", default: -1 },
|
|
301
|
+
steps: { path: "3.inputs.steps", type: "number", default: 20 },
|
|
302
|
+
cfg: { path: "3.inputs.cfg", type: "number", default: 1 },
|
|
303
|
+
width: { path: "5.inputs.width", type: "number", default: 1024 },
|
|
304
|
+
height: { path: "5.inputs.height", type: "number", default: 1024 }
|
|
305
|
+
},
|
|
306
|
+
outputs: {
|
|
307
|
+
"9": "image"
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
"svd-img2vid": {
|
|
311
|
+
name: "Stable Video Diffusion Image-to-Video",
|
|
312
|
+
apiFormat: svd_img2vid_default,
|
|
313
|
+
inputs: {
|
|
314
|
+
image: { path: "10.inputs.image", type: "string", required: true },
|
|
315
|
+
seed: { path: "3.inputs.seed", type: "number", default: -1 },
|
|
316
|
+
steps: { path: "3.inputs.steps", type: "number", default: 20 },
|
|
317
|
+
cfg: { path: "3.inputs.cfg", type: "number", default: 2.5 },
|
|
318
|
+
width: { path: "12.inputs.width", type: "number", default: 1024 },
|
|
319
|
+
height: { path: "12.inputs.height", type: "number", default: 576 },
|
|
320
|
+
video_frames: { path: "12.inputs.video_frames", type: "number", default: 14 },
|
|
321
|
+
motion_bucket_id: { path: "12.inputs.motion_bucket_id", type: "number", default: 127 },
|
|
322
|
+
fps: { path: "12.inputs.fps", type: "number", default: 6 }
|
|
323
|
+
},
|
|
324
|
+
outputs: {
|
|
325
|
+
"20": "video"
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
var ComfyUIProvider = class extends MediaProvider {
|
|
330
|
+
static id = "comfyui";
|
|
331
|
+
name = "comfyui";
|
|
332
|
+
supportedOperations = ["image.generate", "image.edit", "video.generate"];
|
|
333
|
+
/**
|
|
334
|
+
* §0.6 capability declarations. ComfyUI exposes per-node progress as it executes a
|
|
335
|
+
* workflow (we surface it via the F6 progress bridge during pollForCompletion), but
|
|
336
|
+
* does not push outbound webhooks.
|
|
337
|
+
*/
|
|
338
|
+
supportsStreaming = /* @__PURE__ */ new Set(["image.generate", "image.edit", "video.generate"]);
|
|
339
|
+
supportsWebhooks = false;
|
|
340
|
+
/**
|
|
341
|
+
* F2 cacheConfig per plan §F10 "Per-implementation features":
|
|
342
|
+
* "det per fixed `seed`; cache enabled when seed is provided and non-negative"
|
|
343
|
+
*
|
|
344
|
+
* All workflow inputs are deterministic given a fixed seed. The non-det list is
|
|
345
|
+
* empty because there is no provider-side request id (ComfyUI's `prompt_id` is
|
|
346
|
+
* generated post-submit and is not user-supplied). Normalize trims string params.
|
|
347
|
+
*/
|
|
348
|
+
static cacheConfig = {
|
|
349
|
+
deterministicParams: [
|
|
350
|
+
"prompt",
|
|
351
|
+
"negative_prompt",
|
|
352
|
+
"seed",
|
|
353
|
+
"steps",
|
|
354
|
+
"cfg",
|
|
355
|
+
"denoise",
|
|
356
|
+
"width",
|
|
357
|
+
"height",
|
|
358
|
+
"image",
|
|
359
|
+
"model",
|
|
360
|
+
"sampler",
|
|
361
|
+
"scheduler",
|
|
362
|
+
"video_frames",
|
|
363
|
+
"motion_bucket_id",
|
|
364
|
+
"fps",
|
|
365
|
+
"dimensions"
|
|
366
|
+
],
|
|
367
|
+
nonDeterministicParams: [],
|
|
368
|
+
normalize: (inputs) => {
|
|
369
|
+
const out = {};
|
|
370
|
+
for (const [k, v] of Object.entries(inputs)) {
|
|
371
|
+
out[k] = typeof v === "string" ? v.trim().replace(/\s+/g, " ") : v;
|
|
372
|
+
}
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
baseUrl;
|
|
377
|
+
pollIntervalMs;
|
|
378
|
+
retentionMs;
|
|
379
|
+
downloadOutputs;
|
|
380
|
+
workflows;
|
|
381
|
+
workflowsDir;
|
|
382
|
+
workflowsDirLoaded = false;
|
|
383
|
+
constructor(config = {}) {
|
|
384
|
+
super();
|
|
385
|
+
this.baseUrl = config.baseUrl ?? "http://localhost:8188";
|
|
386
|
+
this.pollIntervalMs = config.pollIntervalMs ?? 1e3;
|
|
387
|
+
this.retentionMs = config.retentionMs ?? 6e5;
|
|
388
|
+
this.downloadOutputs = config.downloadOutputs ?? true;
|
|
389
|
+
this.workflows = { ...BUILT_IN_WORKFLOWS };
|
|
390
|
+
this.workflowsDir = config.workflowsDir;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Plan §F10 "Mechanism (ComfyUI) #1": load user workflows from `workflowsDir/*.json`
|
|
394
|
+
* at construction time. We do it lazily (first execute / explicit call) so that the
|
|
395
|
+
* constructor stays sync and tests can mock fs.
|
|
396
|
+
*
|
|
397
|
+
* Each file becomes `custom/<basename>` workflow. The JSON must declare `apiFormat`,
|
|
398
|
+
* `inputs`, and `outputs` keys (the ComfyUIWorkflow shape). Files that don't match
|
|
399
|
+
* the shape are skipped with a warning.
|
|
400
|
+
*/
|
|
401
|
+
async loadWorkflowsFromDir() {
|
|
402
|
+
if (this.workflowsDirLoaded || !this.workflowsDir) return;
|
|
403
|
+
this.workflowsDirLoaded = true;
|
|
404
|
+
try {
|
|
405
|
+
const fs = await import("fs/promises");
|
|
406
|
+
const path = await import("path");
|
|
407
|
+
const entries = await fs.readdir(this.workflowsDir);
|
|
408
|
+
for (const entry of entries) {
|
|
409
|
+
if (!entry.endsWith(".json")) continue;
|
|
410
|
+
const full = path.join(this.workflowsDir, entry);
|
|
411
|
+
try {
|
|
412
|
+
const raw = await fs.readFile(full, "utf8");
|
|
413
|
+
const parsed = JSON.parse(raw);
|
|
414
|
+
if (typeof parsed.apiFormat !== "object" || typeof parsed.inputs !== "object") {
|
|
415
|
+
console.warn(`ComfyUI: skipping workflow ${entry} \u2014 missing apiFormat/inputs`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const slug = `custom/${path.basename(entry, ".json")}`;
|
|
419
|
+
this.workflows[slug] = parsed;
|
|
420
|
+
} catch (err) {
|
|
421
|
+
console.warn(`ComfyUI: failed to load workflow ${entry}: ${err.message}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.warn(
|
|
426
|
+
`ComfyUI: workflowsDir '${this.workflowsDir}' unreadable: ${err.message}`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async healthCheck() {
|
|
431
|
+
const startTime = Date.now();
|
|
432
|
+
try {
|
|
433
|
+
const response = await fetch(`${this.baseUrl}/system_stats`, {
|
|
434
|
+
signal: AbortSignal.timeout(1e4)
|
|
435
|
+
});
|
|
436
|
+
if (response.ok) {
|
|
437
|
+
return {
|
|
438
|
+
healthy: true,
|
|
439
|
+
latency: Date.now() - startTime
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
healthy: false,
|
|
444
|
+
latency: Date.now() - startTime,
|
|
445
|
+
error: `HTTP ${response.status}: ${response.statusText}`
|
|
446
|
+
};
|
|
447
|
+
} catch (error) {
|
|
448
|
+
return {
|
|
449
|
+
healthy: false,
|
|
450
|
+
latency: Date.now() - startTime,
|
|
451
|
+
error: error.message
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async estimateCost(_input) {
|
|
456
|
+
return {
|
|
457
|
+
costUsd: 0,
|
|
458
|
+
currency: "USD"
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
registerWorkflow(name, workflow) {
|
|
462
|
+
this.workflows[name] = workflow;
|
|
463
|
+
}
|
|
464
|
+
getWorkflow(name) {
|
|
465
|
+
return this.workflows[name];
|
|
466
|
+
}
|
|
467
|
+
listWorkflows() {
|
|
468
|
+
return Object.keys(this.workflows);
|
|
469
|
+
}
|
|
470
|
+
async execute(input) {
|
|
471
|
+
await this.loadWorkflowsFromDir();
|
|
472
|
+
const explicit = this.resolveWorkflowName(input);
|
|
473
|
+
if (explicit) {
|
|
474
|
+
return this.runWorkflow(explicit, input);
|
|
475
|
+
}
|
|
476
|
+
switch (input.operation) {
|
|
477
|
+
case "image.generate":
|
|
478
|
+
return this.runWorkflow("sdxl-text2img", input);
|
|
479
|
+
case "image.edit":
|
|
480
|
+
return this.runWorkflow("sdxl-img2img", input);
|
|
481
|
+
case "video.generate":
|
|
482
|
+
return this.runWorkflow("svd-img2vid", input);
|
|
483
|
+
default:
|
|
484
|
+
throw new Error(`Unsupported operation: ${input.operation}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
resolveWorkflowName(input) {
|
|
488
|
+
const model = input.params?.model ?? input.model;
|
|
489
|
+
if (!model) return null;
|
|
490
|
+
if (!model.startsWith("workflow:")) return null;
|
|
491
|
+
return model.slice("workflow:".length);
|
|
492
|
+
}
|
|
493
|
+
async runWorkflow(workflowName, input) {
|
|
494
|
+
const startTime = Date.now();
|
|
495
|
+
const workflow = this.workflows[workflowName];
|
|
496
|
+
if (!workflow) {
|
|
497
|
+
throw new WorkflowNotFoundError(workflowName);
|
|
498
|
+
}
|
|
499
|
+
const prompt = structuredClone(workflow.apiFormat);
|
|
500
|
+
for (const [paramName, spec] of Object.entries(workflow.inputs)) {
|
|
501
|
+
const value = input.params[paramName] ?? spec.default;
|
|
502
|
+
if (value === void 0 && spec.required) {
|
|
503
|
+
throw new InvalidInputError(`Missing required input: ${paramName}`);
|
|
504
|
+
}
|
|
505
|
+
if (value !== void 0) {
|
|
506
|
+
this.setNestedValue(prompt, spec.path, value);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const dims = input.params.dimensions;
|
|
510
|
+
if (dims) {
|
|
511
|
+
const parts = dims.split("x").map(Number);
|
|
512
|
+
if (parts.length === 2 && !Number.isNaN(parts[0]) && !Number.isNaN(parts[1])) {
|
|
513
|
+
this.setNestedValue(prompt, "5.inputs.width", parts[0]);
|
|
514
|
+
this.setNestedValue(prompt, "5.inputs.height", parts[1]);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const response = await fetch(`${this.baseUrl}/prompt`, {
|
|
518
|
+
method: "POST",
|
|
519
|
+
headers: { "Content-Type": "application/json" },
|
|
520
|
+
body: JSON.stringify({ prompt })
|
|
521
|
+
});
|
|
522
|
+
if (!response.ok) {
|
|
523
|
+
const errorText = await response.text();
|
|
524
|
+
throw new Error(`ComfyUI error: ${errorText}`);
|
|
525
|
+
}
|
|
526
|
+
const promptResult = await response.json();
|
|
527
|
+
const promptId = promptResult.prompt_id;
|
|
528
|
+
const outputs = await this.pollForCompletion(promptId);
|
|
529
|
+
if (this.downloadOutputs && Object.keys(outputs).length > 0) {
|
|
530
|
+
const firstOutput = Object.values(outputs)[0];
|
|
531
|
+
if (firstOutput.images && firstOutput.images.length > 0) {
|
|
532
|
+
const img = firstOutput.images[0];
|
|
533
|
+
const imageUrl = `${this.baseUrl}/view?filename=${img.filename}&subfolder=${img.subfolder}&type=${img.type}`;
|
|
534
|
+
const imageResponse = await fetch(imageUrl);
|
|
535
|
+
if (!imageResponse.ok) {
|
|
536
|
+
throw new Error(`Failed to download output image: ${imageResponse.statusText}`);
|
|
537
|
+
}
|
|
538
|
+
const arrayBuffer = await imageResponse.arrayBuffer();
|
|
539
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
540
|
+
return {
|
|
541
|
+
data: buffer,
|
|
542
|
+
mimeType: this.mimeTypeFromFilename(img.filename),
|
|
543
|
+
metadata: {
|
|
544
|
+
type: "image",
|
|
545
|
+
prompt_id: promptId,
|
|
546
|
+
workflow: workflowName,
|
|
547
|
+
filename: img.filename
|
|
548
|
+
},
|
|
549
|
+
costUsd: 0,
|
|
550
|
+
durationMs: Date.now() - startTime
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
throw new Error("No output images produced by workflow");
|
|
555
|
+
}
|
|
556
|
+
async pollForCompletion(promptId) {
|
|
557
|
+
const deadline = Date.now() + this.retentionMs;
|
|
558
|
+
while (Date.now() < deadline) {
|
|
559
|
+
await this.sleep(this.pollIntervalMs);
|
|
560
|
+
try {
|
|
561
|
+
const response = await fetch(`${this.baseUrl}/history/${promptId}`, {
|
|
562
|
+
signal: AbortSignal.timeout(1e4)
|
|
563
|
+
});
|
|
564
|
+
if (response.status === 404) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (!response.ok) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const history = await response.json();
|
|
571
|
+
const entry = history[promptId];
|
|
572
|
+
if (entry?.status.completed) {
|
|
573
|
+
return entry.outputs;
|
|
574
|
+
}
|
|
575
|
+
if (entry && entry.status.status_str === "error") {
|
|
576
|
+
throw new Error("ComfyUI workflow execution failed");
|
|
577
|
+
}
|
|
578
|
+
} catch {
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
throw new WorkflowExpiredError();
|
|
582
|
+
}
|
|
583
|
+
setNestedValue(obj, path, value) {
|
|
584
|
+
const keys = path.split(".");
|
|
585
|
+
let current = obj;
|
|
586
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
587
|
+
const key = keys[i];
|
|
588
|
+
if (!(key in current)) {
|
|
589
|
+
current[key] = {};
|
|
590
|
+
}
|
|
591
|
+
current = current[key];
|
|
592
|
+
}
|
|
593
|
+
const lastKey = keys[keys.length - 1];
|
|
594
|
+
if (lastKey === "width" || lastKey === "height") {
|
|
595
|
+
current[lastKey] = typeof value === "string" ? Number(value) : value;
|
|
596
|
+
} else {
|
|
597
|
+
current[lastKey] = value;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
mimeTypeFromFilename(filename) {
|
|
601
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
602
|
+
switch (ext) {
|
|
603
|
+
case "png":
|
|
604
|
+
return "image/png";
|
|
605
|
+
case "jpg":
|
|
606
|
+
case "jpeg":
|
|
607
|
+
return "image/jpeg";
|
|
608
|
+
case "webp":
|
|
609
|
+
return "image/webp";
|
|
610
|
+
case "mp4":
|
|
611
|
+
return "video/mp4";
|
|
612
|
+
case "webm":
|
|
613
|
+
return "video/webm";
|
|
614
|
+
default:
|
|
615
|
+
return "application/octet-stream";
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
sleep(ms) {
|
|
619
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
function createComfyUIProvider(config) {
|
|
623
|
+
return new ComfyUIProvider(config);
|
|
624
|
+
}
|
|
625
|
+
export {
|
|
626
|
+
ComfyUIProvider,
|
|
627
|
+
createComfyUIProvider
|
|
628
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reaatech/media-pipeline-mcp-comfyui",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "ComfyUI provider — local Stable Diffusion workflows via ComfyUI API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Rick Somers <rick@reaatech.com> (https://reaatech.com)",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/reaatech/media-pipeline-mcp.git",
|
|
10
|
+
"directory": "packages/comfyui"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/reaatech/media-pipeline-mcp/tree/main/packages/comfyui#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/reaatech/media-pipeline-mcp/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"require": "./dist/index.cjs"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@reaatech/media-pipeline-mcp-core": "0.3.0",
|
|
35
|
+
"@reaatech/media-pipeline-mcp-provider-core": "0.3.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.11.0",
|
|
39
|
+
"tsup": "^8.4.0",
|
|
40
|
+
"typescript": "^5.8.3",
|
|
41
|
+
"vitest": "^3.1.1"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:coverage": "vitest run --coverage",
|
|
47
|
+
"clean": "rm -rf dist"
|
|
48
|
+
}
|
|
49
|
+
}
|