comfyui-mcp 0.52.87 → 0.52.89

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.
@@ -1001,6 +1001,7 @@ const RETRY_SAFE_CMDS = new Set([
1001
1001
  "graph_query",
1002
1002
  "get_todo",
1003
1003
  "workflow_list",
1004
+ "nodes_search",
1004
1005
  "nodes_list",
1005
1006
  "nodes_queue_status",
1006
1007
  "node_queue_status",
@@ -4949,6 +4950,7 @@ const QUERY_GRAPH_MAX_CHARS_DEFAULT = 12000;
4949
4950
  const QUERY_GRAPH_MAX_CHARS_CEILING = 60000;
4950
4951
  const DETAIL_WIDGET_MAX_CHARS_DEFAULT = 2048;
4951
4952
  const DETAIL_WIDGET_MAX_CHARS_CEILING = 32768;
4953
+ const QUERY_GRAPH_WIDGET_MAX_CHARS_NOTE = "widget_max_chars_note";
4952
4954
  /** The panel's own clamp for graph_query's budget, mirrored so the figure this module
4953
4955
  * reports and enforces is the one the panel was actually working to. */
4954
4956
  function clampQueryGraphMaxChars(v) {
@@ -4957,6 +4959,49 @@ function clampQueryGraphMaxChars(v) {
4957
4959
  return QUERY_GRAPH_MAX_CHARS_DEFAULT;
4958
4960
  return Math.min(Math.max(Math.floor(n), QUERY_GRAPH_MAX_CHARS_FLOOR), QUERY_GRAPH_MAX_CHARS_CEILING);
4959
4961
  }
4962
+ /**
4963
+ * `widget_max_chars` is deliberately a pinpoint-only escape hatch. Keep the
4964
+ * precondition at the MCP seam as well as in the Panel serializer: older Panel
4965
+ * builds silently ignored the argument, and broad reads must never accidentally
4966
+ * trade away the query's token bound for one large widget.
4967
+ */
4968
+ function widgetMaxCharsRequest(args) {
4969
+ const requested = args.widget_max_chars;
4970
+ if (typeof requested !== "number")
4971
+ return {};
4972
+ if (args.fields !== "detail") {
4973
+ return {
4974
+ note: `widget_max_chars=${requested} was not applied: it is accepted only with ` +
4975
+ `fields:"detail" and exactly one explicit ids entry; this query used ` +
4976
+ `${args.fields === undefined ? "an omitted fields projection" : `fields:"${String(args.fields)}"`}. ` +
4977
+ `The default ${DETAIL_WIDGET_MAX_CHARS_DEFAULT}-char per-widget cap remains in force.`,
4978
+ };
4979
+ }
4980
+ if (!Array.isArray(args.ids) || args.ids.length !== 1) {
4981
+ return {
4982
+ note: `widget_max_chars=${requested} was not applied: it is accepted only with ` +
4983
+ `fields:"detail" and exactly one explicit ids entry; this query supplied ` +
4984
+ `${Array.isArray(args.ids) ? `${args.ids.length} ids` : "no explicit ids"}. ` +
4985
+ `The default ${DETAIL_WIDGET_MAX_CHARS_DEFAULT}-char per-widget cap remains in force.`,
4986
+ };
4987
+ }
4988
+ return { value: requested };
4989
+ }
4990
+ /** A legacy Panel can accept the command but still serialize at its old cap. */
4991
+ function legacyWidgetMaxCharsNote(res, requested) {
4992
+ if (typeof requested !== "number" || requested <= DETAIL_WIDGET_MAX_CHARS_DEFAULT)
4993
+ return undefined;
4994
+ const text = res.content
4995
+ .filter((block) => block.type === "text")
4996
+ .map((block) => (block.type === "text" ? block.text : ""))
4997
+ .join("\n");
4998
+ if (!/2048-char per-widget cap/.test(text))
4999
+ return undefined;
5000
+ return (`widget_max_chars=${requested} was not applied: the connected Panel returned its ` +
5001
+ `default ${DETAIL_WIDGET_MAX_CHARS_DEFAULT}-char per-widget cap. Update the Panel ` +
5002
+ `to a build that supports graph_query.widget_max_chars, then retry this exact ` +
5003
+ `detail read; the query result below is otherwise unchanged.`);
5004
+ }
4960
5005
  /** The reply fields that are CONTEXT rather than answer. `viewing` is deliberately not
4961
5006
  * one of them: it identifies which graph the answer describes, it is a couple of small
4962
5007
  * fields, and a reply that cannot say what it is about is worse than a bigger one. */
@@ -4971,6 +5016,7 @@ const QUERY_GRAPH_RIDER_KEYS = [
4971
5016
  * (independent gate P0). `max_chars` is re-set unconditionally just below. */
4972
5017
  const QUERY_GRAPH_OWNED_KEYS = new Set([
4973
5018
  "max_chars",
5019
+ QUERY_GRAPH_WIDGET_MAX_CHARS_NOTE,
4974
5020
  "groups_membership_omitted",
4975
5021
  "groups_omitted",
4976
5022
  "rails_omitted",
@@ -5017,7 +5063,7 @@ function groupIndexEntry(g) {
5017
5063
  * than silently ignored — `max_chars` bounds characters, and calling image bytes
5018
5064
  * characters would be its own false figure.
5019
5065
  */
5020
- function fitQueryGraphReply(res, requested) {
5066
+ function fitQueryGraphReply(res, requested, widgetMaxCharsNote) {
5021
5067
  const payload = parseToolResultJson(res);
5022
5068
  if (!payload)
5023
5069
  return res;
@@ -5064,6 +5110,8 @@ function fitQueryGraphReply(res, requested) {
5064
5110
  // trustworthy, and a caller who learns to distrust it will ignore the real ones. This
5065
5111
  // accounting is the sole author of these fields, so it is also their sole source.
5066
5112
  answer.max_chars = budget;
5113
+ if (widgetMaxCharsNote)
5114
+ answer[QUERY_GRAPH_WIDGET_MAX_CHARS_NOTE] = widgetMaxCharsNote;
5067
5115
  const groups = Array.isArray(riders.groups) ? riders.groups : null;
5068
5116
  const hasGroups = !!groups && groups.length > 0;
5069
5117
  const hasRails = riders.rails !== undefined;
@@ -11979,7 +12027,7 @@ export function buildPanelToolDefs() {
11979
12027
  // Local helper so each def reads like the original `tool(...)` call.
11980
12028
  const def = (name, description, schema, handler) => ({ name, description, schema, handler });
11981
12029
  const defs = [
11982
- def("panel_query_graph", "FILTER or TRAVERSE a SUBSET of the live canvas, for when you ALREADY KNOW what you're looking for. NOT for 'show me the canvas' or any whole-graph overview — call panel_graph_outline FIRST for that. NOT get_workflow's query action (that queries a saved file or JSON you provide, not the live canvas). Filters, traverses, projects and aggregates over the workflow the user is CURRENTLY VIEWING without dumping the whole graph (replaces the old panel_get_graph full-JSON dump; output is TOKEN-BOUNDED with an explicit truncation marker, so a big graph can never flood your context). Combine: `types` (node type contains any), `title` (contains), `where` widget predicates ANDed ('cfg>7', 'steps<=20', 'sampler_name=euler', 'text~sunset' — ops = != >= <= > < ~contains), `ids` (exact nodes — THE way to read ONE node's exact slot/widget detail: {ids:[42], fields:'detail'}), `upstream_of`/`downstream_of` + `depth` (dependency traversal: upstream = what FEEDS that node, downstream = what CONSUMES it; seed at depth 0), `fields` ('compact' one line per node [default], 'ids', 'detail' = the full node summary with slots + connections + mode), `group_by:'type'` (counts only), `limit` (default 40). detail rows include each node's MODE — a 'bypass' node is skipped and a 'mute' node kills everything downstream, so check modes on the path you care about before running (fix with panel_set_node_mode). Every result also carries `groups` (id, title, member node_ids — groups are geometric, trust this list) and, when viewing a SUBGRAPH (after panel_enter_subgraph), `rails` (boundary rail ids/slots). `max_chars` bounds the WHOLE result, those riders included, and the rows you asked for are spent first: on a big graph the riders lose their member ids, then drop out entirely, rather than starving your query — and each says in-band when it did, with the true counts. `widget_max_chars` raises the per-widget cap only for `fields:'detail'`; use it only with exactly one explicit `ids` entry (for example `{ids:[42], fields:'detail', widget_max_chars:8192}`), with a default of 2048 and a maximum of 32768. It does not change compact, ids, or broad reads. Typical flow: panel_graph_outline to orient → panel_query_graph to pinpoint/inspect → edit. Read-only.", {
12030
+ def("panel_query_graph", "FILTER or TRAVERSE a SUBSET of the live canvas, for when you ALREADY KNOW what you're looking for. NOT for 'show me the canvas' or any whole-graph overview — call panel_graph_outline FIRST for that. NOT get_workflow's query action (that queries a saved file or JSON you provide, not the live canvas). Filters, traverses, projects and aggregates over the workflow the user is CURRENTLY VIEWING without dumping the whole graph (replaces the old panel_get_graph full-JSON dump; output is TOKEN-BOUNDED with an explicit truncation marker, so a big graph can never flood your context). Combine: `types` (node type contains any), `title` (contains), `where` widget predicates ANDed ('cfg>7', 'steps<=20', 'sampler_name=euler', 'text~sunset' — ops = != >= <= > < ~contains), `ids` (exact nodes — THE way to read ONE node's exact slot/widget detail: {ids:[42], fields:'detail'}), `upstream_of`/`downstream_of` + `depth` (dependency traversal: upstream = what FEEDS that node, downstream = what CONSUMES it; seed at depth 0), `fields` ('compact' one line per node [default], 'ids', 'detail' = the full node summary with slots + connections + mode), `group_by:'type'` (counts only), `limit` (default 40). detail rows include each node's MODE — a 'bypass' node is skipped and a 'mute' node kills everything downstream, so check modes on the path you care about before running (fix with panel_set_node_mode). Every result also carries `groups` (id, title, member node_ids — groups are geometric, trust this list) and, when viewing a SUBGRAPH (after panel_enter_subgraph), `rails` (boundary rail ids/slots). `max_chars` bounds the WHOLE result, those riders included, and the rows you asked for are spent first: on a big graph the riders lose their member ids, then drop out entirely, rather than starving your query — and each says in-band when it did, with the true counts. `widget_max_chars` raises the per-widget cap only for `fields:'detail'`; use it only with exactly one explicit `ids` entry (for example `{ids:[42], fields:'detail', widget_max_chars:8192}`), with a default of 2048 and a maximum of 32768. If supplied without those preconditions, it is not sent and the result says why; a legacy Panel that still returns the default cap is named in the result. It does not change compact, ids, or broad reads. Typical flow: panel_graph_outline to orient → panel_query_graph to pinpoint/inspect → edit. Read-only.", {
11983
12031
  types: z.array(z.string()).optional().describe("Node type contains ANY of these (case-insensitive)."),
11984
12032
  title: z.string().optional().describe("Node title contains this."),
11985
12033
  where: z
@@ -12030,21 +12078,27 @@ export function buildPanelToolDefs() {
12030
12078
  // The panel bounds `text`; the `groups`/`rails` riders were never in that
12031
12079
  // accounting, so on a large graph the reply could be many times the budget it
12032
12080
  // announced. Nothing is shed while the whole reply fits.
12033
- async (args, ctx) => fitQueryGraphReply(await ctx.call({
12034
- cmd: "graph_query",
12035
- types: args.types,
12036
- title: args.title,
12037
- where: args.where,
12038
- ids: args.ids,
12039
- upstream_of: args.upstream_of,
12040
- downstream_of: args.downstream_of,
12041
- depth: args.depth,
12042
- fields: args.fields,
12043
- group_by: args.group_by,
12044
- limit: args.limit,
12045
- max_chars: args.max_chars,
12046
- widget_max_chars: args.widget_max_chars,
12047
- }), args.max_chars)),
12081
+ async (args, ctx) => {
12082
+ const widgetMaxChars = widgetMaxCharsRequest(args);
12083
+ const panelReply = await ctx.call({
12084
+ cmd: "graph_query",
12085
+ types: args.types,
12086
+ title: args.title,
12087
+ where: args.where,
12088
+ ids: args.ids,
12089
+ upstream_of: args.upstream_of,
12090
+ downstream_of: args.downstream_of,
12091
+ depth: args.depth,
12092
+ fields: args.fields,
12093
+ group_by: args.group_by,
12094
+ limit: args.limit,
12095
+ max_chars: args.max_chars,
12096
+ ...(widgetMaxChars.value === undefined
12097
+ ? {}
12098
+ : { widget_max_chars: widgetMaxChars.value }),
12099
+ });
12100
+ return fitQueryGraphReply(panelReply, args.max_chars, widgetMaxChars.note ?? legacyWidgetMaxCharsNote(panelReply, widgetMaxChars.value));
12101
+ }),
12048
12102
  def("panel_graph_outline", "READ THE LIVE CANVAS the user is looking at, as text. 'Show me what's on the canvas' / 'what's on the graph right now' / 'read the current workflow' / 'describe the open graph' -> THIS TOOL, with no arguments. NOT visualize_workflow (it DRAWS A DIAGRAM of a workflow you PASS IN — a saved file or JSON — and never sees the live canvas). NOT panel_query_graph (that FILTERS a SUBSET, for when you already know what you're looking for). Returns one `outline` string covering the WHOLE open graph, topologically sorted (sources first, sinks last): each node as `id Type \"title\" [bypass/mute] [OUTPUT] · group:X widget=value …` with `← inputs` (source_node.output_name) and `→ outputs` (target_node.input_name), after a GROUPS index (title → member node ids). It gives you the WIRING you would otherwise reconstruct by hand — read it FIRST to get oriented, then panel_query_graph to inspect one node ({ids:[42], fields:'detail'}) or panel_find_nodes for free-text search. Over `max_chars` it never cuts the graph short: it sheds per-node detail, or refuses with a reason — never a partial outline. Read-only.", {
12049
12103
  max_chars: z
12050
12104
  .number()