comfyui-mcp 0.50.21 → 0.50.23
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.
- package/dist/comfyui/cloud-client.js +97 -8
- package/dist/comfyui/cloud-client.js.map +1 -1
- package/dist/comfyui/fetch.js +71 -7
- package/dist/comfyui/fetch.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +69 -10
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/secure-bridge.js +11 -0
- package/dist/services/secure-bridge.js.map +1 -1
- package/dist/services/ui-bridge.js +35 -2
- package/dist/services/ui-bridge.js.map +1 -1
- package/package.json +1 -1
- package/scripts/export-vocabulary.mts +12 -4
- package/scripts/smoke-install.mjs +134 -3
- package/scripts/tools-dump.mts +48 -2
|
@@ -10,10 +10,50 @@
|
|
|
10
10
|
* crashed on a missing file. Unit tests (`npm test`) never exercise the packed
|
|
11
11
|
* tarball, so this is the gap. Run in CI and as a pre-publish gate.
|
|
12
12
|
*/
|
|
13
|
-
import { execSync, spawnSync } from "node:child_process";
|
|
14
|
-
import { mkdtempSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
13
|
+
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
14
|
+
import { mkdtempSync, writeFileSync, existsSync, readdirSync, readFileSync } from "node:fs";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
+
import { pathToFileURL } from "node:url";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Ask the installed server for its tool list over MCP stdio. Resolves to the
|
|
21
|
+
* names, or null if it never answers within the budget — a silence this script
|
|
22
|
+
* reports rather than reading as "no tools".
|
|
23
|
+
*/
|
|
24
|
+
function listTools(entry) {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const child = spawn(process.execPath, [entry], {
|
|
27
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
28
|
+
env: { ...process.env, COMFYUI_URL: "http://127.0.0.1:59999" },
|
|
29
|
+
});
|
|
30
|
+
const send = (o) => child.stdin.write(JSON.stringify(o) + "\n");
|
|
31
|
+
let buf = "";
|
|
32
|
+
const done = (v) => { try { child.kill(); } catch { /* already gone */ } resolve(v); };
|
|
33
|
+
const timer = setTimeout(() => done(null), 60_000);
|
|
34
|
+
child.stdout.on("data", (d) => {
|
|
35
|
+
buf += d.toString();
|
|
36
|
+
let i;
|
|
37
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
38
|
+
const line = buf.slice(0, i);
|
|
39
|
+
buf = buf.slice(i + 1);
|
|
40
|
+
if (!line.trim()) continue;
|
|
41
|
+
let msg;
|
|
42
|
+
try { msg = JSON.parse(line); } catch { continue; }
|
|
43
|
+
if (msg.id === 1) send({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
|
|
44
|
+
if (msg.id === 2) {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
done((msg.result?.tools ?? []).map((t) => t.name));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
child.on("error", () => { clearTimeout(timer); done(null); });
|
|
51
|
+
send({
|
|
52
|
+
jsonrpc: "2.0", id: 1, method: "initialize",
|
|
53
|
+
params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "smoke", version: "1" } },
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
17
57
|
|
|
18
58
|
const run = (cmd, opts = {}) => {
|
|
19
59
|
console.log(`$ ${cmd}${opts.cwd ? ` (cwd: ${opts.cwd})` : ""}`);
|
|
@@ -67,4 +107,95 @@ if (boot.signal === "SIGTERM" || boot.status === 0) {
|
|
|
67
107
|
process.exit(1);
|
|
68
108
|
}
|
|
69
109
|
|
|
70
|
-
|
|
110
|
+
// 5. The published surface actually REGISTERS. Booting proves the process starts;
|
|
111
|
+
// it does not prove the tools are there. A packaging or registration
|
|
112
|
+
// regression that leaves the server up but short a tool would pass step 4 and
|
|
113
|
+
// ship — the user finds out when the tool they wanted is simply absent.
|
|
114
|
+
//
|
|
115
|
+
// Driven over real MCP stdio against the INSTALLED tarball, so this is the
|
|
116
|
+
// surface a user gets, not the one the repo builds. COMFYUI_URL points at a
|
|
117
|
+
// dead port on purpose: registration must not depend on a reachable ComfyUI.
|
|
118
|
+
// The ledger IS the list, not a ceiling: TOOL_NAMES holds every core tool, and
|
|
119
|
+
// MAX_TOOLS beside it is a budget target that merely happens to match today.
|
|
120
|
+
// Counting the list keeps this honest if the two ever diverge.
|
|
121
|
+
const LEDGER = readFileSync(join(process.cwd(), "src/tools/vocabulary.ts"), "utf-8");
|
|
122
|
+
const EXPECTED_CORE = (
|
|
123
|
+
LEDGER.match(/TOOL_NAMES\s*=\s*\[[\s\S]*?\n\]/)?.[0].match(/"[a-z][a-z0-9_]*"/g) ?? []
|
|
124
|
+
).length;
|
|
125
|
+
const surface = await listTools(join(pkg, "dist/index.js"));
|
|
126
|
+
if (surface === null) {
|
|
127
|
+
console.error("❌ the installed server never answered tools/list");
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
// The three compact-mode meta tools (list_tools / describe_tool / call_tool) are
|
|
131
|
+
// registered ALONGSIDE the core surface and are deliberately absent from the
|
|
132
|
+
// ledger, so they are counted out rather than expected in it.
|
|
133
|
+
const META = ["list_tools", "describe_tool", "call_tool"];
|
|
134
|
+
const core = surface.filter((n) => !META.includes(n));
|
|
135
|
+
const missingMeta = META.filter((n) => !surface.includes(n));
|
|
136
|
+
if (EXPECTED_CORE > 0 && core.length !== EXPECTED_CORE) {
|
|
137
|
+
console.error(
|
|
138
|
+
`❌ installed surface has ${core.length} core tools, ledger declares ${EXPECTED_CORE}` +
|
|
139
|
+
`\n registered: ${core.join(", ")}`,
|
|
140
|
+
);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
if (missingMeta.length) {
|
|
144
|
+
console.error(`❌ compact-mode tools missing from the installed surface: ${missingMeta.join(", ")}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
console.log(`✅ installed surface registers ${core.length} core tools + ${META.length} compact-mode tools`);
|
|
148
|
+
|
|
149
|
+
// 6. The PANEL surface too. Those 91 tools are the whole reason the panel can
|
|
150
|
+
// drive a live canvas, they ship in the same tarball, and nothing else here
|
|
151
|
+
// checks that the published build still builds them. buildPanelToolDefs is
|
|
152
|
+
// pure — no server, no socket — so this is a direct call into the INSTALLED
|
|
153
|
+
// dist rather than another stdio round trip.
|
|
154
|
+
//
|
|
155
|
+
// Compared against THIS REPO'S build, not against docs/design/panel-surface.txt.
|
|
156
|
+
// That baseline is frozen HISTORY, not the current surface: check-tool-vocabulary
|
|
157
|
+
// requires live ⊆ baseline, so the baseline only ever grows, and a retired panel
|
|
158
|
+
// tool keeps its line there forever. Counting against it passes today only
|
|
159
|
+
// because nothing has been retired yet — the first legitimate retirement would
|
|
160
|
+
// read as "installed build makes 90, ledger declares 92" and fail a RELEASE, the
|
|
161
|
+
// one gate a false alarm is most expensive in. The ledger relationship is already
|
|
162
|
+
// enforced on every PR by check:vocabulary, from both sides; what only this script
|
|
163
|
+
// can see is whether the TARBALL carries the same surface the repo builds, which
|
|
164
|
+
// is a packaging question and is what it now asks.
|
|
165
|
+
//
|
|
166
|
+
// `prepare: tsc` runs during npm pack in step 1, so ./dist is a fresh build of
|
|
167
|
+
// this source. Two separate module instances, same process and env.
|
|
168
|
+
async function panelNames(root, label) {
|
|
169
|
+
try {
|
|
170
|
+
const mod = await import(pathToFileURL(join(root, "dist/orchestrator/panel-tools.js")).href);
|
|
171
|
+
if (typeof mod.buildPanelToolDefs !== "function") {
|
|
172
|
+
console.error(`❌ the ${label} build exports no buildPanelToolDefs`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
return mod.buildPanelToolDefs().map((d) => d.name);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error(`❌ could not load the ${label} panel tools: ${err?.message ?? err}`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const installedPanel = await panelNames(pkg, "installed");
|
|
182
|
+
const repoPanel = await panelNames(process.cwd(), "repo");
|
|
183
|
+
// A comparison of two empty lists is equal and proves nothing — the vacuous pass
|
|
184
|
+
// that the sibling gate tests each carry a floor assertion against.
|
|
185
|
+
if (repoPanel.length < 50) {
|
|
186
|
+
console.error(`❌ the repo build makes only ${repoPanel.length} panel tools — check is not looking at a real surface`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
const onlyInstalled = installedPanel.filter((n) => !repoPanel.includes(n));
|
|
190
|
+
const onlyRepo = repoPanel.filter((n) => !installedPanel.includes(n));
|
|
191
|
+
if (onlyInstalled.length || onlyRepo.length) {
|
|
192
|
+
console.error(
|
|
193
|
+
`❌ the tarball's panel surface differs from this repo's build` +
|
|
194
|
+
(onlyRepo.length ? `\n in the repo but NOT in the tarball: ${onlyRepo.join(", ")}` : "") +
|
|
195
|
+
(onlyInstalled.length ? `\n in the tarball but NOT in the repo: ${onlyInstalled.join(", ")}` : ""),
|
|
196
|
+
);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
console.log(`✅ tarball's ${installedPanel.length} panel tools match this repo's build`);
|
|
200
|
+
|
|
201
|
+
console.log("✅ pack/install smoke passed — tarball installs cleanly, boots, and registers its tools");
|
package/scripts/tools-dump.mts
CHANGED
|
@@ -9,9 +9,13 @@
|
|
|
9
9
|
* npx tsx scripts/tools-dump.mts # JSON: {count, tools:[…]}
|
|
10
10
|
* npx tsx scripts/tools-dump.mts --names # one name per line, registration order
|
|
11
11
|
* npx tsx scripts/tools-dump.mts --max 30 # exit 1 if count > 30
|
|
12
|
-
* npx tsx scripts/tools-dump.mts --golden
|
|
12
|
+
* npx tsx scripts/tools-dump.mts --golden path/to/expected.txt
|
|
13
13
|
* # diff names vs a committed golden
|
|
14
|
-
* npx tsx scripts/tools-dump.mts --write-golden
|
|
14
|
+
* npx tsx scripts/tools-dump.mts --write-golden path/to/expected.txt
|
|
15
|
+
*
|
|
16
|
+
* The golden is a snapshot of the LIVE surface. It is NOT docs/design/tool-surface.txt
|
|
17
|
+
* or docs/design/panel-surface.txt — those are hash-pinned retirement baselines and
|
|
18
|
+
* both invocations are refused against them; see RETIREMENT_BASELINES below.
|
|
15
19
|
*
|
|
16
20
|
* COMFYUI_URL is set so src/config.ts skips its network port-probe at import
|
|
17
21
|
* time (same reason npm run docs:gen sets it).
|
|
@@ -116,6 +120,48 @@ if (max !== undefined) {
|
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The two hash-pinned RETIREMENT BASELINES. Neither is a live-surface golden, and
|
|
125
|
+
* pointing this script at either is a footgun in both directions.
|
|
126
|
+
*
|
|
127
|
+
* They are history, not state: docs/design/tool-surface.txt holds every core name
|
|
128
|
+
* that has EVER existed (195), against which check-tool-vocabulary enforces
|
|
129
|
+
* `BASELINE \ TOOL_NAMES ⊆ DEAD_NAMES`. The live surface is 37 after the 0.50.0
|
|
130
|
+
* consolidation and will never equal it again.
|
|
131
|
+
*
|
|
132
|
+
* So `--golden docs/design/tool-surface.txt` — an invocation this script's own
|
|
133
|
+
* usage text used to advertise — reports 158 removals and exits 1 on a perfectly
|
|
134
|
+
* healthy repo. That alone is only noise. The danger is the remedy printed
|
|
135
|
+
* underneath it: "re-run with --write-golden", which would overwrite the baseline
|
|
136
|
+
* with the 37 live names and DELETE the other 158. Deleting a line from a
|
|
137
|
+
* retirement baseline is exactly how the ratchet is disarmed for that name — every
|
|
138
|
+
* stale reference to it stops being an error. One typo-free, instruction-following
|
|
139
|
+
* run would have silently disarmed the repo's main vocabulary gate for 158 names,
|
|
140
|
+
* leaving only the SHA pin, whose own error text invites updating the hash.
|
|
141
|
+
*
|
|
142
|
+
* Refused rather than warned: there is no correct reading of either invocation.
|
|
143
|
+
* The baseline is maintained by APPENDING a newly shipped name and updating its
|
|
144
|
+
* SHA deliberately, which is a reviewed edit rather than a bulk rewrite.
|
|
145
|
+
*/
|
|
146
|
+
const RETIREMENT_BASELINES = ["tool-surface.txt", "panel-surface.txt"];
|
|
147
|
+
const isRetirementBaseline = (p: string): boolean =>
|
|
148
|
+
RETIREMENT_BASELINES.some((b) => p.replace(/\\/g, "/").endsWith(`docs/design/${b}`));
|
|
149
|
+
|
|
150
|
+
for (const opt of ["--golden", "--write-golden"] as const) {
|
|
151
|
+
const p = value(opt);
|
|
152
|
+
if (p !== undefined && isRetirementBaseline(p)) {
|
|
153
|
+
usage(
|
|
154
|
+
`${p} is a hash-pinned RETIREMENT BASELINE (every name that has ever existed), ` +
|
|
155
|
+
`not a golden of the live surface — ${opt} would ` +
|
|
156
|
+
(opt === "--write-golden"
|
|
157
|
+
? `overwrite it with today's ${names.length} live names and delete the rest, disarming the dead-name ratchet for every name dropped.`
|
|
158
|
+
: `report every retired name as a removal and exit 1 on a healthy repo.`) +
|
|
159
|
+
`\n What enforces it: npm run check:vocabulary (BASELINE \\ TOOL_NAMES ⊆ DEAD_NAMES).` +
|
|
160
|
+
`\n To add a newly shipped tool: append the name, then update BASELINE_SHA256 in src/tools/vocabulary.ts.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
119
165
|
const writeGolden = value("--write-golden");
|
|
120
166
|
if (writeGolden) {
|
|
121
167
|
writeFileSync(writeGolden, `${names.join("\n")}\n`, "utf8");
|