comfyui-mcp
Version:
Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),
197 lines • 10 kB
JavaScript
/**
* Tool ↔ bridge-command correspondence used by agent-facing refusals.
*
* Two maps, one reverse index. The forward maps are NOT injective (graph_load
* is dispatched by two tools), so the reverse index omits ambiguous commands
* rather than guessing — a confidently wrong tool name is the defect this
* module exists to prevent (#1933, #1935).
*
* Lives here, not in panel-tools.ts, so ui-bridge can name tools in its own
* refusals without a circular import (panel-tools already imports ui-bridge).
*/
/**
* #694 — the MUTATING panel tools that accept retry_of, keyed to the bridge
* command each dispatches. Covers every command the bridge fences to a workflow
* (GRAPH_CMD_EFFECT "targeted" — see requiresWorkflowStampEnforcement) plus the
* four workflow mutators (workflow_save / workflow_save_as / workflow_rename /
* workflow_close). Navigation/creation (workflow_open / workflow_new) and the
* tools whose descriptions declare them view/read-only (panel_find_nodes,
* panel_canvas, panel_screenshot, panel_list_subgraphs) are excluded: a read must
* never mint or carry a retry token — its retry could be answered from the ledger
* with a STALE outcome (codex gate).
*
* Four UI-STATE commands are admitted on purpose (graph_select_nodes,
* graph_enter/exit_subgraph, graph_copy_nodes): they change selection, subgraph
* scope or clipboard idempotently, so a deduped retry is a no-op. THIS MAP — not
* the workflow fence — is what decides whether a token is minted and whether a
* caller-supplied one reaches the wire, so those four keep behaving exactly as
* they did before #778 reclassified them as `inert` for the fence. The two
* questions are related but not the same, and answering one with the other is
* the defect #778 is about.
*
* EXPLICIT MAP, mirroring the RETRY_SAFE_CMDS / MUTATING_GRAPH_EDIT_CMDS
* maintenance model — keep in sync when mutating tools are added. Exported for
* the #694 surface-integrity test.
*/
export const RETRY_TOKEN_CMD_BY_TOOL = {
panel_add_node: "graph_add_node",
panel_edit_node: "graph_edit_node",
panel_remove_node: "graph_remove_node",
panel_clear: "graph_clear",
panel_flatten_workflow: "graph_load",
panel_load_workflow: "graph_load",
panel_connect: "graph_connect",
panel_disconnect: "graph_disconnect",
// Idempotent: inputs/outputs/default_mode are absolute values, so a deduped
// retry writes the same metadata again rather than doubling anything.
panel_configure_app_mode: "graph_configure_app_mode",
panel_set_widget: "graph_set_widget",
panel_remove_widget: "graph_remove_widget",
panel_set_property: "graph_set_node_property",
panel_move_node: "graph_move_node",
panel_resize_node: "graph_resize_node",
panel_auto_layout: "graph_auto_layout",
panel_run: "graph_run",
panel_save_workflow: "workflow_save", // workflow_save_as when `name` is given
panel_rename_workflow: "workflow_rename",
panel_close_workflow: "workflow_close",
panel_select_nodes: "graph_select_nodes",
panel_create_subgraph: "graph_create_subgraph",
panel_subgraph_group: "graph_subgraph_group",
panel_copy_nodes: "graph_copy_nodes",
panel_paste_nodes: "graph_paste_nodes",
panel_save_subgraph: "graph_save_subgraph",
panel_add_subgraph: "graph_add_subgraph",
panel_create_group: "graph_create_group",
panel_move_group: "graph_move_group",
panel_edit_group: "graph_edit_group",
panel_remove_group: "graph_remove_group",
panel_set_node_title: "graph_set_title",
panel_set_node_collapsed: "graph_set_node_collapsed",
panel_set_node_mode: "graph_set_node_mode",
panel_set_node_color: "graph_set_node_color",
panel_enter_subgraph: "graph_enter_subgraph",
panel_exit_subgraph: "graph_exit_subgraph",
panel_move_rail: "graph_move_rail",
panel_promote_widget: "graph_promote_widget",
panel_expose_subgraph_output: "graph_expose_subgraph_output",
panel_expose_subgraph_input: "graph_expose_subgraph_input",
panel_unexpose_subgraph_output: "graph_unexpose_subgraph_output",
panel_unexpose_subgraph_input: "graph_unexpose_subgraph_input",
panel_unpack_subgraph: "graph_unpack_subgraph",
panel_update_node: "graph_update_node",
};
/**
* #1935 — the READ / view-only tools that agent-facing refusals may advertise,
* keyed to the bridge command each dispatches.
*
* Separate from RETRY_TOKEN_CMD_BY_TOOL on purpose: a read must never mint a
* retry token (that map's own doc). The instance-mismatch refusal in ui-bridge
* still has to NAME these, and the mapping is NOT the `panel_` prefix
* mechanically applied — `graph_query`'s tool is `panel_query_graph`, not
* `panel_graph_query`.
*
* `graph_get_state` is omitted: it is an internal pre-0.8.2 fallback
* (panel_strip_workflow reconstruction), not a registered tool. Naming it would
* send the agent to an "unknown tool" the same way the bare bridge commands did.
*/
export const GRAPH_READ_CMD_BY_TOOL = {
panel_graph_outline: "graph_outline",
panel_query_graph: "graph_query",
panel_get_errors: "graph_get_errors",
panel_find_nodes: "graph_find_nodes",
panel_list_subgraphs: "graph_list_subgraphs",
panel_screenshot: "graph_screenshot",
panel_canvas: "graph_canvas",
};
/**
* #1933 — the READ TOOLS the QUEUE BUSY refusal advertises, by their REGISTERED
* tool name.
*
* They must be tool names, not the `graph_outline` / `graph_query` /
* `graph_get_errors` bridge commands they dispatch: this string is the whole
* recovery contract for a fenced write, and an agent that follows it calls what
* it is told to call. A bridge command is not callable, so the advice returned
* "unknown tool" — a hard failure mid-degraded-state that the agent cannot
* diagnose, and from which it can only conclude that reads are unavailable
* during a render, which is precisely the state panel#1489 fixed and this
* sentence exists to advertise (reported from the field in
* artokun/comfyui-mcp-panel#1549).
*
* Written out (not derived by prefixing `panel_`) because `graph_query`'s tool
* is `panel_query_graph`. `graph-read-during-render.test.ts` asserts every one
* of them is in `buildPanelToolDefs()` — the registry the caller actually
* dispatches against. That test is what stops this list from drifting again:
* the old literal was green for the whole of its life with all three names
* wrong, because the assertion matched `/graph_outline/`, which a wrong name
* and a right one both satisfy.
*
* #1935 reuses this same list in the workflow-instance-stamp refusal's
* refused/unknown branches, rather than a third hand-written name list.
*/
export const QUEUE_BUSY_READ_TOOLS = [
"panel_graph_outline",
"panel_query_graph",
"panel_get_errors",
// #1968 — `graph_find_nodes` has been `inert` since #778, so the queue-busy
// fence has ALWAYS let it through; it was simply never advertised. A refusal
// that names three usable reads when there are four teaches the agent the
// surface is narrower than it is, and the field report drew exactly that
// conclusion ("find_nodes is not among them"). Under-advertising a working
// read is its own defect: the list IS the recovery contract, and a fenced
// write's only guidance about what still works.
//
// `panel_list_subgraphs`, `panel_screenshot` and `panel_canvas` are inert too
// and stay OUT: this sentence offers reads to "inspect the graph", and a
// blueprint listing, a picture and a viewport move do not answer that. The
// execute-branch `viewTools` list in ui-bridge already names all of them where
// the claim is the broader "reads and view-only commands still work".
"panel_find_nodes",
];
/**
* #1933/#1935 — the reverse of RETRY_TOKEN_CMD_BY_TOOL ∪ GRAPH_READ_CMD_BY_TOOL:
* which TOOL does a caller reach this bridge command through?
*
* An agent-facing message written below the tool layer can only see `cmd.cmd`;
* `PanelToolCtx.call` takes a command object and no tool identity, so by the
* time a pre-dispatch refusal is worded the tool name is genuinely gone. This
* recovers it from the maps that already maintain the correspondence, rather
* than adding another hand-written list that can drift out of step with the
* first.
*
* IT REFUSES TO GUESS. The forward maps are not injective — `graph_load` is
* dispatched by both `panel_load_workflow` and `panel_flatten_workflow` — and
* naming one of two possible tools is a confidently wrong answer, which is worse
* than declining to name one. Ambiguous commands are therefore OMITTED, and
* callers must handle `undefined`.
*
* Coverage of mutations is not assumed either: every `targeted` GRAPH_CMD_EFFECT
* command except `graph_run` is a value of the retry map, so the queue-busy
* fence resolves a tool for everything it can refuse except `graph_load`.
* `graph-read-during-render.test.ts` pins both halves — the resolution and the
* refusal to guess. Reads resolve through GRAPH_READ_CMD_BY_TOOL; `graph_get_state`
* is intentionally absent (no callable tool).
*/
export const PANEL_TOOL_BY_GRAPH_CMD = (() => {
const claims = new Map();
for (const map of [RETRY_TOKEN_CMD_BY_TOOL, GRAPH_READ_CMD_BY_TOOL]) {
for (const [tool, cmd] of Object.entries(map)) {
const prior = claims.get(cmd);
if (prior)
prior.push(tool);
else
claims.set(cmd, [tool]);
}
}
const unique = new Map();
for (const [cmd, tools] of claims)
if (tools.length === 1)
unique.set(cmd, tools[0]);
return unique;
})();
/** #1933/#1935 — the registered tool a bridge command is reached through, or
* `undefined` when no tool, or more than one tool, claims it. */
export function panelToolForGraphCmd(cmd) {
return PANEL_TOOL_BY_GRAPH_CMD.get(cmd);
}
//# sourceMappingURL=panel-graph-cmd-tools.js.map