comfyui-mcp 0.52.192 → 0.52.194

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.
@@ -28,7 +28,7 @@ import { createHash, randomUUID } from "node:crypto";
28
28
  import { resolveToolSurfacePolicy, toolActionPolicyError, toolAllowed, } from "../tools/tool-surface-filter.js";
29
29
  import { logger as toolPolicyLogger } from "../utils/logger.js";
30
30
  import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
31
- import { forwardedByReferenceNote, oversizedInlineRefusal, resolveServableViewRef, stageFileIntoServedDir, stagedForDisplayNote, readShowMediaAck, unaccountedShowMediaNote, unverifiedViewRefNote, } from "../services/comfy-view-ref.js";
31
+ import { forwardedByReferenceNote, mediaKindFromMime, oversizedInlineRefusal, remoteViewRefInlineFailedNote, resolveServableViewRef, stageFileIntoServedDir, stagedForDisplayNote, readShowMediaAck, unaccountedShowMediaNote, unverifiedViewRefNote, viewRefTypeForFetch, } from "../services/comfy-view-ref.js";
32
32
  import { extname, isAbsolute, join, resolve, sep } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
34
  import { comfyuiFetch } from "../comfyui/fetch.js";
@@ -205,7 +205,7 @@ import { QueueMonitor, RUNNING_UNCONFIRMED_MS } from "../services/queue-monitor.
205
205
  import { stallThresholdMs } from "../services/stall-threshold.js";
206
206
  import { RunCompletions } from "./run-completion-journal.js";
207
207
  import { AskAnswers, askFingerprint, PANEL_ASK_ID_PREFIX, } from "./ask-answer-journal.js";
208
- import { getLogs, getQueueVerified, resetClient, resetObjectInfoCache, } from "../comfyui/client.js";
208
+ import { fetchImage, getLogs, getQueueVerified, resetClient, resetObjectInfoCache, } from "../comfyui/client.js";
209
209
  import { convertUiToApi, collectNodeTypes } from "../services/workflow-converter.js";
210
210
  import { outputNodeObjectInfoForPanel, recoverOutputNodeScopedRun, stampOutputNodeFlagsOnToolResult, } from "../services/output-node.js";
211
211
  import { captureComfyUITargetFence, restartComfyUI, preflightLocalRestart, readServingArgv, resolveVerifiedProxyRestartTarget, describeArgvDrift, recordRestartDispatch, clearRestartDispatch, getRestartDispatchRecord, RESTART_DISPATCH_CAUSATION_WINDOW_MS, PROCESS_WIDE_RESTART_DISPATCH_TOKEN, __processControlTestHooks, targetFenceMatchesCurrent, targetFencesEqual, } from "../services/process-control.js";
@@ -21555,10 +21555,14 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
21555
21555
  .optional()
21556
21556
  .describe("Reconnect pasted nodes' inputs to existing nodes where they line up (default false)."),
21557
21557
  }, async (args, ctx) => ctx.call({ cmd: "graph_paste_nodes", pos: args.pos, connect_inputs: args.connect_inputs }, 15000)),
