Skip to content

Multi-backend pool, async-completion hardening, LLM-friendly responses, 42-workflow library - #15

Open
svilendotorg wants to merge 26 commits into
joenorton:mainfrom
svilendotorg:feat/improvements
Open

Multi-backend pool, async-completion hardening, LLM-friendly responses, 42-workflow library#15
svilendotorg wants to merge 26 commits into
joenorton:mainfrom
svilendotorg:feat/improvements

Conversation

@svilendotorg

Copy link
Copy Markdown

This is an umbrella PR for ~20 patches accumulated in svilendotorg/ComfyUI-MCP-Server over a homelab deployment. Full per-patch write-up + rationale lives in IMPROVEMENTS.md. I'm happy to split into smaller PRs for any subset you want to land — just say which ones and I'll cherry-pick onto fresh branches.

Headline features

Multi-backend pool with VRAM-aware routing — comfyui_pool.py (new)

Drop-in replacement for ComfyUIClient that fronts multiple ComfyUI instances over COMFYUI_URLS=name=url,.... Routing modes:

  • Default: least-queue across reachable backends; ties broken by env order. Backends with foreign_vram > POOL_MAX_FOREIGN_VRAM_GB (default 4 GB) are excluded — distinguishes "ComfyUI's own warm checkpoint" (allowed, fast) from "another process is squatting" (skip).
  • Explicit: every workflow tool now accepts a backend="..." parameter that overrides the filter.

VRAM signal sourced from a small companion custom_node (ComfyUI-ForeignVRAM) that exposes /foreign_vram[?max_foreign_gb=N] (returns 503 when over threshold so an upstream Traefik / NGINX healthcheck can also kick busy backends out of LB rotation).

Async-completion asset registration — tools/job.py

Workflows over the 30s sync timeout return a running handle. Polling get_job(prompt_id) previously returned outputs but never wrote to AssetRegistry, so list_assets / view_image couldn't find them. Now get_job walks completed-prompt outputs and registers each via AssetRegistry.register_asset. Pool stamps _pool_backend_url onto the returned history dict so the registration uses the correct hostname.

LLM-friendly response shape

Every workflow + get_job response now includes markdown_preview — a ready-to-paste markdown snippet. Image MIMEs → ![alt](url) (Claude Desktop renders inline images in chat). Audio → [🔊 filename](url). Video → [🎬 ...]. 3D → [🎲 ...]. Tool descriptions are augmented with a hint to paste the field.

Optional topic slug + auto-derive

Every workflow tool accepts an optional topic arg (1–2 word slug). Server slugifies and injects between the date and workflow id in filename_prefix:

images/2026-05-03-giraffe_picture_sdxl_t2i_00001_.png

When the LLM skips topic, the server auto-derives one — first from prompt/tags (drop English stopwords + generic words like realistic, image); failing that, parse the source image filename and carry the slug forward (so sdxl_t2i → hunyuan3d_i2mesh chains keep one coherent topic).

Friendlier image / audio inputs

