Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a275a18e58 | |||
| 20228f8d46 | |||
| 3b1d5bf35c | |||
| e12479a63a | |||
| 64c0a62b49 | |||
| f24aafec69 | |||
| 4165018ab2 | |||
| 2f01ff0639 | |||
| 7a21b02081 | |||
| 1340d3098f | |||
| 8cbeb6571b | |||
| b0ce5c55ed |
@@ -12,7 +12,9 @@
|
|||||||
"git_diff_unstaged",
|
"git_diff_unstaged",
|
||||||
"git_log",
|
"git_log",
|
||||||
"git_add",
|
"git_add",
|
||||||
"git_commit"
|
"git_commit",
|
||||||
|
"git_branch",
|
||||||
|
"git_create_branch"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"filesystem": {
|
"filesystem": {
|
||||||
|
|||||||
@@ -0,0 +1,622 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Create all 7 wiki pages for pi_mcps on Gitea."""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
GITEA_URL = "http://192.168.188.119:30008"
|
||||||
|
OWNER = "pplate"
|
||||||
|
REPO = "pi_mcps"
|
||||||
|
TOKEN = "8bf0c734ebda3e61d9c9068489ce58a2bf8d33db"
|
||||||
|
IMG_BASE = f"{GITEA_URL}/{OWNER}/{REPO}/raw/branch/main/docs/wiki/images"
|
||||||
|
|
||||||
|
PAGES = {}
|
||||||
|
|
||||||
|
PAGES["Home"] = f"""# 🔧 pi_mcps — Patrick's Homelab Monorepo
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Welcome to **pi_mcps**, Patrick's personal homelab monorepo. This repository houses MCP (Model Context Protocol) servers, Java projects, and homelab tooling — all built and maintained on a Fedora Linux workstation with an AMD Ryzen 5900X + RX 7900 XTX.
|
||||||
|
|
||||||
|
## What's in this repo?
|
||||||
|
|
||||||
|
| Directory | Contents |
|
||||||
|
|---|---|
|
||||||
|
| [`mcp/mcp-image-gen/`](../src/branch/main/mcp/mcp-image-gen) | 🎨 AI image generation via ComfyUI + FLUX.1-schnell |
|
||||||
|
| [`mcp/webscraper/`](../src/branch/main/mcp/webscraper) | 🕸️ Web scraping and data extraction |
|
||||||
|
| [`mcp/bigmind/`](../src/branch/main/mcp/bigmind) | 🧠 Persistent AI memory system |
|
||||||
|
| [`java/`](../src/branch/main/java) | ☕ Java EE / Spring projects |
|
||||||
|
| [`plans/`](../src/branch/main/plans) | 📋 Architecture decisions and health reports |
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Language:** Python 3.11+ (MCP servers), Java 17 (legacy projects)
|
||||||
|
- **MCP Framework:** FastMCP 2.x
|
||||||
|
- **Package Manager:** `uv` (all Python projects)
|
||||||
|
- **Testing:** `pytest`
|
||||||
|
- **GPU:** AMD RX 7900 XTX (ROCm / HSA)
|
||||||
|
- **Server:** TrueNAS.local at `192.168.188.119` (Gitea, Docker)
|
||||||
|
|
||||||
|
## MCP Servers
|
||||||
|
|
||||||
|
Three production-ready MCP servers power Patrick's AI development environment:
|
||||||
|
|
||||||
|
| Server | Status | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| [mcp-image-gen](mcp-image-gen) | ✅ Live | Generate images from text prompts via ComfyUI |
|
||||||
|
| [mcp-webscraper](mcp-webscraper) | ✅ Live | Scrape web pages, extract tables, fetch links |
|
||||||
|
| [BigMind](BigMind) | ✅ Live | Persistent AI memory across all sessions |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Built and maintained by Patrick Plate (pplate) · Homelab: TrueNAS.local · AI Colleague: Lumen*
|
||||||
|
"""
|
||||||
|
|
||||||
|
PAGES["MCP-Servers-Overview"] = f"""# 🔌 MCP Servers Overview
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This repo contains three production-grade MCP (Model Context Protocol) servers, each specialized for a different capability domain. Together they give Roo Code / Claude Desktop a complete set of superpowers.
|
||||||
|
|
||||||
|
## The Three Pillars
|
||||||
|
|
||||||
|
```
|
||||||
|
Roo Code / Claude Desktop
|
||||||
|
│
|
||||||
|
├── bigmind ──────────► ~/.mcp/bigmind/memory.db (persistent memory)
|
||||||
|
├── mcp-image-gen ────► ComfyUI @ localhost:8188 (image generation)
|
||||||
|
└── webscraper ───────► Internet / Intranet (web scraping)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparison Table
|
||||||
|
|
||||||
|
| Feature | mcp-image-gen | webscraper | bigmind |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Purpose** | Generate images from text | Scrape & parse web | Persistent AI memory |
|
||||||
|
| **Tools** | 4 | 7 | 15+ |
|
||||||
|
| **Backend** | ComfyUI / FLUX.1-schnell | httpx + BeautifulSoup4 | SQLite + FTS5 |
|
||||||
|
| **GPU required** | ✅ AMD RX 7900 XTX | ❌ | ❌ |
|
||||||
|
| **Tests** | 19/19 ✅ | ✅ | 297/297 ✅ |
|
||||||
|
| **Schema version** | n/a | n/a | v7 |
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- 🎨 [mcp-image-gen](mcp-image-gen) — Image generation docs
|
||||||
|
- 🕸️ [mcp-webscraper](mcp-webscraper) — Web scraping docs
|
||||||
|
- 🧠 [BigMind](BigMind) — Memory system docs
|
||||||
|
- 🛠️ [Development Conventions](Development-Conventions) — How all servers are built
|
||||||
|
|
||||||
|
## Adding a New Server
|
||||||
|
|
||||||
|
All servers follow the [FastMCP convention](Development-Conventions). Use the `new-mcp-server` Roo skill to scaffold:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In Roo Code orchestrator, load skill:
|
||||||
|
# skill: new-mcp-server
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
PAGES["mcp-image-gen"] = f"""# 🎨 mcp-image-gen — AI Image Generation
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**mcp-image-gen** is a FastMCP server that wraps the ComfyUI REST API, enabling Roo Code and Claude Desktop to generate images directly from text prompts using FLUX.1-schnell running on an AMD RX 7900 XTX GPU.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Roo Code / Claude Desktop
|
||||||
|
│ MCP (stdio)
|
||||||
|
▼
|
||||||
|
mcp-image-gen (FastMCP, Python 3.11+)
|
||||||
|
│ HTTP REST
|
||||||
|
▼
|
||||||
|
ComfyUI @ localhost:8188
|
||||||
|
│ ROCm / HSA_OVERRIDE_GFX_VERSION=11.0.0
|
||||||
|
▼
|
||||||
|
FLUX.1-schnell (~8s/image @ 1024×1024)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `generate_image` | Generate PNG from text prompt; returns file path + inline base64 |
|
||||||
|
| `list_available_models` | List ComfyUI checkpoint models |
|
||||||
|
| `get_generation_status` | Check status of a queued/running job |
|
||||||
|
| `get_output_directory` | Return configured output directory path |
|
||||||
|
|
||||||
|
## Key Parameters — `generate_image`
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `prompt` | required | Text description of the image |
|
||||||
|
| `width` | `1024` | Image width in pixels |
|
||||||
|
| `height` | `1024` | Image height in pixels |
|
||||||
|
| `steps` | `4` | Inference steps (FLUX.1-schnell is 4-step) |
|
||||||
|
| `model` | `flux1-schnell.safetensors` | Model checkpoint name |
|
||||||
|
| `seed` | `-1` (random) | Generation seed for reproducibility |
|
||||||
|
| `negative_prompt` | `""` | Things to avoid in the image |
|
||||||
|
| `output_dir` | `~/Pictures/mcp-generated` | Where to save output PNG |
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `COMFYUI_URL` | `http://localhost:8188` | ComfyUI API endpoint |
|
||||||
|
| `IMAGE_OUTPUT_DIR` | `~/Pictures/mcp-generated` | Default output directory |
|
||||||
|
| `COMFYUI_TIMEOUT` | `120` | Request timeout in seconds |
|
||||||
|
|
||||||
|
## Return Value
|
||||||
|
|
||||||
|
The tool returns **two content items**:
|
||||||
|
1. `TextContent` — file path, seed used, elapsed time
|
||||||
|
2. `ImageContent` — base64-encoded PNG (displays inline in Roo Code chat)
|
||||||
|
|
||||||
|
> ⚠️ **Known FastMCP Bug:** Never use `fastmcp.utilities.types.Image` as return type — it breaks serialization in FastMCP 3.x. Use `mcp.types.ImageContent` directly.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
See [ComfyUI Setup Guide](mcp-image-gen-ComfyUI-Setup) for full installation instructions.
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mcp/mcp-image-gen
|
||||||
|
uv sync
|
||||||
|
# Set COMFYUI_URL if ComfyUI is not on localhost
|
||||||
|
./run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mcp/mcp-image-gen
|
||||||
|
uv run pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lumen Profile Images
|
||||||
|
|
||||||
|
The first images generated with this server were Lumen's visual identity portraits, stored in [`mcp/mcp-image-gen/lumen_profiles/`](../src/branch/main/mcp/mcp-image-gen/lumen_profiles):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*Primary profile: seed `568659042` — constellation face interpretation of Lumen.*
|
||||||
|
"""
|
||||||
|
|
||||||
|
PAGES["mcp-image-gen-ComfyUI-Setup"] = f"""# ⚙️ ComfyUI Setup Guide (AMD ROCm)
|
||||||
|
|
||||||
|
This guide covers installing ComfyUI with FLUX.1-schnell on a Fedora Linux system with an AMD GPU.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- AMD GPU with ROCm support (tested: RX 7900 XTX)
|
||||||
|
- Fedora Linux (tested: Fedora 43 / kernel 6.19)
|
||||||
|
- Python 3.11+
|
||||||
|
- ~15GB free disk space (model weights)
|
||||||
|
- HuggingFace account with FLUX license accepted
|
||||||
|
|
||||||
|
## Step 1: Install ComfyUI
|
||||||
|
|
||||||
|
ComfyUI is **not on PyPI** — must be cloned from source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~
|
||||||
|
git clone https://github.com/comfyanonymous/ComfyUI
|
||||||
|
cd ComfyUI
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
# Install PyTorch ROCm build (CRITICAL for AMD GPUs)
|
||||||
|
pip install torch torchvision --index-url https://download.pytorch.org/whl/rocm6.2
|
||||||
|
|
||||||
|
# Install ComfyUI dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Download FLUX.1-schnell
|
||||||
|
|
||||||
|
FLUX.1-schnell is **gated on HuggingFace** — you must:
|
||||||
|
1. Create a HuggingFace account
|
||||||
|
2. Accept the FLUX.1-schnell license at https://huggingface.co/black-forest-labs/FLUX.1-schnell
|
||||||
|
3. Generate an access token at https://huggingface.co/settings/tokens
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install huggingface_hub
|
||||||
|
pip install huggingface_hub
|
||||||
|
|
||||||
|
# Download model (requires HF token)
|
||||||
|
huggingface-cli download black-forest-labs/FLUX.1-schnell \\
|
||||||
|
flux1-schnell.safetensors \\
|
||||||
|
--local-dir ~/ComfyUI/models/checkpoints \\
|
||||||
|
--token YOUR_HF_TOKEN_HERE
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Download VAE and CLIP Models
|
||||||
|
|
||||||
|
FLUX.1-schnell also requires VAE and CLIP text encoders:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# VAE
|
||||||
|
huggingface-cli download black-forest-labs/FLUX.1-schnell \\
|
||||||
|
ae.safetensors \\
|
||||||
|
--local-dir ~/ComfyUI/models/vae
|
||||||
|
|
||||||
|
# CLIP models (T5 and CLIP-L)
|
||||||
|
huggingface-cli download comfyanonymous/flux_text_encoders \\
|
||||||
|
t5xxl_fp8_e4m3fn.safetensors clip_l.safetensors \\
|
||||||
|
--local-dir ~/ComfyUI/models/clip
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Start ComfyUI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/ComfyUI
|
||||||
|
|
||||||
|
# AMD GPU REQUIRES this environment variable
|
||||||
|
HSA_OVERRIDE_GFX_VERSION=11.0.0 \\
|
||||||
|
nohup .venv/bin/python main.py --listen --port 8188 > /tmp/comfyui.log 2>&1 &
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8188/system_stats
|
||||||
|
# Should return JSON with GPU info
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6: Configure mcp-image-gen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/pi_mcps/mcp/mcp-image-gen
|
||||||
|
cp .env.example .env # if exists, or set manually
|
||||||
|
|
||||||
|
# .env contents:
|
||||||
|
COMFYUI_URL=http://localhost:8188
|
||||||
|
IMAGE_OUTPUT_DIR=~/Pictures/mcp-generated
|
||||||
|
COMFYUI_TIMEOUT=120
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| GPU | Model | Resolution | Steps | Time |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| AMD RX 7900 XTX | FLUX.1-schnell | 1024×1024 | 4 | ~8s |
|
||||||
|
| AMD RX 7900 XTX | FLUX.1-schnell | 1280×512 | 4 | ~7s |
|
||||||
|
|
||||||
|
## 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 |
|
||||||
|
| Ollama image gen | As of April 2026: macOS-only, not available on Linux |
|
||||||
|
"""
|
||||||
|
|
||||||
|
PAGES["mcp-webscraper"] = f"""# 🕸️ mcp-webscraper — Web Scraping
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**mcp-webscraper** is a FastMCP server providing comprehensive web scraping and data extraction capabilities. It fetches pages, converts HTML to clean Markdown, extracts tables, links, CSS sections, metadata, and sitemaps.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `webscraper_fetch(url, max_chars=5000)` | Title + full page as Markdown + metadata |
|
||||||
|
| `webscraper_fetch_links(url, deduplicate=True)` | All `href` links found on the page |
|
||||||
|
| `webscraper_fetch_tables(url)` | All HTML tables converted to Markdown |
|
||||||
|
| `webscraper_fetch_all(url, max_chars=5000)` | Everything in one call (fetch + links + tables) |
|
||||||
|
| `webscraper_fetch_section(url, selector)` | Specific CSS selector section only |
|
||||||
|
| `webscraper_fetch_meta(url)` | Title, description, Open Graph tags |
|
||||||
|
| `webscraper_fetch_sitemap(url, max_urls=100)` | Parse sitemap.xml, return URL list |
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **HTTP client:** `httpx` (async, with SSL support)
|
||||||
|
- **HTML parser:** `BeautifulSoup4` + `lxml`
|
||||||
|
- **Markdown converter:** `html2text`
|
||||||
|
- **SSL:** Custom cert bundle for Fedora 43 compatibility
|
||||||
|
|
||||||
|
## SSL Note — Fedora 43 Comodo Root CA
|
||||||
|
|
||||||
|
Fedora 43 is missing the **Comodo AAA Services Root CA** needed for Cloudflare-protected sites. The fix is bundled at [`mcp/webscraper/certs/comodo-aaa-services-root.pem`](../src/branch/main/mcp/webscraper/certs/).
|
||||||
|
|
||||||
|
The server automatically uses this cert bundle — no manual configuration needed.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mcp/webscraper
|
||||||
|
uv sync
|
||||||
|
./run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In Roo Code / Claude Desktop via MCP:
|
||||||
|
|
||||||
|
# Fetch a page as Markdown
|
||||||
|
webscraper_fetch("https://docs.fastmcp.dev", max_chars=10000)
|
||||||
|
|
||||||
|
# Extract all links from Gitea repo
|
||||||
|
webscraper_fetch_links("http://192.168.188.119:30008/pplate/pi_mcps")
|
||||||
|
|
||||||
|
# Get all tables from a documentation page
|
||||||
|
webscraper_fetch_tables("https://pypi.org/project/fastmcp/")
|
||||||
|
|
||||||
|
# Get Open Graph metadata
|
||||||
|
webscraper_fetch_meta("https://github.com/comfyanonymous/ComfyUI")
|
||||||
|
|
||||||
|
# Fetch specific section by CSS selector
|
||||||
|
webscraper_fetch_section("https://docs.python.org", "#content")
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
PAGES["BigMind"] = f"""# 🧠 BigMind — Persistent AI Memory
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**BigMind** is the persistent memory backbone for all AI development sessions. It provides SQLite-backed tiered memory with FTS5 full-text search, hypothesis tracking, session management, and token efficiency logging. It is the reason Lumen (Patrick's AI colleague) remembers everything across sessions.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Tiered Memory
|
||||||
|
| Tier | Name | Content |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | **Session Index** | Lightweight list: ID, date, one-liner |
|
||||||
|
| 1 | **Topic Index** | Per-session topic tags and metadata |
|
||||||
|
| 2 | **Narrative** | Full 3-8 sentence session summaries |
|
||||||
|
| 3 | **Flagged Exchanges** | Specific important moments, decisions, code |
|
||||||
|
|
||||||
|
### Facts Store
|
||||||
|
Atomic, reusable knowledge pieces categorized by type:
|
||||||
|
- `user-preference` — Patrick's tool/style preferences
|
||||||
|
- `architecture-decision` — System design choices
|
||||||
|
- `codebase-convention` — How code is structured
|
||||||
|
- `environment-config` — Server IPs, paths, credentials
|
||||||
|
- `bug-pattern` — Known bugs and fixes
|
||||||
|
- `api-contract` — MCP tool signatures
|
||||||
|
|
||||||
|
## Key Tools
|
||||||
|
|
||||||
|
### Session Lifecycle
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `memory_start_session()` | Open new session, load prior context |
|
||||||
|
| `memory_end_session(...)` | Close session with summary, topics, outcome |
|
||||||
|
| `memory_announce_focus(...)` | Declare files to be touched this session |
|
||||||
|
| `memory_close_stale_sessions(...)` | Clean up crashed IDE sessions |
|
||||||
|
|
||||||
|
### Search
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `memory_search_facts(query, limit=10)` | FTS5 search over stored facts |
|
||||||
|
| `memory_search_chunks(query, limit=10)` | FTS5 search over conversation chunks |
|
||||||
|
| `memory_list_sessions(limit=20)` | Browse session history |
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `memory_store_fact(category, fact)` | Store atomic reusable fact |
|
||||||
|
| `memory_append_chunk(session_id, content, role)` | Store conversation chunk |
|
||||||
|
| `memory_flag_important(session_id, content, role, flag_reason)` | Flag critical exchange |
|
||||||
|
| `memory_log_token_save(session_id, description, tokens_saved, method_used)` | Track efficiency |
|
||||||
|
|
||||||
|
### Hypotheses
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `memory_add_hypothesis(session_id, hypothesis, confidence)` | Form testable prediction |
|
||||||
|
| `memory_resolve_hypothesis(hypothesis_id, status, resolution)` | Confirm/refute prediction |
|
||||||
|
| `memory_list_hypotheses(status)` | Review open/closed predictions |
|
||||||
|
|
||||||
|
## FTS5 Search Tips
|
||||||
|
|
||||||
|
BigMind uses SQLite FTS5 — **every token must match**. Use 2-3 focused keywords:
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ memory_search_facts("TrueNAS Docker")
|
||||||
|
✅ memory_search_facts("mcp.json config")
|
||||||
|
❌ memory_search_facts("homelab infrastructure TrueNAS Docker server") → 0 results
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stats (2026-04-04)
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|---|---|
|
||||||
|
| DB size | 744KB |
|
||||||
|
| Sessions | 98 |
|
||||||
|
| Facts | 97+ |
|
||||||
|
| Chunks | 41 |
|
||||||
|
| Schema version | v7 |
|
||||||
|
|
||||||
|
## DB Location
|
||||||
|
|
||||||
|
`~/.mcp/bigmind/memory.db` — outside the repo, never committed.
|
||||||
|
|
||||||
|
## Session Ritual
|
||||||
|
|
||||||
|
Every session **must** follow this ritual:
|
||||||
|
|
||||||
|
**Start:**
|
||||||
|
1. `memory_start_session()`
|
||||||
|
2. `memory_list_hypotheses()`
|
||||||
|
3. `memory_announce_focus(...)`
|
||||||
|
4. `memory_close_stale_sessions(...)`
|
||||||
|
|
||||||
|
**End:**
|
||||||
|
1. `memory_end_session(one_liner, topics, outcome, summary, importance)`
|
||||||
|
"""
|
||||||
|
|
||||||
|
PAGES["Development-Conventions"] = """# 🛠️ Development Conventions
|
||||||
|
|
||||||
|
All MCP servers in this repo follow a consistent set of conventions to ensure maintainability, testability, and compatibility with Roo Code tooling.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
Each MCP server lives at `mcp/<server-name>/` with this layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
mcp/<server-name>/
|
||||||
|
├── src/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── server.py ← FastMCP server entry point
|
||||||
|
├── tests/
|
||||||
|
│ └── test_server.py ← pytest test suite
|
||||||
|
├── pyproject.toml ← uv-managed dependencies
|
||||||
|
├── run.sh ← launch script
|
||||||
|
├── README.md ← server documentation
|
||||||
|
├── PLAN.md ← architecture plan (pre-implementation)
|
||||||
|
└── ASSESSMENT.md ← pre-implementation assessment
|
||||||
|
```
|
||||||
|
|
||||||
|
## FastMCP Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
|
mcp = FastMCP("server-name")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def my_tool(param: str) -> str:
|
||||||
|
\"\"\"Tool description shown to the AI.\"\"\"
|
||||||
|
return result
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
mcp.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package Management
|
||||||
|
|
||||||
|
**All projects use `uv`** — never `pip` directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create new server
|
||||||
|
uv init mcp/my-server
|
||||||
|
cd mcp/my-server
|
||||||
|
uv add fastmcp httpx
|
||||||
|
|
||||||
|
# Sync dependencies
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# Run server
|
||||||
|
uv run python src/server.py
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
uv run pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## pyproject.toml Template
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project]
|
||||||
|
name = "mcp-my-server"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"fastmcp>=2.0.0",
|
||||||
|
"httpx",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
mcp-my-server = "src.server:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Conventions
|
||||||
|
|
||||||
|
- Tests live in `tests/test_server.py`
|
||||||
|
- Use `pytest` via `uv run pytest`
|
||||||
|
- Mock external dependencies (ComfyUI, web URLs) for unit tests
|
||||||
|
- All tests must pass before committing (`git push` should only happen with green tests)
|
||||||
|
|
||||||
|
## Commit Convention
|
||||||
|
|
||||||
|
Follow **Conventional Commits** format:
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: add webscraper_fetch_section tool
|
||||||
|
fix: handle ComfyUI timeout gracefully
|
||||||
|
docs: update mcp-image-gen README with AMD setup
|
||||||
|
test: add unit tests for generate_image tool
|
||||||
|
refactor: extract workflow builder to separate module
|
||||||
|
chore: bump fastmcp to 2.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating a New MCP Server
|
||||||
|
|
||||||
|
Use the `new-mcp-server` Roo skill in MCP Builder mode for full scaffolding:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Switch to 🔧 MCP Builder mode in Roo Code
|
||||||
|
2. Say: "Create a new MCP server for <purpose>"
|
||||||
|
3. Roo will load the new-mcp-server skill and scaffold everything
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gitea Repository
|
||||||
|
|
||||||
|
Code is hosted at: `http://192.168.188.119:30008/pplate/pi_mcps`
|
||||||
|
|
||||||
|
Push with the `gitea-push` Roo skill to ensure conventional commit format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def create_wiki_page(title: str, content: str) -> bool:
|
||||||
|
content_b64 = base64.b64encode(content.encode("utf-8")).decode("ascii")
|
||||||
|
payload = json.dumps({
|
||||||
|
"title": title,
|
||||||
|
"content_base64": content_b64,
|
||||||
|
"message": f"docs: create {title} wiki page"
|
||||||
|
}).encode("utf-8")
|
||||||
|
|
||||||
|
url = f"{GITEA_URL}/api/v1/repos/{OWNER}/{REPO}/wiki/pages"
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=payload,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"token {TOKEN}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method="POST"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
data = json.loads(resp.read().decode())
|
||||||
|
print(f"✅ Created: {data.get('title', title)}")
|
||||||
|
return True
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
body = e.read().decode()
|
||||||
|
print(f"❌ Failed [{title}]: HTTP {e.code} — {body[:200]}")
|
||||||
|
return False
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"❌ Failed [{title}]: {ex}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
results = {}
|
||||||
|
for title, content in PAGES.items():
|
||||||
|
ok = create_wiki_page(title, content)
|
||||||
|
results[title] = ok
|
||||||
|
|
||||||
|
print("\n=== Summary ===")
|
||||||
|
for title, ok in results.items():
|
||||||
|
status = "✅" if ok else "❌"
|
||||||
|
print(f"{status} {title}")
|
||||||
|
|
||||||
|
total = sum(results.values())
|
||||||
|
print(f"\n{total}/{len(results)} pages created successfully")
|
||||||
|
After Width: | Height: | Size: 737 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 798 KiB |
|
After Width: | Height: | Size: 888 KiB |
|
After Width: | Height: | Size: 745 KiB |
|
After Width: | Height: | Size: 541 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 457 KiB |
|
After Width: | Height: | Size: 501 KiB |
|
After Width: | Height: | Size: 778 KiB |
|
After Width: | Height: | Size: 814 KiB |
@@ -0,0 +1,619 @@
|
|||||||
|
# mcp-image-gen — Usage Guide
|
||||||
|
|
||||||
|
> **Comprehensive reference for using the ComfyUI-backed image generation MCP server**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Prerequisites — ComfyUI Setup](#1-prerequisites--comfyui-setup)
|
||||||
|
2. [Quick Start — Running the MCP Server](#2-quick-start--running-the-mcp-server)
|
||||||
|
3. [How to Ask Lumen to Generate Images](#3-how-to-ask-lumen-to-generate-images)
|
||||||
|
4. [Available Tools](#4-available-tools)
|
||||||
|
5. [Parameters Reference](#5-parameters-reference)
|
||||||
|
6. [Output Format](#6-output-format)
|
||||||
|
7. [Environment Variables](#7-environment-variables)
|
||||||
|
8. [Test Status](#8-test-status)
|
||||||
|
9. [Prompt Tips for FLUX.1-schnell](#9-prompt-tips-for-flux1-schnell)
|
||||||
|
10. [Known Limitations](#10-known-limitations)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Prerequisites — ComfyUI Setup
|
||||||
|
|
||||||
|
### ComfyUI must be running before any image generation tool call succeeds.
|
||||||
|
|
||||||
|
The MCP server connects to ComfyUI's REST API at `http://localhost:8188`. If ComfyUI is not running, `generate_image` and `list_available_models` will return a graceful error message — no crash.
|
||||||
|
|
||||||
|
### Install ComfyUI
|
||||||
|
|
||||||
|
> ⚠️ **ComfyUI is NOT on PyPI** — `pip install comfyui` will fail with "No matching distribution found".
|
||||||
|
> It must be installed from source via `git clone`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone from source (the only correct installation method)
|
||||||
|
git clone https://github.com/comfyanonymous/ComfyUI.git
|
||||||
|
cd ComfyUI
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install PyTorch with ROCm (AMD RX 7900 XTX)
|
||||||
|
|
||||||
|
Patrick's RX 7900 XTX (gfx1100, 24GB VRAM) uses the ROCm backend. Standard CUDA builds **will not work** on AMD hardware.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PyTorch with ROCm 6.1 support
|
||||||
|
pip install torch torchvision --index-url https://download.pytorch.org/whl/rocm6.1
|
||||||
|
```
|
||||||
|
|
||||||
|
> **ROCm version note:** ROCm 7.2.1 is the current production release as of April 2026.
|
||||||
|
> Check `rocm-smi` to confirm your ROCm version before installing torch.
|
||||||
|
|
||||||
|
### Download FLUX.1-schnell (Primary Model)
|
||||||
|
|
||||||
|
FLUX.1-schnell is the recommended model — fast (4 steps), Apache 2.0 licensed, excellent quality.
|
||||||
|
|
||||||
|
> ⚠️ **FLUX.1-schnell is a gated model on HuggingFace.**
|
||||||
|
> A bare `wget` on the URL returns HTTP 401. You must:
|
||||||
|
> 1. Accept the license at https://huggingface.co/black-forest-labs/FLUX.1-schnell (click **"Agree and access repository"** — one-time)
|
||||||
|
> 2. Create a HuggingFace access token with **Read** permissions at https://huggingface.co/settings/tokens
|
||||||
|
|
||||||
|
#### Option A — `huggingface-cli` (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install the HuggingFace Hub CLI
|
||||||
|
pip install huggingface_hub
|
||||||
|
|
||||||
|
# Log in — paste your Read token when prompted
|
||||||
|
huggingface-cli login
|
||||||
|
|
||||||
|
# Download (~8GB) directly into ComfyUI checkpoints
|
||||||
|
huggingface-cli download black-forest-labs/FLUX.1-schnell \
|
||||||
|
flux1-schnell.safetensors \
|
||||||
|
--local-dir ~/ComfyUI/models/checkpoints/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option B — `wget` with Authorization header
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget --header="Authorization: Bearer hf_YOUR_TOKEN_HERE" \
|
||||||
|
https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/flux1-schnell.safetensors \
|
||||||
|
-O ~/ComfyUI/models/checkpoints/flux1-schnell.safetensors
|
||||||
|
```
|
||||||
|
|
||||||
|
> Replace `hf_YOUR_TOKEN_HERE` with your actual HuggingFace token from https://huggingface.co/settings/tokens
|
||||||
|
|
||||||
|
#### Alternative: fp8 quantized variant (~8.1GB, faster inference)
|
||||||
|
|
||||||
|
If you want slightly faster inference with near-identical quality, the fp8 quantized version is also available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
huggingface-cli download black-forest-labs/FLUX.1-schnell-fp8 \
|
||||||
|
flux1-schnell-fp8.safetensors \
|
||||||
|
--local-dir ~/ComfyUI/models/checkpoints/
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Download note:** Both variants are ~8GB — expect 10–30 minutes depending on connection speed.
|
||||||
|
|
||||||
|
You'll also need the CLIP and VAE models — see the [ComfyUI FLUX guide](https://github.com/comfyanonymous/ComfyUI/blob/master/README.md) for full model list.
|
||||||
|
|
||||||
|
### Start ComfyUI (AMD ROCm)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Standard start — listens on all interfaces at port 8188
|
||||||
|
HSA_OVERRIDE_GFX_VERSION=11.0.0 python main.py --listen
|
||||||
|
|
||||||
|
# Or with explicit port
|
||||||
|
HSA_OVERRIDE_GFX_VERSION=11.0.0 python main.py --listen --port 8188
|
||||||
|
```
|
||||||
|
|
||||||
|
> **`HSA_OVERRIDE_GFX_VERSION=11.0.0`** — Required for RX 7900 XTX (gfx1100).
|
||||||
|
> Without this, ROCm may fail to detect the GPU correctly. This tells the HIP runtime
|
||||||
|
> to treat the GPU as gfx1100 architecture.
|
||||||
|
|
||||||
|
### Verify ComfyUI is Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:8188/system_stats | python3 -m json.tool | head -20
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected response includes `system` object with `python_version`, `pytorch_version`, `embedded_python`, and `comfyui_version`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Quick Start — Running the MCP Server
|
||||||
|
|
||||||
|
### Via `run.sh` (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/pplate/pi_mcps/mcp/mcp-image-gen
|
||||||
|
./run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
[`run.sh`](run.sh) automatically:
|
||||||
|
- Sets `PATH` to include `~/.local/bin` for `uv`
|
||||||
|
- Creates `IMAGE_OUTPUT_DIR` (`~/Pictures/mcp-generated`) if it doesn't exist
|
||||||
|
- Launches the FastMCP server via `uv run src/server.py` (stdio transport)
|
||||||
|
|
||||||
|
### Via uv directly
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/pplate/pi_mcps/mcp/mcp-image-gen
|
||||||
|
uv run src/server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wired into `.roo/mcp.json`
|
||||||
|
|
||||||
|
The server is already configured in [`.roo/mcp.json`](../../.roo/mcp.json):
|
||||||
|
|
||||||
|
```json
|
||||||
|
"mcp-image-gen": {
|
||||||
|
"command": "uv",
|
||||||
|
"args": [
|
||||||
|
"--directory", "/home/pplate/pi_mcps/mcp/mcp-image-gen",
|
||||||
|
"run", "src/server.py"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"COMFYUI_URL": "http://localhost:8188",
|
||||||
|
"IMAGE_OUTPUT_DIR": "/home/pplate/Pictures/mcp-generated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Roo Code / Claude Desktop will auto-start the server when any image generation tool is invoked. The MCP server itself starts in ~1 second — ComfyUI must already be running separately.
|
||||||
|
|
||||||
|
### Install dependencies (first time)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/pplate/pi_mcps/mcp/mcp-image-gen
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. How to Ask Lumen to Generate Images
|
||||||
|
|
||||||
|
Just speak naturally. Lumen will call the appropriate MCP tool automatically.
|
||||||
|
|
||||||
|
### Basic generation
|
||||||
|
|
||||||
|
> *"Generate an image of a futuristic city at sunset"*
|
||||||
|
|
||||||
|
```
|
||||||
|
→ generate_image(prompt="futuristic city at sunset", width=1024, height=1024, steps=4)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specific style and size
|
||||||
|
|
||||||
|
> *"Create a portrait of a red fox in watercolor style, 1024x1024"*
|
||||||
|
|
||||||
|
```
|
||||||
|
→ generate_image(
|
||||||
|
prompt="portrait of a red fox, watercolor style, detailed fur, soft brushstrokes",
|
||||||
|
width=1024, height=1024
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reproducible with a fixed seed
|
||||||
|
|
||||||
|
> *"Make an image with seed 42 so I can reproduce it"*
|
||||||
|
|
||||||
|
```
|
||||||
|
→ generate_image(prompt="...", seed=42)
|
||||||
|
```
|
||||||
|
|
||||||
|
The seed is reported in the text output so you can use the same seed again.
|
||||||
|
|
||||||
|
### Landscape format
|
||||||
|
|
||||||
|
> *"Generate a wide cinematic landscape of a Norwegian fjord, 1920x1080"*
|
||||||
|
|
||||||
|
```
|
||||||
|
→ generate_image(prompt="Norwegian fjord, cinematic, golden hour", width=1920, height=1080)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Excluding unwanted elements
|
||||||
|
|
||||||
|
> *"Generate a clean product photo of a coffee mug, no background clutter, no text"*
|
||||||
|
|
||||||
|
```
|
||||||
|
→ generate_image(
|
||||||
|
prompt="product photo of a ceramic coffee mug, studio lighting, white background",
|
||||||
|
negative_prompt="clutter, text, watermark, blurry, shadows"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### More inference steps for higher quality
|
||||||
|
|
||||||
|
> *"Generate a highly detailed oil painting of a medieval castle, use 20 steps"*
|
||||||
|
|
||||||
|
```
|
||||||
|
→ generate_image(
|
||||||
|
prompt="oil painting of a medieval castle, highly detailed, dramatic lighting",
|
||||||
|
steps=20,
|
||||||
|
model="flux1-dev.safetensors" # FLUX.1-dev supports higher step counts better
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check what models are available
|
||||||
|
|
||||||
|
> *"List what models are available in ComfyUI"*
|
||||||
|
|
||||||
|
```
|
||||||
|
→ list_available_models()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check status of a long-running job
|
||||||
|
|
||||||
|
> *"What's the status of prompt ID abc-123?"*
|
||||||
|
|
||||||
|
```
|
||||||
|
→ get_generation_status(prompt_id="abc-123")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find out where images are saved
|
||||||
|
|
||||||
|
> *"Where are my generated images being saved?"*
|
||||||
|
|
||||||
|
```
|
||||||
|
→ get_output_directory()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Available Tools
|
||||||
|
|
||||||
|
### `generate_image`
|
||||||
|
|
||||||
|
Generate an image from a text prompt using ComfyUI's FLUX.1-schnell workflow.
|
||||||
|
|
||||||
|
**Full signature:**
|
||||||
|
```python
|
||||||
|
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 = "",
|
||||||
|
) -> list[TextContent | ImageContent]
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Loads the bundled `flux_schnell.json` ComfyUI API workflow template
|
||||||
|
2. Injects your prompt, dimensions, seed, model into the correct workflow nodes
|
||||||
|
3. Submits the workflow to ComfyUI via `POST /api/prompt`
|
||||||
|
4. Polls `/api/queue` every 2 seconds until the job leaves the queue
|
||||||
|
5. Fetches history via `/api/history/{prompt_id}` to find the output filename
|
||||||
|
6. Downloads the PNG from `/api/view`
|
||||||
|
7. Saves the PNG to disk as `YYYYMMDD_HHMMSS_{seed}.png`
|
||||||
|
8. Returns `[TextContent(path + metadata), ImageContent(base64 PNG)]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `list_available_models`
|
||||||
|
|
||||||
|
List all checkpoint models currently available in ComfyUI.
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def list_available_models() -> list[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
Calls `/object_info/CheckpointLoaderSimple` and extracts the checkpoint name list. Use this to discover what models are installed before passing a `model` name to `generate_image`.
|
||||||
|
|
||||||
|
**Example return:**
|
||||||
|
```json
|
||||||
|
["flux1-schnell.safetensors", "flux1-dev.safetensors", "sd_xl_base_1.0.safetensors"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_generation_status`
|
||||||
|
|
||||||
|
Check the status of a queued or running generation job.
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def get_generation_status(prompt_id: str) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
**Return values:**
|
||||||
|
|
||||||
|
| `status` | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `"pending"` | Job is in the queue, not yet started |
|
||||||
|
| `"running"` | Job is currently being processed |
|
||||||
|
| `"completed"` | Job finished — image is in ComfyUI's history |
|
||||||
|
| `"not_found"` | Unknown prompt_id — may have expired from history |
|
||||||
|
| `"error"` | ComfyUI was unreachable |
|
||||||
|
|
||||||
|
Useful when `generate_image` times out (default 120s) — the job may still be running in ComfyUI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_output_directory`
|
||||||
|
|
||||||
|
Return the absolute path where generated images will be saved.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_output_directory() -> str
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the expanded, absolute path derived from `IMAGE_OUTPUT_DIR` env var (or `~/Pictures/mcp-generated` default). The directory may not exist yet — `generate_image` creates it on first use.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Parameters Reference
|
||||||
|
|
||||||
|
Full parameter table for `generate_image`:
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `prompt` | `str` | *(required)* | Text description of the image. Goes into the positive CLIP text encoder node. |
|
||||||
|
| `width` | `int` | `1024` | Image width in pixels. FLUX.1-schnell: 512–2048 recommended. |
|
||||||
|
| `height` | `int` | `1024` | Image height in pixels. FLUX.1-schnell: 512–2048 recommended. |
|
||||||
|
| `steps` | `int` | `4` | Number of KSampler inference steps. FLUX.1-schnell is designed for 1–8 steps. |
|
||||||
|
| `model` | `str` | `"flux1-schnell.safetensors"` | Checkpoint model filename as listed by `list_available_models`. |
|
||||||
|
| `seed` | `int` | `-1` | RNG seed for reproducibility. `-1` = new random seed each call (0 to 2³²−1). |
|
||||||
|
| `negative_prompt` | `str` | `""` | Text description of things to exclude. Goes into negative CLIP encoder node. |
|
||||||
|
| `output_dir` | `str` | `""` | Override save directory. Empty = uses `IMAGE_OUTPUT_DIR` env var or default. |
|
||||||
|
|
||||||
|
### Recommended dimensions
|
||||||
|
|
||||||
|
| Use case | Width | Height |
|
||||||
|
|---|---|---|
|
||||||
|
| Square (default) | 1024 | 1024 |
|
||||||
|
| Portrait | 768 | 1024 |
|
||||||
|
| Landscape | 1024 | 768 |
|
||||||
|
| Widescreen | 1280 | 720 |
|
||||||
|
| HD widescreen | 1920 | 1080 |
|
||||||
|
| Tall portrait | 512 | 768 |
|
||||||
|
|
||||||
|
> **VRAM note:** Patrick's RX 7900 XTX has 24GB VRAM. FLUX.1-schnell requires ~8GB,
|
||||||
|
> so you can comfortably run 1920×1080 and even larger. FLUX.1-dev requires ~12GB.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Output Format
|
||||||
|
|
||||||
|
`generate_image` returns a list with **two items** when successful:
|
||||||
|
|
||||||
|
### Item 1 — `TextContent` (file path + metadata)
|
||||||
|
|
||||||
|
```
|
||||||
|
Generated: /home/pplate/Pictures/mcp-generated/20260404_121500_3847291045.png
|
||||||
|
Seed: 3847291045
|
||||||
|
Elapsed: 8.3s
|
||||||
|
Size: 1024x1024, Steps: 4, Model: flux1-schnell.safetensors
|
||||||
|
```
|
||||||
|
|
||||||
|
The filename format is `YYYYMMDD_HHMMSS_{seed}.png` — the seed is embedded so you can reproduce the exact image by passing it back as the `seed` parameter.
|
||||||
|
|
||||||
|
### Item 2 — `ImageContent` (inline base64 PNG)
|
||||||
|
|
||||||
|
The image displays **directly in Roo Code / Claude Desktop chat** as an inline image — no need to open a file browser. The same PNG is also saved to disk at the path shown in the TextContent.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"mimeType": "image/png",
|
||||||
|
"data": "<base64-encoded PNG bytes>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error responses
|
||||||
|
|
||||||
|
When ComfyUI is unreachable or an error occurs, only **one** `TextContent` is returned (no ImageContent):
|
||||||
|
|
||||||
|
```
|
||||||
|
ComfyUI not reachable at http://localhost:8188. Start it with: python main.py --listen
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Generation timed out after 120s. prompt_id=abc-123 — use get_generation_status to check
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Environment Variables
|
||||||
|
|
||||||
|
Configure via environment variables in [`.roo/mcp.json`](../../.roo/mcp.json) or shell:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `COMFYUI_URL` | `http://localhost:8188` | Base URL of the running ComfyUI REST API. Change this if ComfyUI runs on a different host or port. |
|
||||||
|
| `IMAGE_OUTPUT_DIR` | `~/Pictures/mcp-generated` | Directory where generated PNG files are saved. Supports `~` expansion. Created automatically on first generation. |
|
||||||
|
| `COMFYUI_TIMEOUT` | `120` | Maximum seconds to wait for a generation job before returning a timeout error. Increase for very large images or slow hardware. |
|
||||||
|
|
||||||
|
### Setting via shell
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export COMFYUI_URL="http://localhost:8188"
|
||||||
|
export IMAGE_OUTPUT_DIR="/home/pplate/Pictures/ai-art"
|
||||||
|
export COMFYUI_TIMEOUT="300"
|
||||||
|
./run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setting via mcp.json env block
|
||||||
|
|
||||||
|
```json
|
||||||
|
"mcp-image-gen": {
|
||||||
|
"command": "uv",
|
||||||
|
"args": ["--directory", "/home/pplate/pi_mcps/mcp/mcp-image-gen", "run", "src/server.py"],
|
||||||
|
"env": {
|
||||||
|
"COMFYUI_URL": "http://localhost:8188",
|
||||||
|
"IMAGE_OUTPUT_DIR": "/home/pplate/Pictures/mcp-generated",
|
||||||
|
"COMFYUI_TIMEOUT": "120"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Test Status
|
||||||
|
|
||||||
|
**19 pytest tests — all passing.** Tests mock all ComfyUI HTTP calls using [respx](https://lundberg.github.io/respx/). No running ComfyUI instance is needed to run the tests.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/pplate/pi_mcps/mcp/mcp-image-gen
|
||||||
|
uv run pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test coverage breakdown
|
||||||
|
|
||||||
|
| Test file | Tests | Coverage area |
|
||||||
|
|---|---|---|
|
||||||
|
| [`tests/test_server.py`](tests/test_server.py) | 19 | All 4 tools + workflow builder |
|
||||||
|
|
||||||
|
| Test name | What it verifies |
|
||||||
|
|---|---|
|
||||||
|
| `test_build_flux_workflow_structure` | Workflow has correct node class_types |
|
||||||
|
| `test_build_flux_workflow_params_injected` | All params injected into correct nodes |
|
||||||
|
| `test_negative_prompt_included` | Negative prompt goes to node 33 |
|
||||||
|
| `test_random_seed_generated` | `seed=-1` produces a valid integer in `_meta` |
|
||||||
|
| `test_list_available_models` | Returns model list from mocked `/object_info` |
|
||||||
|
| `test_list_available_models_comfyui_offline` | ConnectError → graceful error string |
|
||||||
|
| `test_get_generation_status_pending` | `prompt_id` in queue_pending → `"pending"` |
|
||||||
|
| `test_get_generation_status_running` | `prompt_id` in queue_running → `"running"` |
|
||||||
|
| `test_get_generation_status_complete` | Not in queue + in history → `"completed"` |
|
||||||
|
| `test_get_output_directory_default` | No env var → `~/Pictures/mcp-generated` expanded |
|
||||||
|
| `test_get_output_directory_custom` | Custom env var → that path returned |
|
||||||
|
| `test_generate_image_success` | Full lifecycle: queue→poll→history→view→save |
|
||||||
|
| `test_generate_image_comfyui_unavailable` | ConnectError → single TextContent error |
|
||||||
|
| `test_generate_image_timeout` | COMFYUI_TIMEOUT=0 → timeout TextContent |
|
||||||
|
| `test_generate_image_empty_prompt` | Empty string prompt → still succeeds |
|
||||||
|
| `test_generate_image_long_prompt` | 500-char prompt → not truncated, succeeds |
|
||||||
|
| `test_generate_image_invalid_model` | 404 from /prompt → error TextContent, no file saved |
|
||||||
|
| `test_generate_image_custom_output_dir` | Custom `output_dir` param → saved there, dir created |
|
||||||
|
| `test_generate_image_random_seed_variance` | `seed=-1` × 2 → different seeds, different filenames |
|
||||||
|
|
||||||
|
### Test mock stack
|
||||||
|
|
||||||
|
- **[respx](https://lundberg.github.io/respx/)** — HTTP-level mocking for all ComfyUI API endpoints
|
||||||
|
- **[Pillow](https://pillow.readthedocs.io/)** (in conftest) — generates real PNG bytes for image response fixtures
|
||||||
|
- **monkeypatch** — env vars (`IMAGE_OUTPUT_DIR`, `COMFYUI_URL`, `COMFYUI_TIMEOUT`) and server module attributes
|
||||||
|
|
||||||
|
Real image generation requires ComfyUI to be running. Tests prove the tool logic is correct at the protocol level.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Prompt Tips for FLUX.1-schnell
|
||||||
|
|
||||||
|
FLUX.1-schnell is a guidance-distilled model designed for speed at 1–8 steps. It responds differently from SDXL or SD1.5.
|
||||||
|
|
||||||
|
### Prompt structure that works well
|
||||||
|
|
||||||
|
```
|
||||||
|
[subject], [style/medium], [lighting], [camera/composition], [mood/atmosphere], [quality modifiers]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
ancient library at night, oil painting, warm candlelight, wide angle, mysterious atmosphere, highly detailed, sharp focus
|
||||||
|
```
|
||||||
|
|
||||||
|
### Style keywords
|
||||||
|
|
||||||
|
| Style | Prompt keywords |
|
||||||
|
|---|---|
|
||||||
|
| Photography | `cinematic photograph, DSLR, 85mm lens, shallow depth of field, bokeh` |
|
||||||
|
| Oil painting | `oil painting, thick brushstrokes, textured canvas, impressionist` |
|
||||||
|
| Watercolor | `watercolor painting, soft washes, paper texture, flowing colors` |
|
||||||
|
| Digital art | `digital art, concept art, artstation, octane render` |
|
||||||
|
| Anime/illustration | `anime style, cel shading, vibrant colors, clean linework` |
|
||||||
|
| Sketch | `pencil sketch, hand drawn, crosshatching, charcoal` |
|
||||||
|
|
||||||
|
### Lighting keywords
|
||||||
|
|
||||||
|
- `golden hour`, `blue hour`, `dramatic lighting`, `rim lighting`
|
||||||
|
- `studio lighting`, `soft diffused light`, `volumetric light`
|
||||||
|
- `neon glow`, `bioluminescent`, `moonlit`, `candlelight`
|
||||||
|
|
||||||
|
### What works well with FLUX.1-schnell
|
||||||
|
|
||||||
|
- **Clear subject + style** — "red panda in a cozy library, watercolor style"
|
||||||
|
- **Landscape scenes** — fjords, forests, cities, abstract environments
|
||||||
|
- **Portrait shots** — animals and characters with descriptive appearance
|
||||||
|
- **Concept art** — futuristic cities, sci-fi environments, fantasy scenes
|
||||||
|
- **Low step counts** — 4 steps is designed to be near-optimal for this model
|
||||||
|
|
||||||
|
### What to avoid
|
||||||
|
|
||||||
|
- **Booru-style tag dumps** (FLUX handles natural language better than SD1.5)
|
||||||
|
- **Contradictory instructions** — "dark AND bright", "realistic AND cartoon"
|
||||||
|
- **Overly complex scenes** at very small resolutions
|
||||||
|
|
||||||
|
### Using the negative prompt
|
||||||
|
|
||||||
|
FLUX.1-schnell has reduced CFG guidance so negative prompts have less impact than in SDXL.
|
||||||
|
Use them for broad exclusions:
|
||||||
|
|
||||||
|
```
|
||||||
|
negative_prompt="blurry, out of focus, watermark, text, signature, low quality, artifacts"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reproducibility
|
||||||
|
|
||||||
|
Always save the seed from the TextContent output if you want to reproduce a result:
|
||||||
|
|
||||||
|
```
|
||||||
|
Seed: 3847291045
|
||||||
|
```
|
||||||
|
|
||||||
|
Then pass it back: `seed=3847291045`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Known Limitations
|
||||||
|
|
||||||
|
### ComfyUI must run locally
|
||||||
|
|
||||||
|
The MCP server connects to `COMFYUI_URL` (default: `http://localhost:8188`). ComfyUI is a local application — it does not have a cloud API. You must start it before requesting image generation. The server returns a clear error message if ComfyUI is not reachable.
|
||||||
|
|
||||||
|
### Model must be pre-loaded
|
||||||
|
|
||||||
|
ComfyUI loads checkpoint models into VRAM on first use. The first generation with a model takes longer as VRAM is allocated (FLUX.1-schnell: ~8GB). Subsequent generations with the same model are faster.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify model is installed before generation
|
||||||
|
# → ask Lumen: "list available models in ComfyUI"
|
||||||
|
```
|
||||||
|
|
||||||
|
### AMD ROCm setup complexity
|
||||||
|
|
||||||
|
AMD GPU support requires:
|
||||||
|
1. ROCm drivers installed (`rocm-smi` working)
|
||||||
|
2. PyTorch built with ROCm support (not the default CUDA build)
|
||||||
|
3. `HSA_OVERRIDE_GFX_VERSION=11.0.0` for RX 7900 XTX (gfx1100)
|
||||||
|
|
||||||
|
Without these, ComfyUI will fall back to CPU — very slow (minutes per image vs. ~8 seconds on RX 7900 XTX).
|
||||||
|
|
||||||
|
Check GPU is being used:
|
||||||
|
```bash
|
||||||
|
# In another terminal while generating:
|
||||||
|
watch -n 1 rocm-smi
|
||||||
|
# VRAM usage should spike to ~8GB during generation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeout on large images
|
||||||
|
|
||||||
|
The default `COMFYUI_TIMEOUT=120` (2 minutes) may not be enough for:
|
||||||
|
- Very large resolutions (2048×2048+)
|
||||||
|
- High step counts (20+)
|
||||||
|
- First generation loading a new model
|
||||||
|
|
||||||
|
Increase via env var:
|
||||||
|
```bash
|
||||||
|
export COMFYUI_TIMEOUT=300 # 5 minutes
|
||||||
|
```
|
||||||
|
|
||||||
|
If `generate_image` returns a timeout error, the job may still be running in ComfyUI. Use `get_generation_status(prompt_id)` to check.
|
||||||
|
|
||||||
|
### Ollama image gen is macOS-only (April 2026)
|
||||||
|
|
||||||
|
Ollama launched experimental image generation in January 2026, but it is **macOS-only** as of April 2026. Linux support is announced as "coming soon." When Linux support arrives, the server can switch backends via `IMAGE_BACKEND=ollama` without changing any tool signatures.
|
||||||
|
|
||||||
|
### ComfyUI history is ephemeral
|
||||||
|
|
||||||
|
ComfyUI keeps generation history in memory — it is lost on restart. The `get_generation_status` tool will return `"not_found"` for old prompt IDs after a ComfyUI restart. The saved PNG file on disk persists regardless.
|
||||||
|
After Width: | Height: | Size: 992 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 860 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -40,7 +40,9 @@ class ComfyUIClient:
|
|||||||
|
|
||||||
async def queue_prompt(self, workflow: dict) -> str:
|
async def queue_prompt(self, workflow: dict) -> str:
|
||||||
"""Submit a workflow to ComfyUI and return the prompt_id."""
|
"""Submit a workflow to ComfyUI and return the prompt_id."""
|
||||||
payload = {"prompt": workflow}
|
# Strip internal metadata keys (e.g. "_meta") — they are not ComfyUI nodes
|
||||||
|
clean_workflow = {k: v for k, v in workflow.items() if not k.startswith("_")}
|
||||||
|
payload = {"prompt": clean_workflow}
|
||||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
resp = await client.post(f"{self.base_url}/api/prompt", json=payload)
|
resp = await client.post(f"{self.base_url}/api/prompt", json=payload)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
@@ -115,7 +117,8 @@ def build_flux_workflow(
|
|||||||
wf["27"]["inputs"]["height"] = height
|
wf["27"]["inputs"]["height"] = height
|
||||||
wf["13"]["inputs"]["steps"] = steps
|
wf["13"]["inputs"]["steps"] = steps
|
||||||
wf["13"]["inputs"]["seed"] = actual_seed
|
wf["13"]["inputs"]["seed"] = actual_seed
|
||||||
wf["30"]["inputs"]["ckpt_name"] = model
|
# Node 32 = UNETLoader (flux1-schnell.safetensors is UNet-only, not all-in-one checkpoint)
|
||||||
|
wf["32"]["inputs"]["unet_name"] = model
|
||||||
|
|
||||||
# Attach the actual seed as metadata so callers can retrieve it
|
# Attach the actual seed as metadata so callers can retrieve it
|
||||||
wf["_meta"] = {"actual_seed": actual_seed}
|
wf["_meta"] = {"actual_seed": actual_seed}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"6": {
|
"6": {
|
||||||
"class_type": "CLIPTextEncode",
|
"class_type": "CLIPTextEncode",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"clip": ["30", 1],
|
"clip": ["30", 0],
|
||||||
"text": "PROMPT_PLACEHOLDER"
|
"text": "PROMPT_PLACEHOLDER"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"class_type": "VAEDecode",
|
"class_type": "VAEDecode",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"samples": ["13", 0],
|
"samples": ["13", 0],
|
||||||
"vae": ["30", 2]
|
"vae": ["31", 0]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"9": {
|
"9": {
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"cfg": 1.0,
|
"cfg": 1.0,
|
||||||
"denoise": 1.0,
|
"denoise": 1.0,
|
||||||
"latent_image": ["27", 0],
|
"latent_image": ["27", 0],
|
||||||
"model": ["30", 0],
|
"model": ["32", 0],
|
||||||
"negative": ["33", 0],
|
"negative": ["33", 0],
|
||||||
"positive": ["6", 0],
|
"positive": ["6", 0],
|
||||||
"sampler_name": "euler",
|
"sampler_name": "euler",
|
||||||
@@ -44,15 +44,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"30": {
|
"30": {
|
||||||
"class_type": "CheckpointLoaderSimple",
|
"class_type": "DualCLIPLoader",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"ckpt_name": "flux1-schnell.safetensors"
|
"clip_name1": "t5xxl_fp8_e4m3fn.safetensors",
|
||||||
|
"clip_name2": "clip_l.safetensors",
|
||||||
|
"type": "flux",
|
||||||
|
"device": "default"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"31": {
|
||||||
|
"class_type": "VAELoader",
|
||||||
|
"inputs": {
|
||||||
|
"vae_name": "ae.safetensors"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"32": {
|
||||||
|
"class_type": "UNETLoader",
|
||||||
|
"inputs": {
|
||||||
|
"unet_name": "flux1-schnell.safetensors",
|
||||||
|
"weight_dtype": "fp8_e4m3fn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"33": {
|
"33": {
|
||||||
"class_type": "CLIPTextEncode",
|
"class_type": "CLIPTextEncode",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"clip": ["30", 1],
|
"clip": ["30", 0],
|
||||||
"text": "NEGATIVE_PLACEHOLDER"
|
"text": "NEGATIVE_PLACEHOLDER"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ COMFYUI_BASE = "http://test-comfyui:8188"
|
|||||||
# build_flux_workflow — pure function, no mocking needed
|
# build_flux_workflow — pure function, no mocking needed
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def test_build_flux_workflow_structure():
|
def test_build_flux_workflow_structure():
|
||||||
"""Verify build_flux_workflow returns a dict with correct node types."""
|
"""Verify build_flux_workflow returns a dict with correct node types."""
|
||||||
wf = build_flux_workflow(
|
wf = build_flux_workflow(
|
||||||
@@ -45,7 +44,9 @@ def test_build_flux_workflow_structure():
|
|||||||
assert wf["9"]["class_type"] == "SaveImage"
|
assert wf["9"]["class_type"] == "SaveImage"
|
||||||
assert wf["13"]["class_type"] == "KSampler"
|
assert wf["13"]["class_type"] == "KSampler"
|
||||||
assert wf["27"]["class_type"] == "EmptySD3LatentImage"
|
assert wf["27"]["class_type"] == "EmptySD3LatentImage"
|
||||||
assert wf["30"]["class_type"] == "CheckpointLoaderSimple"
|
assert wf["30"]["class_type"] == "DualCLIPLoader"
|
||||||
|
assert wf["31"]["class_type"] == "VAELoader"
|
||||||
|
assert wf["32"]["class_type"] == "UNETLoader"
|
||||||
assert wf["33"]["class_type"] == "CLIPTextEncode"
|
assert wf["33"]["class_type"] == "CLIPTextEncode"
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ def test_build_flux_workflow_params_injected():
|
|||||||
assert wf["27"]["inputs"]["height"] == 768
|
assert wf["27"]["inputs"]["height"] == 768
|
||||||
assert wf["13"]["inputs"]["steps"] == 8
|
assert wf["13"]["inputs"]["steps"] == 8
|
||||||
assert wf["13"]["inputs"]["seed"] == 12345
|
assert wf["13"]["inputs"]["seed"] == 12345
|
||||||
assert wf["30"]["inputs"]["ckpt_name"] == "sdxl.safetensors"
|
assert wf["32"]["inputs"]["unet_name"] == "sdxl.safetensors"
|
||||||
|
|
||||||
|
|
||||||
def test_negative_prompt_included():
|
def test_negative_prompt_included():
|
||||||
@@ -103,7 +104,6 @@ def test_random_seed_generated():
|
|||||||
# list_available_models
|
# list_available_models
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@respx.mock
|
@respx.mock
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_list_available_models():
|
async def test_list_available_models():
|
||||||
@@ -146,7 +146,6 @@ async def test_list_available_models_comfyui_offline():
|
|||||||
# get_generation_status
|
# get_generation_status
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@respx.mock
|
@respx.mock
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_generation_status_pending(queue_with_pending):
|
async def test_get_generation_status_pending(queue_with_pending):
|
||||||
@@ -191,7 +190,6 @@ async def test_get_generation_status_complete(queue_empty, mock_history_response
|
|||||||
# get_output_directory
|
# get_output_directory
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def test_get_output_directory_default(monkeypatch):
|
def test_get_output_directory_default(monkeypatch):
|
||||||
"""No IMAGE_OUTPUT_DIR env var → returns expanded ~/Pictures/mcp-generated."""
|
"""No IMAGE_OUTPUT_DIR env var → returns expanded ~/Pictures/mcp-generated."""
|
||||||
monkeypatch.delenv("IMAGE_OUTPUT_DIR", raising=False)
|
monkeypatch.delenv("IMAGE_OUTPUT_DIR", raising=False)
|
||||||
@@ -216,7 +214,6 @@ def test_get_output_directory_custom(monkeypatch, tmp_path):
|
|||||||
# generate_image
|
# generate_image
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@respx.mock
|
@respx.mock
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_generate_image_success(
|
async def test_generate_image_success(
|
||||||
@@ -300,3 +297,253 @@ async def test_generate_image_timeout(monkeypatch, queue_with_pending):
|
|||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
assert "timed out" in result[0].text.lower()
|
assert "timed out" in result[0].text.lower()
|
||||||
assert "test-uuid-1234" in result[0].text
|
assert "test-uuid-1234" in result[0].text
|
||||||
|
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_generate_image_empty_prompt(tmp_path, sample_image_bytes, mock_history_response, queue_empty, monkeypatch):
|
||||||
|
"""Empty prompt → workflow has empty text in positive node, but generation succeeds."""
|
||||||
|
monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))
|
||||||
|
|
||||||
|
respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
|
||||||
|
return_value=httpx.Response(200, json={"prompt_id": "test-empty-uuid"})
|
||||||
|
)
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/queue").mock(
|
||||||
|
return_value=httpx.Response(200, json=queue_empty)
|
||||||
|
)
|
||||||
|
mock_history_empty = {
|
||||||
|
"test-empty-uuid": {
|
||||||
|
"outputs": {
|
||||||
|
"9": {
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"filename": "mcp-image-gen_00001_.png",
|
||||||
|
"subfolder": "",
|
||||||
|
"type": "output",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {"completed": True},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/history/test-empty-uuid").mock(
|
||||||
|
return_value=httpx.Response(200, json=mock_history_empty)
|
||||||
|
)
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/view").mock(
|
||||||
|
return_value=httpx.Response(200, content=sample_image_bytes)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await generate_image(prompt="", output_dir=str(tmp_path))
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
text_content = result[0]
|
||||||
|
image_content = result[1]
|
||||||
|
assert "Generated:" in text_content.text
|
||||||
|
assert str(tmp_path) in text_content.text
|
||||||
|
# Verify workflow was built with empty prompt (indirectly via success)
|
||||||
|
assert image_content.mimeType == "image/png"
|
||||||
|
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_generate_image_long_prompt(tmp_path, sample_image_bytes, mock_history_response, queue_empty, monkeypatch):
|
||||||
|
"""Very long prompt → passed as-is to workflow without truncation."""
|
||||||
|
monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))
|
||||||
|
|
||||||
|
long_prompt = "a " + "very long descriptive prompt " * 50 # ~500 chars
|
||||||
|
|
||||||
|
respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
|
||||||
|
return_value=httpx.Response(200, json={"prompt_id": "test-long-uuid"})
|
||||||
|
)
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/queue").mock(
|
||||||
|
return_value=httpx.Response(200, json=queue_empty)
|
||||||
|
)
|
||||||
|
mock_history_long = {
|
||||||
|
"test-long-uuid": {
|
||||||
|
"outputs": {
|
||||||
|
"9": {
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"filename": "mcp-image-gen_00001_.png",
|
||||||
|
"subfolder": "",
|
||||||
|
"type": "output",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {"completed": True},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/history/test-long-uuid").mock(
|
||||||
|
return_value=httpx.Response(200, json=mock_history_long)
|
||||||
|
)
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/view").mock(
|
||||||
|
return_value=httpx.Response(200, content=sample_image_bytes)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await generate_image(prompt=long_prompt, output_dir=str(tmp_path))
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
# Success implies long prompt was accepted (ComfyUI handles it)
|
||||||
|
saved_files = list(tmp_path.glob("*.png"))
|
||||||
|
assert len(saved_files) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_generate_image_invalid_model(tmp_path, monkeypatch):
|
||||||
|
"""Invalid model → ComfyUI /prompt returns 500 or 404, tool returns error TextContent."""
|
||||||
|
monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))
|
||||||
|
|
||||||
|
respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
|
||||||
|
return_value=httpx.Response(404, json={"error": "Model not found"})
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await generate_image(
|
||||||
|
prompt="a cat",
|
||||||
|
model="nonexistent-model.safetensors",
|
||||||
|
output_dir=str(tmp_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert "404" in result[0].text
|
||||||
|
assert "Model not found" in result[0].text
|
||||||
|
# No file saved
|
||||||
|
saved_files = list(tmp_path.glob("*.png"))
|
||||||
|
assert len(saved_files) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_generate_image_custom_output_dir(tmp_path, sample_image_bytes, mock_history_response, queue_empty, monkeypatch):
|
||||||
|
"""Custom output_dir → image saved there, path reflects it."""
|
||||||
|
custom_dir = tmp_path / "custom"
|
||||||
|
monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path)) # Base for default
|
||||||
|
|
||||||
|
respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
|
||||||
|
return_value=httpx.Response(200, json={"prompt_id": "test-custom-uuid"})
|
||||||
|
)
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/queue").mock(
|
||||||
|
return_value=httpx.Response(200, json=queue_empty)
|
||||||
|
)
|
||||||
|
mock_history_custom = {
|
||||||
|
"test-custom-uuid": {
|
||||||
|
"outputs": {
|
||||||
|
"9": {
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"filename": "mcp-image-gen_00001_.png",
|
||||||
|
"subfolder": "",
|
||||||
|
"type": "output",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {"completed": True},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/history/test-custom-uuid").mock(
|
||||||
|
return_value=httpx.Response(200, json=mock_history_custom)
|
||||||
|
)
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/view").mock(
|
||||||
|
return_value=httpx.Response(200, content=sample_image_bytes)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await generate_image(
|
||||||
|
prompt="a dog",
|
||||||
|
output_dir=str(custom_dir),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
text_content = result[0]
|
||||||
|
assert str(custom_dir) in text_content.text
|
||||||
|
# Directory was created
|
||||||
|
assert custom_dir.exists()
|
||||||
|
saved_files = list(custom_dir.glob("*.png"))
|
||||||
|
assert len(saved_files) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_generate_image_random_seed_variance(tmp_path, sample_image_bytes, mock_history_response, queue_empty, monkeypatch):
|
||||||
|
"""seed=-1 → different actual_seed each call, reflected in filename."""
|
||||||
|
monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))
|
||||||
|
|
||||||
|
# First generation
|
||||||
|
respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
|
||||||
|
return_value=httpx.Response(200, json={"prompt_id": "seed1-uuid"})
|
||||||
|
)
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/queue").mock(
|
||||||
|
return_value=httpx.Response(200, json=queue_empty)
|
||||||
|
)
|
||||||
|
mock_history_seed1 = {
|
||||||
|
"seed1-uuid": {
|
||||||
|
"outputs": {
|
||||||
|
"9": {
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"filename": "mcp-image-gen_00001_.png",
|
||||||
|
"subfolder": "",
|
||||||
|
"type": "output",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {"completed": True},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/history/seed1-uuid").mock(
|
||||||
|
return_value=httpx.Response(200, json=mock_history_seed1)
|
||||||
|
)
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/view").mock(
|
||||||
|
return_value=httpx.Response(200, content=sample_image_bytes)
|
||||||
|
)
|
||||||
|
|
||||||
|
result1 = await generate_image(prompt="cat", seed=-1, output_dir=str(tmp_path))
|
||||||
|
seed1 = [line for line in result1[0].text.split("\n") if "Seed:" in line][0].split(": ")[1]
|
||||||
|
filename1 = Path(result1[0].text.split("Generated: ")[1].split("\n")[0]).name
|
||||||
|
assert "Seed:" in result1[0].text
|
||||||
|
assert int(seed1) != 0 # Not default
|
||||||
|
|
||||||
|
# Reset mocks for second call
|
||||||
|
respx.reset()
|
||||||
|
respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
|
||||||
|
return_value=httpx.Response(200, json={"prompt_id": "seed2-uuid"})
|
||||||
|
)
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/queue").mock(
|
||||||
|
return_value=httpx.Response(200, json=queue_empty)
|
||||||
|
)
|
||||||
|
mock_history_seed2 = {
|
||||||
|
"seed2-uuid": {
|
||||||
|
"outputs": {
|
||||||
|
"9": {
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"filename": "mcp-image-gen_00002_.png",
|
||||||
|
"subfolder": "",
|
||||||
|
"type": "output",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {"completed": True},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/history/seed2-uuid").mock(
|
||||||
|
return_value=httpx.Response(200, json=mock_history_seed2)
|
||||||
|
)
|
||||||
|
respx.get(f"{COMFYUI_BASE}/api/view").mock(
|
||||||
|
return_value=httpx.Response(200, content=sample_image_bytes)
|
||||||
|
)
|
||||||
|
|
||||||
|
result2 = await generate_image(prompt="cat", seed=-1, output_dir=str(tmp_path))
|
||||||
|
seed2 = [line for line in result2[0].text.split("\n") if "Seed:" in line][0].split(": ")[1]
|
||||||
|
filename2 = Path(result2[0].text.split("Generated: ")[1].split("\n")[0]).name
|
||||||
|
|
||||||
|
# Different seeds and filenames
|
||||||
|
assert seed1 != seed2
|
||||||
|
assert filename1 != filename2
|
||||||
|
# Both saved
|
||||||
|
saved_files = list(tmp_path.glob("*.png"))
|
||||||
|
assert len(saved_files) == 2
|
||||||
|
|||||||