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

97 lines • 5.22 kB
/** * Ground-truth introspection of the registered MCP tool surface. * * Boots the REAL McpServer with the REAL registration pass and asks it over a * real MCP client, so the name, description, annotations and inputSchema are * exactly what a client is told in `tools/list` — in particular the JSON Schema * the SDK actually derives from each zod shape, which is where a registration can * silently normalise to `{}`. * * It is NOT the whole ToolSchema: `title`, `icons`, `outputSchema`, `execution` and * `_meta` are dropped, so a change to any of those is invisible here. Nor does it * see anything behind the CONFIGURED server — createConfiguredServer() (src/boot.ts) * chooses full versus compact registration, and this calls registerAllTools() * directly. Stated explicitly because "what a client sees" invites the reading that * anything a client could notice is covered, and it is not. * * Why this exists rather than `collectToolCatalog()` (src/tools/catalog.ts): * the catalog is a duck-typed stand-in that infers "description" and "schema" * positionally from the `server.tool(...)` arguments. That makes it blind to * anything the SDK would reject or transform — a registration the SDK drops, or * a schema it normalises to `{}`, still "counts" there. Going through the SDK * means a tool only counts if a client can genuinely see it. * * Used by: scripts/tools-dump.mts (the per-phase surface probe) and * src/__tests__/tools/registry-surface.test.ts (the CI surface gate). * * NOT yet used by scripts/asset-counts.mjs, which is `ci.yml`'s count gate and * still imports the duck-typed `collectToolCatalog()` from `dist/`. Until that * migrates (comfyui-mcp-3nh), the count CI publishes is derived from a stale * build by a capture that cannot see SDK rejection — the exact blindness this * module exists to remove. Stated here rather than implied, because a comment * describing intended wiring reads as a guarantee that CI already enforces it. */ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { registerAllTools, registerFullTools } from "./index.js"; /** * Every tool a client can see, in registration order. * * Follows `nextCursor` to completion: `tools/list` is a paginated request, and a * single-page assumption would silently under-report once the surface grows. */ export async function listRegisteredTools(surface = "registration") { const server = new McpServer({ name: "comfyui-mcp-introspect", version: "0.0.0" }); if (surface === "served") await registerFullTools(server); else await registerAllTools(server); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: "comfyui-mcp-introspect", version: "0.0.0" }); // The connects are INSIDE the try. With them outside it, a Promise.all where one // side connects and the other rejects skips the finally entirely and leaks the // connected half — which keeps the event loop alive, so a CLI probe hangs // instead of reporting the error that caused it. try { await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); const tools = []; const seenCursors = new Set(); let cursor; do { const page = await client.listTools(cursor ? { cursor } : undefined); for (const t of page.tools) { tools.push({ name: t.name, description: t.description ?? "", inputSchema: t.inputSchema, ...(t.annotations ? { annotations: t.annotations } : {}), }); } cursor = page.nextCursor; // A server that returns the same nextCursor twice would spin forever, // appending duplicates until the process dies of memory exhaustion. Today's // in-process SDK server does not paginate at all, so this cannot trigger — // it guards a DEPENDENCY behaviour we do not control, in a probe that CI // depends on. Throwing beats a hang: a hang has no diagnosis. if (cursor !== undefined) { if (seenCursors.has(cursor)) { throw new Error(`tools/list returned a repeated nextCursor (${JSON.stringify(cursor)}) after ` + `${tools.length} tools — refusing to loop forever.`); } seenCursors.add(cursor); } } while (cursor); return tools; } finally { // Close both ends: an unclosed in-memory pair keeps the event loop alive and // a CLI probe would hang instead of exiting. await Promise.allSettled([client.close(), server.close()]); } } /** Just the names, in registration order (which `tools/list` preserves). */ export async function listRegisteredToolNames(surface = "registration") { return (await listRegisteredTools(surface)).map((t) => t.name); } //# sourceMappingURL=introspect.js.map