feat(mcp-image-gen): merge ComfyUI auto-start health check + systemd service

This commit is contained in:
Patrick Plate
2026-04-06 10:43:44 +02:00
3 changed files with 222 additions and 43 deletions
+81 -10
View File
@@ -62,7 +62,52 @@ huggingface-cli download comfyanonymous/flux_text_encoders \
--local-dir ~/ComfyUI/models/clip
```
## Step 4: Start ComfyUI
## Step 4: Install the systemd User Service (Recommended)
Installing ComfyUI as a systemd user service ensures it starts automatically on login and restarts on failure.
```bash
# Copy the bundled service file to the systemd user directory
mkdir -p ~/.config/systemd/user
cp ~/pi_mcps/mcp/mcp-image-gen/comfyui.service ~/.config/systemd/user/comfyui.service
# Reload systemd, enable + start the service
systemctl --user daemon-reload
systemctl --user enable --now comfyui
# Verify it is running
systemctl --user status comfyui
```
> ⚠️ `HSA_OVERRIDE_GFX_VERSION=11.0.0` is already set in the service file — it is mandatory for RX 7900 XTX on ROCm. Without it, model loading fails silently.
### Enable lingering (start ComfyUI even without a login session)
```bash
loginctl enable-linger $USER
```
This ensures the service starts at boot even before you log in — recommended for headless / homelab setups.
### Managing the service
```bash
# Follow live logs
journalctl --user -u comfyui -f
# Restart after model changes
systemctl --user restart comfyui
# Stop temporarily
systemctl --user stop comfyui
# Disable autostart
systemctl --user disable comfyui
```
## Step 5: Manual Start (without systemd)
If you prefer to start ComfyUI manually (e.g. for debugging):
```bash
cd ~/ComfyUI
@@ -74,26 +119,36 @@ HSA_OVERRIDE_GFX_VERSION=11.0.0 \
echo "ComfyUI PID: $!"
```
> ⚠️ `HSA_OVERRIDE_GFX_VERSION=11.0.0` is mandatory for RX 7900 XTX on ROCm. Without it, model loading fails silently.
## Step 5: Verify ComfyUI is Running
## Step 6: Verify ComfyUI is Running
```bash
curl http://localhost:8188/system_stats
# Should return JSON with GPU info
```
## Step 6: Configure mcp-image-gen
## Step 7: Configure mcp-image-gen
```bash
cd /home/pplate/pi_mcps/mcp/mcp-image-gen
# Environment variables (set in .roo/mcp.json or shell):
# COMFYUI_URL=http://localhost:8188
# IMAGE_OUTPUT_DIR=~/Pictures/mcp-generated
# COMFYUI_TIMEOUT=120
# COMFYUI_URL=http://localhost:8188 — ComfyUI API endpoint
# IMAGE_OUTPUT_DIR=~/Pictures/mcp-generated — where generated images are saved
# COMFYUI_TIMEOUT=120 — max wait time (seconds) per image
# COMFYUI_DIR=~/ComfyUI — path to ComfyUI install (used by auto-start)
```
### Auto-start behaviour
`mcp-image-gen` includes a **startup health check** in its lifespan. Every time the MCP server starts it:
1. Pings `http://localhost:8188/system_stats`
2. **If reachable** — logs `ComfyUI is already running ✓` and proceeds normally.
3. **If not reachable** — attempts to launch ComfyUI as a background subprocess from `COMFYUI_DIR` using `.venv/bin/python main.py --listen --port 8188` with `HSA_OVERRIDE_GFX_VERSION=11.0.0` injected automatically.
4. Polls up to 30 s for ComfyUI to become ready.
With the systemd service enabled, step 3 is never needed in practice — but the check acts as a safety net.
## Performance
| GPU | Model | Resolution | Steps | Time |
@@ -101,12 +156,28 @@ cd /home/pplate/pi_mcps/mcp/mcp-image-gen
| AMD RX 7900 XTX | FLUX.1-schnell | 1024×1024 | 4 | ~8s |
| AMD RX 7900 XTX | FLUX.1-schnell | 1280×512 | 4 | ~7s |
## Architecture Overview
```
Boot
└─ systemd --user (comfyui.service)
└─ ComfyUI at localhost:8188
VS Code / Roo Code
└─ mcp-image-gen MCP server (stdio)
├─ lifespan startup: ping localhost:8188
│ └─ if down: subprocess.Popen ComfyUI, wait ≤30s
└─ tools: generate_image, list_available_models, …
```
## Troubleshooting
| Problem | Solution |
|---|---|
| `HTTP 401` downloading model | Accept FLUX license on HuggingFace first |
| GPU not detected | Ensure `HSA_OVERRIDE_GFX_VERSION=11.0.0` is set |
| `Connection refused` from mcp-image-gen | Start ComfyUI first, check port 8188 |
| Slow generation (>60s) | ComfyUI may be running on CPU — check ROCm install |
| `Connection refused` from mcp-image-gen | Check `systemctl --user status comfyui`; or set `COMFYUI_DIR` so auto-start can locate the install |
| Slow generation (>60s) | ComfyUI may be running on CPU — check ROCm install and `HSA_OVERRIDE_GFX_VERSION` |
| Ollama image gen | As of April 2026: macOS-only, not available on Linux |
| Auto-start logs | `journalctl --user -u comfyui -f` or check mcp-image-gen server logs |
| Service not starting at boot | Run `loginctl enable-linger $USER` to enable session-less startup |
+21
View File
@@ -0,0 +1,21 @@
[Unit]
Description=ComfyUI — Local AI Image Generation (AMD ROCm / FLUX.1-schnell)
Documentation=https://github.com/comfyanonymous/ComfyUI
After=network.target
[Service]
Type=simple
WorkingDirectory=%h/ComfyUI
ExecStart=%h/ComfyUI/.venv/bin/python main.py --listen --port 8188
Restart=on-failure
RestartSec=10
# AMD RX 7900 XTX ROCm GFX override — required for correct GPU detection
Environment=HSA_OVERRIDE_GFX_VERSION=11.0.0
# Redirect output — follow with: journalctl --user -u comfyui -f
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
+120 -33
View File
@@ -4,16 +4,23 @@ import asyncio
import base64
import copy
import json
import logging
import os
import random
import re
import subprocess
import time
from contextlib import asynccontextmanager
from datetime import datetime
from pathlib import Path
from typing import Annotated
import httpx
from fastmcp import FastMCP
from mcp.types import ImageContent, TextContent
from pydantic import Field
logger = logging.getLogger("mcp-image-gen")
# ---------------------------------------------------------------------------
# Configuration
@@ -23,13 +30,112 @@ COMFYUI_URL = os.environ.get("COMFYUI_URL", "http://localhost:8188").rstrip("/")
IMAGE_OUTPUT_DIR = os.environ.get("IMAGE_OUTPUT_DIR", "~/Pictures/mcp-generated")
COMFYUI_TIMEOUT = int(os.environ.get("COMFYUI_TIMEOUT", "120"))
# Directory where ComfyUI is installed (used for auto-start only)
# Override via COMFYUI_DIR env var. Systemd service sets this automatically.
COMFYUI_DIR = Path(
os.environ.get("COMFYUI_DIR", "~/ComfyUI")
).expanduser().resolve()
# Maximum number of images allowed in a single batch call
MAX_COUNT = 10
# Path to the bundled FLUX.1-schnell workflow template
_WORKFLOW_PATH = Path(__file__).parent / "workflows" / "flux_schnell.json"
mcp = FastMCP("mcp-image-gen")
# ---------------------------------------------------------------------------
# ComfyUI health check + auto-start
# ---------------------------------------------------------------------------
async def _ping_comfyui(url: str, timeout: float = 5.0) -> bool:
"""Return True if ComfyUI is reachable at *url*/system_stats."""
try:
async with httpx.AsyncClient(timeout=timeout) as client:
resp = await client.get(f"{url}/system_stats")
return resp.status_code == 200
except (httpx.ConnectError, httpx.TimeoutException, OSError):
return False
async def check_and_start_comfyui() -> None:
"""Ping ComfyUI; if not reachable, attempt to launch it as a subprocess.
Called once at server startup from the lifespan context manager.
Uses COMFYUI_DIR to locate the installation and its venv Python.
The HSA_OVERRIDE_GFX_VERSION=11.0.0 env var is injected automatically
for AMD ROCm / RX 7900 XTX compatibility.
"""
if await _ping_comfyui(COMFYUI_URL):
logger.info("ComfyUI is already running at %s", COMFYUI_URL)
return
logger.warning(
"ComfyUI not reachable at %s — attempting to start from %s",
COMFYUI_URL, COMFYUI_DIR,
)
python = COMFYUI_DIR / ".venv" / "bin" / "python"
main_py = COMFYUI_DIR / "main.py"
if not python.exists():
logger.error(
"ComfyUI venv Python not found at %s. "
"Install ComfyUI first (see docs/wiki/pages/mcp-image-gen-ComfyUI-Setup.md).",
python,
)
return
if not main_py.exists():
logger.error(
"ComfyUI main.py not found at %s — is COMFYUI_DIR correct?",
main_py,
)
return
# Build environment: inherit current env, set ROCm override for AMD RX 7900 XTX
env = os.environ.copy()
env.setdefault("HSA_OVERRIDE_GFX_VERSION", "11.0.0")
try:
proc = subprocess.Popen(
[str(python), str(main_py), "--listen", "--port", "8188"],
cwd=str(COMFYUI_DIR),
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True, # detach from MCP server process group
)
logger.info("ComfyUI launched (PID %d) — waiting for readiness…", proc.pid)
except OSError as exc:
logger.error("Failed to start ComfyUI subprocess: %s", exc)
return
# Wait up to 30 s for ComfyUI to become ready (polls every 2 s)
wait_limit = 30
for attempt in range(wait_limit // 2):
await asyncio.sleep(2)
if await _ping_comfyui(COMFYUI_URL):
logger.info(
"ComfyUI ready at %s after ~%ds ✓", COMFYUI_URL, (attempt + 1) * 2
)
return
logger.warning(
"ComfyUI did not respond within %ds. "
"Generation calls will fail until it is ready. "
"Check logs: journalctl --user -u comfyui -f",
wait_limit,
)
@asynccontextmanager
async def lifespan(app):
"""FastMCP lifespan: run ComfyUI health check at server startup."""
await check_and_start_comfyui()
yield # server is live here
# Nothing to tear down — ComfyUI is managed by systemd, not this process
mcp = FastMCP("mcp-image-gen", lifespan=lifespan)
# ---------------------------------------------------------------------------
@@ -332,40 +438,22 @@ async def _generate_single(
@mcp.tool()
async def generate_image(
prompt: str,
width: int = 1024,
height: int = 1024,
steps: int = 4,
model: str = "flux1-schnell.safetensors",
seed: int = -1,
negative_prompt: str = "",
output_dir: str = "",
name: str = "",
count: int = 1,
prompt: Annotated[str, Field(description="Text description of the image to generate.")],
width: Annotated[int, Field(description="Image width in pixels (default: 1024).")] = 1024,
height: Annotated[int, Field(description="Image height in pixels (default: 1024).")] = 1024,
steps: Annotated[int, Field(description="Number of inference steps. FLUX.1-schnell works well at 4.")] = 4,
model: Annotated[str, Field(description="ComfyUI model filename (default: flux1-schnell.safetensors).")] = "flux1-schnell.safetensors",
seed: Annotated[int, Field(description="Random seed for reproducibility. -1 = random. When count > 1 and seed != -1, seeds are incremented per image (seed, seed+1, seed+2, ...) to produce deterministic variation.")] = -1,
negative_prompt: Annotated[str, Field(description="Things to exclude from the image (optional).")] = "",
output_dir: Annotated[str, Field(description="Override output directory. Defaults to IMAGE_OUTPUT_DIR env var or ~/Pictures/mcp-generated.")] = "",
name: Annotated[str, Field(description="Optional filename prefix. Saved as {name}_{timestamp}_{seed}.png. Useful to avoid confusion with auto-generated timestamp filenames.")] = "",
count: Annotated[int, Field(description="Number of images to generate (110). Each image is generated sequentially. Partial failures are returned inline — the batch continues even if one image fails.")] = 1,
) -> list:
"""Generate an image from a text prompt using ComfyUI.
Returns both a file path (for persistence) and an inline base64 image
(for display in Claude / Roo Code chat).
Args:
prompt: Text description of the image to generate.
width: Image width in pixels (default: 1024).
height: Image height in pixels (default: 1024).
steps: Number of inference steps. FLUX.1-schnell works well at 4.
model: ComfyUI model filename (default: flux1-schnell.safetensors).
seed: Random seed for reproducibility. -1 = random.
When count > 1 and seed != -1, seeds are incremented per image
(seed, seed+1, seed+2, ...) to produce deterministic variation.
negative_prompt: Things to exclude from the image (optional).
output_dir: Override output directory. Defaults to IMAGE_OUTPUT_DIR env var
or ~/Pictures/mcp-generated.
name: Optional filename prefix. Saved as {name}_{timestamp}_{seed}.png.
Useful to avoid confusion with auto-generated timestamp filenames.
count: Number of images to generate (110). Each image is generated
sequentially. Partial failures are returned inline — the batch
continues even if one image fails.
Returns:
Flat interleaved list: [TextContent1, ImageContent1, TextContent2, ImageContent2, ...]
On error for any single image, that slot contains only [TextContent(error)].
@@ -442,12 +530,11 @@ async def list_available_models() -> list[str]:
@mcp.tool()
async def get_generation_status(prompt_id: str) -> dict:
async def get_generation_status(
prompt_id: Annotated[str, Field(description="The prompt ID returned by a previous generate_image call.")],
) -> dict:
"""Check the status of a queued or running generation job.
Args:
prompt_id: The prompt ID returned by a previous generate_image call.
Returns:
Dict with 'status' key: "pending", "running", "completed", or "not_found".
"""