High-level architecture and design decisions for ComfyUI MCP Server.
The server bridges MCP (Model Context Protocol) and ComfyUI, providing a standardized interface for AI agents to generate media through ComfyUI workflows.
Purpose: Discovers, loads, and processes ComfyUI workflow JSON files.
Responsibilities:
- Scan
workflows/directory for JSON files - Extract parameters from
PARAM_*placeholders - Build
WorkflowToolDefinitionobjects - Render workflows with provided parameters
- Apply constrained overrides (for
run_workflow)
Key Methods:
_load_workflows(): Discovery and loading_extract_parameters(): Placeholder parsingrender_workflow(): Parameter substitutionapply_workflow_overrides(): Constrained parameter updates
Purpose: Manages default values with configurable precedence.
Precedence Order:
- Per-call values (explicit parameters)
- Runtime defaults (
set_defaultstool) - Config file (
~/.config/comfy-mcp/config.json) - Environment variables
- Hardcoded defaults
Key Methods:
get_default(): Resolve value with precedenceset_defaults(): Set runtime defaultspersist_defaults(): Write to config file
Purpose: Track generated assets for viewing and management.
Features:
- UUID-based asset IDs for external reference
- Stable identity using
(filename, subfolder, type)tuple (robust to URL changes) - TTL-based expiration (default 24 hours)
- O(1) lookups via dual-index structure (
_assetsand_asset_key_to_id) - Full provenance storage (
comfy_history,submitted_workflow) - Session tracking for conversation isolation
- Automatic cleanup of expired assets
Key Methods:
register_asset(): Register new asset with stable identity, returnAssetRecordget_asset(): Retrieve by ID (checks expiration)list_assets(): List assets with optional filtering (workflow_id, session_id)cleanup_expired(): Remove expired assets
Stable Identity Design:
Assets are identified by (filename, subfolder, folder_type) instead of URLs, making the system robust to:
- Hostname/port/base-url changes
- Resilient to ComfyUI restarts for already-known output identities
URLs are computed on-the-fly from the stable identity when needed.
Note: Stable identity prevents URL/base changes from breaking computed URLs, but does not imply persistence of the asset registry across MCP server restarts.
Purpose: Interface with ComfyUI API as a thin adapter.
Responsibilities:
- Queue workflows via
/promptendpoint - Poll for completion via
/history/{prompt_id} - Extract asset info (filename, subfolder, type) from outputs (stable identity)
- Fetch asset metadata (size, dimensions, mime type)
- Direct passthrough to ComfyUI queue and history endpoints
- Cancel queued/running jobs
Key Methods:
run_custom_workflow(): Execute workflow and wait for completion, returns stable identity + provenance_queue_workflow(): Submit workflow to ComfyUI_wait_for_prompt(): Poll until completion_extract_first_asset_info(): Extract(filename, subfolder, type)from outputsget_queue(): Direct passthrough to/queueendpointget_history(prompt_id): Direct passthrough to/historyendpointcancel_prompt(prompt_id): Cancel queued or running jobs
Thin Adapter Philosophy: The client delegates execution to ComfyUI rather than reimplementing queue logic. ComfyUI is the source of truth for job state.
WorkflowManagerscansworkflows/directory- Loads JSON files (skips
.meta.jsonfiles) - Extracts
PARAM_*placeholders - Builds parameter definitions with types and bindings
- Creates
WorkflowToolDefinitionobjects
Placeholder Format: PARAM_<TYPE?>_<NAME>
Examples:
PARAM_PROMPT→prompt: str(required)PARAM_INT_STEPS→steps: int(optional)PARAM_FLOAT_CFG→cfg: float(optional)
Binding: Maps to [node_id, input_name] in workflow JSON
register_workflow_generation_tools()iterates over definitions- Creates dynamic tool functions with proper signatures
- Handles type coercion (JSON-RPC strings → Python types)
- Registers with FastMCP via
@mcp.tool()decorator
- Tool called with parameters
render_workflow()substitutes placeholders with values- Defaults applied for missing optional parameters
- Workflow queued to ComfyUI via
/promptendpoint - Server polls for completion via
/history/{prompt_id} - Asset info extracted from outputs:
(filename, subfolder, type)(stable identity) - Full history snapshot fetched from ComfyUI
- Asset registered in
AssetRegistrywith:- Stable identity (filename, subfolder, folder_type)
- Provenance data (
comfy_history,submitted_workflow) - Session ID (if provided)
- Asset URL computed from stable identity
- Response returned with
asset_id,asset_url, and metadata
- Workflow executes in ComfyUI
- Asset saved to ComfyUI output directory
- ComfyUI returns output metadata
AssetRegistry.register_asset()called with(filename, subfolder, type)stable identity- UUID generated for
asset_id(external reference) - Stable identity key created:
f"{folder_type}:{subfolder}:{filename}" - Deduplication check: if asset with same identity exists and not expired, return existing
- Expiration time calculated (now + TTL)
AssetRecordcreated with:- Stable identity fields (filename, subfolder, folder_type)
- Provenance data (
comfy_history,submitted_workflow) - Session ID (for conversation isolation)
- Dual-index storage:
_assets[asset_id]→AssetRecord(UUID lookup)_asset_key_to_id[asset_key]→asset_id(identity lookup)
view_imagecalled withasset_idAssetRegistry.get_asset()retrieves record by UUID- Expiration checked (returns None if expired)
- Asset URL computed from stable identity:
get_asset_url(base_url) - Asset bytes fetched from ComfyUI
/viewendpoint (URL-encoded for special characters) - Image processed (downscale, re-encode as WebP)
- Base64-encoded thumbnail returned
URL Computation: URLs are computed on-the-fly from stable identity, ensuring they work even if ComfyUI base URL changes:
asset_url = f"{base_url}/view?filename={quote(filename)}&subfolder={quote(subfolder)}&type={folder_type}"- Assets expire after TTL (default 24 hours)
cleanup_expired()removes expired records- Called periodically during
view_imageoperations
Convert large images to small, context-friendly thumbnails for inline display in chat (i.e. for image injection into AI context).
- Size limit: 100KB base64 payload (configurable)
- Dimension limit: 512px max dimension (configurable)
- Format: WebP (efficient compression)
- Fetch: Download image bytes from ComfyUI
- Load: Open with Pillow, apply EXIF orientation
- Downscale: Resize to fit within
max_dim(maintain aspect ratio) - Optimize: Quality ladder (70 → 55 → 40) to fit budget
- Encode: Save as WebP, base64 encode
- Validate: Check total payload size (base64 + data URI prefix)
- Cache: Store result (LRU cache, max 100 entries)
- Better compression than PNG/JPEG
- Supports transparency
- Widely supported in modern clients
- Good quality/size tradeoff
- Context window limits in AI chat interfaces
- Base64 encoding adds ~33% overhead
- Large images cause context bloat and crashes
- Thumbnails provide visual feedback without cost
Different use cases need different defaults:
- Hardcoded: Sensible defaults for new users
- Config file: Persistent preferences
- Runtime: Session-specific overrides
- Environment: Deployment-specific settings
def get_default(namespace, key, provided_value):
if provided_value is not None:
return provided_value # Explicit wins
# Check in order of precedence
if key in runtime_defaults[namespace]:
return runtime_defaults[namespace][key]
if key in config_defaults[namespace]:
return config_defaults[namespace][key]
if key in env_defaults[namespace]:
return env_defaults[namespace][key]
if key in hardcoded_defaults[namespace]:
return hardcoded_defaults[namespace][key]
return None # No default foundWorkflow IDs are sanitized before file access:
safe_id = workflow_id.replace("/", "_").replace("\\", "_").replace("..", "_")
safe_id = "".join(c for c in safe_id if c.isalnum() or c in ("_", "-"))Resolved paths are validated to be within workflows/ directory.
Special characters in filenames are properly URL-encoded when constructing asset URLs:
from urllib.parse import quote
encoded_filename = quote(filename, safe='')
encoded_subfolder = quote(subfolder, safe='') if subfolder else ''Prevents injection attacks and ensures valid URLs for all filenames.
- Only assets generated by this server can be viewed
asset_idmust exist in registry (UUID lookup)- Expired assets are automatically removed
- No direct file system access from tools
- Asset URLs computed from stable identity (validated)
- Overrides constrained to declared parameters
- Type coercion with validation
- Constraints enforced (min/max/enum) if metadata provided
- Workflow metadata (
.meta.json) provides additional validation
- Workflows: Cached after first load (in-memory)
- Image previews: LRU cache (max 100 entries)
- Model list: Cached in
ComfyUIClient(refreshed on init)
- 1-second intervals
- Maximum 30 attempts (30 seconds)
- Exponential backoff considered but not implemented
- Asset registry: In-memory dict with dual-index structure
_assets: UUID → AssetRecord (O(1) lookup)_asset_key_to_id: Stable identity → UUID (O(1) lookup)
- Image cache: Limited to 100 entries (LRU)
- Expired assets cleaned up automatically
- Provenance data: Stored as-is (no compression), TTL limits growth
- Asset by ID: O(1) via
_assetsdict - Asset by identity: O(1) via
_asset_key_to_iddict - List assets: O(n log n) for sorting (n = total assets, typically small)
- URL encoding: Applied only when computing URLs (not stored)
comfy_history can be large for complex workflows:
- Stored as-is (no compression in v1)
- TTL-based expiration (24h) limits growth
- Future: Consider compression or selective field storage
The server provides tools for monitoring and controlling ComfyUI job execution, enabling AI agents to work asynchronously.
get_queue_status() provides async awareness:
- Check if ComfyUI is busy before submitting new jobs
- Monitor queue depth
- Determine if a job is queued vs running
Implementation:
Direct passthrough to ComfyUI /queue endpoint - no reimplementation of queue logic.
get_job(prompt_id) polls job completion:
- Checks queue first (running/queued status)
- Falls back to history endpoint for completed jobs
- Returns structured status:
completed,running,queued,error,not_found - Includes full history snapshot when available
Error Handling:
- Gracefully handles missing prompt_ids
- Distinguishes between "not found" and "error" states
- Handles ComfyUI unavailability
list_assets() enables AI memory and iteration:
- Lists recently generated assets
- Filterable by
workflow_id(e.g., only images) - Filterable by
session_id(conversation isolation) - Returns stable identity for reliable follow-ups
get_asset_metadata(asset_id) provides full provenance:
- Complete asset details (dimensions, size, type)
- Full ComfyUI history snapshot
- Original submitted workflow (enables regeneration)
- Creation and expiration timestamps
regenerate(asset_id, param_overrides) enables iteration:
- Retrieves original workflow from
submitted_workflow - Applies parameter overrides (e.g.,
{"steps": 30, "cfg": 10.0}) - Re-submits to ComfyUI with modifications
- Preserves session ID for conversation continuity
Implementation:
Uses deep copy of stored workflow, applies overrides via _update_workflow_params(), handles seed separately.
cancel_job(prompt_id) cancels queued or running jobs:
- Direct passthrough to ComfyUI
/queuewith delete action - Provides user control and resource management
- Better scalability than WebSocket
- Cloud-ready (works behind load balancers)
- Standard HTTP tooling
- Stateless (easier to scale horizontally)
Problem: URL-based identity breaks with hostname/port changes.
Solution: Use (filename, subfolder, type) tuple as stable identity:
- Works across different ComfyUI instances (localhost, 127.0.0.1, different ports)
- Resilient to ComfyUI restarts for already-known output identities (URL computation)
- URLs computed on-the-fly from base_url
- O(1) lookups via dual-index structure
Benefits:
- Robust to configuration changes
- No "thor:8188" hostname bugs
- Portable across deployments
- Globally unique external reference
- Opaque (doesn't leak internal structure)
- Standard format
- Separate from stable identity (internal lookup)
Stored Data:
comfy_history: Full/history/{prompt_id}response snapshotsubmitted_workflow: Original workflow JSON submitted to ComfyUI
Benefits:
- Free reproducibility (can regenerate with exact parameters)
- Debugging becomes trivial (see exactly what was submitted)
- Enables
regenerate()tool without re-specifying everything - Complete audit trail
Trade-offs:
- History snapshots can be large for complex workflows
- TTL-based expiration (24h) limits growth
- Future: Consider compression or selective field storage
- Automatic cleanup reduces memory usage
- No manual management needed
- Predictable behavior
- Configurable per deployment
- Lazy loading: Only fetch/process when needed
- Size control: Enforce limits at viewing time
- Format conversion: Optimize for display
- Separation of concerns: Generation vs. viewing
The server delegates execution to ComfyUI rather than reimplementing:
- Queue logic: Direct passthrough to
/queueendpoint - History tracking: Direct passthrough to
/historyendpoint - Job cancellation: Direct passthrough to
/queuewith delete action
Benefits:
- No sync issues (ComfyUI is source of truth)
- Minimal code surface
- Leverages ComfyUI's native capabilities
- Easier to maintain (changes in ComfyUI automatically reflected)
- Persistent asset registry: Database backend for production (SQLite option)
- History compression: Compress large
comfy_historysnapshots - Rate limiting: Prevent spam polling in
get_job() - Health checks: ComfyUI connectivity monitoring
- Metrics: Generation time, success rates
- Batch operations: Generate multiple assets
- Streaming: Real-time progress updates
- Session filtering enhancements: More granular conversation isolation
Current design is single-instance. For scale:
- Add database backend for asset registry
- Implement distributed locking for workflow execution
- Add queue system for high-volume scenarios
- Consider caching layer for ComfyUI responses