comfyui-loop-mcp: subgraphs that expand, video the loop can see, and a name I gave up

The loop server got its largest update since I open-sourced it. Subgraphs expand instead of being skipped, the looking tools finally work on video, a pre-flight answers "will this graph run here" before a GPU minute is spent, and the package has a different name than it did last week. The repo is here.
The name
Comfy-Org shipped their own local server as comfy-mcp: same PyPI name, same import package, first commit one day after mine (theirs was public first). Arguing about a namespace with the people who own the namespace is a losing position, and I'd rather spend the time on the code. The distribution is now comfyui-loop-mcp, the import package is comfy_loop, and the command matches the distribution. Runs already on your disk keep working, since the pre-rename ~/.comfy-mcp/runs directory is read until it's empty.
While updating the README I also fixed something that bothered me more than the name. My comparison table had described this project as "hobby code, one author" next to their "production, ComfyUI team," which gave away the wrong thing. The real difference is scope. A platform vendor is the right owner for the whole install surface, because that surface moves whenever ComfyUI, comfy-cli, or the partner APIs move. I own the loop, because the loop is the part Alienrobot is on the hook for in its own production work.
Subgraphs expand now
A subgraph is a canvas convenience. It exists in the litegraph JSON that the ComfyUI editor saves, and /prompt has never heard of it. My converter used to skip subgraph instances, which sounds conservative and isn't: skipping the instance doesn't produce a smaller graph, it produces one with a hole where a pipe used to be. That matters more than I expected, because 254 of the roughly 550 templates in the bundled catalog reference subgraphs.
comfy_loop/subgraph.py expands them properly. Interiors come through with namespaced ids in the form <instance>:<inner>, links get rewired across both the input and output boundary, nesting recurses, and promoted widgets keep their values. I verified it against 12 real catalog templates and got 12 clean conversions.
Two assumptions I started with were wrong, and the shipped template files are what disproved them. The boundary maps by slot name, not by index, because an instance only lists the sockets it actually draws and lists them in its own order. And widgets_values runs positionally over the definition's slots while skipping any slot that's wired.
The bug that ran fine
That second discovery exposed something live in the existing code, and it's the reason I'm writing this section at all.
When you convert a widget to an input in ComfyUI, the widget still occupies its position in widgets_values. My code skipped it. Every later widget then shifted up by one, silently.
What that produces is not a crash. An EmptySD3LatentImage with width and height wired handed 1024 to batch_size. A KSampler with a promoted seed received the seed integer where the sampler name belongs. The graph runs. It queues, it executes, it returns images, and nothing in any error log suggests you just asked for a batch of 1024 latents.
This is the exact failure class the whole project exists for. A run with zero node_errors is valid, not correct, and the only place the difference shows up is in the pixels. I've been saying that about mangled hands and matte edges since the first commit; it turns out my own converter was a fine example.
Video: the loop could not see mp4
get_result has always reported gifs and videos next to images, so a video workflow would hand the model a filename. Every looking tool downstream of that was Pillow-only, and Pillow cannot decode an mp4. For VHS, AnimateDiff, or WAN graphs, the loop's central instruction (call get_image on each output and look) was literally unexecutable. The model received a filename it could not open, and judgment collapsed back to vibes, which is the thing the ratchet exists to prevent.
Four new tools restore it by turning video back into images: video_info, get_video_frame, compare_video_frames, and video_temporal_stats.
Using ffmpeg here is a deliberate exception. imaging.py stays Pillow-only because an MCP client has no shell, but anything capable of emitting an mp4 already shipped ffmpeg (VHS_VideoCombine shells out to it), so an install that can write one can read one. When it genuinely isn't there, the tools say so plainly instead of failing with a decode error.
The trap worth knowing about: comparing two clips by timestamp is wrong the moment their lengths differ. A frame cap, a trimmed input, or a different fps lands you on different moments, and you draw a confident conclusion from two unrelated expressions. Everything here indexes by frame number, and compare_video_frames refuses a quietly-wrong comparison by flagging a frame-count mismatch rather than rendering it anyway.
Answering the whole question before spending a GPU minute
/prompt does validate a graph, which is what node_errors is, but it validates by queueing and reports one failure at a time. A model that submits in order to discover what's wrong burns a round trip per mistake. The failures it surfaces first are rarely the ones that matter either: a missing checkpoint and a missing node pack arrive as the same red box, though one is a download and the other needs a restart.
check_workflow reads object_info and answers all of it at once, sorted by what you have to do:
- install a node pack, because the class doesn't exist here
- fetch a model, because the file isn't in that loader's list
- fix the graph, because a required input is missing or a wire points at nothing
- look again, because a value sits outside the node's declared range
Missing classes resolve to pack ids in the same pass. The logic is pure (dict in, dict out, no HTTP), so it's testable offline and the same function serves a graph you just built, a template you fetched, and a run you're about to repeat.
The rest of the surface
loop_sweep runs one input across N values with everything else held identical, and writes the value-to-prompt_id table into the run, so context compaction can't lose the mapping. It records one pass, not N.
The control tools cover the gaps that made me reach for a browser tab mid-loop: job_status reports where a run is without blocking and includes queue position, cancel_job drops a queued job without interrupting the running one, free_vram clears the cached passes that OOM the next one, comfyui_logs retrieves what a mid-execution death explains nowhere else, and update_comfyui goes through Manager and refuses to pretend a 404 was a success.
Several existing tools got more honest. check_comfyui is a real pre-flight now (versions, free VRAM, whether Manager is present, whether the queue is busy). get_result names the failing node instead of shrugging with "produced no outputs." find_missing_nodes accepts a graph you already hold. template_slots returns the template author's Note text as quoted, clearly untrusted data, since it's arbitrary text arriving from a file you downloaded.
MCP SDK 2.0 also renamed FastMCP to MCPServer and moved Image, which killed a fresh install outright. It imports either way now and the tests run under both.
Step zero, which the agent can now finish
Everything above assumes a ComfyUI that is already running. Getting there was the part I had been treating as somebody else's problem, and the framing was wrong. "This server cannot install ComfyUI" is true, since it speaks HTTP and the URL may well point at a box it has no shell on. It is also beside the point, because the agent calling it usually does have a shell, and in practice that is how every install here happens.
An unreachable server no longer returns a dead end. It returns instructions aimed at the caller, specific to what is actually on the machine. Installed but stopped gets the start command built from that install's own venv python, with a note to background it, since main.py never returns. Nothing installed gets clone, venv, ComfyUI-Manager, launch, plus the observation that Python is already solved: the interpreter running this server is a suitable one, and the message names it. A remote URL gets no local install offer at all, because that would leave a second unused ComfyUI on the wrong box, so it suggests the SSH tunnel instead. The search for an existing install covers $COMFYUI_PATH, comfy-cli's recorded workspace, ~/ComfyUI, ~/comfy, ~/code, ~/github and /opt.
That guidance is attached at the transport, so all 43 tools carry it. Hanging it on check_comfyui alone would have meant one tool explaining the situation while the other 42 raised a bare ConnectError at an agent that skipped step zero.
There is a comfy_install prompt now too, a third one beside comfy_loop and comfy_skill, so the recipe is something an agent pulls deliberately rather than discovers by failing. It gets filled in from the box it runs on, which is the part a generic README cannot do: whether an install already exists (start that one, do not clone a second), which interpreter to build the venv from, whether git and comfy-cli are present, and the torch story for this specific accelerator. That last one matters more than it sounds. A CPU wheel on an NVIDIA box renders at a crawl and reports nothing wrong.
ComfyUI-Manager goes in at install time, because half the EXTEND tools need it and discovering that later costs a reinstall. The recipe also warns about where models land, since checkpoints run 2 to 12 GB each and filling the root filesystem takes down considerably more than ComfyUI.
Three states that look like a healthy install and cannot render are now reported by check_comfyui as work for the agent rather than facts to relay: torch running on the CPU (read from the device type in /system_stats, roughly 50x slower, nothing errors), zero weights on disk (a completely functional ComfyUI that can render nothing, counted from the loaders' own enums), and a missing ComfyUI-Manager. That last one used to dead-end in four separate places. It is now the two commands with the detected install path already in them, plus the detail that trips people up: restart_comfyui is itself a Manager route, so it cannot be the fix for Manager being absent.
Windows installs were invisible before this, which meant telling someone to clone a second ComfyUI next to the one they already run. The portable build nests a level deeper and carries python_embeded\python.exe.
Registering the server stopped needing a terminal as well. .mcp.json ships in the repo, so cloning it and opening Claude Code offers the server with nothing typed, and the copy-paste config runs straight from GitHub through uvx, with nothing to clone and nothing to pip install. The README now opens with a sentence you paste at the agent instead of a bash block. A server.json is prepared for the MCP registry, though publishing it needs an account decision I have not made.
Retiring the companion repo fell out of the same pass. comfyui-llm-onboarding-prompt is done: its prompts and skill were already vendored here and packaged into the wheel, so the cross-reference bought nothing except a second, staler copy of the install steps. Two defaults in those older docs deserved to go with it. A hardcoded --index-url pointing at cu124 silently installs the wrong torch on anything that is not CUDA 12.x, and launching with --listen 0.0.0.0 exposes an unauthenticated server that reads and writes files, as a default, to save a flag nobody asked for.
What I left out, and the one thing I changed my mind about
Accounts, credits, and hosted partner models stay out, and the README says so explicitly. They contradict the one promise this server makes, which is that nothing leaves your box.
Owning the ComfyUI process is the item I was wrong about, though not in the way that phrase suggests. The server still does not start anything, and an HTTP client still cannot launch a server that is not running. What I had wrong was treating that fact as the end of the conversation rather than the beginning of the instructions.
Three new offline suites came with the subgraph work, covering expansion (19 checks), the pre-flight (12), and the control tools against a stub ComfyUI on a real socket (27), and the install work added more against that same stub. They pass under both MCP SDK versions. One fixture detail cost me an afternoon and is worth writing down: the stub had to speak HTTP/1.1, because httpx pools connections, and reusing one that an HTTP/1.0 stub had already hung up on surfaces as a RemoteProtocolError that reads exactly like a bug in the code under test.
That brings the server to 43 tools and three prompts, which is more than I would have guessed when this was a weekend experiment about making an agent look at its own output.
This post was drafted with Claude from my commit notes. The work, the numbers, and the bug are mine.