Skip to content

feat: expand workflow template library#26

Merged
hybridindie merged 2 commits intomainfrom
feat/issue-13-expanded-workflow-templates-v2
Mar 12, 2026
Merged

feat: expand workflow template library#26
hybridindie merged 2 commits intomainfrom
feat/issue-13-expanded-workflow-templates-v2

Conversation

@hybridindie
Copy link
Owner

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

  • Added 8 new templates in src/comfyui_mcp/workflow/templates.py:
    • controlnet_canny
    • controlnet_depth
    • controlnet_openpose
    • ip_adapter
    • lora_stack
    • face_restore
    • flux_txt2img
    • sdxl_txt2img
  • Extended template parameter mapping for new workflows:
    • controlnet_model, control_strength
    • ipadapter_model, ipadapter_weight, clip_vision_model
    • lora_name, lora_strength
    • face_restore_model, face_restore_fidelity
  • Updated create_workflow tool docs to reflect expanded template list.
  • Updated README create_workflow documentation.

Tests

  • Expanded template unit coverage in tests/test_workflow_templates.py:
    • registry includes new templates
    • new template class/type sanity checks
    • parameter override behavior checks
  • Added workflow tool coverage in tests/test_tools_workflow.py for creating controlnet_canny via 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

Copilot AI review requested due to automatic review settings March 12, 2026 19:00
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_workflow tool.
  • 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.

Comment on lines +33 to +42
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.
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +274 to +476
# --- 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],
},
},
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
# --- 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",
)

Copilot uses AI. Check for mistakes.
@hybridindie
Copy link
Owner Author

Addressed both review threads in commit 4e0ac8a.

What changed:

  • Added sanitization for filename-like create_workflow params in src/comfyui_mcp/tools/workflow.py.
    • Introduced _sanitize_template_params(...) and applied it before create_from_template(...).
    • Path-like params now go through PathSanitizer checks to block traversal/null-byte/path-separator style input.
    • Added regression test test_rejects_path_traversal_param in tests/test_tools_workflow.py.
  • Reduced duplication for ControlNet templates in src/comfyui_mcp/workflow/templates.py.
    • Added _build_controlnet_template(...) helper and rewired canny/depth/openpose templates to use it.

Wiring update:

  • Passed sanitizer into workflow tool registration from src/comfyui_mcp/server.py.

Validation:

  • uv run pytest -v tests/test_tools_workflow.py tests/test_workflow_templates.py tests/test_server.py
  • uv run ruff check src/ tests/
  • uv run ruff format --check src/ tests/
  • uv run mypy src/comfyui_mcp/

All passed locally.

@hybridindie hybridindie merged commit e7cfaa7 into main Mar 12, 2026
3 checks passed
hybridindie pushed a commit that referenced this pull request Mar 12, 2026
- 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>
hybridindie pushed a commit that referenced this pull request Mar 12, 2026
- 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>
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.

Expanded workflow template library

2 participants