@fre4x/comfyui 1.0.62 → 1.0.64

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.
Files changed (2) hide show
  1. package/dist/index.js +188 -103
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -30630,9 +30630,16 @@ function convertWebUIToAPI(webUIJson, objectInfo) {
30630
30630
  const prompt = {};
30631
30631
  const links = webUIJson.links || [];
30632
30632
  const nodes = webUIJson.nodes || [];
30633
+ const serverNodeIds = /* @__PURE__ */ new Set();
30634
+ for (const node of nodes) {
30635
+ if (!node.id || !node.type || !objectInfo[node.type]) {
30636
+ continue;
30637
+ }
30638
+ serverNodeIds.add(String(node.id));
30639
+ }
30633
30640
  const linkMap = /* @__PURE__ */ new Map();
30634
30641
  for (const link of links) {
30635
- if (isLinkData(link)) {
30642
+ if (isLinkData(link) && serverNodeIds.has(String(link[3]))) {
30636
30643
  linkMap.set(link[0], link);
30637
30644
  }
30638
30645
  }
@@ -30650,7 +30657,7 @@ function convertWebUIToAPI(webUIJson, objectInfo) {
30650
30657
  for (const input of node.inputs) {
30651
30658
  if (input.name && input.link !== null && input.link !== void 0) {
30652
30659
  const linkData = linkMap.get(input.link);
30653
- if (Array.isArray(linkData) && linkData.length >= 6) {
30660
+ if (Array.isArray(linkData) && linkData.length >= 6 && String(linkData[3]) === nodeIdStr) {
30654
30661
  const originNodeId = String(linkData[1]);
30655
30662
  const originSlot = linkData[2];
30656
30663
  promptNode.inputs[input.name] = [
@@ -30840,7 +30847,7 @@ async function fetchComfyJson(apiPath, options) {
30840
30847
  }
30841
30848
  return body;
30842
30849
  }
30843
- async function fetchImageBase64(filename, type, subfolder) {
30850
+ async function fetchImageAsset(filename, type, subfolder) {
30844
30851
  const params = new URLSearchParams({ filename, type });
30845
30852
  if (subfolder) params.set("subfolder", subfolder);
30846
30853
  const url2 = new URL(
@@ -30853,10 +30860,47 @@ async function fetchImageBase64(filename, type, subfolder) {
30853
30860
  const arrayBuffer = await response.arrayBuffer();
30854
30861
  const mimeType = response.headers.get("content-type") || "image/png";
30855
30862
  return {
30856
- data: Buffer.from(arrayBuffer).toString("base64"),
30863
+ buffer: Buffer.from(arrayBuffer),
30857
30864
  mimeType
30858
30865
  };
30859
30866
  }
30867
+ function createMockImageAsset() {
30868
+ return {
30869
+ buffer: Buffer.from("mock-image-data"),
30870
+ mimeType: "image/png"
30871
+ };
30872
+ }
30873
+ function getSessionWorkspaceDir() {
30874
+ return process.env.MCP_WORKSPACE_DIR;
30875
+ }
30876
+ function getSafePathSegments(subfolder) {
30877
+ if (!subfolder) return [];
30878
+ return subfolder.split(/[/\\]/).filter((s) => s.length > 0 && s !== ".." && s !== ".");
30879
+ }
30880
+ async function saveToWorkspace(promptId, image, asset) {
30881
+ const workspaceDir = getSessionWorkspaceDir();
30882
+ if (!workspaceDir) {
30883
+ return void 0;
30884
+ }
30885
+ const safeFileName = path.basename(image.filename);
30886
+ const safeSubfolderSegments = getSafePathSegments(image.subfolder);
30887
+ const relativePath = path.join(
30888
+ "comfyui",
30889
+ promptId,
30890
+ ...safeSubfolderSegments,
30891
+ safeFileName
30892
+ );
30893
+ const workspacePath = path.join(workspaceDir, relativePath);
30894
+ await fs.mkdir(path.dirname(workspacePath), { recursive: true });
30895
+ await fs.writeFile(workspacePath, asset.buffer);
30896
+ return {
30897
+ workspace_path: workspacePath,
30898
+ relative_path: relativePath,
30899
+ filename: safeFileName,
30900
+ mime_type: asset.mimeType,
30901
+ subfolder: image.subfolder
30902
+ };
30903
+ }
30860
30904
  function handleComfyError(error48) {
30861
30905
  if (error48 instanceof ZodError) {
30862
30906
  return createValidationError(
@@ -30881,33 +30925,27 @@ ${detailSummary}` : error48.message
30881
30925
  isError: true
30882
30926
  };
30883
30927
  }
30884
- return createInternalError(error48);
30885
- }
30886
- async function validateWorkflowPath(targetPath) {
30887
- const resolvedPath = path.resolve(targetPath);
30888
- const allowedRoots = getWorkflowDirectories();
30889
- const isAllowed = allowedRoots.some(
30890
- (root) => resolvedPath.startsWith(path.resolve(root))
30891
- );
30892
- if (!isAllowed) {
30893
- throw new WorkflowInputError(
30894
- `Access denied: ${targetPath} is outside allowed workflow directories.`,
30895
- "workflow_file_path"
30896
- );
30897
- }
30898
- try {
30899
- await fs.access(resolvedPath);
30900
- return resolvedPath;
30901
- } catch {
30902
- throw new WorkflowInputError(
30903
- `Workflow file not found: ${targetPath}`,
30904
- "workflow_file_path",
30905
- "not_found"
30906
- );
30928
+ const errorMessage = error48 instanceof Error ? error48.message : String(error48);
30929
+ if (errorMessage.includes("Timeout waiting for workflow")) {
30930
+ return {
30931
+ content: [
30932
+ {
30933
+ type: "text",
30934
+ text: `${errorMessage}.
30935
+
30936
+ The workflow is likely still running on the server. You can re-attach to it and wait for completion by calling 'comfyui_wait_for_workflow' again with the same prompt_id.`
30937
+ }
30938
+ ],
30939
+ isError: true
30940
+ };
30907
30941
  }
30942
+ return createInternalError(error48);
30908
30943
  }
30909
30944
  function getWorkflowDirectories(extraPath) {
30910
30945
  const directories = [
30946
+ process.cwd(),
30947
+ path.join(process.cwd(), "workflows"),
30948
+ path.join(homedir(), ".fre4x-comfyui", "workflows"),
30911
30949
  path.join(
30912
30950
  homedir(),
30913
30951
  "comfy",
@@ -30915,9 +30953,7 @@ function getWorkflowDirectories(extraPath) {
30915
30953
  "user",
30916
30954
  "default",
30917
30955
  "workflows"
30918
- ),
30919
- path.join(homedir(), ".fre4x-comfyui", "workflows"),
30920
- path.join(process.cwd(), "workflows")
30956
+ )
30921
30957
  ];
30922
30958
  if (extraPath) {
30923
30959
  directories.push(extraPath);
@@ -30942,12 +30978,6 @@ function normalizeWorkflowFileName(fileName) {
30942
30978
  "output_file_name"
30943
30979
  );
30944
30980
  }
30945
- if (path.basename(trimmed) !== trimmed) {
30946
- throw new WorkflowInputError(
30947
- "Filename must not include directory separators.",
30948
- "output_file_name"
30949
- );
30950
- }
30951
30981
  return trimmed.endsWith(".json") ? trimmed : `${trimmed}.json`;
30952
30982
  }
30953
30983
  function isConnectionValue(value) {
@@ -30961,21 +30991,30 @@ async function getObjectInfo() {
30961
30991
  async function resolveWorkflowReference(args) {
30962
30992
  if (args.workflow_file_path) {
30963
30993
  const targetPath = args.workflow_file_path;
30964
- if (path.basename(targetPath) === targetPath) {
30965
- const candidates = getWorkflowNameCandidates(targetPath);
30966
- for (const dir of getWorkflowDirectories()) {
30967
- for (const candidate of candidates) {
30968
- const candidatePath = path.join(dir, candidate);
30969
- try {
30970
- const content = await fs.readFile(
30971
- candidatePath,
30972
- "utf8"
30973
- );
30974
- return {
30975
- targetPath: candidatePath,
30976
- content
30977
- };
30978
- } catch {
30994
+ try {
30995
+ const resolvedPath = path.resolve(targetPath);
30996
+ const content = await fs.readFile(resolvedPath, "utf8");
30997
+ return {
30998
+ targetPath: resolvedPath,
30999
+ content
31000
+ };
31001
+ } catch {
31002
+ if (path.basename(targetPath) === targetPath) {
31003
+ const candidates = getWorkflowNameCandidates(targetPath);
31004
+ for (const dir of getWorkflowDirectories()) {
31005
+ for (const candidate of candidates) {
31006
+ const candidatePath = path.join(dir, candidate);
31007
+ try {
31008
+ const content = await fs.readFile(
31009
+ candidatePath,
31010
+ "utf8"
31011
+ );
31012
+ return {
31013
+ targetPath: candidatePath,
31014
+ content
31015
+ };
31016
+ } catch {
31017
+ }
30979
31018
  }
30980
31019
  }
30981
31020
  }
@@ -30985,11 +31024,6 @@ async function resolveWorkflowReference(args) {
30985
31024
  "not_found"
30986
31025
  );
30987
31026
  }
30988
- const validatedPath = await validateWorkflowPath(targetPath);
30989
- return {
30990
- targetPath: validatedPath,
30991
- content: await fs.readFile(validatedPath, "utf8")
30992
- };
30993
31027
  }
30994
31028
  if (args.workflow_id) {
30995
31029
  const requestedId = args.workflow_id.trim();
@@ -31136,6 +31170,9 @@ function analyzeEditableInputs(prompt) {
31136
31170
  continue;
31137
31171
  }
31138
31172
  const classType = typeof rawNode.class_type === "string" ? rawNode.class_type : "";
31173
+ if (!classType || ["Note", "MarkdownNote", "PrimitiveNode"].includes(classType) || nodeId === "meta") {
31174
+ continue;
31175
+ }
31139
31176
  for (const [inputName, value] of Object.entries(rawNode.inputs)) {
31140
31177
  if (!isConnectionValue(value)) {
31141
31178
  continue;
@@ -31156,6 +31193,9 @@ function analyzeEditableInputs(prompt) {
31156
31193
  continue;
31157
31194
  }
31158
31195
  const classType = typeof rawNode.class_type === "string" ? rawNode.class_type : "";
31196
+ if (!classType || ["Note", "MarkdownNote", "PrimitiveNode"].includes(classType) || nodeId === "meta") {
31197
+ continue;
31198
+ }
31159
31199
  for (const [inputName, value] of Object.entries(rawNode.inputs)) {
31160
31200
  if (isConnectionValue(value)) {
31161
31201
  continue;
@@ -31206,7 +31246,10 @@ function applyOverrides(prompt, overrides) {
31206
31246
  for (let i = 0; i < parts.length - 1; i++) {
31207
31247
  const part = parts[i];
31208
31248
  if (typeof current[part] !== "object" || current[part] === null) {
31209
- current[part] = {};
31249
+ throw new WorkflowInputError(
31250
+ `Invalid override path: '${key}'. The segment '${part}' does not exist or is not an object. Please check the editable inputs using 'comfyui_get_workflow' to find the correct path.`,
31251
+ "overrides"
31252
+ );
31210
31253
  }
31211
31254
  current = current[part];
31212
31255
  }
@@ -31245,7 +31288,7 @@ var SaveWorkflowSchema = z3.object({
31245
31288
  workflow_file_path: z3.string().optional().describe("Existing workflow path or filename to save."),
31246
31289
  workflow_id: z3.string().optional().describe("Existing workflow identifier to save."),
31247
31290
  overrides: z3.record(z3.string(), z3.unknown()).optional().describe("Dot-notation overrides to persist before saving."),
31248
- output_file_name: z3.string().optional().describe("Filename to write under ./workflows."),
31291
+ output_file_name: z3.string().optional().describe("Filename to write locally. (default: anchored to ./)"),
31249
31292
  overwrite: z3.boolean().default(false).describe("Replace an existing local file when true."),
31250
31293
  include_prompt: z3.boolean().default(false).describe("Include saved API JSON in structuredContent.")
31251
31294
  });
@@ -31260,8 +31303,11 @@ async function handleInspectNode(args) {
31260
31303
  );
31261
31304
  if (args.node_class) {
31262
31305
  const node = rawInfo[args.node_class];
31263
- if (!node)
31264
- return createNotFoundError(`Node class ${args.node_class}`);
31306
+ if (!node) {
31307
+ return createNotFoundError(
31308
+ `Node class '${args.node_class}' not found. Use 'comfyui_inspect_node' without arguments to see all available nodes or use the 'query' parameter to search for it.`
31309
+ );
31310
+ }
31265
31311
  return {
31266
31312
  content: [
31267
31313
  { type: "text", text: JSON.stringify(node, null, 2) }
@@ -31311,19 +31357,7 @@ function getWorkflowId(absolutePath) {
31311
31357
  return crypto.createHash("sha256").update(absolutePath).digest("hex").substring(0, 8);
31312
31358
  }
31313
31359
  async function handleDiscoverWorkflows(args) {
31314
- const pathsToScan = [
31315
- path.join(
31316
- homedir(),
31317
- "comfy",
31318
- "ComfyUI",
31319
- "user",
31320
- "default",
31321
- "workflows"
31322
- ),
31323
- path.join(homedir(), ".fre4x-comfyui", "workflows"),
31324
- path.join(process.cwd(), "workflows")
31325
- ];
31326
- if (args.directory_path) pathsToScan.push(args.directory_path);
31360
+ const pathsToScan = getWorkflowDirectories(args.directory_path);
31327
31361
  const foundWorkflows = [];
31328
31362
  for (const dir of pathsToScan) {
31329
31363
  try {
@@ -31367,13 +31401,14 @@ async function handleDiscoverWorkflows(args) {
31367
31401
  }
31368
31402
  }
31369
31403
  if (foundWorkflows.length === 0) {
31404
+ const text2 = `No workflows found in searched directories.
31405
+
31406
+ Searched paths:
31407
+ ${pathsToScan.map((p) => `- ${p}`).join("\n")}
31408
+
31409
+ Hint: If your workflows are stored elsewhere, provide a custom path using the 'directory_path' parameter.`;
31370
31410
  return {
31371
- content: [
31372
- {
31373
- type: "text",
31374
- text: `No workflows found in searched directories.`
31375
- }
31376
- ],
31411
+ content: [{ type: "text", text: text2 }],
31377
31412
  structuredContent: {
31378
31413
  workflows: [],
31379
31414
  searched_paths: pathsToScan
@@ -31428,6 +31463,7 @@ async function handleWaitForWorkflow(args) {
31428
31463
  args.timeout
31429
31464
  );
31430
31465
  const outputsRecord = historyEntry.outputs || {};
31466
+ const workspaceOutputs = [];
31431
31467
  const content = [];
31432
31468
  let summaryText = `Workflow ${args.prompt_id} completed.
31433
31469
  `;
@@ -31435,36 +31471,54 @@ async function handleWaitForWorkflow(args) {
31435
31471
  if (output && Array.isArray(output.images)) {
31436
31472
  for (const image of output.images) {
31437
31473
  if (image.filename && image.type) {
31438
- if (!IS_MOCK) {
31439
- try {
31440
- const base643 = await fetchImageBase64(
31441
- image.filename,
31442
- image.type,
31443
- image.subfolder
31444
- );
31445
- content.push({
31446
- type: "image",
31447
- data: base643.data,
31448
- mimeType: base643.mimeType
31449
- });
31450
- summaryText += `Fetched image: ${image.filename}
31474
+ try {
31475
+ const asset = IS_MOCK ? createMockImageAsset() : await fetchImageAsset(
31476
+ image.filename,
31477
+ image.type,
31478
+ image.subfolder
31479
+ );
31480
+ content.push({
31481
+ type: "image",
31482
+ data: asset.buffer.toString("base64"),
31483
+ mimeType: asset.mimeType
31484
+ });
31485
+ summaryText += `Fetched image: ${image.filename}
31451
31486
  `;
31452
- } catch (e) {
31453
- summaryText += `Failed to fetch image ${image.filename}: ${String(e)}
31487
+ const summary = await saveToWorkspace(
31488
+ args.prompt_id,
31489
+ image,
31490
+ asset
31491
+ );
31492
+ if (summary) {
31493
+ workspaceOutputs.push(summary);
31494
+ summaryText += `Copied image to workspace: ${summary.relative_path}
31454
31495
  `;
31455
31496
  }
31456
- } else {
31457
- summaryText += `Mock image: ${image.filename}
31497
+ } catch (e) {
31498
+ summaryText += `Failed to fetch image ${image.filename}: ${String(e)}
31458
31499
  `;
31459
31500
  }
31460
31501
  }
31461
31502
  }
31462
31503
  }
31463
31504
  }
31505
+ if (IS_MOCK && outputsRecord) {
31506
+ for (const [, output] of Object.entries(outputsRecord)) {
31507
+ if (output && Array.isArray(output.images)) {
31508
+ for (const image of output.images) {
31509
+ summaryText += `Mock image: ${image.filename}
31510
+ `;
31511
+ }
31512
+ }
31513
+ }
31514
+ }
31464
31515
  content.unshift({ type: "text", text: summaryText });
31465
31516
  return {
31466
31517
  content,
31467
- structuredContent: historyEntry
31518
+ structuredContent: {
31519
+ ...historyEntry,
31520
+ workspace_outputs: workspaceOutputs
31521
+ }
31468
31522
  };
31469
31523
  } catch (error48) {
31470
31524
  return handleComfyError(error48);
@@ -31488,7 +31542,20 @@ async function handleGetWorkflow(args) {
31488
31542
  editable_inputs: editableInputs
31489
31543
  };
31490
31544
  if (args.include_prompt) {
31491
- structuredContent.prompt = loadedWorkflow.prompt;
31545
+ const filteredPrompt = {};
31546
+ for (const [nodeId, node] of Object.entries(
31547
+ loadedWorkflow.prompt
31548
+ )) {
31549
+ if (isRecord(node)) {
31550
+ const classType = typeof node.class_type === "string" ? node.class_type : "";
31551
+ if (classType && !["Note", "MarkdownNote", "PrimitiveNode"].includes(
31552
+ classType
31553
+ ) && nodeId !== "meta") {
31554
+ filteredPrompt[nodeId] = node;
31555
+ }
31556
+ }
31557
+ }
31558
+ structuredContent.prompt = filteredPrompt;
31492
31559
  }
31493
31560
  return {
31494
31561
  content: [{ type: "text", text }],
@@ -31519,13 +31586,18 @@ async function handleSaveWorkflow(args) {
31519
31586
  if (args.overrides) {
31520
31587
  applyOverrides(prompt, args.overrides);
31521
31588
  }
31522
- const workflowsDir = path.join(process.cwd(), "workflows");
31523
- await fs.mkdir(workflowsDir, { recursive: true });
31524
31589
  const outputFileName = deriveSavedWorkflowFileName(
31525
31590
  sourcePath,
31526
31591
  args.output_file_name
31527
31592
  );
31528
- const outputPath = path.join(workflowsDir, outputFileName);
31593
+ if (outputFileName.includes("..")) {
31594
+ return createValidationError(
31595
+ "output_file_name",
31596
+ "Directory traversal sequences (..) are not allowed."
31597
+ );
31598
+ }
31599
+ const outputPath = path.resolve(process.cwd(), outputFileName);
31600
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
31529
31601
  let existedBeforeSave = false;
31530
31602
  try {
31531
31603
  await fs.access(outputPath);
@@ -31555,7 +31627,18 @@ async function handleSaveWorkflow(args) {
31555
31627
  editable_inputs: editableInputs
31556
31628
  };
31557
31629
  if (args.include_prompt) {
31558
- structuredContent.prompt = prompt;
31630
+ const filteredPrompt = {};
31631
+ for (const [nodeId, node] of Object.entries(prompt)) {
31632
+ if (isRecord(node)) {
31633
+ const classType = typeof node.class_type === "string" ? node.class_type : "";
31634
+ if (classType && !["Note", "MarkdownNote", "PrimitiveNode"].includes(
31635
+ classType
31636
+ ) && nodeId !== "meta") {
31637
+ filteredPrompt[nodeId] = node;
31638
+ }
31639
+ }
31640
+ }
31641
+ structuredContent.prompt = filteredPrompt;
31559
31642
  }
31560
31643
  const persistedChanges = args.overrides && Object.keys(args.overrides).length > 0 ? `Persisted ${Object.keys(args.overrides).length} override(s).` : "No overrides were applied.";
31561
31644
  const text = [
@@ -31609,7 +31692,9 @@ async function handleWorkflowRun(args) {
31609
31692
  content: [
31610
31693
  {
31611
31694
  type: "text",
31612
- text: `Workflow submitted. prompt_id: ${result.prompt_id}, queue: ${result.number}`
31695
+ text: `Workflow submitted. prompt_id: ${result.prompt_id}, queue: ${result.number}.
31696
+
31697
+ To retrieve the final results or check its status later, use the 'comfyui_wait_for_workflow' tool with this prompt_id.`
31613
31698
  }
31614
31699
  ],
31615
31700
  structuredContent: result
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fre4x/comfyui",
3
- "version": "1.0.62",
3
+ "version": "1.0.64",
4
4
  "description": "MCP server for ComfyUI. Execute workflows and probe server state remotely.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",