image, image_last, and audio parameters now accept any of:

  • a full URL (existing — auto-uploaded to ComfyUI's input/)
  • an asset_id returned by a previous tool call (resolved via the registry)
  • a bare output filename (resolved via get_asset_by_identity scan over known output subfolders)

Lets the LLM chain workflows naturally without manually constructing URLs.

Mesh / 3D output keys

Output-key matching previously knew only images / image / gifs / gif / audio / audios / files. Hunyuan3D workflows emit a 3d key — files landed on disk but the MCP raised an exception and the LLM never saw the asset. Adds MESH_OUTPUT_KEYS = ("3d", "mesh", "glb", "gltf", "obj") and extends _guess_output_preferences to detect mesh-class workflows.

upload_file MCP tool — tools/upload.py (new)

A new tool for pushing arbitrary binary files (images / audio / video) into ComfyUI's input folder via base64. Single-shot for ≤ 100 KB; chunked mode (chunk_index + total_chunks + upload_id) for larger files (≤ 100 MB total) — MCP tool calls don't handle multi-MB base64 payloads gracefully otherwise.

Workflow library — 42 workflows + embedded _meta

42 workflow JSONs covering image / audio / video / 3D mesh tasks. Each has a top-level _meta key with timing / resolution / best-for / param hints; WorkflowManager._load_workflow_metadata reads from there and uses it as the MCP tool description. The _meta key is stripped before submission to ComfyUI's /prompt endpoint.

Naming: {model}_{task}[_{variant}] — tasks include t2i i2i t2v i2v d2v fl2v t2a t2mv i2mesh a2v. Namespace (image / video / audio / mesh) auto-detected from the task suffix.

Output subfolder convention

Every workflow's filename_prefix writes into a typed subfolder of output/images/ for *_t2i* / *_i2i*, audio/ for *_t2a*, video/ for *_t2v* / *_i2v* / *_fl2v* / *_d2v* / sonic_a2v, mesh/ for hunyuan3d_*.

Misc fixes

  • Substitute %date:yyyy-MM-dd% patterns in filename_prefix at submit time (ComfyUI's SaveImage doesn't do it natively).
  • Translate model paths between Windows (backslash) and Linux (forward slash) so the same workflow runs on either backend.
  • Pool stamps result["backend_url"] = client.base_url after submit so asset URLs returned to the caller point at the actual backend hostname, not the registry's localhost default.
  • A pile of small JSON corrections: EmptyFlux2LatentImage / EmptyLTXVLatentVideo batch_size copy-paste bugs (set to 1), ResizeImageMaskNode DynamicCombo dot-prefix sub-fields, ComfyMathExpression values.a / values.b re-binding, removed ClownSampler paths from LTX-2.3 workflows, PARAM_AUDIO injection for LoadAudio nodes, second LoadImage node in wan22_fl2v correctly bound to PARAM_IMAGE_LAST.

Splitting

If any subset is welcome and others aren't, I can rebase individual commits onto separate branches and open them as standalone PRs. Just say which ones.

Tested

Two RTX 3090s on one host (LXC, dual-instance ComfyUI) + an RTX 4090 on a separate Windows host, fronted by Traefik LB and the multi-backend pool. All 42 workflows run end-to-end across all three backends.

svilendotorg and others added 21 commits May 2, 2026 17:12
- Add 42 production workflows (image/video/audio/3D) replacing default stubs
- Add .meta.json sidecar per workflow with timing, resolution, and best-for hints
  (loaded by _load_workflows to populate MCP tool descriptions)
- Add ui2api_full.py: advanced UI→API converter (UUID subgraphs, Reroutes,
  bypassed nodes, nested subgraphs, inject_params for PARAM_ placeholders)
- Patch workflow_manager.py: read sidecar metadata description in _load_workflows,
  skip .meta.json files in glob, date substitution in filename_prefix fields
- Patch comfyui_client.py: (upstream patches applied)
- Remove default stub workflows (basic_api_test, generate_image, generate_song)
- comfyui_pool.py: ComfyUIPool drop-in for ComfyUIClient
  - parse_backends(): COMFYUI_URLS=name=url,name=url env format
  - least-queue routing via /queue endpoint (depth 999 = unreachable)
  - explicit routing via backend= kwarg (name or URL substring)
- server.py: use ComfyUIPool when COMFYUI_URLS env set; startup check uses first pool backend
- tools/generation.py: inject backend: Optional[str] = None into all workflow tool signatures
- tools/workflow.py: add backend param to run_workflow
Adds tools/upload.py with upload_file(filename, base64_content, backend, content_type, target).
Routes through pool _pick_client; supports explicit backend param. 100MB max decoded.
MIME inferred from extension; falls back to application/octet-stream.

Tested: PNG → rtx3090, WAV → rtx4090. Both fetchable via view_url after upload.
Single-shot base64 doesn't scale — large payloads explode tool-call response tokens
and truncate the model output. Adds chunk_index/total_chunks/upload_id params so
clients can split a file into sequential ≤140 KB base64 calls. Server buffers in
memory keyed by upload_id (uuid), assembles on final chunk, posts to ComfyUI.

Also: overwrite=False default with auto-suffix (foo.jpg → foo_1.jpg) so re-tests
don't silently clobber. Stale upload sessions GC after 30 min.

Tested: 256 KB binary in 4 chunks → byte-exact roundtrip. Auto-suffix verified.
Pool now queries each backend's /foreign_vram endpoint (custom_node, served by
ComfyUI itself via pynvml) and excludes backends where another process is holding
>= POOL_MAX_FOREIGN_VRAM_GB (default 4 GB) on the pinned GPU. Distinguishes
ComfyUI's own warm-loaded checkpoint (own_vram, allowed) from external squatters
like a llama.cpp server (foreign_vram, disqualifies).

