feat: expand workflow template library#26
Conversation
There was a problem hiding this comment.
Pull request overview
Expands the built-in ComfyUI workflow template registry to include additional “expanded” pipelines (ControlNet, IP-Adapter, LoRA stacking, face restoration, Flux, SDXL) and adds/updates tests and documentation so these templates can be created via create_workflow with parameter overrides.
Changes:
- Added multiple new workflow templates (ControlNet canny/depth/openpose, IP-Adapter, LoRA stack, face restore, Flux txt2img, SDXL txt2img) and new parameter override mappings.
- Added test coverage for the new templates and for creating an expanded template via the MCP
create_workflowtool. - Updated tool/help documentation (docstring + README) to list the newly supported templates.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/comfyui_mcp/workflow/templates.py |
Adds new template definitions and extends the parameter override map + registry. |
src/comfyui_mcp/tools/workflow.py |
Updates create_workflow documentation to include the new templates/params. |
tests/test_workflow_templates.py |
Adds tests validating node presence and param overrides for the new templates. |
tests/test_tools_workflow.py |
Adds an integration-style test for creating an expanded template via the MCP tool. |
README.md |
Updates the public tool list to mention the expanded templates supported by create_workflow. |
You can also share your feedback on Copilot code review. Take the survey.
| Available templates: txt2img, img2img, upscale, inpaint, txt2vid_animatediff, | ||
| txt2vid_wan, controlnet_canny, controlnet_depth, controlnet_openpose, | ||
| ip_adapter, lora_stack, face_restore, flux_txt2img, sdxl_txt2img. | ||
|
|
||
| Args: | ||
| template: Template name (e.g. 'txt2img', 'img2img') | ||
| params: Optional JSON string of parameter overrides. | ||
| Common params: prompt, negative_prompt, width, height, | ||
| steps, cfg, model, denoise. | ||
| steps, cfg, model, denoise, controlnet_model, | ||
| control_strength, lora_name, lora_strength. |
There was a problem hiding this comment.
create_workflow now documents additional params that are effectively filenames/model identifiers (e.g. controlnet_model, lora_name, clip_vision_model, ipadapter_model). The tool currently forwards user-provided strings directly into the workflow JSON without any path/filename sanitization. Per project security rules, tools that accept filenames/subfolders should validate/sanitize these inputs (e.g. via PathSanitizer) before returning a workflow, so callers can’t accidentally (or intentionally) generate workflows that reference absolute paths / traversal sequences / null bytes.
| # --- controlnet_canny template --- | ||
| _CONTROLNET_CANNY: dict[str, dict[str, Any]] = { | ||
| "1": { | ||
| "class_type": "CheckpointLoaderSimple", | ||
| "inputs": {"ckpt_name": "v1-5-pruned-emaonly.safetensors"}, | ||
| }, | ||
| "2": { | ||
| "class_type": "LoadImage", | ||
| "inputs": {"image": "control.png"}, | ||
| }, | ||
| "3": { | ||
| "class_type": "CannyEdgePreprocessor", | ||
| "inputs": {"image": ["2", 0], "low_threshold": 100, "high_threshold": 200}, | ||
| }, | ||
| "4": { | ||
| "class_type": "ControlNetLoader", | ||
| "inputs": {"control_net_name": "control_v11p_sd15_canny.safetensors"}, | ||
| }, | ||
| "5": { | ||
| "class_type": "CLIPTextEncode", | ||
| "inputs": {"text": "", "clip": ["1", 1]}, | ||
| }, | ||
| "6": { | ||
| "class_type": "CLIPTextEncode", | ||
| "inputs": {"text": "bad quality, blurry", "clip": ["1", 1]}, | ||
| }, | ||
| "7": { | ||
| "class_type": "ControlNetApplyAdvanced", | ||
| "inputs": { | ||
| "positive": ["5", 0], | ||
| "negative": ["6", 0], | ||
| "control_net": ["4", 0], | ||
| "image": ["3", 0], | ||
| "strength": 1.0, | ||
| "start_percent": 0.0, | ||
| "end_percent": 1.0, | ||
| }, | ||
| }, | ||
| "8": { | ||
| "class_type": "EmptyLatentImage", | ||
| "inputs": {"width": 512, "height": 512, "batch_size": 1}, | ||
| }, | ||
| "9": { | ||
| "class_type": "KSampler", | ||
| "inputs": { | ||
| "seed": 0, | ||
| "steps": 20, | ||
| "cfg": 7.0, | ||
| "sampler_name": "euler", | ||
| "scheduler": "normal", | ||
| "denoise": 1.0, | ||
| "model": ["1", 0], | ||
| "positive": ["7", 0], | ||
| "negative": ["7", 1], | ||
| "latent_image": ["8", 0], | ||
| }, | ||
| }, | ||
| "10": { | ||
| "class_type": "VAEDecode", | ||
| "inputs": {"samples": ["9", 0], "vae": ["1", 2]}, | ||
| }, | ||
| "11": { | ||
| "class_type": "SaveImage", | ||
| "inputs": {"filename_prefix": "comfyui-mcp-controlnet-canny", "images": ["10", 0]}, | ||
| }, | ||
| } | ||
|
|
||
| # --- controlnet_depth template --- | ||
| _CONTROLNET_DEPTH: dict[str, dict[str, Any]] = { | ||
| "1": { | ||
| "class_type": "CheckpointLoaderSimple", | ||
| "inputs": {"ckpt_name": "v1-5-pruned-emaonly.safetensors"}, | ||
| }, | ||
| "2": { | ||
| "class_type": "LoadImage", | ||
| "inputs": {"image": "control.png"}, | ||
| }, | ||
| "3": { | ||
| "class_type": "MiDaS-DepthMapPreprocessor", | ||
| "inputs": {"image": ["2", 0], "a": 2.0, "bg_threshold": 0.1}, | ||
| }, | ||
| "4": { | ||
| "class_type": "ControlNetLoader", | ||
| "inputs": {"control_net_name": "control_v11f1p_sd15_depth.safetensors"}, | ||
| }, | ||
| "5": { | ||
| "class_type": "CLIPTextEncode", | ||
| "inputs": {"text": "", "clip": ["1", 1]}, | ||
| }, | ||
| "6": { | ||
| "class_type": "CLIPTextEncode", | ||
| "inputs": {"text": "bad quality, blurry", "clip": ["1", 1]}, | ||
| }, | ||
| "7": { | ||
| "class_type": "ControlNetApplyAdvanced", | ||
| "inputs": { | ||
| "positive": ["5", 0], | ||
| "negative": ["6", 0], | ||
| "control_net": ["4", 0], | ||
| "image": ["3", 0], | ||
| "strength": 1.0, | ||
| "start_percent": 0.0, | ||
| "end_percent": 1.0, | ||
| }, | ||
| }, | ||
| "8": { | ||
| "class_type": "EmptyLatentImage", | ||
| "inputs": {"width": 512, "height": 512, "batch_size": 1}, | ||
| }, | ||
| "9": { | ||
| "class_type": "KSampler", | ||
| "inputs": { | ||
| "seed": 0, | ||
| "steps": 20, | ||
| "cfg": 7.0, | ||
| "sampler_name": "euler", | ||
| "scheduler": "normal", | ||
| "denoise": 1.0, | ||
| "model": ["1", 0], | ||
| "positive": ["7", 0], | ||
| "negative": ["7", 1], | ||
| "latent_image": ["8", 0], | ||
| }, | ||
| }, | ||
| "10": { | ||
| "class_type": "VAEDecode", | ||
| "inputs": {"samples": ["9", 0], "vae": ["1", 2]}, | ||
| }, | ||
| "11": { | ||
| "class_type": "SaveImage", | ||
| "inputs": {"filename_prefix": "comfyui-mcp-controlnet-depth", "images": ["10", 0]}, | ||
| }, | ||
| } | ||
|
|
||
| # --- controlnet_openpose template --- | ||
| _CONTROLNET_OPENPOSE: dict[str, dict[str, Any]] = { | ||
| "1": { | ||
| "class_type": "CheckpointLoaderSimple", | ||
| "inputs": {"ckpt_name": "v1-5-pruned-emaonly.safetensors"}, | ||
| }, | ||
| "2": { | ||
| "class_type": "LoadImage", | ||
| "inputs": {"image": "control.png"}, | ||
| }, | ||
| "3": { | ||
| "class_type": "DWPreprocessor", | ||
| "inputs": {"image": ["2", 0], "resolution": 512}, | ||
| }, | ||
| "4": { | ||
| "class_type": "ControlNetLoader", | ||
| "inputs": {"control_net_name": "control_v11p_sd15_openpose.safetensors"}, | ||
| }, | ||
| "5": { | ||
| "class_type": "CLIPTextEncode", | ||
| "inputs": {"text": "", "clip": ["1", 1]}, | ||
| }, | ||
| "6": { | ||
| "class_type": "CLIPTextEncode", | ||
| "inputs": {"text": "bad quality, blurry", "clip": ["1", 1]}, | ||
| }, | ||
| "7": { | ||
| "class_type": "ControlNetApplyAdvanced", | ||
| "inputs": { | ||
| "positive": ["5", 0], | ||
| "negative": ["6", 0], | ||
| "control_net": ["4", 0], | ||
| "image": ["3", 0], | ||
| "strength": 1.0, | ||
| "start_percent": 0.0, | ||
| "end_percent": 1.0, | ||
| }, | ||
| }, | ||
| "8": { | ||
| "class_type": "EmptyLatentImage", | ||
| "inputs": {"width": 512, "height": 512, "batch_size": 1}, | ||
| }, | ||
| "9": { | ||
| "class_type": "KSampler", | ||
| "inputs": { | ||
| "seed": 0, | ||
| "steps": 20, | ||
| "cfg": 7.0, | ||
| "sampler_name": "euler", | ||
| "scheduler": "normal", | ||
| "denoise": 1.0, | ||
| "model": ["1", 0], | ||
| "positive": ["7", 0], | ||
| "negative": ["7", 1], | ||
| "latent_image": ["8", 0], | ||
| }, | ||
| }, | ||
| "10": { | ||
| "class_type": "VAEDecode", | ||
| "inputs": {"samples": ["9", 0], "vae": ["1", 2]}, | ||
| }, | ||
| "11": { | ||
| "class_type": "SaveImage", | ||
| "inputs": { | ||
| "filename_prefix": "comfyui-mcp-controlnet-openpose", | ||
| "images": ["10", 0], | ||
| }, | ||
| }, | ||
| } |
There was a problem hiding this comment.
These new ControlNet templates are largely copy/paste variants (canny/depth/openpose) with only the preprocessor node + default model name differing. Keeping three near-identical dict literals increases the chance of future divergence/bugs when one gets updated but the others don’t. Consider generating these templates from a shared helper/base template (e.g., build the common graph once and parameterize the preprocessor node + control_net_name/filename_prefix).
| # --- controlnet_canny template --- | |
| _CONTROLNET_CANNY: dict[str, dict[str, Any]] = { | |
| "1": { | |
| "class_type": "CheckpointLoaderSimple", | |
| "inputs": {"ckpt_name": "v1-5-pruned-emaonly.safetensors"}, | |
| }, | |
| "2": { | |
| "class_type": "LoadImage", | |
| "inputs": {"image": "control.png"}, | |
| }, | |
| "3": { | |
| "class_type": "CannyEdgePreprocessor", | |
| "inputs": {"image": ["2", 0], "low_threshold": 100, "high_threshold": 200}, | |
| }, | |
| "4": { | |
| "class_type": "ControlNetLoader", | |
| "inputs": {"control_net_name": "control_v11p_sd15_canny.safetensors"}, | |
| }, | |
| "5": { | |
| "class_type": "CLIPTextEncode", | |
| "inputs": {"text": "", "clip": ["1", 1]}, | |
| }, | |
| "6": { | |
| "class_type": "CLIPTextEncode", | |
| "inputs": {"text": "bad quality, blurry", "clip": ["1", 1]}, | |
| }, | |
| "7": { | |
| "class_type": "ControlNetApplyAdvanced", | |
| "inputs": { | |
| "positive": ["5", 0], | |
| "negative": ["6", 0], | |
| "control_net": ["4", 0], | |
| "image": ["3", 0], | |
| "strength": 1.0, | |
| "start_percent": 0.0, | |
| "end_percent": 1.0, | |
| }, | |
| }, | |
| "8": { | |
| "class_type": "EmptyLatentImage", | |
| "inputs": {"width": 512, "height": 512, "batch_size": 1}, | |
| }, | |
| "9": { | |
| "class_type": "KSampler", | |
| "inputs": { | |
| "seed": 0, | |
| "steps": 20, | |
| "cfg": 7.0, | |
| "sampler_name": "euler", | |
| "scheduler": "normal", | |
| "denoise": 1.0, | |
| "model": ["1", 0], | |
| "positive": ["7", 0], | |
| "negative": ["7", 1], | |
| "latent_image": ["8", 0], | |
| }, | |
| }, | |
| "10": { | |
| "class_type": "VAEDecode", | |
| "inputs": {"samples": ["9", 0], "vae": ["1", 2]}, | |
| }, | |
| "11": { | |
| "class_type": "SaveImage", | |
| "inputs": {"filename_prefix": "comfyui-mcp-controlnet-canny", "images": ["10", 0]}, | |
| }, | |
| } | |
| # --- controlnet_depth template --- | |
| _CONTROLNET_DEPTH: dict[str, dict[str, Any]] = { | |
| "1": { | |
| "class_type": "CheckpointLoaderSimple", | |
| "inputs": {"ckpt_name": "v1-5-pruned-emaonly.safetensors"}, | |
| }, | |
| "2": { | |
| "class_type": "LoadImage", | |
| "inputs": {"image": "control.png"}, | |
| }, | |
| "3": { | |
| "class_type": "MiDaS-DepthMapPreprocessor", | |
| "inputs": {"image": ["2", 0], "a": 2.0, "bg_threshold": 0.1}, | |
| }, | |
| "4": { | |
| "class_type": "ControlNetLoader", | |
| "inputs": {"control_net_name": "control_v11f1p_sd15_depth.safetensors"}, | |
| }, | |
| "5": { | |
| "class_type": "CLIPTextEncode", | |
| "inputs": {"text": "", "clip": ["1", 1]}, | |
| }, | |
| "6": { | |
| "class_type": "CLIPTextEncode", | |
| "inputs": {"text": "bad quality, blurry", "clip": ["1", 1]}, | |
| }, | |
| "7": { | |
| "class_type": "ControlNetApplyAdvanced", | |
| "inputs": { | |
| "positive": ["5", 0], | |
| "negative": ["6", 0], | |
| "control_net": ["4", 0], | |
| "image": ["3", 0], | |
| "strength": 1.0, | |
| "start_percent": 0.0, | |
| "end_percent": 1.0, | |
| }, | |
| }, | |
| "8": { | |
| "class_type": "EmptyLatentImage", | |
| "inputs": {"width": 512, "height": 512, "batch_size": 1}, | |
| }, | |
| "9": { | |
| "class_type": "KSampler", | |
| "inputs": { | |
| "seed": 0, | |
| "steps": 20, | |
| "cfg": 7.0, | |
| "sampler_name": "euler", | |
| "scheduler": "normal", | |
| "denoise": 1.0, | |
| "model": ["1", 0], | |
| "positive": ["7", 0], | |
| "negative": ["7", 1], | |
| "latent_image": ["8", 0], | |
| }, | |
| }, | |
| "10": { | |
| "class_type": "VAEDecode", | |
| "inputs": {"samples": ["9", 0], "vae": ["1", 2]}, | |
| }, | |
| "11": { | |
| "class_type": "SaveImage", | |
| "inputs": {"filename_prefix": "comfyui-mcp-controlnet-depth", "images": ["10", 0]}, | |
| }, | |
| } | |
| # --- controlnet_openpose template --- | |
| _CONTROLNET_OPENPOSE: dict[str, dict[str, Any]] = { | |
| "1": { | |
| "class_type": "CheckpointLoaderSimple", | |
| "inputs": {"ckpt_name": "v1-5-pruned-emaonly.safetensors"}, | |
| }, | |
| "2": { | |
| "class_type": "LoadImage", | |
| "inputs": {"image": "control.png"}, | |
| }, | |
| "3": { | |
| "class_type": "DWPreprocessor", | |
| "inputs": {"image": ["2", 0], "resolution": 512}, | |
| }, | |
| "4": { | |
| "class_type": "ControlNetLoader", | |
| "inputs": {"control_net_name": "control_v11p_sd15_openpose.safetensors"}, | |
| }, | |
| "5": { | |
| "class_type": "CLIPTextEncode", | |
| "inputs": {"text": "", "clip": ["1", 1]}, | |
| }, | |
| "6": { | |
| "class_type": "CLIPTextEncode", | |
| "inputs": {"text": "bad quality, blurry", "clip": ["1", 1]}, | |
| }, | |
| "7": { | |
| "class_type": "ControlNetApplyAdvanced", | |
| "inputs": { | |
| "positive": ["5", 0], | |
| "negative": ["6", 0], | |
| "control_net": ["4", 0], | |
| "image": ["3", 0], | |
| "strength": 1.0, | |
| "start_percent": 0.0, | |
| "end_percent": 1.0, | |
| }, | |
| }, | |
| "8": { | |
| "class_type": "EmptyLatentImage", | |
| "inputs": {"width": 512, "height": 512, "batch_size": 1}, | |
| }, | |
| "9": { | |
| "class_type": "KSampler", | |
| "inputs": { | |
| "seed": 0, | |
| "steps": 20, | |
| "cfg": 7.0, | |
| "sampler_name": "euler", | |
| "scheduler": "normal", | |
| "denoise": 1.0, | |
| "model": ["1", 0], | |
| "positive": ["7", 0], | |
| "negative": ["7", 1], | |
| "latent_image": ["8", 0], | |
| }, | |
| }, | |
| "10": { | |
| "class_type": "VAEDecode", | |
| "inputs": {"samples": ["9", 0], "vae": ["1", 2]}, | |
| }, | |
| "11": { | |
| "class_type": "SaveImage", | |
| "inputs": { | |
| "filename_prefix": "comfyui-mcp-controlnet-openpose", | |
| "images": ["10", 0], | |
| }, | |
| }, | |
| } | |
| # --- controlnet templates --- | |
| def _build_controlnet_template( | |
| preprocessor_class: str, | |
| preprocessor_inputs: dict[str, Any], | |
| control_net_name: str, | |
| filename_prefix: str, | |
| ) -> dict[str, dict[str, Any]]: | |
| return { | |
| "1": { | |
| "class_type": "CheckpointLoaderSimple", | |
| "inputs": {"ckpt_name": "v1-5-pruned-emaonly.safetensors"}, | |
| }, | |
| "2": { | |
| "class_type": "LoadImage", | |
| "inputs": {"image": "control.png"}, | |
| }, | |
| "3": { | |
| "class_type": preprocessor_class, | |
| "inputs": preprocessor_inputs, | |
| }, | |
| "4": { | |
| "class_type": "ControlNetLoader", | |
| "inputs": {"control_net_name": control_net_name}, | |
| }, | |
| "5": { | |
| "class_type": "CLIPTextEncode", | |
| "inputs": {"text": "", "clip": ["1", 1]}, | |
| }, | |
| "6": { | |
| "class_type": "CLIPTextEncode", | |
| "inputs": {"text": "bad quality, blurry", "clip": ["1", 1]}, | |
| }, | |
| "7": { | |
| "class_type": "ControlNetApplyAdvanced", | |
| "inputs": { | |
| "positive": ["5", 0], | |
| "negative": ["6", 0], | |
| "control_net": ["4", 0], | |
| "image": ["3", 0], | |
| "strength": 1.0, | |
| "start_percent": 0.0, | |
| "end_percent": 1.0, | |
| }, | |
| }, | |
| "8": { | |
| "class_type": "EmptyLatentImage", | |
| "inputs": {"width": 512, "height": 512, "batch_size": 1}, | |
| }, | |
| "9": { | |
| "class_type": "KSampler", | |
| "inputs": { | |
| "seed": 0, | |
| "steps": 20, | |
| "cfg": 7.0, | |
| "sampler_name": "euler", | |
| "scheduler": "normal", | |
| "denoise": 1.0, | |
| "model": ["1", 0], | |
| "positive": ["7", 0], | |
| "negative": ["7", 1], | |
| "latent_image": ["8", 0], | |
| }, | |
| }, | |
| "10": { | |
| "class_type": "VAEDecode", | |
| "inputs": {"samples": ["9", 0], "vae": ["1", 2]}, | |
| }, | |
| "11": { | |
| "class_type": "SaveImage", | |
| "inputs": {"filename_prefix": filename_prefix, "images": ["10", 0]}, | |
| }, | |
| } | |
| # --- controlnet_canny template --- | |
| _CONTROLNET_CANNY: dict[str, dict[str, Any]] = _build_controlnet_template( | |
| preprocessor_class="CannyEdgePreprocessor", | |
| preprocessor_inputs={ | |
| "image": ["2", 0], | |
| "low_threshold": 100, | |
| "high_threshold": 200, | |
| }, | |
| control_net_name="control_v11p_sd15_canny.safetensors", | |
| filename_prefix="comfyui-mcp-controlnet-canny", | |
| ) | |
| # --- controlnet_depth template --- | |
| _CONTROLNET_DEPTH: dict[str, dict[str, Any]] = _build_controlnet_template( | |
| preprocessor_class="MiDaS-DepthMapPreprocessor", | |
| preprocessor_inputs={ | |
| "image": ["2", 0], | |
| "a": 2.0, | |
| "bg_threshold": 0.1, | |
| }, | |
| control_net_name="control_v11f1p_sd15_depth.safetensors", | |
| filename_prefix="comfyui-mcp-controlnet-depth", | |
| ) | |
| # --- controlnet_openpose template --- | |
| _CONTROLNET_OPENPOSE: dict[str, dict[str, Any]] = _build_controlnet_template( | |
| preprocessor_class="DWPreprocessor", | |
| preprocessor_inputs={ | |
| "image": ["2", 0], | |
| "resolution": 512, | |
| }, | |
| control_net_name="control_v11p_sd15_openpose.safetensors", | |
| filename_prefix="comfyui-mcp-controlnet-openpose", | |
| ) |
|
Addressed both review threads in commit What changed:
Wiring update:
Validation:
All passed locally. |
- Add negative TTL (5 min) to ModelManagerDetector so a late-starting Model Manager is detected without requiring server restart (#13) - Add path injection prevention tests for prompt_id, node_class, task_id, and HTTP method validation (#19) - Rewrite integration tests to use register_*_tools() return dicts instead of accessing private _tool_manager._tools SDK attrs (#20) - Gate Docker image push on test/lint job success via needs: (#22) - Fix docker-compose volume paths for non-root container user (#23) - Document SSE transport security risks with warning callout (#24) - Add audit vs enforce mode operational guidance table (#25) - Add audit log rotation docs with logrotate example (#26) - Remove duplicate row in threat model table (#52) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add negative TTL (5 min) to ModelManagerDetector so a late-starting Model Manager is detected without requiring server restart (#13) - Add path injection prevention tests for prompt_id, node_class, task_id, and HTTP method validation (#19) - Rewrite integration tests to use register_*_tools() return dicts instead of accessing private _tool_manager._tools SDK attrs (#20) - Gate Docker image push on test/lint job success via needs: (#22) - Fix docker-compose volume paths for non-root container user (#23) - Document SSE transport security risks with warning callout (#24) - Add audit vs enforce mode operational guidance table (#25) - Add audit log rotation docs with logrotate example (#26) - Remove duplicate row in threat model table (#52) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Expands the built-in workflow template library beyond the original 6 templates to cover common ComfyUI patterns and model-family defaults.
What was added
src/comfyui_mcp/workflow/templates.py:controlnet_cannycontrolnet_depthcontrolnet_openposeip_adapterlora_stackface_restoreflux_txt2imgsdxl_txt2imgcontrolnet_model,control_strengthipadapter_model,ipadapter_weight,clip_vision_modellora_name,lora_strengthface_restore_model,face_restore_fidelitycreate_workflowtool docs to reflect expanded template list.create_workflowdocumentation.Tests
tests/test_workflow_templates.py:tests/test_tools_workflow.pyfor creatingcontrolnet_cannyvia MCP tool.Validation
uv run pytest -v tests/test_workflow_templates.py tests/test_tools_workflow.py(33 passed)uv run ruff check src/ tests/(passed)uv run ruff format --check src/ tests/(passed)uv run mypy src/comfyui_mcp/(passed)Closes #13