UNPKG

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),

167 lines • 8.02 kB
/** * #1425 — the wire contract for a node id, including the SUBGRAPH-QUALIFIED shape. * * #845 made the tools accept back an id they had printed: the graph readers return * ids as strings (`"42"`), so `"42"` and `42` both had to mean node 42. That change * deliberately stopped short of `"5:12"`: * * > a subgraph-qualified id is a real shape in newer ComfyUI, and silently * > truncating it to `5` would target the WRONG node rather than fail. If those * > need supporting, that is a separate, deliberate change to the wire contract. * * This is that change. Unpacking a subgraph leaves genuine ROOT-level nodes carrying * ids like `120:104` and `120:113:78`; the readers list them and render them, and * every write tool refused them — 18 of 21 nodes uneditable in the reporter's * workflow. So the refusal was not protecting anything by then, it was just the * conservative half of a trade whose other half never arrived. * * The two halves have to move TOGETHER. Widening the pattern alone would send * `"263:78"` through `Number.parseInt(…, 10)` and yield `263` — turning a loud * refusal into a silent edit of a DIFFERENT node, which is the exact outcome #845 * refused to risk. So a qualified id is never converted: it stays the string the * reader printed, all the way to the panel. */ /** * A plain integer id, the form the wire has always carried. * * Exported because one argument needs this shape ON ITS OWN: panel_run's * `to_node_id` (#1497). Everything else takes NODE_ID_PATTERN, which admits the * qualified form too — but a run-to-node target is resolved by NUMBER all the way * down (the panel's findNodeInScopes does `Number(id)`, and its reply reports * `ran_to_node: Number(to_node_id)`), so admitting `"120:104"` there would trade a * clear schema refusal for a panel-side "node not found" about a node that exists. * The shape lives here so the two patterns cannot drift apart. */ export const PLAIN_NODE_ID_PATTERN = /^-?\d+$/; /** * A subgraph-qualified id: integer segments joined by colons (`120:104`, * `120:113:78`). No depth limit — nesting is arbitrary, and a limit would fail the * deep case in exactly the way this fixes the shallow one. Only the FIRST segment * may be negative: a leading `-` marks the boundary-rail ids the readers emit, and * a `-` inside a path would be a shape nothing produces. */ const QUALIFIED = /^-?\d+(?::\d+)+$/; /** * #2855 — a named id the graph readers print for API-style / string-id graphs * (`sampler`, `mac_studio_vlm`). Must start with a letter or underscore so it * cannot collide with the integer/`42px` cases: `42px` is still refused, never * truncated via parseInt. */ const NAMED = /^[A-Za-z_][A-Za-z0-9_-]*$/; /** * Integer, subgraph-qualified, and named spellings in ONE pattern, so it can be * handed to `z.string().regex()`. * * That matters beyond tidiness: `.regex()` puts a `pattern` on the advertised MCP * input schema, while `.refine()` renders as a bare `{"type":"string"}` (measured * with zod 4's toJSONSchema). Validating through a refine would therefore have told * every client LESS about a node id than the old integer-only rule did. */ export const NODE_ID_PATTERN = /^(?:-?\d+(?::\d+)*|[A-Za-z_][A-Za-z0-9_-]*)$/; /** Does this string name a node at all — either spelling? */ export function isNodeIdString(v) { return NODE_ID_PATTERN.test(v); } /** True for the qualified form specifically (`"5:12"`), false for `"5"`. */ export function isQualifiedNodeId(v) { return QUALIFIED.test(v); } /** * Normalize an accepted id to what goes on the wire. * * A plain id becomes the NUMBER the panel has always received, so nothing about * the existing surface changes. A qualified id and a named id are returned * VERBATIM — parsing them is precisely the bug. Callers must therefore treat a * node id as `number | string` rather than assuming the number. */ export function normalizeNodeId(v) { if (typeof v === "number") return v; if (QUALIFIED.test(v)) return v; if (NAMED.test(v)) return v; return Number.parseInt(v, 10); } /** The message a rejected id gets. Names the qualified and named shapes, because a * caller holding `263:78` or `sampler` needs to know whether it is unsupported or malformed. */ export const NODE_ID_MESSAGE = "a node id must be an integer (e.g. 42), a subgraph-qualified id (e.g. 120:104), or a named id the graph readers printed (e.g. sampler)"; /** * #1889 — a bare `z.union` renders as the word `Invalid input` and nothing else. * * Zod 4 reports a failed union as ONE `invalid_union` issue whose own `message` * is the constant `"Invalid input"`; the per-member messages (NODE_ID_MESSAGE * among them) live in a nested `errors` array. Every renderer that reaches an * agent flattens `issue.message` and drops that nest — the MCP SDK's * `getParseErrorMessage` (1.30) joins `` `${i.message} at ${path}` ``, zod's own * `prettifyError` prints `✖ ${i.message}`, and 1.29 JSON-stringifies the issues. * So a node-id rejection read: * * Invalid input at node_id * Invalid input: expected string, received undefined at title * * NODE_ID_MESSAGE was never dead prose — a BAD STRING (`"42px"`) fails only the * string member, zod collapses that to a plain `invalid_format` issue, and the * message prints. It is unreachable only for inputs that fail EVERY member * (undefined, null, 4.5, an object), which is the common case and the reported * one. This is what makes the union say the same thing there. * * `received` is carried too, because the complaint was the asymmetry with the * sibling `title`, and half of what `title` said was what it actually got. */ export function unionErrorFor(expected) { return (iss) => `${expected}; received ${describeReceived(iss.input)}`; } /** How long a rendered value may get before it is cut. A node id is a handful of * characters; anything near this is a caller error worth showing, not quoting. */ const RECEIVED_MAX = 60; /** * Render an arbitrary rejected input for a message, TOTALLY. * * This runs inside zod's error path, so it must never throw: a formatter that * throws converts a clean validation refusal into a 500. `JSON.stringify` alone * is not safe here — it throws on a BigInt and on a circular object, returns the * bare `undefined` value for a symbol or a function, and renders NaN/Infinity as * `null`, which would tell a caller who passed NaN that they passed null. Each * of those is handled before the stringify, and the stringify itself is caught. */ export function describeReceived(input) { if (input === null) return "null"; switch (typeof input) { case "undefined": return "undefined"; case "bigint": return `${input}n`; case "symbol": return input.toString(); case "function": return "a function"; case "number": case "boolean": // `String` and not `JSON.stringify`: the latter renders NaN and ±Infinity // as `null`, which would tell a caller who passed NaN that they passed null. return String(input); default: break; } let rendered; try { rendered = JSON.stringify(input) ?? String(input); } catch { // The fallback needs its own guard: `Array.isArray` THROWS on a revoked // proxy ("Cannot perform 'IsArray' on a proxy that has been revoked"), so // the naive recovery path was itself a way for this function to throw. // Measured, not assumed — it is the one hostile input of ten that got past // the outer catch. try { return Array.isArray(input) ? "an array" : "an object"; } catch { return "an object"; } } return rendered.length > RECEIVED_MAX ? `${rendered.slice(0, RECEIVED_MAX)}…` : rendered; } //# sourceMappingURL=node-id.js.map