Falls back to all backends if every one is VRAM-tight (better to try than to
hard-fail). Explicit backend= override always honored.

Tested: simulated 8 GB hog on GPU 0 → pool log emits "Skipping rtx3090-0: foreign
VRAM 8.9GB >= 4.0GB" and routes elsewhere. After hog dies, foreign returns to 0.
…/mesh)

Centralized output dir surfaced an inconsistency — some workflows wrote to
output/ root, others to subfolders. Now uniform:
- images/ for all *_t2i and *_i2i workflows
- audio/ for all *_t2a workflows
- mesh/ for hunyuan3d_*
- video/ already correct (unchanged)

31 changes across 23 workflows. qwen25_i2i_angles has 8 SaveImage nodes (one
per camera angle) — all updated.
ComfyUIPool was a partial drop-in replacement — run_custom_workflow worked but
get_queue_status / get_job / cancel_job MCP tools all errored with
"ComfyUIPool object has no attribute get_queue" because those methods existed
only on ComfyUIClient, not the pool wrapper.

- get_queue: aggregates queue_running + queue_pending across all backends, tags
  each entry with the backend name so callers can see pool-wide queue depth.
- get_history(prompt_id): fans out, returns the first match. Each prompt_id
  lives on exactly one backend (the one it ran on) so the result is unambiguous.
- cancel_prompt: fans out; only the backend running the prompt will succeed.
When a workflow exceeds the 30s sync timeout, run_custom_workflow returns a
"running" handle and the asset never gets registered. Subsequent get_job
calls returned the outputs but list_assets / view_image saw nothing because
the registry was empty.

Now:
- get_job walks completed-prompt outputs and registers each via the
  AssetRegistry. Idempotent (registry dedupes on filename+subfolder+type).
- ComfyUIPool.get_history stamps _pool_backend_url onto the returned dict.
- get_job uses that to set the asset record base_url so generated URLs
  point at the correct backend hostname (not localhost:8188 fallback).
- ComfyUIPool.get_queue now returns the same flat list[list] shape as
  ComfyUIClient.get_queue (no per-backend wrapping) so existing
  prompt_id matching code works unchanged.

Tested: ltx2_t2v-style async submit → get_job → list_assets shows the
video with correct comfyui<N>.svilen.org asset_url.
Adds a ready-to-paste Markdown snippet to every tool result so the assistant
can render the asset inline in chat by simply echoing the field. Image MIMEs
get "![alt](url)" (rendered as image by Claude Desktop); audio/video/3D get
"[emoji label](url)" (clickable link — Claude Desktop does not inline-render
non-image content blocks).

Background: Anthropic moved MCP image content into the collapsed tool-output
expander, so FastMCP Image responses are no longer visible inline. The fix
relies on the LLM emitting markdown in the conversation text, which Claude
Desktop renders natively.

Tool descriptions now include a one-liner instructing the LLM to use the field.
Workflows like hunyuan3d_i2mesh, hunyuan3d_t2mv, sonic_a2v take only an image
(and audio) — no prompt/tags to auto-derive a topic from. Now the resolver
parses the source filename for an embedded topic slug and reuses it.

