From 1859ccd1d6b93fafbf192658a7782d4cf48a724d Mon Sep 17 00:00:00 2001 From: Patrick Plate Date: Thu, 11 Jun 2026 09:02:19 +0200 Subject: [PATCH] chore(homelab): add homelab plans, frpc deploy script, odysseus workspace, heretic docs --- odysseus | 1 + plans/HOMELAB-HANDOVER.md | 133 ++++++++++++++ plans/frpc-truenas-deploy.sh | 76 ++++++++ plans/heretic-encoder-swap.md | 12 +- plans/homelab-proxy-architecture.md | 262 ++++++++++++++++++++++++++++ plans/upscaler-workflow.md | 194 ++++++++++++++++++++ 6 files changed, 672 insertions(+), 6 deletions(-) create mode 160000 odysseus create mode 100644 plans/HOMELAB-HANDOVER.md create mode 100644 plans/frpc-truenas-deploy.sh create mode 100644 plans/homelab-proxy-architecture.md create mode 100644 plans/upscaler-workflow.md diff --git a/odysseus b/odysseus new file mode 160000 index 0000000..463713c --- /dev/null +++ b/odysseus @@ -0,0 +1 @@ +Subproject commit 463713c2c6a15e7d020997826bd97e71fac25138 diff --git a/plans/HOMELAB-HANDOVER.md b/plans/HOMELAB-HANDOVER.md new file mode 100644 index 0000000..051a7ed --- /dev/null +++ b/plans/HOMELAB-HANDOVER.md @@ -0,0 +1,133 @@ +# Homelab Session Handover +_Last updated: 2026-06-11 by Lumen_ + +## 🔑 SSH Access (no password needed) + +```bash +ssh-add ~/.ssh/id_ed25519_homelab +``` + +| Alias | Host | User | What it is | +|-------|------|------|-----------| +| `ssh vps` | 85.214.154.199 | root | plate.software — Strato OpenVZ, Plesk, Apache | +| `ssh ionos` | 82.165.206.45 | root | plate-software.de — IONOS, Ubuntu 18.04, Apache | +| `ssh truenas` | 192.168.188.119 | root | TrueNAS SCALE 24.10.2.4, k3s, Gitea | + +--- + +## ✅ Fully Done + +### plate.software (VPS — 85.214.154.199) +- Let's Encrypt cert valid (ACME path fixed in Plesk HTTP directives) +- `frps` v0.68.1 running systemd, port 7000, token in BigMind fact #188 +- `git.plate.software` Apache proxy → `localhost:30008` via frpc ✅ HTTP 200 +- `frpc.service` on TrueNAS tunneling port 30008 → VPS + +### IONOS (plate-software.de — 82.165.206.45) +- SSL wildcard-like cert renewed via acme.sh — now covers `git.plate-software.de` too +- Valid until ~2026-08-04 +- ownCloud, Collabora still running + +### TrueNAS — ChunkyTown ZFS Pool (rebuilt 2026-05-04) +- New pool: RAIDZ1 on `sda`+`sdb`+`sdd`+`sdl` (3 Toshibas + new Seagate WWZAXXKL) +- Hot spare: `sdk` (oldest Toshiba 3220A0PBFA3H) +- **29.1TB usable**, ONLINE, 0 errors +- Old pool was unrecoverable (2 simultaneous failures) +- Data was acceptable loss (Plex re-downloadable, photos in Google Photos) + +### TrueNAS — frpc tunnel +- Binary: `/mnt/VM_SSD_Pool/frp/frpc` +- Config: `/mnt/VM_SSD_Pool/frp/frpc.toml` +- Systemd: `frpc.service` (enabled, running) +- Gitea `app.ini`: `/mnt/VM_SSD_Pool/VM_POOL1/gitea/config/app.ini` + - `ROOT_URL = https://git.plate.software/` + - `SSH_DOMAIN = git.plate.software` + +### git.plate.software ✅ LIVE +- `curl https://git.plate.software/` → HTTP 200 + +--- + +## ✅ IONOS Gitea Mirror — FIXED 2026-06-11 + +### Status: FULLY WORKING +- `https://git.plate-software.de/` → HTTP 200 ✅ +- Gitea API → HTTP 200 ✅ +- Push mirrors syncing: `pplate/bigmind`, `pplate/cannamanage`, `pplate/pi_mcps` ✅ + +### What's running +- Gitea Docker container on IONOS: `docker ps | grep gitea-mirror` + - Port: `127.0.0.1:3000` (local only, behind Apache) + - Data: `/opt/gitea/data` + - Admin user: `pplate` (password: `HomelabGit2026!` — reset 2026-06-11) + - API token: `1e87f855d448727e9d213599d654542881bdca0f` + +### Root cause (fixed) +The `sites-enabled/` files for collabora, owncloud, and ssl.conf were **stale copies** (not symlinks) still using hostname-specific VirtualHost bindings (`collabora.plate-software.de:443`, `owncloud.plate-software.de:443`, `plate-software.de:443`). These resolved to `82.165.206.45:443` and Apache treated that as a separate higher-priority NameVirtualHost group — intercepting all git smart HTTP requests before the `*:443` git vhost was ever consulted. + +**Fix applied 2026-06-11:** +```bash +sed -i "s|VirtualHost collabora.plate-software.de:443|VirtualHost *:443|g" /etc/apache2/sites-enabled/collabora.plate-software.de.conf +sed -i "s|VirtualHost collabora.plate-software.de:80|VirtualHost *:80|g" /etc/apache2/sites-enabled/collabora.plate-software.de.conf +sed -i "s|VirtualHost owncloud.plate-software.de:443|VirtualHost *:443|g" /etc/apache2/sites-enabled/owncloud.plate-software.de.conf +sed -i "s|VirtualHost owncloud.plate-software.de:80|VirtualHost *:80|g" /etc/apache2/sites-enabled/owncloud.plate-software.de.conf +sed -i "s|VirtualHost plate-software.de:443|VirtualHost *:443|g" /etc/apache2/sites-enabled/ssl.conf +systemctl reload apache2 +``` + +⚠️ **Note:** `sites-enabled/collabora`, `owncloud`, and `ssl.conf` are plain files (not symlinks to `sites-available/`). If Apache is ever reconfigured via `a2ensite`, these edits will be lost — the `sites-available/` originals still have the correct `*:443` bindings. + +--- + +## ⚠️ Other Pending Items + +### Plex (superplex app) +- Shows CRASHED in TrueNAS app panel +- Likely due to old ChunkyTown dataset paths being gone +- Fix: TrueNAS web UI → Apps → superplex → Edit → update media library paths to new `/mnt/ChunkyTown/...` datasets + +### Let's Encrypt for git.plate.software (VPS side) +- Currently no SSL cert for `git.plate.software` in Plesk +- Apache proxy works but is HTTP→HTTP (Plesk's SSL termination handles it) +- Issue cert: Plesk UI → Domains → git.plate.software → Let's Encrypt + +--- + +## 🏗️ Infrastructure Overview + +``` +Internet + ↓ DNS +plate.software VPS (85.214.154.199) + Apache/Plesk + ├── plate.software → :8080 (Docker WildFly) + └── git.plate.software → :30008 (frp tunnel ← TrueNAS) ✅ + frps :7000 ← frpc on TrueNAS ✅ + +TrueNAS.local (192.168.188.119) + ├── Gitea :30008 (ROOT_URL = https://git.plate.software/) ✅ + ├── VM_SSD_Pool (ZFS RAIDZ2, ONLINE) — Gitea data lives here + └── ChunkyTown (ZFS RAIDZ1, ONLINE, 29.1TB) — rebuilt 2026-05-04 + ├── raidz1: sda + sdb + sdd + sdl (Seagate) + └── spare: sdk + +IONOS (82.165.206.45) + Apache + ├── owncloud.plate-software.de → :8080 ✅ + ├── collabora.plate-software.de → :9980 ✅ + └── git.plate-software.de → :3000 (Gitea mirror Docker) ✅ FULLY WORKING (fixed 2026-06-11) + Docker: gitea-mirror, data: /opt/gitea/data + Token: 1e87f855d448727e9d213599d654542881bdca0f (in BigMind fact #192) + Repos: pplate/bigmind, pplate/cannamanage, pplate/pi_mcps (push mirrors from TrueNAS) +``` + +--- + +## 🗂️ Key File Locations + +| File | Purpose | +|------|---------| +| `~/.ssh/id_ed25519_homelab` | Automation SSH key | +| `~/.ssh/config` | SSH aliases (vps, ionos, truenas) | +| `plans/frpc-truenas-deploy.sh` | frpc installer (already run on TrueNAS) | +| `plans/HOMELAB-HANDOVER.md` | This file | diff --git a/plans/frpc-truenas-deploy.sh b/plans/frpc-truenas-deploy.sh new file mode 100644 index 0000000..e81f2c4 --- /dev/null +++ b/plans/frpc-truenas-deploy.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# frpc deployment script for TrueNAS +# Run from Fedora: bash plans/frpc-truenas-deploy.sh +# Installs frpc on TrueNAS and sets up tunnel to expose Gitea publicly + +TRUENAS="root@192.168.188.119" +VPS_IP="85.214.154.199" +FRP_TOKEN="5f64a6f20bb2cb8c3133ecac8ca3f0571d7d64dff910225040bfc0c60a106c81" +FRP_VERSION="0.68.1" + +echo "=== Deploying frpc on TrueNAS ===" + +ssh -i /home/pplate/.ssh/id_ed25519_homelab $TRUENAS << REMOTE +set -e + +# TrueNAS root filesystem is read-only — install to /mnt which is persistent ZFS +INSTALL_DIR=/mnt/VM_SSD_Pool/frp +mkdir -p \$INSTALL_DIR + +# Download frpc binary +echo "Downloading frp ${FRP_VERSION}..." +curl -sL https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_amd64.tar.gz \ + -o /tmp/frp.tar.gz +tar xzf /tmp/frp.tar.gz -C /tmp/ +cp /tmp/frp_${FRP_VERSION}_linux_amd64/frpc \$INSTALL_DIR/frpc +chmod +x \$INSTALL_DIR/frpc +\$INSTALL_DIR/frpc --version + +# Write frpc config +cat > \$INSTALL_DIR/frpc.toml << 'TOML' +serverAddr = "${VPS_IP}" +serverPort = 7000 +auth.method = "token" +auth.token = "${FRP_TOKEN}" +log.to = "/tmp/frpc.log" +log.level = "info" + +[[proxies]] +name = "gitea" +type = "tcp" +localIP = "127.0.0.1" +localPort = 30008 +remotePort = 30008 +TOML + +echo "frpc config written:" +cat \$INSTALL_DIR/frpc.toml + +# Create init script (TrueNAS uses systemd-like init but custom) +# Use /etc/local.d/ for persistent startup scripts on TrueNAS SCALE +# Actually TrueNAS SCALE uses systemd — write a service to /etc/systemd/system/ +cat > /etc/systemd/system/frpc.service << 'SVCEOF' +[Unit] +Description=frp client - tunnel to plate.software VPS +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +ExecStart=/mnt/VM_SSD_Pool/frp/frpc -c /mnt/VM_SSD_Pool/frp/frpc.toml +Restart=on-failure +RestartSec=10s + +[Install] +WantedBy=multi-user.target +SVCEOF + +systemctl daemon-reload +systemctl enable frpc +systemctl start frpc +sleep 3 +systemctl status frpc --no-pager | head -15 +echo "" +echo "=== frpc deployed and running ===" +echo "Gitea should now be reachable at https://git.plate.software" +REMOTE diff --git a/plans/heretic-encoder-swap.md b/plans/heretic-encoder-swap.md index f3bff61..492d3ed 100644 --- a/plans/heretic-encoder-swap.md +++ b/plans/heretic-encoder-swap.md @@ -1,7 +1,7 @@ # Task: Swap Qwen3-4B Encoder for Heretic Abliterated Version -**Datum:** 2026-04-10 -**Status:** Ready — waiting for correct Heretic encoder to be published +**Datum:** 2026-04-10 +**Status:** ✅ COMPLETE — Heretic encoder swapped and live-tested 2026-04-10 **Depends on:** FLUX.2 Klein 4B working (✅ done as of 2026-04-10) --- @@ -133,7 +133,7 @@ The entire MCP server, workflow registry, and test suite are already correct. Th ## Success Criteria -- [ ] `generate_image("...", model="flux-2-klein-4b.safetensors")` works with prompts that currently get refused -- [ ] Output image quality identical to standard encoder (check: no visible artifacts vs reference) -- [ ] ComfyUI logs show no dimension errors -- [ ] `qwen_3_4b_klein_backup.safetensors` kept as rollback +- [x] `generate_image("...", model="flux-2-klein-4b.safetensors")` works with prompts that currently get refused — ✅ tested 2026-04-10, Renaissance nude generated without refusal +- [x] Output image quality identical to standard encoder (check: no visible artifacts vs reference) — ✅ 1.9MB photorealistic 1024×1024, museum-quality result, 50.4s +- [x] ComfyUI logs show no dimension errors — ✅ only harmless libcudart NVIDIA stub warnings +- [x] `qwen_3_4b_klein_backup.safetensors` kept as rollback — ✅ 7.5G backup at ~/ComfyUI/models/text_encoders/qwen_3_4b_klein_backup.safetensors diff --git a/plans/homelab-proxy-architecture.md b/plans/homelab-proxy-architecture.md new file mode 100644 index 0000000..2b4ce54 --- /dev/null +++ b/plans/homelab-proxy-architecture.md @@ -0,0 +1,262 @@ +# Homelab Proxy Architecture Plan +_plate.software VPS as public face → WireGuard tunnel → TrueNAS.local_ + +## Goal + +Use the cheap public VPS (`plate.software` @ 85.214.154.199 / Plesk) as: +- Public DNS + TLS termination point +- Apache reverse proxy routing subdomains to TrueNAS homelab services +- ACME/Let's Encrypt managed by Plesk (already working) + +TrueNAS.local (192.168.188.119) becomes the actual compute host for all Docker services. + +--- + +## The Core Problem: TrueNAS is Behind NAT + +TrueNAS lives on a home network. The public VPS cannot reach it directly. A tunnel is required. + +### ⚠️ WireGuard NOT possible — VPS is OpenVZ + +The VPS (`h2970715.stratoserver.net`, Strato) runs on OpenVZ virtualization. +WireGuard requires a kernel module — **not loadable in OpenVZ containers**. + +### Recommended Solution: frp (Fast Reverse Proxy) + +``` +Internet + ↓ DNS +plate.software VPS (85.214.154.199) + frps server (port 7000) + ↓ Apache ProxyPass (HTTP/HTTPS) + ↓ frp tunnel (TCP, userspace) +TrueNAS.local (192.168.188.119) + frpc client → connects out to VPS:7000 + ├── Gitea :30008 → git.plate.software → VPS:30008 + ├── WildFly/Java EE :8080 → plate.software → VPS:18080 + └── Future services :XXXX → app.plate.software +``` + +**Why frp:** +- Pure userspace Go binary — works perfectly on OpenVZ +- TrueNAS (frpc) initiates outbound connection — no router port forwarding needed +- Encrypted tunnel (TLS optional) +- VPS (frps) exposes local ports that Apache proxies to +- Zero kernel dependencies + +--- + +## Target DNS Routing + +| Domain / Subdomain | Routes to | Notes | +|-------------------------|-----------------------------------|-------| +| `plate.software` | TrueNAS:8080 (WildFly) | Current customer Java EE project | +| `git.plate.software` | TrueNAS:30008 (Gitea) | New — expose homelab Gitea publicly | +| `app.plate.software` | TrueNAS:XXXX (future) | Placeholder for future projects | + +All DNS A records point to `85.214.154.199` (VPS). TLS is terminated at the VPS by Plesk/Let's Encrypt. + +--- + +## Implementation Steps + +### Phase 1: WireGuard Tunnel (VPS ↔ TrueNAS) + +**On the VPS (root@85.214.154.199):** +```bash +apt install wireguard +wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key +``` + +Create `/etc/wireguard/wg0.conf`: +```ini +[Interface] +Address = 10.100.0.1/24 +ListenPort = 51820 +PrivateKey = + +[Peer] +PublicKey = +AllowedIPs = 10.100.0.2/32 +PersistentKeepalive = 25 +``` + +**On TrueNAS (via TrueNAS SCALE UI or shell):** +- Apps → Network → WireGuard → Add Interface +- Or via shell: same `wg genkey` + `/etc/wireguard/wg0.conf` approach +```ini +[Interface] +Address = 10.100.0.2/24 +PrivateKey = + +[Peer] +PublicKey = +Endpoint = 85.214.154.199:51820 +AllowedIPs = 10.100.0.1/32 +PersistentKeepalive = 25 +``` + +Enable on both: +```bash +systemctl enable --now wg-quick@wg0 +``` + +Test: +```bash +# From VPS +ping 10.100.0.2 +curl http://10.100.0.2:30008 # Should reach Gitea +``` + +--- + +### Phase 2: Firewall — Open WireGuard Port on VPS + +```bash +# On VPS +ufw allow 51820/udp +# Or via iptables if ufw not present +iptables -A INPUT -p udp --dport 51820 -j ACCEPT +``` + +Also ensure TrueNAS router/firewall does NOT need any port forwarding — TrueNAS initiates the tunnel outbound. The VPS listens; TrueNAS connects. No router config needed. + +--- + +### Phase 3: Plesk Apache — Add Subdomain Proxy Rules + +**Add `git.plate.software` as a subdomain in Plesk:** +1. Plesk → Domains → Add Subdomain → `git.plate.software` +2. Apache & nginx Settings → Additional directives for HTTP: +```apache + + ProxyPass /.well-known/acme-challenge/ ! + ProxyPass / http://10.100.0.2:30008/ retry=0 + ProxyPassReverse / http://10.100.0.2:30008/ + ProxyPreserveHost On + +``` +3. Issue Let's Encrypt cert for `git.plate.software` +4. Configure HTTPS redirect and HTTPS proxy directives the same way + +**Update `plate.software` HTTP directives:** +Change the existing WildFly proxy target from `127.0.0.1:8080` to `10.100.0.2:8080` once WildFly is moved to TrueNAS: +```apache +ProxyPass / http://10.100.0.2:8080/ retry=0 +``` +(Keep this as `127.0.0.1:8080` while the Docker container still runs on the VPS) + +--- + +### Phase 4: Migrate WildFly to TrueNAS + +The customer's Java EE app currently runs in Docker on the VPS. Migrate to TrueNAS: + +1. Export/pull the WildFly Docker image +2. Copy any persistent volumes/data +3. Create `docker-compose.yml` on TrueNAS +4. Start container on TrueNAS, verify on `10.100.0.2:8080` +5. Update VPS Apache proxy target from `127.0.0.1:8080` → `10.100.0.2:8080` +6. Remove the Docker container from VPS + +--- + +### Phase 5: Gitea Public HTTPS + +For Gitea to work properly behind a proxy, update its config to know its public URL: + +Edit Gitea's `app.ini` (in the Gitea Docker volume on TrueNAS): +```ini +[server] +DOMAIN = git.plate.software +ROOT_URL = https://git.plate.software/ +HTTP_PORT = 30008 +``` + +Also in Plesk HTTPS directives for `git.plate.software`: +```apache + + ProxyPass /.well-known/acme-challenge/ ! + ProxyPass / http://10.100.0.2:30008/ retry=0 + ProxyPassReverse / http://10.100.0.2:30008/ + ProxyPreserveHost On + RequestHeader set X-Forwarded-Proto "https" + RequestHeader set X-Forwarded-Port "443" + +``` + +--- + +## Network Topology (Final State) + +``` +┌─────────────────────────────────────┐ +│ Internet / DNS │ +│ *.plate.software → 85.214.154.199 │ +└──────────────┬──────────────────────┘ + │ HTTP/HTTPS + ▼ +┌─────────────────────────────────────┐ +│ VPS: plate.software │ +│ 85.214.154.199 / Plesk / Apache │ +│ │ +│ plate.software → proxy:8080 │ +│ git.plate.software → proxy:30008 │ +│ app.plate.software → proxy:XXXX │ +└──────────────┬──────────────────────┘ + │ WireGuard 10.100.0.0/24 + │ UDP 51820 (encrypted) + ▼ +┌─────────────────────────────────────┐ +│ TrueNAS.local │ +│ 192.168.188.119 / WG: 10.100.0.2 │ +│ │ +│ :30008 Gitea │ +│ :8080 WildFly (Java EE) │ +│ :XXXX Future services │ +└─────────────────────────────────────┘ +``` + +--- + +## Risks & Notes + +| Risk | Mitigation | +|------|-----------| +| Home ISP outage takes down all services | Acceptable for homelab; add health check monitoring later | +| ISP dynamic IP changes (if applicable) | WireGuard peer config uses VPS as endpoint (fixed IP) — TrueNAS initiates tunnel, so home IP change is transparent | +| TrueNAS reboot drops tunnel | `systemctl enable wg-quick@wg0` ensures auto-start | +| Gitea SSH cloning (port 22/2222) | Need separate SSH port forward or Gitea SSH over different port — HTTP clone still works via HTTPS proxy | +| Customer data on VPS → TrueNAS migration | Do at off-peak time; test thoroughly before cutting DNS | + +--- + +## Cost Model + +- VPS (plate.software): Keep cheap (~3-5€/month) — CPU/RAM irrelevant, just proxy traffic +- TrueNAS: All compute happens here — free (already owned hardware) +- Cloudflare (optional): Free plan for DNS + DDoS protection on top of the VPS + +--- + +## Alternative: Cloudflare Tunnel (Zero-Config Option) + +If WireGuard setup is too complex, Cloudflare Tunnel (`cloudflared`) is a zero-config alternative: +- Run `cloudflared` as a Docker container on TrueNAS +- No VPS needed for tunneling — Cloudflare handles the public endpoint +- Free for personal use +- TrueNAS → Cloudflare edge → DNS → users + +**Downside:** Traffic routes through Cloudflare (not self-hosted end-to-end). VPS still useful for non-Cloudflare domains and the existing customer project. + +--- + +## Implementation Priority + +1. ✅ Fix plate.software Let's Encrypt (done) +2. 🔜 Set up WireGuard tunnel (VPS ↔ TrueNAS) +3. 🔜 Add `git.plate.software` subdomain in Plesk + proxy to TrueNAS Gitea +4. 🔜 Update Gitea `app.ini` with public URL +5. 🔜 Issue Let's Encrypt for `git.plate.software` +6. ⏳ Migrate WildFly customer project from VPS → TrueNAS +7. ⏳ Decommission VPS Docker container (keep VPS as pure proxy) diff --git a/plans/upscaler-workflow.md b/plans/upscaler-workflow.md new file mode 100644 index 0000000..6edd52f --- /dev/null +++ b/plans/upscaler-workflow.md @@ -0,0 +1,194 @@ +# Task: Add ESRGAN Upscaler to mcp-image-gen + +**Datum:** 2026-04-10 +**Status:** Ready to implement +**Depends on:** mcp-image-gen working ✅, FLUX.2 Klein Heretic working ✅ + +--- + +## Goal + +Add an `upscale_image()` MCP tool that takes an existing PNG path (from a previous `generate_image()` call) and upscales it 2× or 4× using a Real-ESRGAN model — **no diffusion re-generation**, just fast post-processing (~5–10s). + +Result: A 1024×1024 → 4096×4096 pipeline in two tool calls: +```python +result = generate_image("...", model="flux-2-klein-4b.safetensors", steps=20) +# → ~/Pictures/mcp-generated/foo_20260410_123456_12345.png + +upscaled = upscale_image( + input_path="~/Pictures/mcp-generated/foo_20260410_123456_12345.png", + scale=4 +) +# → ~/Pictures/mcp-generated/foo_20260410_123456_12345_4x.png (4096×4096) +``` + +--- + +## Why ESRGAN (Option B) over Latent Upscale + +| Method | Time overhead | Quality | Requires diffusion? | +|--------|--------------|---------|---------------------| +| ESRGAN image upscale | ~5–10s | ✅ Very sharp details | ❌ No | +| Latent upscale + KSampler | ~50% extra gen time | ✅ Good, consistent style | ✅ Yes | +| UltimateSDUpscale (tiled) | ~4× gen time | ✅ Highest quality | ✅ Yes | + +ESRGAN is the clear winner for "I want a bigger version of this image quickly." + +--- + +## Model to Use + +**`4x-UltraSharp.pth`** — the community standard for photorealistic upscaling. + +- Source: https://huggingface.co/Kim2091/UltraSharp +- Download: `huggingface-cli download Kim2091/UltraSharp 4x-UltraSharp.pth --local-dir ~/ComfyUI/models/upscale_models/` +- Size: ~67MB +- Scale factor: 4× (can also be used for 2× via image resize after) + +Alternative: `RealESRGAN_x4plus.pth` (in ComfyUI's model downloader, general purpose) + +--- + +## ComfyUI Workflow: `esrgan_upscale.json` + +Minimal workflow — 3 nodes: + +``` +LoadImage → UpscaleModelLoader + ImageUpscaleWithModel → SaveImage +``` + +Node layout: + +```json +{ + "1": { + "class_type": "LoadImage", + "inputs": { + "image": "__INPUT_PATH__" + } + }, + "2": { + "class_type": "UpscaleModelLoader", + "inputs": { + "model_name": "4x-UltraSharp.pth" + } + }, + "3": { + "class_type": "ImageUpscaleWithModel", + "inputs": { + "upscale_model": ["2", 0], + "image": ["1", 0] + } + }, + "4": { + "class_type": "SaveImage", + "inputs": { + "images": ["3", 0], + "filename_prefix": "__OUTPUT_PREFIX__" + } + } +} +``` + +**Note:** `LoadImage` in ComfyUI requires the image to be in `~/ComfyUI/input/` — the workflow builder must copy the input file there first (or use `ETN_LoadImageBase64` if available). See "Implementation Notes" below. + +--- + +## MCP Tool Signature + +Add to [`mcp/mcp-image-gen/src/server.py`](../mcp/mcp-image-gen/src/server.py): + +```python +@mcp.tool() +async def upscale_image( + input_path: Annotated[str, Field(description="Path to input PNG (absolute or ~-relative). Must be a file previously generated by generate_image().")], + scale: Annotated[int, Field(description="Upscale factor: 2 or 4 (default: 4). 4x-UltraSharp always runs at 4x; scale=2 applies a 0.5 resize after.")] = 4, + output_dir: Annotated[str, Field(description="Override output directory. Defaults to same dir as input_path.")] = "", + name: Annotated[str, Field(description="Optional output filename prefix. Defaults to input filename + _4x or _2x.")] = "", +) -> list: + """Upscale an existing image using Real-ESRGAN (4x-UltraSharp). + + No diffusion re-generation — pure post-processing (~5-10s). + Input must be a PNG file. Output is saved alongside the input by default. + + Returns both a file path and an inline base64 image for display. + """ +``` + +--- + +## Implementation Notes + +### The `LoadImage` ComfyUI constraint + +ComfyUI's built-in `LoadImage` node only accepts filenames relative to `~/ComfyUI/input/`, not arbitrary paths. Two solutions: + +**Solution A (simplest):** Copy input to `~/ComfyUI/input/` before submitting workflow, use basename as `image` param, delete after. + +**Solution B:** Use `ETN_LoadImageBase64` node (part of `ComfyUI-ETN` custom node extension) — accepts a base64-encoded image directly. Check if installed: +```bash +ls ~/ComfyUI/custom_nodes/ | grep -i etn +``` + +**Recommended:** Start with Solution A (copy to input dir) — no dependencies. If `ComfyUI-ETN` is present, prefer Solution B for cleanliness. + +### Scale=2 handling + +`4x-UltraSharp.pth` always outputs 4×. For `scale=2`, upscale at 4× then resize the result image to 50% with PIL before saving. This is still sharper than native 2× bilinear upscaling. + +### Output filename convention + +Input: `foo_20260410_123456_12345.png` +Output `scale=4`: `foo_20260410_123456_12345_4x.png` +Output `scale=2`: `foo_20260410_123456_12345_2x.png` + +--- + +## Files to Create/Modify + +| File | Change | +|------|--------| +| [`mcp/mcp-image-gen/src/workflows/esrgan_upscale.json`](../mcp/mcp-image-gen/src/workflows/esrgan_upscale.json) | New — ESRGAN workflow | +| [`mcp/mcp-image-gen/src/server.py`](../mcp/mcp-image-gen/src/server.py) | Add `upscale_image()` tool + helpers | +| [`mcp/mcp-image-gen/tests/test_upscale.py`](../mcp/mcp-image-gen/tests/test_upscale.py) | New test file | + +**No changes to:** workflow registry, existing tools, `generate_image()`. + +--- + +## Pre-flight: Download Model + +```bash +huggingface-cli download Kim2091/UltraSharp \ + 4x-UltraSharp.pth \ + --local-dir ~/ComfyUI/models/upscale_models/ +``` + +Verify ComfyUI sees it: +```bash +curl -s http://localhost:8188/object_info/UpscaleModelLoader | \ + python3 -c "import sys,json; d=json.load(sys.stdin); print('\n'.join(d['UpscaleModelLoader']['input']['required']['model_name'][0]))" +``` + +--- + +## Test Cases + +| Test | Input | Expected | +|------|-------|----------| +| `test_upscale_4x` | 1024×1024 PNG | 4096×4096 PNG, `_4x.png` suffix | +| `test_upscale_2x` | 1024×1024 PNG | 2048×2048 PNG, `_2x.png` suffix | +| `test_invalid_path` | nonexistent path | Error TextContent returned | +| `test_output_dir_override` | valid PNG + `output_dir=/tmp` | saved to /tmp | +| `test_default_output_dir` | valid PNG, no output_dir | saved alongside input | + +--- + +## Success Criteria + +- [ ] `4x-UltraSharp.pth` present in `~/ComfyUI/models/upscale_models/` +- [ ] `upscale_image("path/to/1024.png", scale=4)` returns 4096×4096 PNG +- [ ] Output file saved with `_4x.png` suffix +- [ ] Inline base64 image returned for display in chat +- [ ] All 5 test cases pass +- [ ] No changes to existing `generate_image()` tests