21558
- def("panel_save_subgraph", "Save a SUBGRAPH node to the user's reusable blueprint LIBRARY (publish), so it can be dropped into any workflow later. Pass node_id to pick the subgraph node (else a single selected subgraph node is used) and name to title the blueprint (defaults to the node's title). Runs programmatically — NO save dialog pops. The blueprint becomes the addable type 'SubgraphBlueprint.<name>' (use panel_add_subgraph or panel_list_subgraphs). Returns {saved: {name, type}}.", {
21558
+ def("panel_save_subgraph", "Save a SUBGRAPH node to the user's reusable blueprint LIBRARY (publish), so it can be dropped into any workflow later. Pass node_id to pick the subgraph node (else a single selected subgraph node is used) and name to title the blueprint (defaults to the node's title). On a name collision with an existing USER blueprint, pass overwrite:true to replace it in place (the panel already implements this; without the flag the call is refused and names overwrite:true as the remedy). Bundled/global blueprints cannot be overwritten. Runs programmatically — NO save dialog pops. The blueprint becomes the addable type 'SubgraphBlueprint.<name>' (use panel_add_subgraph or panel_list_subgraphs). Returns {saved: {name, type}}.", {
21559
21559
  node_id: nodeId().optional().describe("Subgraph node id to publish (is_subgraph=true). Omit to use the selected subgraph node."),
21560
21560
  name: z.string().optional().describe("Blueprint name. Defaults to the subgraph node's title."),
21561
- }, async (args, ctx) => ctx.call({ cmd: "graph_save_subgraph", node_id: args.node_id, name: args.name }, 20000)),
21561
+ overwrite: z
21562
+ .boolean()
21563
+ .optional()
21564
+ .describe("Replace an existing USER blueprint of the same name in place. Required on a name collision; omitted/false refuses. Bundled/global blueprints stay refused."),
21565
+ }, async (args, ctx) => ctx.call({ cmd: "graph_save_subgraph", node_id: args.node_id, name: args.name, overwrite: args.overwrite }, 20000)),
21562
21566
  def("panel_list_subgraphs", "List the saved subgraph BLUEPRINTS in the user's library (from panel_save_subgraph, plus any global/bundled ones). Each entry has {name, type, display_name, description, is_global} — use name/type with panel_add_subgraph to drop it onto the canvas. Read-only.\n\nTOKEN-BOUNDED like the other reads (panel#690). The reply's count field is always the LIBRARY TOTAL, so compare it against the returned array. When entries are withheld the reply carries truncated:true, a returned field, and a note — an absent entry in a TRUNCATED list is NOT evidence the blueprint does not exist, so narrow with `filter` (or raise `limit`, up to 500) before concluding anything. When a filter is applied the reply's matched field reports how many the filter selected, distinct from the count field, so matched:0 against a non-zero count means the filter missed, not that the library is empty.", {
21563
21567
  filter: z
21564
21568
  .string()
@@ -23850,21 +23854,27 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
23850
23854
  resolved.push({ kind, dataUrl, filename, caption: item.caption });
23851
23855
  }
23852
23856
  else {
23853
- // ComfyUI /view ref. A browser panel fetches it same-origin — but a
23854
- // HEADLESS (mobile/remote) client can't reach ComfyUI, so resolve the
23855
- // bytes HERE and inline them as a data URL. Best-effort: any failure
23856
- // (fetch error, non-media, too big) falls back to forwarding the ref,
23857
- // which the client renders as a caption card.
23857
+ // ComfyUI /view ref. A local browser panel fetches it same-origin —
23858
+ // but a HEADLESS client cannot, and a REMOTE/CLOUD browser panel's
23859
+ // /view is a different route from get_image's authenticated client
23860
+ // (the panel 404s while get_image succeeds #2861).
23858
23861
  //
23859
23862
  // #2012 — judge the DESTINATION, not the address. `ctx.tabId` may be
23860
23863
  // a scope / prefix / alias; `isHeadless` on the address is a lookup
23861
23864
  // miss that answers false and a phone gets a /view URL it cannot
23862
- // fetch. Positive requirement: inline only a proven headless
23863
- // destination.
23865
+ // fetch. Positive requirement: inline a proven headless destination,
23866
+ // or a proven interactive destination whose ComfyUI is not local.
23867
+ //
23868
+ // #877/#899 — remote bytes come from fetchImage (the get_image
23869
+ // path), never a same-named file on the orchestrator disk. A remote
23870
+ // interactive tab that cannot be inlined is refused rather than
23871
+ // handed a viewRef the panel will drop (painted:0).
23864
23872
  let inlined = false;
23865
- if (resolveClientKind(ctx).kind === "headless") {
23873
+ const dest = resolveClientKind(ctx);
23874
+ const remoteTarget = isRemoteMode() || isCloudMode();
23875
+ if (dest.kind === "headless") {
23866
23876
  try {
23867
- const base = (process.env.COMFYUI_URL ?? "http://127.0.0.1:8188").replace(/\/+$/, "");
23877
+ const base = getComfyUIBaseUrl().replace(/\/+$/, "");
23868
23878
  const qs = new URLSearchParams({ filename: src.filename, type: src.type ?? "output" });
23869
23879
  if (src.subfolder)
23870
23880
  qs.set("subfolder", src.subfolder);
@@ -23872,9 +23882,10 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
23872
23882
  if (resp.ok) {
23873
23883
  const mime = resp.headers.get("content-type") ?? "";
23874
23884
  const buf = Buffer.from(await resp.arrayBuffer());
23875
- if ((mime.startsWith("image/") || mime.startsWith("video/")) && buf.length <= MAX_BYTES) {
23885
+ const kindFromMime = mediaKindFromMime(mime);
23886
+ if (kindFromMime && kindFromMime !== "audio" && buf.length <= MAX_BYTES) {
23876
23887
  resolved.push({
23877
- kind: mime.startsWith("video/") ? "video" : "image",
23888
+ kind: kindFromMime,
23878
23889
  dataUrl: `data:${mime};base64,${buf.toString("base64")}`,
23879
23890
  filename: src.filename,
23880
23891
  caption: item.caption,
@@ -23887,6 +23898,41 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
23887
23898
  // fall through to the viewRef path
23888
23899
  }
23889
23900
  }
23901
+ else if (remoteTarget && dest.kind === "interactive") {
23902
+ try {
23903
+ const fetched = await fetchImage(src.filename, viewRefTypeForFetch(src.type), src.subfolder ?? "", { maxBytes: MAX_BYTES });
23904
+ const mime = fetched.mimeType.split(";")[0].trim();
23905
+ const buf = Buffer.from(fetched.base64, "base64");
23906
+ const kindFromMime = mediaKindFromMime(mime);
23907
+ if (kindFromMime && buf.length <= MAX_BYTES) {
23908
+ resolved.push({
23909
+ kind: kindFromMime,
23910
+ dataUrl: `data:${mime};base64,${fetched.base64}`,
23911
+ filename: src.filename,
23912
+ caption: item.caption,
23913
+ });
23914
+ inlined = true;
23915
+ }
23916
+ else {
23917
+ const detail = !kindFromMime
23918
+ ? `The MCP client fetched it, but the body was not media (content-type "${mime || "unset"}").`
23919
+ : `The MCP client fetched it, but it is ${buf.length} bytes — over the ${MAX_BYTES} byte inline cap.`;
23920
+ return fail(remoteViewRefInlineFailedNote({
23921
+ filename: src.filename,
23922
+ detail,
23923
+ capBytes: MAX_BYTES,
23924
+ }));
23925
+ }
23926
+ }
23927
+ catch (err) {
23928
+ const detail = err instanceof Error ? err.message : String(err);
23929
+ return fail(remoteViewRefInlineFailedNote({
23930
+ filename: src.filename,
23931
+ detail,
23932
+ capBytes: MAX_BYTES,
23933
+ }));
23934
+ }
23935
+ }
23890
23936
  if (!inlined) {
23891
23937
  resolved.push({
23892
23938
  kind: "viewRef",