Mechanism: regex out _NNNNN_, _comfyui<N>, and the trailing workflow_id
(matched against the loaded _KNOWN_WORKFLOW_IDS list, longest-first); strip
the leading date prefix; whatever remains is the prior topic. Empty when
the source had no topic baked in either.

Result: 3D mesh / sonic video filenames inherit the topic of the input
image — chained workflows keep a coherent slug across the chain.
ComfyUIClient._extract_first_asset_info raised an exception for hunyuan3d_*
because their output nodes use the key 3d/mesh which was not in the
preferred_output_keys set (only images/audio/video). The mesh file landed
on disk fine but the MCP tool error prevented asset registration.

Adds MESH_OUTPUT_KEYS = ("3d", "mesh", "glb", "gltf", "obj") and
extends _guess_output_preferences to detect mesh-class workflows
(hy3d / hunyuan3d / mesh / 3d in class_type).
…decars

The 42 sidecar files have been merged into their workflow JSONs under a
top-level "_meta" key. Loader now reads from there directly; submit
pipeline strips _meta from the in-memory copy so ComfyUIs /prompt never
sees it.

Result: half as many files in workflows/, single-source-of-truth metadata,
no behaviour change for callers.
@svilendotorg
svilendotorg requested a review from joenorton as a code owner May 3, 2026 11:14
@svilendotorg

Copy link
Copy Markdown
Author

@joenorton

svilendotorg and others added 5 commits May 6, 2026 23:21
… off)

Every *.json in workflows/ was registered both as a typed MCP tool and
via the run_workflow/list_workflows dispatcher that already covers it.
With ~40 workflows this is ~20k tokens of duplicate schema in client
context per session.

New env var COMFY_MCP_REGISTER_PER_WORKFLOW_TOOLS (default: false) gates
the per-workflow registration. Default-off saves the context; set to
true to restore the legacy per-workflow tool layout. Dispatcher and
regenerate tools are unaffected.
The dispatcher (run_workflow) loads workflows via load_workflow which
did not strip the _meta metadata key, causing ComfyUI to reject the
workflow with 'missing_node_type' on the _meta entry. The per-workflow
tool path went through render_workflow which was already _meta-aware,
so the bug only surfaces when run_workflow is the primary entrypoint.
Clones of flux_klein_9b_t2i and flux_klein_9b_i2i_distilled with the UNet
swapped to fluxtraitFLUX2KleinFLUXZ_klein9bV3.safetensors (Flux 2 Klein 9B
BF16, FluxTrait portrait/skin tune from CivitAI 2086049). Trigger word:
Fluxtrait. Verified e2e on comfyui1, comfyui2, comfyui3.
Pony Diffusion V6 XL + serene-yoga-studio Pony LoRA (0.8) + YogaTreePony
pose LoRA (0.8). DPM++ 2M Karras, CFG 7, 30 steps, 832x1216. Trigger words
baked into the recommended prompt prefix; documented in _meta. Verified
e2e on comfyui1, comfyui2, comfyui3.
pony_monika_t2i: Pony V6 XL + monika-pony-v1 LoRA (0.85). Standalone
likeness for any prompt.

pony_yoga_monika_t2i: 3-LoRA stack \(serene-yoga-studio 0.8 + YogaTreePony
0.8 + monika-pony-v1 0.85\). One-shot "Monika doing tree pose in a yoga
studio".

Both verified e2e on comfyui1 - generation succeeds, image saved.
ralphgrewe added a commit to ralphgrewe/comfyui-mcp-server that referenced this pull request Jul 18, 2026
…oenorton#15)

publish_asset previously could only write into the server's own fixed
publish_root (project_root/public/gen at startup), with no way to target
a caller-specified directory. Add an optional target_subdir param,
backed by a persisted publish_base_dir (configured once via the new
set_publish_base_dir tool, mirroring set_comfyui_output_root), so a
caller can publish straight into e.g. a game's assets/ folder without
going through the demo publish_root. target_subdir is validated
(relative, no traversal) and containment-checked against the base dir
on every call.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant