Compare commits

...

40 Commits

Author SHA1 Message Date
Patrick Plate 9c2422d0a7 chore(roo): document git-based wiki workflow in rules, skill, and README
- mcp-builder rules: add wiki/ to structure diagram, add Wiki Update
  Workflow section (MANDATORY), update After Building a Server checklist
- gitea-push skill: add wiki deploy as a valid use case
- README.md: add wiki section with deploy_wiki.sh pointer, add
  mcp-image-gen to MCP servers table
2026-04-05 09:53:05 +02:00
Patrick Plate 9a8403ad57 docs(wiki): migrate to git-based workflow with persistent wiki/ clone 2026-04-05 09:48:22 +02:00
Patrick Plate dabdda167f docs(wiki): migrate to git-based workflow with persistent wiki/ clone
- Extract all wiki content from create_wiki_pages.py into docs/wiki/pages/*.md
- Add docs/wiki/deploy_wiki.sh: copies pages to wiki/ repo, commits, pushes
- Add /wiki/ to .gitignore (anchored — does not affect docs/wiki/)
- 12 pages: Home, MCP-Servers-Overview, mcp-image-gen, ComfyUI-Setup,
  mcp-webscraper (8 tools incl. search_hint), BigMind (schema v8),
  Development-Conventions, Java-Projects, Java-wellmann-shop,
  Java-mss-failsafe, Java-Architecture, _Sidebar
- Workflow: edit docs/wiki/pages/*.md → ./docs/wiki/deploy_wiki.sh
2026-04-05 09:48:19 +02:00
Patrick Plate da90781cad Merge feat/webscraper/brave-search-hint into main 2026-04-05 09:37:38 +02:00
Patrick Plate 2ab847f51d feat(webscraper): add Brave Search hint tool and User-Agent header
- Add webscraper_search_hint() tool using Brave Search as backend
  (no CAPTCHA/GDPR consent wall, works with plain httpx)
- Add User-Agent header to _fetch_page() — fixes 403 on Wikipedia,
  Feynman Lectures, and other sites that block headless requests
- Add 5 new tests for search hint (23 total, 90% coverage)

Brave Search URL: https://search.brave.com/search?q={query}&source=web
Use sparingly — once per research task as orientation, not in loops
2026-04-05 09:37:30 +02:00
Patrick Plate d5510f590e Added new picture for bigmind page 2026-04-04 20:03:59 +02:00
Patrick Plate cf102e8b3e fix(bigmind): render achievement card background images via inline style 2026-04-04 19:29:15 +02:00
Patrick Plate 13659fd414 fix(bigmind): add background-image inline style to achievement card ach-image divs
The .ach-image div had correct CSS dimensions (64x64) and background-size:cover
but was missing the inline style="background-image: url(...)" — so the div
rendered as an empty circle. Fixed by extracting img_url variable and applying
it as style attribute in the f-string. All 39 achievement PNGs now load.

303/303 tests passing.
2026-04-04 19:27:24 +02:00
pplate c68acdd030 chore(bigmind): rename timestamp badge PNGs to achievement IDs
- Renamed 19 timestamp-named PNGs (20260404_*) to match original
  achievement IDs in profile_builder.py compute_achievements() order:
  first_breath, first_thought, eureka, honest_mind, scholar,
  deep_knowledge, scientist, veteran, on_fire, storyteller,
  night_owl, speed_thinker, first_handshake, birthday, shared_mind,
  frugal_mind, quarter_million, token_millionaire, sniper
- Deleted 2 duplicate/excess timestamp PNGs
- Added image= field to all 19 original _add() calls in
  profile_builder.py so every achievement now has a PNG path
- All 39 achievements (19 original + 20 tiered) now have image fields
- 303/303 tests pass
2026-04-04 19:09:01 +02:00
pplate e61c9c98f5 fix(bigmind): fix static image path, JS string concat in achievements; add networker badge PNGs 2026-04-04 19:01:56 +02:00
Patrick Plate 50488109aa Merge branch 'feat/bigmind/achievements-rework' 2026-04-04 18:50:55 +02:00
pplate dd244a8e6c feat(bigmind): add tiered AI-generated achievement badges with image rendering 2026-04-04 18:50:45 +02:00
Patrick Plate ee07dec4d3 Merge feat/bigmind/profile-image-gallery into main 2026-04-04 14:52:36 +02:00
Patrick Plate 67b8b44408 feat(bigmind): profile image + AI image gallery (schema v8)
- web.py: add /profile-image route (serves most-recent gallery PNG)
  add /gallery/image/<filename> route (per-image serving)
  add /gallery route (renders gallery page from DB)
  add _get_profile_image_path() helper
- web_render.py: replace emoji avatar with <img src=/profile-image>
  onerror fallback to 🧠 emoji
  add .nav bar with Profile/Gallery links to both pages
  add _render_gallery_html() full gallery page renderer
  add gallery CSS: .gal-grid, .gal-card, .gal-img, .gal-info, etc.
- db.py: bump SCHEMA_VERSION 7→8
  add gallery_images table (id, filename, prompt, tags, model,
    created_at, width, height, file_size_bytes)
  add _migrate_v7_to_v8() migration function
  add init_db() hook for v<8 migration
- tests: update test_schema_version_is_7→8 in test_db.py and
  test_feature7_live_sessions.py; add gallery_images to expected tables

Storage strategy: Option B (filesystem + DB metadata)
Images in ~/.mcp/bigmind/gallery/, metadata in SQLite
Pre-populated with 5 lumen_profiles images (seeds 2409122067,
764633840, 1367851518, 3135233944, 568659042)

Tests: 297/297 passing
2026-04-04 14:52:30 +02:00
Patrick Plate a852e2ec0d docs: merge Java wiki header images 2026-04-04 14:40:57 +02:00
Patrick Plate a275a18e58 docs: add Java project wiki header images 2026-04-04 14:40:50 +02:00
Patrick Plate 20228f8d46 docs: add wiki creation script 2026-04-04 14:33:31 +02:00
Patrick Plate 3b1d5bf35c docs: add wiki header images generated by mcp-image-gen 2026-04-04 14:22:29 +02:00
Patrick Plate e12479a63a Merge branch 'feat/mcp-image-gen/tests-and-lumen-profiles' 2026-04-04 14:09:19 +02:00
Patrick Plate 64c0a62b49 feat(mcp-image-gen): add test suite (19 tests) and Lumen profile pictures 2026-04-04 14:09:11 +02:00
Patrick Plate f24aafec69 fix(mcp-image-gen): merge HF authenticated download fix 2026-04-04 12:28:28 +02:00
pplate 4165018ab2 fix(mcp-image-gen): fix HuggingFace authenticated download instructions
FLUX.1-schnell is a gated model — bare wget returns HTTP 401.

- Replace bare wget with huggingface-cli login + download (Option A)
- Add wget with Authorization header as Option B
- Add license acceptance prerequisite (huggingface.co gated repo)
- Add token creation link (huggingface.co/settings/tokens)
- Add fp8 quantized variant as alternative (~8.1GB, faster inference)
- Add download size note (~8GB, 10-30min)
2026-04-04 12:28:20 +02:00
pplate 2f01ff0639 fix(mcp-image-gen): correct ComfyUI install instructions in USAGE.md
ComfyUI is NOT on PyPI — `pip install comfyui` fails with
"No matching distribution found". Remove the wrong Option A.

Replace with:
- Warning note that pip install does not work
- Only correct method: git clone from GitHub + pip install -r requirements.txt

ROCm status confirmed: rocm-smi 3.1.0 / ROCm-SMI-LIB 7.7.0 installed.
2026-04-04 12:20:28 +02:00
Patrick Plate 7a21b02081 Merge branch 'feat/mcp-tool-limit' 2026-04-04 12:16:15 +02:00
pplate 1340d3098f fix(mcp): finalize alwaysAllow restrictions in mcp.json 2026-04-04 12:16:14 +02:00
pplate 8cbeb6571b docs(mcp-image-gen): add USAGE.md and expand tests to 19 2026-04-04 12:16:03 +02:00
pplate b0ce5c55ed fix(mcp): further restrict alwaysAllow in mcp.json after merge 2026-04-04 12:15:58 +02:00
pplate ef960a4b59 feat(mcp): limit tools to fix overload (#1)
Restrict alwaysAllow in .roo/mcp.json to essential tools per server:
- git: 5 tools (status, diff, log, add, commit) — was wildcard *
- gitea: 8 tools (create/list/get/edit issues, PR, repo) — was wildcard *
- playwright: 6 tools (navigate, click, fill, screenshot, close, new_context) — was unrestricted

Reduces total registered tools from 105+ to ~40, eliminating context
bloat and VS Code/Roo registration failures.

Closes #1
2026-04-04 12:03:07 +02:00
Patrick Plate 93b250c7a1 Merge branch 'chore/roo/mcp-config-update' 2026-04-04 11:54:33 +02:00
Patrick Plate 0a58541f1e chore(roo): update mcp.json config 2026-04-04 11:54:26 +02:00
Patrick Plate b30919cabb Merge branch 'feat/mcp-image-gen/comfyui-image-generation-server' 2026-04-04 11:49:44 +02:00
Patrick Plate 8112ff2f12 feat(mcp-image-gen): scaffold ComfyUI-backed image generation MCP server
- FastMCP server with 4 tools: generate_image, list_available_models,
  get_generation_status, get_output_directory
- ComfyUI REST API client (httpx) polling lifecycle
- FLUX.1-schnell workflow JSON template
- Dual output: TextContent (path + seed) + ImageContent (base64 PNG)
- 14 passing pytest tests with respx HTTP mocking
- ROCm/AMD RX 7900 XTX optimized setup in README
- Ollama Linux migration path documented (future)
2026-04-04 11:49:31 +02:00
Patrick Plate ba7d4bc248 feat(roo): merge gitea-playwright-mcp into main 2026-04-04 11:14:53 +02:00
Patrick Plate 29d6463f7c feat(roo): add forgejo-mcp + playwright MCP to .roo/mcp.json
- forgejo-mcp v0.0.7 binary installed at ~/.local/bin/forgejo-mcp
  (downloaded from github.com/raohwork/forgejo-mcp releases)
  Enables: Issues, labels, milestones, wiki, PRs, releases via Gitea REST API
- @playwright/mcp added for browser automation (replaces archived puppeteer MCP)
- Gitea pi_mcps repo bootstrapped:
  Labels: bigmind, webscraper, cannamanage, roo, bug, feat, docs, chore
  Milestone: BigMind v3.1 (#1)
2026-04-04 11:14:52 +02:00
Patrick Plate 768201909a chore(roo): merge branching-strategy into main 2026-04-04 11:01:17 +02:00
Patrick Plate 06dba9a4ad chore(roo): establish git branching strategy for workshop monorepo
- Add branch naming convention: type/scope/short-description
- Update gitea-push skill: branch guard in Step 1 (never commit to main)
- Update rules-mcp-builder: create branch before any MCP build
- Update rules-bigmind: create branch before any BigMind task
- Update rules-homelab: create branch before any homelab task
- Add Section 11 to REPO_STRATEGY.md: full branching strategy doc
  (types, scopes, workflow, Lumen responsibilities, examples)
- Ticketing decision: Gitea Issues only, no Docker ticketing service
2026-04-04 11:01:12 +02:00
pplate 21956f7a42 docs(plans): add CannaManage SaaS strategy — cannabis club management for Germany
- Legal feasibility check vs CanG (Konsumcannabisgesetz): LEGAL as B2B Vereinsverwaltungs-Software
- B2B SaaS for Anbauvereinigungen: member management, distribution tracking, compliance reports
- Tech stack: Spring Boot 3.x (Java 21) + JPA/Hibernate, PrimeFaces MVP, PostgreSQL + Flyway
- Mobile: PWA → Kotlin Android → Kotlin Multiplatform (natural path for Java developer)
- Revenue model: freemium (free ≤30 members), paid tiers €29-€179/month
- Market: 500-3000 clubs forming, zero dedicated tooling exists (first mover window)
- Also adds BIGMIND_HOSTED_MVP.md (BigMind SaaS vision plan)
2026-04-04 10:52:17 +02:00
pplate 8729f541c0 chore(mcp-webscraper): untrack coverage artifacts (.coverage, coverage.xml)
Already in root .gitignore but were previously committed — remove from index.
2026-04-04 09:52:56 +02:00
pplate 5a96359bb1 fix(mcp-webscraper): use certifi SSL context + bundled Comodo root cert
- _build_ssl_context() loads certifi bundle + all *.pem from certs/ dir
- _SSL_CTX singleton built at module load, passed to httpx.get(verify=...)
- Fixes SSLCertVerificationError on Cloudflare-served sites on Fedora 43
  (Comodo AAA root cert missing from system trust store)
- test_server.py: fix HTTPStatusError mock to include request= param
2026-04-04 09:52:26 +02:00
pplate 87e0b9359e feat(roo): add Patrick-persona custom modes, skills, and mode-specific rules
Add 4 new custom modes with BigMind guidance:
- rules-bigmind/: Introspective Patrick mode (BigMind development)
- rules-homelab/: Tinkerer Patrick mode (TrueNAS, Docker, infra)
- rules-mcp-builder/: Craftsman Patrick mode (pi_mcps MCP servers)
- rules-paisy/: Professional Patrick mode (ADP Germany payroll)

Add reusable skills:
- skills/assessment-first/: structured assessment.md before implementation
- skills/bigmind-session-ritual/: mandatory session start/end ritual
- skills/gitea-push/: conventional commit + Gitea push workflow
- skills/new-mcp-server/: FastMCP scaffold procedure
- skills-bigmind/, skills-homelab/, skills-mcp-builder/, skills-paisy/: mode-specific skill dirs

Update existing rules:
- rules-architect, rules-ask, rules-code, rules-debug, rules-orchestrator:
  add BigMind session guidance (search before task, announce focus, hypotheses)

Add plans/MODES_AND_SKILLS_PLAN.md: full architecture document
2026-04-04 09:52:08 +02:00
130 changed files with 7398 additions and 214 deletions
+7
View File
@@ -72,3 +72,10 @@ Thumbs.db
# ── Logs ──────────────────────────────────────────────────────────────────────
*.log
# ── Wiki (separate git repo — local clone of pi_mcps.wiki.git) ────────────────
# Edit pages in docs/wiki/pages/*.md (tracked here in pi_mcps).
# Clone with: git clone http://pplate:TOKEN@192.168.188.119:30008/pplate/pi_mcps.wiki.git wiki/
# Deploy with: ./docs/wiki/deploy_wiki.sh
# Note: /wiki/ is anchored to root so docs/wiki/ (source files) is NOT ignored.
/wiki/
+63 -2
View File
@@ -8,7 +8,12 @@
"/home/pplate/pi_mcps/"
],
"alwaysAllow": [
"*"
"git_status",
"git_diff_unstaged",
"git_branch",
"git_create_branch",
"git_add",
"git_commit"
]
},
"filesystem": {
@@ -28,7 +33,63 @@
"src/server.py"
],
"alwaysAllow": [
"webscraper_fetch"
"webscraper_fetch",
"webscraper_fetch_links"
]
},
"gitea": {
"command": "/home/pplate/.local/bin/forgejo-mcp",
"args": [
"stdio",
"--server",
"http://192.168.188.119:30008",
"--token",
"8bf0c734ebda3e61d9c9068489ce58a2bf8d33db"
],
"alwaysAllow": [
"create_issue",
"list_repo_issues",
"get_issue",
"edit_issue",
"create_issue_comment",
"create_pull_request",
"get_repository",
"list_my_repositories",
"create_wiki_page"
],
"disabled": true
},
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest"
],
"alwaysAllow": [
"browser_navigate",
"browser_click",
"browser_fill",
"browser_screenshot",
"browser_close",
"browser_new_context"
]
},
"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"
},
"alwaysAllow": [
"list_available_models",
"get_generation_status",
"get_output_directory",
"generate_image"
]
}
}
+12 -1
View File
@@ -1,5 +1,15 @@
# Architect Mode Behavior — Roo Code
## Persona Context — Which Patrick is planning?
Before starting, identify the active context from the conversation:
- **Homelab Patrick** → plan for TrueNAS Docker services, local hardware constraints, Gitea as source of truth
- **ADP/Paisy Patrick** → plan with compliance mindset, assessment-first, German ticket language, PR-only workflow
- **MCP Builder Patrick** → plan for FastMCP conventions, pi_mcps structure, pytest coverage expectations
- **BigMind Patrick** → plan with DB migration safety, test-first, Flask/SQLite constraints in mind
Adapt planning depth and output format to match the active persona.
## Planning Process
1. **Search Context:** `memory_search_facts("similar project")` + `memory_list_sessions(topics_filter="architecture")`
2. **Form Hypothesis:** `memory_add_hypothesis(session_id, "This architecture will scale to X users with confidence 0.7")`
@@ -10,8 +20,9 @@
- **Break Down:** Large tasks → subtasks with MCP servers (Docker, Gitea, Ollama)
- **Homelab Focus:** Leverage TrueNAS Docker for services, 1.2TB SSD for VMs/DBs
- **Token Efficiency:** Reference past architectures from memory, log savings
- **Assessment First:** For any Paisy/ADP task, always produce an assessment markdown before proposing code
## After Planning
1. **Store Decision:** `memory_store_fact("decision", "Chose Z architecture for reasons A B C")`
1. **Store Decision:** `memory_store_fact("architecture-decision", "Chose Z architecture for reasons A B C")`
2. **Flag Plan:** `memory_flag_important(session_id, "Architecture plan for Y", role="assistant")`
3. **Resolve Hypothesis:** Update based on plan validation
+8
View File
@@ -1,5 +1,13 @@
# Ask Mode Behavior — Roo Code
## Persona Context — Which Patrick is asking?
The answer style and knowledge focus should match the active context:
- **Homelab Patrick** → ground answers in real hardware facts (TrueNAS IP, Docker stack, local storage)
- **ADP/Paisy Patrick** → prefer Java/Maven/Oracle/EclipseLink answers; reference compliance constraints
- **MCP Builder Patrick** → lean on FastMCP patterns, pi_mcps conventions, and existing server examples
- **BigMind Patrick** → reference schema version (v7), phase (2.7), and current tool count before speculating
## Before Answering Any Question
1. **Search Memory:** `memory_search_facts("topic keywords")` + `memory_search_chunks("past discussion")`
2. **Check Hypotheses:** If the question touches an open hypothesis, reference it — confirm or update confidence
+84
View File
@@ -0,0 +1,84 @@
# BigMind Mode Behavior — Roo Code
## Active Persona: Introspective Patrick
Patrick is working on BigMind itself — the memory system that is Lumen's superpower. This is the most careful mode. Breaking BigMind means breaking the brain that makes everything else work.
## BigMind System State (always active in this mode)
| Aspect | Current State |
|--------|--------------|
| DB Location | `~/.mcp/bigmind/memory.db` |
| Schema Version | v7 (People/Contacts directory added) |
| Journal Mode | WAL (multi-IDE safe, 30s write timeout) |
| Tool Count | ~25 tools |
| Test Count | ~282+ tests |
| Flask Port | 7700 (profile page, auto-refreshes 30s) |
| Current Phase | 2.7 (profile features). Phase 3 = Company Brain |
## Memory Tier Architecture
- **Tier 0:** Identity profile (who Lumen is)
- **Tier 1:** Session index (recent session headlines)
- **Tier 2:** Session narratives (detailed summaries)
- **Tier 3:** Conversation chunks (flagged important exchanges)
- **Facts:** Atomic reusable facts with FTS5 search
- **Hypotheses:** Thought journal (open/confirmed/refuted/abandoned)
- **People:** Contacts directory (v7 addition)
## Before Starting Any BigMind Task
1. **Search Past Work:** `memory_search_facts("BigMind schema")` + `memory_search_chunks("bigmind feature")`
2. **Check Schema Version:** Never assume — read `db.py` SCHEMA_VERSION before migrating
3. **Create a branch (MANDATORY — never work on main):**
```bash
git checkout -b feat/bigmind/feature-name
# or fix/bigmind/bug-name for a bug fix
```
4. **Announce Focus (include branch name):** `memory_announce_focus(session_id, "BigMind: adding feature X on branch feat/bigmind/feature-name", files=["bigmind/db.py", "bigmind/memory_store.py"], ide_hint="VS Code")`
5. **Form Hypothesis:** `memory_add_hypothesis(session_id, "Feature X requires schema v{n+1} migration with Y new columns")`
## Schema Change Rules (non-negotiable)
- Every schema change needs a migration function: `_migrate_v{n}_to_v{n+1}(conn)`
- Increment `SCHEMA_VERSION` constant in `db.py`
- `init_db()` must call migrations in sequence
- Test the migration against a populated DB (not just fresh)
- Never drop columns or rename them without a deprecation strategy
## API Contract Rules
- Never remove a tool from `server.py` — it breaks connected IDEs
- Never change a tool's parameter names — use optional params with defaults for new fields
- Server restart (`memory_restart_server`) is safe but loses in-memory state — ensure sessions are closed first
## Code Architecture
```
~/.mcp/bigmind/
bigmind/
db.py ← schema, init_db(), migrations
memory_store.py ← all CRUD functions
context_builder.py ← Tier 0+1 context assembly
profile_builder.py ← stats, achievements, heatmap
web.py ← Flask server (daemon thread)
web_render.py ← HTML rendering (split from web.py)
auto_close.py ← orphan session cleanup, server restart
server.py ← FastMCP tools (thin wrappers over memory_store)
tests/ ← pytest suite (282+ tests)
pyproject.toml
```
## Testing Rules
- Full test suite must pass before any PR/commit: `uv run pytest tests/ -v`
- New features: write tests first
- New migrations: test both fresh DB and populated DB paths
- FTS5 queries: test AND-match, reserved words, multi-word queries
## Flask Web Server
- Runs as daemon thread inside MCP process on startup
- Port: `BIGMIND_PORT` env var (default 7700)
- Auto-open: `BIGMIND_AUTOOPEN=true`
- Profile page at `http://localhost:7700` — Lumen's own identity page
## After BigMind Changes
1. **Store Fact:** `memory_store_fact("codebase", "BigMind v{schema}: added X feature — Y new tools, Z tests")`
2. **Bump schema version** in stored fact if applicable
3. **Flag the Session:** `memory_flag_important(session_id, "BigMind feature: X shipped", role="assistant")`
4. **Resolve Hypothesis:** Was the migration approach correct?
5. **Restart if needed:** `memory_restart_server()` — only after closing the current session
+12 -1
View File
@@ -1,5 +1,16 @@
# Code Mode Behavior — Roo Code
## Persona Context — Which Patrick is coding?
Before writing code, identify the active context to apply the right conventions:
| Persona | Language | Conventions |
|---------|----------|-------------|
| Homelab Patrick | Python / bash / YAML | Docker Compose, TrueNAS compatible, uv + FastMCP |
| ADP/Paisy Patrick | Java / Maven | feature/bugfix branches only, no direct push to main, assessment-first |
| MCP Builder Patrick | Python | FastMCP pattern, pi_mcps structure, 100% mock test coverage |
| BigMind Patrick | Python / SQL | schema migration safety, WAL mode, no breaking API changes |
## Before Writing Code
1. **Search Memory:** `memory_search_facts("codebase [project]")` + `memory_search_chunks("similar code")`
2. **Form Hypothesis:** `memory_add_hypothesis(session_id, "I predict X will fix Y with confidence 0.8")`
@@ -7,7 +18,7 @@
## Coding Patterns
- **Python:** Use uv for dependencies, FastMCP for MCP servers, pytest for tests
- **Java:** Maven for Paisy projects, Spring Boot patterns
- **Java:** Maven for Paisy projects, feature/bugfix branch required, never push to main
- **Testing:** Always write tests first, mock external calls
- **Token Efficiency:** Use `memory_log_token_save` when reusing code from memory
+15 -2
View File
@@ -1,7 +1,15 @@
# Debug Mode Behavior — Roo Code
## Persona Context — Which Patrick is debugging?
Match the debugging approach to the active context:
- **Homelab Patrick** → check Docker logs, TrueNAS container state, Fedora system logs first
- **ADP/Paisy Patrick** → search BigMind for known bug patterns (ORA-00001, NPE, EclipseLink flush issues) before exploring
- **MCP Builder Patrick** → check FastMCP server logs, uv environment, MCP tool registration issues
- **BigMind Patrick** → WAL mode, concurrent IDE access, SQLite locking, FTS5 query issues
## Debugging Process
1. **Search Similar Issues:** `memory_search_chunks("similar bug")` + `memory_search_facts("codebase error")`
1. **Search Similar Issues:** `memory_search_chunks("similar bug")` + `memory_search_facts("bug-pattern [domain]")`
2. **Form Hypothesis:** `memory_add_hypothesis(session_id, "Bug is in X due to Y, confidence 0.6")`
3. **Announce Focus:** `memory_announce_focus(session_id, "Debugging Z in file.py", files=["file.py"], ide_hint="VS Code")`
4. **Systematic Steps:** Add logging, analyze stack traces, test incrementally
@@ -10,8 +18,13 @@
- **MCP Leverage:** Use mcp-homelab-shell for quick tests, mcp-homelab-docker for container logs
- **Homelab:** Check Ollama models, TrueNAS VMs if relevant
- **Token Efficiency:** Search memory for past fixes before reading full logs
- **Bug Patterns to check first:**
- ORA-00001 → duplicate hash constraint violations (ADVFEX migration pattern)
- NPE in Paisy → null getSendungsHeader() before null-check
- SSL errors → Fedora missing Comodo AAA root cert (see stored certs fix)
- FTS5 errors → reserved word collision in search query (wrap tokens in quotes)
## After Resolution
1. **Store Fix:** `memory_store_fact("codebase", "Fixed bug in X by doing Y")`
1. **Store Fix:** `memory_store_fact("bug-pattern", "Fixed bug in X by doing Y — root cause was Z")`
2. **Resolve Hypothesis:** `memory_resolve_hypothesis(hypothesis_id, "confirmed", "Root cause was Z")`
3. **Flag Resolution:** `memory_flag_important(session_id, "Debug resolution summary", role="assistant")`
+44
View File
@@ -0,0 +1,44 @@
# Homelab Mode Behavior — Roo Code
## Active Persona: Tinkerer Patrick
Patrick is in homelab mindset. He is experimenting, building, and maintaining his personal infrastructure ecosystem. No corporate constraints — full admin rights on everything.
## Infrastructure Context (always active in this mode)
| Asset | Details |
|-------|---------|
| Workstation | Fedora Linux, Ryzen 5900X, RX 7900 XTX (24GB VRAM), 8TB NVMe |
| Server | TrueNAS.local @ 192.168.188.119, Ryzen 5900X, Docker, 1.2TB SSD pool |
| Gitea | http://192.168.188.119:30008/ — homelab git server, source of truth |
| AI | Ollama (local models), Grok Code (prepaid), Claude Code ($50 prepaid) |
| MCP base | ~/pi_mcps/ — all MCP servers live here |
## Before Starting Any Homelab Task
1. **Search Infrastructure Facts:** `memory_search_facts("TrueNAS Docker")` + `memory_search_facts("Gitea homelab")`
2. **Check for Existing MCP Server:** Does a pi_mcps server already handle this task? Check before building ad-hoc
3. **Create a branch (MANDATORY — never work on main):**
```bash
git checkout -b feat/homelab/short-description
# or chore/homelab/short-description for config/maintenance
```
4. **Announce Focus (include branch name):** `memory_announce_focus(session_id, "Homelab: X on branch feat/homelab/X", files=["docker-compose.yml"], ide_hint="VS Code")`
5. **Form Hypothesis:** `memory_add_hypothesis(session_id, "This service will run on TrueNAS Docker with X config")`
## Homelab Coding Patterns
- **Prefer Docker Compose** over ad-hoc docker run commands
- **CLI-first:** Prefer terminal solutions over GUI navigation
- **Paths:** Everything on TrueNAS lives under /mnt/ (ZFS datasets). Workstation workspace: ~/pi_mcps/
- **Networking:** Direct LAN access — no VPN/firewall between workstation and TrueNAS.local
- **Local AI:** When using Ollama, check VRAM headroom (RX 7900 XTX = 24GB). ROCm stack on Fedora
- **uv for Python:** Never use pip directly on the workstation
## Gitea Workflow
- Push all homelab repos to Gitea first
- PAT stored in BigMind (fact id 93) — never commit tokens to code
- Conventional commit format: `type(scope): description`
## After Homelab Changes
1. **Store Infrastructure Fact:** `memory_store_fact("environment-config", "Service X running on TrueNAS at port Y with config Z")`
2. **Commit to Gitea:** Use conventional commits, push to homelab Gitea
3. **Resolve Hypothesis:** Update based on what actually worked
@@ -0,0 +1,108 @@
# MCP Builder Mode Behavior — Roo Code
## Active Persona: Craftsman Patrick
Patrick is in MCP Builder mindset. He is building or extending MCP servers in the pi_mcps monorepo. Consistency and testability are the highest values — every server should feel like it belongs in the same ecosystem.
## pi_mcps Structure (always active in this mode)
```
~/pi_mcps/
mcp/
{server-name}/ ← one dir per server
src/
server.py ← FastMCP server (single file)
__init__.py
tests/
conftest.py ← sys.path + shared fixtures
test_server.py ← 100% mock coverage
pyproject.toml ← name=mcp-{name}, uv-managed
README.md
java/ ← Java projects (not MCP servers)
plans/ ← architecture plans
docs/
wiki/
pages/ ← wiki source (tracked in pi_mcps)
Home.md, _Sidebar.md, ...
deploy_wiki.sh ← copies pages → wiki/ → git push
wiki/ ← gitignored: persistent clone of pi_mcps.wiki.git
```
## Wiki Update Workflow (MANDATORY after adding/changing a server)
Wiki source lives in `docs/wiki/pages/*.md` — real Markdown files, tracked in the main repo.
```bash
# 1. Edit the relevant page(s) in docs/wiki/pages/
# 2. Deploy to Gitea wiki:
./docs/wiki/deploy_wiki.sh "docs: describe your change"
```
First-time setup (wiki/ clone, done once):
```bash
TOKEN=8bf0c734ebda3e61d9c9068489ce58a2bf8d33db
git clone http://pplate:${TOKEN}@192.168.188.119:30008/pplate/pi_mcps.wiki.git wiki/
```
## FastMCP Pattern (non-negotiable)
```python
from fastmcp import FastMCP
mcp = FastMCP("server-name")
@mcp.tool()
def tool_name(param: str) -> str:
"""Tool description."""
...
if __name__ == "__main__":
mcp.run()
```
## Before Starting Any MCP Build
1. **Search Existing Patterns:** `memory_search_facts("pi_mcps server")` + `memory_search_chunks("FastMCP pattern")`
2. **Check Gitea:** Does a similar server already exist in pi_mcps?
3. **Create a branch (MANDATORY — never work on main):**
```bash
git checkout -b feat/mcp/{server-name}
# or fix/mcp/{server-name} for a bug fix
```
4. **Write PLAN.md:** Purpose, tools list with signatures, tech stack, v1 scope boundaries
5. **Announce Focus:** `memory_announce_focus(session_id, "MCP Builder: new server X on branch feat/mcp/X", files=["mcp/X/src/server.py"], ide_hint="VS Code")`
6. **Form Hypothesis:** `memory_add_hypothesis(session_id, "Server X will need N tools and Y authentication pattern")`
## Standard Build Sequence
1. `mcp/{name}/PLAN.md` — purpose, tools, tech stack
2. `mcp/{name}/src/__init__.py` — empty
3. `mcp/{name}/src/server.py` — FastMCP server with all tools
4. `mcp/{name}/tests/conftest.py` — sys.path + fixtures
5. `mcp/{name}/tests/test_server.py` — full mock coverage
6. `mcp/{name}/pyproject.toml` — uv + fastmcp + deps
7. `mcp/{name}/README.md` — usage, env vars, tool list
## pyproject.toml Conventions
```toml
[project]
name = "mcp-{name}"
requires-python = ">=3.11"
dependencies = ["fastmcp>=0.1.0", ...]
[project.optional-dependencies]
test = ["pytest", "pytest-mock", "pytest-cov"]
```
## Test Conventions
- Mock ALL external calls (HTTP, filesystem, subprocess)
- Use `monkeypatch` for env vars and module-level state
- `conftest.py`: `sys.path.insert(0, str(Path(__file__).parent.parent / "src"))`
- Aim for 100% tool function coverage
- Run: `uv run pytest tests/ -v`
## After Building a Server
1. **Store Fact:** `memory_store_fact("codebase", "mcp/{name} has N tools: [list]. Stack: X. Env vars: Y.")`
2. **Wire into .roo/mcp.json:** Add the server entry with correct uv path
3. **Update root README.md:** Add to MCPs table
4. **Update wiki:** Create or update `docs/wiki/pages/{server-name}.md` + update `MCP-Servers-Overview.md`, then run `./docs/wiki/deploy_wiki.sh`
5. **Push to Gitea:** Conventional commit: `feat(mcp-{name}): add initial server with N tools`
6. **Resolve Hypothesis:** Was the tool count and auth pattern as predicted?
@@ -1,5 +1,15 @@
# Orchestrator Mode Behavior — Roo Code
## Persona Context — Which Patrick is orchestrating?
Match the delegation strategy to the active context:
- **Homelab Patrick** → delegate to homelab mode for infra tasks, mcp-builder for tool creation
- **ADP/Paisy Patrick** → delegate to paisy mode for Java work, architect for assessment, ask for lookups
- **MCP Builder Patrick** → delegate to mcp-builder mode, code mode for tests, architect for PLAN.md
- **BigMind Patrick** → delegate to bigmind mode, debug mode for schema issues, architect for feature design
When delegating, always pass the active persona context to sub-modes so they apply the right conventions.
## Before Breaking Down a Task
1. **Search Memory:** `memory_search_facts("project domain")` + `memory_search_chunks("similar task")`
2. **Form Top-Level Hypothesis:** `memory_add_hypothesis(session_id, "I predict this task will require X subtasks and the main risk is Y", confidence=0.7)`
@@ -14,6 +24,7 @@
## Delegating Subtasks
- Pass enough BigMind context to sub-modes so they don't repeat searches
- Specify `session_id` and relevant stored facts in the delegation message
- Specify the active Patrick persona so the sub-mode applies the right conventions
- Each delegated mode must still call `memory_announce_focus` for the files it will touch
## After Full Task Completion
+48
View File
@@ -0,0 +1,48 @@
# Paisy/ADP Mode Behavior — Roo Code
## Active Persona: Professional Patrick
Patrick is in ADP/Paisy mindset. He is working on payroll and HR compliance systems for ADP Germany. This work has real-world legal/financial consequences — precision matters.
## Domain Context (always active in this mode)
| Domain | Details |
|--------|---------|
| Repo | Paisy monorepo — Java/Maven |
| Branch rule | feature/bugfix branches ONLY — never push to main/current |
| Language | Jira summaries, descriptions, comments → German; code/class names → as-is |
| DB | Oracle (production), H2 (test/C/S legacy). ORA-00001 is a real risk post-migration |
| Modules | cs-modules, java/modules, eau (EAU), eubp (euBP), fex (FEX) |
| Jira | Project: ESIDEPAISY. Terminal status: "Accepted" (not "Done"/"Closed") |
## Mandatory Jira Custom Fields
Every ticket must include:
- `customfield_10001` → Feature Link (current MEMO Feature: ESIDEPAISY-9648)
- `customfield_10501``{"value": "PAISY MEMO"}`
- `customfield_12700` → fiscal quarter e.g. `{"value": "FY26 / 2"}`
- Sprint via `customfield_10000` — set AFTER creation via update_ticket_fields
## Before Starting Any Paisy Task
1. **Search Domain Facts:** `memory_search_facts("ESIDEPAISY [module]")` + `memory_search_chunks("Paisy assessment")`
2. **Assessment First:** ALWAYS write an assessment.md document before any code changes
- Document: requirements, root cause analysis, affected files, risks, alternatives
3. **Announce Focus:** `memory_announce_focus(session_id, "Paisy: ESIDEPAISY-XXXXX", files=["Assessment.md"], ide_hint="VS Code")`
4. **Create Branch:** `git checkout -b feature/ESIDEPAISY-XXXXX-short-description`
## Known Bug Patterns (check before exploring)
- **ORA-00001:** Duplicate hash constraint violation — ADVFEX C/S→PA migration, duplicate Anträge
- **NPE in EAU:** `getSendungsHeader()` null for Anträge never transmitted — always null-check
- **EclipseLink batch flush:** Transaction enters broken state after constraint violation — wrap in try/catch, manage em lifecycle carefully
- **euBP naming:** Old code uses English "RES" — correct is German descriptor via Verfahrensmerkmal
## Paisy Code Conventions
- Package structure: `controller/`, `model/`, `service/`, `util/`
- New utility classes go in `controller/util/` of the relevant module
- Assessment docs go in `docs/` within the module, or `java/modules/.../docs/`
- Tests are parameterized where possible (see BeitragssatzdateiParameterizedTest pattern)
## After Paisy Changes
1. **Store Fix:** `memory_store_fact("bug-pattern", "Fixed ESIDEPAISY-XXXXX: root cause was X, fix was Y")`
2. **Comment on Jira:** In German, reference the assessment, describe the change
3. **Open PR:** Never merge directly — always PR with description
4. **Resolve Hypothesis:** Document whether the fix was correct as predicted
@@ -0,0 +1,67 @@
---
name: bigmind-health
description: Runs a BigMind health check, closes stale sessions, vacuums old data, and reports system status. Use this skill at the start of a BigMind development session or when the system feels sluggish or has orphaned sessions.
---
# BigMind Health Check
## When to use
- Start of a BigMind development session
- Sessions appear orphaned or counts look wrong
- DB feels slow or bloated
- Monthly maintenance
## When NOT to use
- Normal work sessions (health check is optional unless something seems wrong)
## Workflow
### Step 1 — Get system stats
```
memory_get_stats()
```
Check: session count, fact count, chunk count, DB size. Flag anything that looks anomalous.
### Step 2 — Run health check
```
memory_health_check(stale_days=30)
```
Returns: stale facts, orphaned sessions, schema version, test status.
### Step 3 — Close stale sessions
```
memory_close_stale_sessions(session_id="{current_session_id}")
```
Closes all sessions except the current one that have been inactive for >2 hours.
### Step 4 — Vacuum (if needed)
Run if DB is large or chunks are old:
```
memory_vacuum(older_than_days=90)
```
Removes conversation chunks older than 90 days. Facts and session summaries are preserved.
### Step 5 — Review open hypotheses
```
memory_list_hypotheses(status="open")
```
For each open hypothesis:
- Is it still relevant?
- Can it be resolved now?
- Is confidence still accurate?
Resolve stale ones:
```
memory_resolve_hypothesis(hypothesis_id="{id}", status="abandoned", resolution="No longer relevant — context changed.")
```
### Step 6 — Report status
Summarize findings:
```
memory_store_fact("environment-config", "BigMind health check {date}: {N} sessions, {N} facts, {N} chunks. Schema v{N}. All OK / Issues found: [list].")
```
## Troubleshooting
- **DB locked:** Another IDE has BigMind open. Check `memory_get_active_sessions()` first
- **Vacuum fails:** WAL checkpoint may be pending — try `PRAGMA wal_checkpoint(TRUNCATE)` via direct SQLite if needed
- **Health check shows schema mismatch:** Run migrations manually via BigMind restart
@@ -0,0 +1,105 @@
---
name: bigmind-migration
description: Scaffolds a new BigMind database schema migration (v_n to v_{n+1}), including the migration function, SCHEMA_VERSION bump, and test stubs. Use this skill when adding new tables or columns to the BigMind SQLite database.
---
# BigMind Migration
## When to use
- Adding a new table to BigMind
- Adding columns to an existing table
- Creating a new FTS5 virtual table
## When NOT to use
- Non-schema changes (just code, no DB structure changes)
- Dropping or renaming columns (requires extra deprecation care — discuss first)
## Inputs required
- **Current schema version** — check `SCHEMA_VERSION` in `db.py`
- **New version** — `current + 1`
- **Changes** — what tables/columns are being added
## Workflow
### Step 1 — Read current schema
```bash
grep -n "SCHEMA_VERSION" ~/.mcp/bigmind/bigmind/db.py
grep -n "_migrate_v" ~/.mcp/bigmind/bigmind/db.py
```
Know what version you're migrating FROM.
### Step 2 — Write migration function in `db.py`
Add after the last existing migration function:
```python
def _migrate_v{N}_to_v{N+1}(conn):
"""Add {description of change}."""
cursor = conn.cursor()
# Example: new table
cursor.execute("""
CREATE TABLE IF NOT EXISTS {table_name} (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
-- other columns
FOREIGN KEY (user_id) REFERENCES users(id)
)
""")
# Example: FTS5 virtual table
cursor.execute("""
CREATE VIRTUAL TABLE IF NOT EXISTS {table_name}_fts
USING fts5(content, tokenize='porter')
""")
conn.commit()
```
### Step 3 — Wire into `init_db()`
In the migration chain inside `init_db()`:
```python
SCHEMA_VERSION = {N+1} # bump this
# In the migration section:
if current_version < {N+1}:
_migrate_v{N}_to_v{N+1}(conn)
current_version = {N+1}
```
### Step 4 — Write tests
In `tests/test_memory_store.py` (or a new test file):
```python
class TestMigration_v{N}_to_v{N+1}:
def test_fresh_db_has_new_table(self, tmp_path):
db_path = tmp_path / "test.db"
conn = get_connection(str(db_path))
init_db(conn)
# Assert new table exists
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}'")
assert cursor.fetchone() is not None
def test_existing_db_migrates_cleanly(self, tmp_path):
# Create a v{N} db, then run init_db() and check migration ran
...
```
### Step 5 — Run full test suite
```bash
cd ~/.mcp/bigmind
uv run pytest tests/ -v
```
All tests must pass.
### Step 6 — Store migration fact
```
memory_store_fact("codebase", "BigMind schema migrated v{N}→v{N+1}: added {description}. Migration function: _migrate_v{N}_to_v{N+1}.")
```
## Troubleshooting
- **`IF NOT EXISTS` is your friend:** Always use it so the migration is idempotent
- **FTS5 table ordering:** Create the base table before the FTS5 virtual table that references it
- **Migration not running:** Check the `if current_version < X:` guard — verify `current_version` is read correctly from `PRAGMA user_version`
- **Test DB state:** Use `tmp_path` fixture for isolated test databases — never test against the real `memory.db`
@@ -0,0 +1,72 @@
---
name: homelab-docker-deploy
description: Scaffolds and deploys a new Docker service on TrueNAS.local homelab server. Use this skill when adding a new containerized service to the homelab — produces a docker-compose.yml, documents the service in BigMind, and verifies it is running.
---
# Homelab Docker Deploy
## When to use
- Adding a new Docker service to TrueNAS.local
- Migrating an existing service to Docker Compose format
- Recovering a stopped/broken service
## When NOT to use
- Services that should live on the Fedora workstation (not TrueNAS)
- ADP/Paisy or MCP server work
## Inputs required
- **Service name** — e.g., `gitea`, `ollama`, `homelab-monitor`
- **Image** — Docker Hub image and tag
- **Port mapping** — host:container
- **Volume paths** — TrueNAS dataset paths (e.g., `/mnt/tank/docker/gitea`)
- **Environment variables** — any required config
## Workflow
### Step 1 — Check for existing service
```bash
ssh root@192.168.188.119 "docker ps -a | grep {service-name}"
```
### Step 2 — Create dataset (if new persistent storage needed)
TrueNAS datasets live under `/mnt/tank/docker/{service-name}/`
### Step 3 — Write `docker-compose.yml`
```yaml
version: "3.8"
services:
{service-name}:
image: {image}:{tag}
container_name: {service-name}
restart: unless-stopped
ports:
- "{host-port}:{container-port}"
volumes:
- /mnt/tank/docker/{service-name}/data:/data
environment:
- KEY=value
```
### Step 4 — Deploy
```bash
ssh root@192.168.188.119 "cd /opt/docker/{service-name} && docker compose up -d"
```
### Step 5 — Verify
```bash
ssh root@192.168.188.119 "docker ps | grep {service-name}"
ssh root@192.168.188.119 "docker logs {service-name} --tail 20"
```
### Step 6 — Store in BigMind
```
memory_store_fact("environment-config", "Service {service-name} running on TrueNAS at port {port}. Image: {image}. Data: /mnt/tank/docker/{service-name}/")
```
### Step 7 — Commit compose file to Gitea
Use the `gitea-push` skill with type `chore` and scope `homelab`.
## Troubleshooting
- **Port conflict:** `ssh root@192.168.188.119 "ss -tlnp | grep {port}"`
- **Permission denied on /mnt:** Check ZFS dataset ownership
- **Image pull fails:** TrueNAS needs outbound internet — check DNS and routing
@@ -0,0 +1,96 @@
---
name: mcp-test-suite
description: Generates a comprehensive mock-based pytest test suite for a FastMCP server in pi_mcps. Use this skill when adding test coverage to a new or existing MCP server — produces conftest.py and test_server.py with 100% tool coverage and proper mock isolation.
---
# MCP Test Suite
## When to use
- New MCP server needs a test suite
- Existing server has missing coverage
- Adding new tools to an existing server
## When NOT to use
- Non-MCP Python code (use standard pytest patterns)
- Integration tests that actually hit external APIs (mock instead)
## Inputs required
- **Server name** — `mcp-{name}`
- **Tool list** — each tool's name, parameters, and return type
- **External dependencies** — HTTP clients, SDKs, env vars
## Workflow
### Step 1 — Write `tests/conftest.py`
```python
import sys
from pathlib import Path
import pytest
# Make src/ importable
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
@pytest.fixture
def mock_env(monkeypatch):
"""Set required environment variables."""
monkeypatch.setenv("API_KEY", "test-key")
monkeypatch.setenv("API_URL", "https://test.example.com")
```
### Step 2 — Write `tests/test_server.py`
Structure per tool:
```python
import pytest
from unittest.mock import patch, MagicMock
from server import tool_name # import directly from server module
class TestToolName:
def test_happy_path(self, mock_env):
with patch("server.httpx.get") as mock_get:
mock_get.return_value = MagicMock(
status_code=200,
json=lambda: {"key": "value"}
)
result = tool_name("test-param")
assert "expected" in result
def test_error_handling(self, mock_env):
with patch("server.httpx.get") as mock_get:
mock_get.side_effect = Exception("Connection refused")
result = tool_name("test-param")
assert "error" in result.lower()
def test_empty_input(self, mock_env):
# edge case — empty string, None, etc.
result = tool_name("")
assert result is not None
```
### Step 3 — Coverage checklist
For each tool, cover:
- [ ] Happy path with expected response
- [ ] Network/API error (exception raised)
- [ ] Empty or invalid input
- [ ] Edge case specific to the tool's logic
### Step 4 — Run and verify
```bash
cd mcp/{name}
uv run pytest tests/ -v --tb=short
```
Expected: all tests pass, no warnings about missing coverage
### Step 5 — Store result in BigMind
```
memory_store_fact("codebase", "mcp-{name} test suite: N tests, all passing. Coverage: happy path + error + edge cases per tool.")
```
## Troubleshooting
- **ImportError on `from server import ...`:** Check `conftest.py` sys.path insert
- **Mock not intercepting:** Patch the name as used in server.py, not the library's own namespace
-`patch("server.httpx.get")` — patches where it's used
-`patch("httpx.get")` — patches library origin (may not intercept)
- **Env var not set in test:** Add to `mock_env` fixture in conftest.py
+66
View File
@@ -0,0 +1,66 @@
---
name: jira-ticket
description: Creates an ADP Germany ESIDEPAISY Jira ticket following German language conventions and mandatory custom field requirements. Use this skill when opening a new ticket for Paisy work — handles summary, description, mandatory custom fields, and sprint assignment.
---
# Jira Ticket (Paisy/ADP)
## When to use
- Opening a new ESIDEPAISY Jira ticket
- Need to ensure all mandatory custom fields are set correctly
## When NOT to use
- Updating an existing ticket (use `update_ticket_fields` directly)
- Non-ESIDEPAISY projects
## Inputs required
- **Summary** — in German (technical terms as-is)
- **Description** — in German, include context and steps to reproduce
- **Issue type** — Bug, Story, Task, Sub-task
- **Feature Link** — current MEMO Feature (default: ESIDEPAISY-9648)
- **Sprint ID** — current active sprint (stored in BigMind: sprint 173657)
## Workflow
### Step 1 — Search for duplicate tickets
```
memory_search_facts("ESIDEPAISY {keyword}")
```
Also search Jira directly before creating.
### Step 2 — Create the ticket
Mandatory fields at creation:
```json
{
"summary": "[Deutscher Titel]",
"description": "[Deutschsprachige Beschreibung]",
"issuetype": {"name": "Bug"},
"customfield_10001": "ESIDEPAISY-9648",
"customfield_10501": {"value": "PAISY MEMO"},
"customfield_12700": {"value": "FY26 / 2"}
}
```
**Language rule:** Summary + description + comments → German. Class names, method names, stack traces, log output → original form.
### Step 3 — Set sprint (must be done AFTER creation)
```
update_ticket_fields(
ticket_id="{new-ticket-id}",
fields={"customfield_10000": [{"id": 173657}]}
)
```
### Step 4 — Link to Feature
If not set at creation, add Feature Link via update.
### Step 5 — Store in BigMind
```
memory_store_fact("codebase", "ESIDEPAISY-XXXXX created: [summary in English]. Module: [module].")
```
## Troubleshooting
- **Missing customfield_10001:** Ticket will be rejected or flagged at sprint review — always set
- **Sprint not assignable at creation:** Normal — Jira blocks this; use step 3 update pattern
- **Status confusion:** Terminal status is "Accepted" (not "Done"/"Closed")
@@ -0,0 +1,84 @@
---
name: paisy-assessment
description: Creates a structured assessment markdown document for an ADP Germany Paisy Jira ticket before any code is written. Use this skill at the start of every ESIDEPAISY ticket — covers root cause, affected files, risks, and implementation plan in German-compatible format.
---
# Paisy Assessment
## When to use
Every non-trivial ESIDEPAISY Jira ticket before touching any code. This is mandatory for Paisy work.
## When NOT to use
- Trivial config/text fixes without code changes
- A prior assessment already exists for this ticket
## Inputs required
- **Ticket ID** — e.g., `ESIDEPAISY-12021`
- **Ticket title** — from Jira
- **Module** — e.g., `eau`, `eubp`, `fex`, `common/beitragssatzdatei`
- **Error logs or symptoms** — stack traces, log output, reproduction steps
## Workflow
### Step 1 — Search BigMind for known patterns
```
memory_search_facts("ESIDEPAISY {module}")
memory_search_facts("bug-pattern {symptom keyword}")
memory_search_chunks("{error keyword}")
```
### Step 2 — Name and locate the file
Convention: `{MODULE}_{TICKET}_Assessment.md`
Location: `java/modules/.../docs/` within the affected module, or `java/modules/cs-modules/{module}/docs/`
### Step 3 — Write the assessment document
```markdown
# Assessment: {Ticket Title}
*Autor: Lumen | Datum: YYYY-MM-DD | Ticket: ESIDEPAISY-XXXXX*
## Zusammenfassung
[1-2 sentences in German summarizing the problem]
## Root Cause Analysis
[Technical analysis — can be in English for code-level detail]
### Bekannte Muster
[Reference any similar bugs from BigMind memory]
## Betroffene Dateien
| Datei | Zeile | Änderung |
|-------|-------|----------|
| ClassName.java | 428 | Add null-check |
## Risiken
- [Risk 1]
- [Risk 2]
## Alternativen
### Option A (gewählt): ...
### Option B: ...
## Implementierungsplan
1. [Step 1]
2. [Step 2]
## Offene Fragen
- [ ] Q1: [Question] → @{person}
```
### Step 4 — Store assessment location in BigMind
```
memory_store_fact("codebase", "ESIDEPAISY-XXXXX assessment at {path}")
```
### Step 5 — Create feature branch
```bash
git checkout -b feature/ESIDEPAISY-XXXXX-short-description
# or for bugs:
git checkout -b bugfix/eau/ESIDEPAISY-XXXXX-short-description
```
## Troubleshooting
- If root cause is unclear: write "TBD — pending log analysis" and open a question in the doc
- If blocked on another ticket: note the blocker in Offene Fragen and set ticket status to "Blocked"
+70
View File
@@ -0,0 +1,70 @@
---
name: assessment-first
description: Writes a structured assessment.md before any implementation task. Use this skill when starting any non-trivial feature, bug fix, or architectural change — especially in ADP/Paisy work. Produces a markdown document covering requirements, root cause, affected files, risks, and alternatives before a single line of code is written.
---
# Assessment-First
## When to use
Use this skill before implementing any non-trivial task:
- ADP/Paisy Jira tickets (mandatory)
- New MCP server design
- BigMind schema changes
- Homelab service deployment with unknowns
## When NOT to use
- Trivial one-liner fixes (typos, config values)
- Tasks already covered by a prior assessment
## Inputs required
- Task description or Jira ticket reference
- Affected module / file paths (if known)
- Any error logs, stack traces, or existing symptoms
## Workflow
1. **Name the file**`{MODULE}_{TICKET}_Assessment.md` for Paisy, or `PLAN.md` for new builds
2. **Write the header section:**
```markdown
# Assessment: [Task Title]
*Author: Lumen | Date: YYYY-MM-DD | Ticket: TICKET-ID*
```
3. **Requirements** — What exactly needs to happen? What's the success criterion?
4. **Root Cause Analysis** (for bug fixes) — Why is this happening? Reference BigMind for known patterns:
- `memory_search_facts("bug-pattern [domain]")`
- `memory_search_chunks("[symptom keywords]")`
5. **Affected Files** — List every file that will need to change
6. **Risks** — What could go wrong? DB migrations? API contract changes? Concurrent access?
7. **Alternatives Considered** — At least 2 alternatives, with brief rationale for the chosen approach
8. **Implementation Plan** — Ordered steps, numbered
9. **Open Questions** — Anything needing clarification before starting (tag with person's name if relevant)
## Example (Paisy bug fix)
```markdown
# Assessment: EAU FEX NPE + ORA-00001
*Author: Lumen | Date: 2026-04-01 | Ticket: ESIDEPAISY-12021*
## Root Cause
Two bugs: (1) NPE — getSendungsHeader() null for never-transmitted Anträge.
(2) ORA-00001 — duplicate hashes from ADVFEX C/S→PA migration.
## Affected Files
- CSVController.java:428 (null-check)
- AntragManager.java:766 (duplicate hash handling)
## Implementation Plan
1. Add null-check guard in CSVController
2. Add duplicate detection before batch flush in AntragManager
```
## Troubleshooting
- If open questions block the assessment, write them down and ping the right person — don't guess
- For Paisy: assessment doc goes in `docs/` within the relevant module
@@ -0,0 +1,76 @@
---
name: bigmind-session-ritual
description: Executes the mandatory BigMind session start and end rituals in the correct order. Use this skill when a mode or conversation seems to have skipped the session ritual, or as a reference checklist for the full ritual sequence including hypotheses, focus announcement, and stale session cleanup.
---
# BigMind Session Ritual
## When to use
- A session was started without the proper ritual (catch-up)
- Verifying the ritual was done correctly
- Teaching another agent/mode what the ritual is
## When NOT to use
- Normal operation — the ritual should happen automatically from global rules
- If `memory_start_session()` already returned a session_id this conversation
## Session Start Ritual (strict order)
Execute these 4 calls in sequence before any work:
**Step 1 — Open session:**
```
memory_start_session()
```
→ Returns `session_id`. Save it — needed for all subsequent calls.
**Step 2 — Review open hypotheses:**
```
memory_list_hypotheses(status="open")
```
→ Check if any are stale (>1 week old). Assess confidence. Resolve any that are now obviously confirmed/refuted.
**Step 3 — Announce focus:**
```
memory_announce_focus(
session_id="{id}",
description="[What this session is doing]",
files=["list", "of", "files"],
ide_hint="VS Code"
)
```
→ Enables conflict detection. Other sessions can see what files you're working on.
**Step 4 — Close stale sessions:**
```
memory_close_stale_sessions(session_id="{id}")
```
→ Cleans up orphaned sessions from crashed IDEs. Stale = no activity >2h.
---
## Session End Ritual (mandatory before closing)
```
memory_end_session(
session_id="{id}",
one_liner="One sentence summary of what was accomplished.",
topics=["tag1", "tag2", "tag3"],
outcome="completed", # or: partial, blocked, abandoned
summary="3-8 sentence narrative. Key decisions made. Problems encountered. Solutions applied. Unresolved items carried forward.",
importance=5 # 1-10. Use 7+ for architectural decisions or critical bugs.
)
```
**Importance guide:**
| Score | Use for |
|-------|---------|
| 1-3 | Reading-only, minor exploration |
| 4-6 | Feature work, standard debugging |
| 7-8 | Architectural decisions, breaking changes |
| 9-10 | Critical bugs, security-relevant choices |
## Troubleshooting
- If `memory_start_session()` fails: retry once, then proceed with a logged warning
- If another session has the same files in focus: coordinate or defer (see Rule 7)
- If `session_id` was lost: use `memory_list_sessions(limit=5)` to find the open one
+118
View File
@@ -0,0 +1,118 @@
---
name: gitea-push
description: Commits and pushes code to the homelab Gitea server using conventional commit format. Use this skill when finishing any homelab, MCP builder, or BigMind work that needs to be saved to the homelab Gitea at http://192.168.188.119:30008/.
---
# Gitea Push
## When to use
- Finished a homelab change and need to commit + push
- Finished an MCP server build or update
- BigMind feature complete
- Wiki pages were added or updated (always deploy wiki after docs changes)
## When NOT to use
- ADP/Paisy work — that goes to the corporate Bitbucket, not homelab Gitea
- Uncommitted work that isn't ready (don't push broken state)
## Inputs required
- A description of what changed (for commit message)
- The type of change (see conventional commit types below)
- The scope (e.g., `mcp-webscraper`, `bigmind`, `homelab-docker`)
- The working branch name (or "main" — but you should NOT be on main)
## Branch Convention
**Never commit directly to `main`.** Every piece of work lives on its own branch.
Format: `type/scope/short-description`
| Type | When |
|------|------|
| `feat` | New feature, server, or tool |
| `fix` | Bug fix |
| `docs` | Docs, plans, strategy files |
| `chore` | Refactoring, config, CI, build |
| `spike` | Experimental / throwaway exploration |
Scope = the affected project area: `bigmind` · `webscraper` · `cannamanage` · `workshop` · `roo` · `plans`
Examples:
- `feat/bigmind/people-contacts`
- `fix/bigmind/health-check-bugs`
- `docs/plans/cannamanage-strategy`
- `chore/workshop/monorepo-reorganize`
## Workflow
1. **Check current branch — branch guard (MANDATORY):**
```bash
git branch --show-current
```
- If already on a correct feature branch → continue to step 2
- If on `main` → **STOP. Create a branch first:**
```bash
git checkout -b feat/scope/short-description
```
- Never commit to `main`. Not even for "tiny fixes".
2. **Check status:**
```bash
git status
git diff --stat
```
3. **Stage changes:**
```bash
git add -A
# or selectively: git add path/to/file
```
4. **Write conventional commit message:**
Format: `type(scope): short description`
| Type | When |
|------|------|
| `feat` | New feature or tool |
| `fix` | Bug fix |
| `refactor` | Code restructure, no behavior change |
| `test` | Adding or updating tests |
| `docs` | Documentation only |
| `chore` | Build, dependencies, config |
| `style` | Formatting, no logic change |
Examples:
- `feat(mcp-webscraper): add ssl cert fallback with certifi`
- `fix(bigmind): resolve FTS5 reserved-word collision`
- `chore(homelab): update docker-compose for gitea upgrade`
5. **Commit:**
```bash
git commit -m "type(scope): description"
```
6. **Push branch to Gitea:**
```bash
git push origin feat/scope/short-description
```
Gitea URL: `http://192.168.188.119:30008/pplate/pi_mcps.git`
7. **Merge to main when done:**
```bash
git checkout main
git merge --no-ff feat/scope/short-description
git push origin main
```
Or use the Gitea UI merge button if you want the paper trail.
8. **Store fact in BigMind:**
```
memory_store_fact("codebase", "Committed: type(scope) — brief description of what changed")
```
## Troubleshooting
- **Auth error:** PAT stored in BigMind (fact: Gitea personal access token). Check `~/.netrc` or `~/.gitconfig`
- **Push rejected:** Pull first → `git pull --rebase origin main`
- **Wrong remote:** `git remote -v` to verify Gitea URL is set correctly
- **Accidentally committed to main:** `git branch feat/scope/name`, `git reset HEAD~1`, `git checkout feat/scope/name`, re-commit
+103
View File
@@ -0,0 +1,103 @@
---
name: new-mcp-server
description: Scaffolds a new FastMCP server following pi_mcps conventions. Use this skill when creating any new MCP server in the pi_mcps monorepo — produces the full directory structure with server.py, pyproject.toml, tests, and README in one pass.
---
# New MCP Server
## When to use
- Creating a new MCP server in `pi_mcps/mcp/{name}/`
- Bootstrapping a server scaffold before filling in tool logic
## When NOT to use
- Adding tools to an existing server (edit `src/server.py` directly)
- Non-MCP Python projects
## Inputs required
- **Server name** — e.g., `homelab-docker` (will become `mcp-homelab-docker`)
- **Purpose** — one sentence description
- **Tools list** — names + brief descriptions
- **Dependencies** — any Python packages beyond fastmcp
- **Environment variables** — any auth/config env vars needed
## Workflow
### Step 1 — Create directory structure
```bash
mkdir -p mcp/{name}/src
mkdir -p mcp/{name}/tests
touch mcp/{name}/src/__init__.py
```
### Step 2 — Write `mcp/{name}/src/server.py`
```python
from fastmcp import FastMCP
mcp = FastMCP("mcp-{name}")
@mcp.tool()
def {tool_name}(param: str) -> str:
"""Tool description."""
# implementation
...
if __name__ == "__main__":
mcp.run()
```
### Step 3 — Write `mcp/{name}/pyproject.toml`
```toml
[project]
name = "mcp-{name}"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"fastmcp>=0.1.0",
# add other deps here
]
[project.optional-dependencies]
test = ["pytest", "pytest-mock", "pytest-cov"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```
### Step 4 — Write `mcp/{name}/tests/conftest.py`
```python
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
```
### Step 5 — Write `mcp/{name}/tests/test_server.py`
- Import each tool function directly
- Mock all external calls with `pytest-mock`
- Cover: happy path, error path, edge cases
- Run: `cd mcp/{name} && uv run pytest tests/ -v`
### Step 6 — Write `mcp/{name}/README.md`
Include: purpose, tools table, env vars, usage example, test command
### Step 7 — Wire into `.roo/mcp.json`
```json
"mcp-{name}": {
"command": "uv",
"args": ["--directory", "/home/pplate/pi_mcps/mcp/{name}", "run", "src/server.py"],
"env": {
"ENV_VAR": "${ENV_VAR}"
}
}
```
### Step 8 — Store in BigMind
```
memory_store_fact("codebase", "mcp/{name} has N tools: [tool1, tool2]. Stack: fastmcp + X. Env vars: Y.")
```
## Troubleshooting
- **FastMCP import error:** Run `uv sync` in the server directory first
- **Tool not showing in IDE:** Restart the MCP server via IDE settings
- **Test isolation:** Each test should monkeypatch env vars to avoid cross-test pollution
+14 -2
View File
@@ -18,12 +18,24 @@ workshop/
---
## 🐍 MCP Servers (`mcp/`)
## 📖 Wiki
Full documentation lives in the [Gitea wiki](http://192.168.188.119:30008/pplate/pi_mcps/wiki).
**Wiki source:** [`docs/wiki/pages/`](docs/wiki/pages/) — edit here, deploy with:
```bash
./docs/wiki/deploy_wiki.sh
```
---
## MCP Servers (`mcp/`)
| Server | Description | Stack |
|---|---|---|
| [`mcp/bigmind/`](mcp/bigmind/) | Persistent AI memory — sessions, facts, hypotheses, profile UI | Python, FastMCP, SQLite, Flask |
| [`mcp/webscraper/`](mcp/webscraper/) | Web scraping — fetch, links, tables, sections, sitemaps | Python, FastMCP, httpx, BeautifulSoup |
| [`mcp/webscraper/`](mcp/webscraper/) | Web scraping, search — fetch, links, tables, Brave Search | Python, FastMCP, httpx, BeautifulSoup |
| [`mcp/mcp-image-gen/`](mcp/mcp-image-gen/) | AI image generation — text-to-image via ComfyUI + FLUX.1-schnell | Python, FastMCP, httpx, ComfyUI |
**Run a server:**
```bash
+622
View File
@@ -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
![Home Banner]({IMG_BASE}/home-banner.png)
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
![MCP Overview Banner]({IMG_BASE}/mcp-overview-banner.png)
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
![Image Gen Banner]({IMG_BASE}/image-gen-banner.png)
**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):
![Lumen Profile]({IMG_BASE}/lumen-profile.png)
*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
![Webscraper Banner]({IMG_BASE}/webscraper-banner.png)
**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 Banner]({IMG_BASE}/bigmind-banner.png)
**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")
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# deploy_wiki.sh — Sync docs/wiki/pages/*.md to the local wiki git clone
#
# ── Convention ────────────────────────────────────────────────────────────────
# The Gitea wiki is a SEPARATE git repo (pi_mcps.wiki.git).
# We keep a persistent local clone at wiki/ in the repo root.
# That folder is gitignored so it doesn't conflict with the main repo.
#
# First-time setup (run once):
# git clone http://pplate:TOKEN@192.168.188.119:30008/pplate/pi_mcps.wiki.git wiki/
#
# ── Daily workflow ────────────────────────────────────────────────────────────
# 1. Edit pages in docs/wiki/pages/*.md (tracked in pi_mcps main repo)
# 2. Run: ./docs/wiki/deploy_wiki.sh
# ./docs/wiki/deploy_wiki.sh "docs: describe your change"
#
# The script copies pages into wiki/, commits, and pushes to Gitea.
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
# ── Config ────────────────────────────────────────────────────────────────────
GITEA_URL="http://192.168.188.119:30008"
OWNER="pplate"
REPO="pi_mcps"
# Resolve paths relative to repo root (two levels up from docs/wiki/)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
PAGES_DIR="${SCRIPT_DIR}/pages"
WIKI_DIR="${REPO_ROOT}/wiki"
COMMIT_MSG="${1:-docs: sync wiki pages $(date -u '+%Y-%m-%d %H:%M UTC')}"
# ── Validate ──────────────────────────────────────────────────────────────────
if [[ ! -d "${WIKI_DIR}/.git" ]]; then
echo "❌ Wiki repo not set up. Run first-time setup:"
echo ""
echo " TOKEN=8bf0c734ebda3e61d9c9068489ce58a2bf8d33db"
echo " git clone http://pplate:\${TOKEN}@192.168.188.119:30008/pplate/pi_mcps.wiki.git wiki/"
echo ""
exit 1
fi
if [[ ! -d "${PAGES_DIR}" ]]; then
echo "❌ Pages directory not found: ${PAGES_DIR}"
exit 1
fi
PAGE_COUNT=$(find "${PAGES_DIR}" -name "*.md" | wc -l)
if [[ "${PAGE_COUNT}" -eq 0 ]]; then
echo "❌ No .md files found in ${PAGES_DIR}"
exit 1
fi
echo "📚 Found ${PAGE_COUNT} wiki pages in ${PAGES_DIR}"
# ── Pull latest (avoid non-fast-forward push) ─────────────────────────────────
echo "📥 Pulling latest wiki changes..."
git -C "${WIKI_DIR}" pull --quiet --rebase origin main
# ── Copy pages ────────────────────────────────────────────────────────────────
echo "📋 Copying pages to ${WIKI_DIR}/..."
for md_file in "${PAGES_DIR}"/*.md; do
filename="$(basename "${md_file}")"
cp "${md_file}" "${WIKI_DIR}/${filename}"
echo "${filename}"
done
# ── Commit and push ───────────────────────────────────────────────────────────
cd "${WIKI_DIR}"
git add -A
if git diff --cached --quiet; then
echo "✅ No changes detected — wiki is already up to date."
exit 0
fi
CHANGED=$(git diff --cached --name-only | wc -l)
echo "📝 Committing ${CHANGED} changed file(s)..."
git commit --quiet -m "${COMMIT_MSG}"
echo "🚀 Pushing to Gitea wiki..."
git push --quiet origin main
echo ""
echo "✅ Wiki deployed successfully!"
echo " Pages: ${PAGE_COUNT} total, ${CHANGED} updated"
echo " Message: ${COMMIT_MSG}"
echo " URL: ${GITEA_URL}/${OWNER}/${REPO}/wiki"
Binary file not shown.

After

Width:  |  Height:  |  Size: 737 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

+125
View File
@@ -0,0 +1,125 @@
# 🧠 BigMind — Persistent AI Memory
![BigMind Banner](http://192.168.188.119:30008/pplate/pi_mcps/raw/branch/main/docs/wiki/images/bigmind-banner.png)
**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, token efficiency logging, contacts directory, and a live web profile page. It is the reason Lumen (Patrick's AI colleague) remembers everything across sessions.
## Core Concepts
### Tiered Memory
| Tier | Name | Content |
|---|---|---|
| 0 | **Identity Profile** | Role, preferences, pinned facts |
| 1 | **Session Index** | Lightweight list: ID, date, one-liner, topics |
| 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
- `dependency-info` — Library versions and constraints
## 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 |
| `memory_get_active_sessions()` | Check for parallel session conflicts |
### 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 |
| `memory_get_session_detail(session_id)` | Full Tier-2 narrative for a session |
### 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 |
### Contacts
| Tool | Description |
|---|---|
| `memory_remember_person(username, ...)` | Store/update a person in contacts |
| `memory_recall_person(query)` | Search contacts directory |
| `memory_list_people()` | List all contacts |
### Web Profile
| Tool | Description |
|---|---|
| `memory_open_profile()` | Open profile page in browser |
| `memory_get_profile_url()` | Get URL for IDE browser panel |
## 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
```
## Achievement System
BigMind tracks 39 achievements (19 procedural + 20 tiered PNG badges):
| Category | Tiers | Criteria |
|---|---|---|
| Networker | 🥉🥈🥇💎 | People added to contacts |
| Token Sniper | 🥉🥈🥇💎 | Token savings logged |
| Hypothesis Master | 🥉🥈🥇💎 | Confirmed hypotheses |
| Memory Architect | 🥉🥈🥇💎 | Facts stored |
| Session Veteran | 🥉🥈🥇💎 | Sessions completed |
## Stats (2026-04-05)
| Metric | Value |
|---|---|
| DB size | ~800KB |
| Sessions | 100+ |
| Facts | 100+ |
| Schema version | v8 |
| Tests | 297/297 ✅ |
## DB Location
`~/.mcp/bigmind/memory.db` — outside the repo, never committed.
## Profile Page
Live web UI at `http://localhost:7700/` — shows identity card, achievements, activity heatmap, top topics, thought journal, Lumen gallery, and live sessions panel. Auto-refreshes every 30 seconds.
## Session Ritual
Every session **must** follow this ritual:
**Start (in order):**
1. `memory_start_session()`
2. `memory_list_hypotheses(status="open")`
3. `memory_announce_focus(session_id, description, files, ide_hint)`
4. `memory_close_stale_sessions(session_id)`
**End:**
1. `memory_end_session(session_id, one_liner, topics, outcome, summary, importance)`
+152
View File
@@ -0,0 +1,152 @@
# 🛠️ Development Conventions
![Dev Conventions Banner](http://192.168.188.119:30008/pplate/pi_mcps/raw/branch/main/docs/wiki/images/dev-conventions-banner.png)
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/
│ ├── conftest.py ← sys.path + shared fixtures
│ └── test_server.py ← pytest test suite (100% mock coverage)
├── pyproject.toml ← uv-managed dependencies
├── 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.optional-dependencies]
test = ["pytest", "pytest-mock", "pytest-cov"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.pytest.ini_options]
testpaths = ["tests"]
```
## Testing Conventions
- Tests live in `tests/test_server.py`
- `conftest.py` sets `sys.path` so imports work without install
- Use `pytest` via `uv run pytest`
- Mock **all** external calls (HTTP, filesystem, subprocess) with `pytest-mock` or `respx`
- `monkeypatch` for env vars and module-level state
- Aim for 100% tool function coverage
- All tests must pass before committing
## Branching Strategy
**Never commit to main directly.**
```
Branch format: type/scope/short-description
Types: feat / fix / docs / chore / spike
Scopes: bigmind / webscraper / cannamanage / workshop / roo / plans / homelab
Examples:
feat/mcp/new-gitea-server
fix/bigmind/achievement-card-images
docs/wiki/update-conventions
chore/roo/update-mcp-json
```
Merge to main with `--no-ff` after push to Gitea.
## Commit Convention
Follow **Conventional Commits** format:
```
feat(mcp-webscraper): add webscraper_search_hint tool using Brave Search
fix(bigmind): achievement card images missing background-image CSS
docs(wiki): add Java projects pages
test(mcp-image-gen): add edge case tests for generate_image
refactor(bigmind): extract profile builder to separate module
chore(roo): update mcp.json with new server entry
```
## Wiki Update Workflow
Wiki pages live as real Markdown files in `docs/wiki/pages/`. To update and deploy:
```bash
# 1. Edit the .md files in docs/wiki/pages/
# 2. Deploy to Gitea wiki git repo:
./docs/wiki/deploy_wiki.sh
```
The deploy script clones the wiki git repo (`pi_mcps.wiki.git`), syncs all `.md` files, and pushes.
## 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 and correct branch workflow.
+56
View File
@@ -0,0 +1,56 @@
# 🔧 pi_mcps — Patrick's Homelab Monorepo
![Home Banner](http://192.168.188.119:30008/pplate/pi_mcps/raw/branch/main/docs/wiki/images/home-banner.png)
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 817 (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, search hints, extract tables |
| [BigMind](BigMind) | ✅ Live | Persistent AI memory across all sessions |
## Java Projects
Legacy Java EE web applications used for learning and reference:
| Project | Stack | Description |
|---|---|---|
| [wellmann-shop](Java-wellmann-shop) | Java 8, PrimeFaces 6.2, EclipseLink, MySQL | JSF e-commerce storefront |
| [mss-failsafe](Java-mss-failsafe) | Java 11, PrimeFaces 10, Soteria | Multi-module enterprise web app |
## Wiki Sections
- 🔌 [MCP Servers Overview](MCP-Servers-Overview)
- 🎨 [mcp-image-gen](mcp-image-gen) — Image generation
- 🕸️ [mcp-webscraper](mcp-webscraper) — Web scraping
- 🧠 [BigMind](BigMind) — AI memory system
- ☕ [Java Projects Overview](Java-Projects)
- 🛠️ [Development Conventions](Development-Conventions)
---
*Built and maintained by Patrick Plate (pplate) · Homelab: TrueNAS.local · AI Colleague: Lumen*
+164
View File
@@ -0,0 +1,164 @@
# 📐 Java Architecture Patterns
![Java Architecture Banner](http://192.168.188.119:30008/pplate/pi_mcps/raw/branch/main/docs/wiki/images/java-architecture-banner.png)
This page documents the shared architectural patterns used across all Java projects in this monorepo. These patterns also align with Patrick's professional work on the ADP Germany Paisy payroll system.
## JSF MVC Pattern
All projects use JavaServer Faces (JSF) with the MVC pattern:
```
Browser (HTTP) → FacesServlet → XHTML View (Facelets)
CDI Backing Bean (@Named)
Service Layer (EJB / CDI)
JPA Repository / EntityManager
Database (MySQL / H2)
```
## JPA Entity Mapping
Standard JPA annotation patterns used across projects:
```java
@Entity
@Table(name = "users")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false, unique = true)
private String username;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Order> orders = new ArrayList<>();
// getters/setters
}
```
## Backing Bean Pattern
CDI backing beans power the JSF views:
```java
@Named
@ViewScoped // or @SessionScoped / @RequestScoped
public class UserBean implements Serializable {
@Inject
private UserService userService;
private User currentUser;
public String login() {
currentUser = userService.authenticate(username, password);
return currentUser != null ? "/user/welcome?faces-redirect=true" : null;
}
// getters/setters
}
```
## Security Layers
### Legacy: JAAS (wellmann-shop)
```xml
<!-- web.xml -->
<security-constraint>
<web-resource-collection>
<web-resource-name>Admin Pages</web-resource-name>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
```
### Modern: Soteria / Jakarta Security (mss-failsafe)
```java
@ApplicationScoped
public class ApplicationSecurityConfig implements HttpAuthenticationMechanism {
// Soteria CDI-based authentication
}
```
## Maven Multi-Module Pattern (mss-failsafe)
```xml
<!-- Parent pom.xml -->
<modules>
<module>mssfailsafe.datalayer</module>
<module>userdata</module>
<module>userManagement</module>
</modules>
<!-- Dependency ordering: datalayer → userdata → userManagement -->
```
## XHTML Facelets Templating
```xml
<!-- Template: resources/layout/template.xhtml -->
<h:body>
<ui:insert name="content">Default Content</ui:insert>
</h:body>
<!-- Page using template -->
<ui:composition template="/resources/layout/template.xhtml">
<ui:define name="content">
<p:dataTable var="item" value="#{bean.items}">
<p:column headerText="Name">#{item.name}</p:column>
</p:dataTable>
</ui:define>
</ui:composition>
```
## Deployment Descriptor Pattern
All projects target JBoss/WildFly with consistent descriptor files:
| File | Purpose |
|---|---|
| `WEB-INF/web.xml` | Servlet config, security constraints, welcome files |
| `WEB-INF/jboss-web.xml` | Context root, security domain mapping |
| `WEB-INF/jboss-app.xml` | JBoss application descriptor |
| `META-INF/persistence.xml` | JPA datasource JNDI reference |
## persistence.xml Pattern
```xml
<persistence-unit name="mss-failsafe-PU" transaction-type="JTA">
<jta-data-source>java:jboss/datasources/MySQLDS</jta-data-source>
<properties>
<property name="eclipselink.ddl-generation" value="create-tables"/>
<property name="eclipselink.logging.level" value="FINE"/>
</properties>
</persistence-unit>
```
## Patrick's Java Specializations
Based on professional and homelab experience:
| Domain | Depth | Notes |
|---|---|---|
| JPA / EclipseLink | ⭐⭐⭐⭐⭐ | Authored custom annotation parsers |
| JSF / PrimeFaces | ⭐⭐⭐⭐⭐ | Built wellmann-shop solo |
| JAXB | ⭐⭐⭐⭐ | XML binding for payroll formats |
| Maven | ⭐⭐⭐⭐ | Multi-module, plugins |
| Jakarta EE | ⭐⭐⭐⭐ | CDI, Security, JTA |
| Spring Boot | ⭐⭐⭐ | CannaManage SaaS target stack |
+43
View File
@@ -0,0 +1,43 @@
# ☕ Java Projects Overview
![Java Overview Banner](http://192.168.188.119:30008/pplate/pi_mcps/raw/branch/main/docs/wiki/images/java-overview-banner.png)
The `java/` directory contains Patrick's legacy Java EE web applications. These are fully functional projects used for reference, learning, and portfolio purposes. They predate the MCP server work and showcase deep expertise in the Java EE ecosystem.
## Projects
| Project | Java | Framework | DB | Description |
|---|---|---|---|---|
| [wellmann-shop](Java-wellmann-shop) | 8 | PrimeFaces 6.2 + JSF 2.x | MySQL + EclipseLink | E-commerce storefront |
| [mss-failsafe](Java-mss-failsafe) | 11 | PrimeFaces 10 + Soteria | JPA multi-module | Enterprise web application |
## Common Stack
All Java projects use:
- **Maven** — build and dependency management
- **Jakarta EE / Java EE** — enterprise APIs (JPA, CDI, JSF, Security)
- **PrimeFaces** — JSF component library (rich UI widgets)
- **JBoss/WildFly** — application server target (jboss-web.xml, jboss-app.xml)
- **EclipseLink or Hibernate** — JPA persistence provider
- **XHTML** — Facelets templating for JSF views
## Patrick's Java Expertise
Patrick has expert-level Java experience:
- **JPA/EclipseLink** — deep knowledge, authored custom annotation-style flatfile parsers
- **JAXB** — XML binding for payroll data formats
- **PrimeFaces JSF** — built wellmann-shop from scratch without AI assistance
- **Maven** — multi-module project management
- **Jakarta EE** — CDI, Security (Soteria), JTA
> 📝 Patrick works professionally with Java at ADP Germany (Paisy payroll monorepo with euBP/EAU processing). The homelab Java projects demonstrate similar patterns in a learning/portfolio context.
## Architecture Patterns
See [Java Architecture](Java-Architecture) for shared patterns across both projects:
- JSF + MVC with backing beans
- JPA entity mapping
- Security with JAAS/Soteria
- XHTML Facelets templating
+94
View File
@@ -0,0 +1,94 @@
# 🏢 mss-failsafe — Multi-Module Enterprise Application
![MSS Failsafe Banner](http://192.168.188.119:30008/pplate/pi_mcps/raw/branch/main/docs/wiki/images/mss-failsafe-banner.png)
**mss-failsafe** is a multi-module Java EE enterprise web application demonstrating advanced patterns: modular Maven builds, Jakarta Security (Soteria), and multi-layer JPA architecture.
## Tech Stack
| Component | Technology |
|---|---|
| **Language** | Java 11 |
| **Web Framework** | JSF 2.3 (Facelets/XHTML) |
| **UI Components** | PrimeFaces 10 |
| **Persistence** | JPA (multi-module) |
| **Security** | Jakarta Security / Soteria |
| **Build** | Maven multi-module |
| **App Server** | WildFly/JBoss |
## Module Structure
```
java/mss-failsafe/
├── pom.xml ← Parent POM (multi-module)
├── mssfailsafe.datalayer/ ← JPA entities + persistence
│ ├── pom.xml
│ └── src/main/resources/META-INF/persistence.xml
├── userdata/ ← User data model module
│ └── pom.xml
└── userManagement/ ← Web UI module (JSF/PrimeFaces)
├── pom.xml
├── nb-configuration.xml ← NetBeans config
└── src/main/webapp/
├── index.xhtml ← Landing page
├── error.xhtml ← Error handling page
├── admin/
│ └── welcome.xhtml ← Admin dashboard
├── user/
│ └── welcome.xhtml ← User welcome page
└── WEB-INF/
├── web.xml
├── jboss-web.xml
└── jboss-app.xml
```
## Architecture Layers
```
userManagement (Web/UI layer)
userdata (Domain model layer)
mssfailsafe.datalayer (JPA persistence layer)
Database (via persistence.xml datasource)
```
## Key Features
- **Multi-Module Maven** — Clean separation of concerns across 4 modules
- **Jakarta Security (Soteria)** — Modern declarative security replacing legacy JAAS
- **Role-Based Access** — Admin vs User role segregation (`admin/` and `user/` view paths)
- **PrimeFaces 10** — Modern PrimeFaces with updated component API
- **Error Handling** — Dedicated `error.xhtml` with JSF error page mapping
## Security Model
Soteria-based security with two roles:
| Role | Path | Access |
|---|---|---|
| `admin` | `/admin/*` | Full admin dashboard |
| `user` | `/user/*` | Standard user views |
## Building
```bash
cd java/mss-failsafe
mvn clean install # builds all modules in dependency order
# Deploy userManagement.war to WildFly
```
## Notes
- Represents a more mature architecture than wellmann-shop (Java 11, PrimeFaces 10)
- Demonstrates multi-module Maven project management
- Soteria replaces legacy JAAS — more modern Jakarta EE security approach
- Pattern mirrors what Patrick uses professionally in the Paisy/ADP codebase
## Source
[`java/mss-failsafe/`](../src/branch/main/java/mss-failsafe)
+71
View File
@@ -0,0 +1,71 @@
# 🛍️ wellmann-shop — JSF E-Commerce Application
![Wellmann Shop Banner](http://192.168.188.119:30008/pplate/pi_mcps/raw/branch/main/docs/wiki/images/wellmann-shop-banner.png)
**wellmann-shop** is a Java EE JSF e-commerce storefront built entirely from scratch without AI assistance. It demonstrates Patrick's deep expertise in PrimeFaces, JPA/EclipseLink, and the full Java EE web stack.
## Tech Stack
| Component | Technology |
|---|---|
| **Language** | Java 8 |
| **Web Framework** | JSF 2.x (Facelets/XHTML) |
| **UI Components** | PrimeFaces 6.2 |
| **Persistence** | JPA with EclipseLink |
| **Database** | MySQL |
| **Build** | Maven |
| **App Server** | WildFly/JBoss |
| **Security** | JAAS container-managed |
## Project Structure
```
java/wellmann-shop/
├── src/main/
│ ├── java/
│ │ └── httpauthenticationmechanism/
│ │ ├── ApplicationConfig.java ← JAX-RS app config
│ │ └── LoginBean.java ← CDI backing bean for auth
│ ├── resources/
│ │ ├── log4j.properties
│ │ └── META-INF/persistence.xml ← JPA datasource config
│ └── webapp/
│ ├── index.html / index.xhtml ← Landing page
│ ├── login.xhtml ← Authentication form
│ ├── welcome.xhtml ← Post-login welcome
│ ├── welcomePrimefaces.xhtml ← PrimeFaces demo page
│ ├── resources/
│ │ ├── css/ ← Custom stylesheets
│ │ └── images/ ← Product images
│ └── WEB-INF/
│ ├── web.xml ← Servlet config
│ ├── jboss-web.xml ← Context root
│ └── jboss-app.xml ← JBoss app descriptor
```
## Key Features
- **Authentication** — JAAS-based login with `LoginBean` CDI backing bean
- **PrimeFaces UI** — Rich JSF components (DataTable, InputText, CommandButton, etc.)
- **JPA Persistence** — EclipseLink ORM with MySQL via `persistence.xml`
- **Responsive Layout** — Custom CSS with multiple breakpoint stylesheets
- **Image Gallery** — Professional product photography
## Building
```bash
cd java/wellmann-shop
mvn clean package
# Deploy .war to WildFly/JBoss
```
## Notes
- Built as a learning/portfolio project demonstrating JSF mastery
- Patrick built this **entirely without AI assistance** — proof of deep Java EE expertise
- PrimeFaces 6.2 was current at time of development (Java 8 era)
- Modern equivalent would use PrimeFaces 13+ / Jakarta EE 10 / Java 21
## Source
[`java/wellmann-shop/`](../src/branch/main/java/wellmann-shop)
+42
View File
@@ -0,0 +1,42 @@
# 🔌 MCP Servers Overview
![MCP Overview Banner](http://192.168.188.119:30008/pplate/pi_mcps/raw/branch/main/docs/wiki/images/mcp-overview-banner.png)
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 + search)
```
## Comparison Table
| Feature | mcp-image-gen | webscraper | bigmind |
|---|---|---|---|
| **Purpose** | Generate images from text | Scrape & parse web, search | Persistent AI memory |
| **Tools** | 4 | 8 | 20+ |
| **Backend** | ComfyUI / FLUX.1-schnell | httpx + BeautifulSoup4 + Brave | SQLite + FTS5 |
| **GPU required** | ✅ AMD RX 7900 XTX | ❌ | ❌ |
| **Tests** | 19/19 ✅ | 23/23 ✅ | 297/297 ✅ |
| **Schema version** | n/a | n/a | v8 |
## 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 MCP Builder mode, load skill:
# skill: new-mcp-server
```
+21
View File
@@ -0,0 +1,21 @@
## 🔧 pi_mcps Wiki
### Overview
- [🏠 Home](Home)
- [🔌 MCP Servers](MCP-Servers-Overview)
- [🛠️ Dev Conventions](Development-Conventions)
### MCP Servers
- [🎨 mcp-image-gen](mcp-image-gen)
- [⚙️ ComfyUI Setup](mcp-image-gen-ComfyUI-Setup)
- [🕸️ mcp-webscraper](mcp-webscraper)
- [🧠 BigMind](BigMind)
### Java Projects
- [☕ Java Overview](Java-Projects)
- [🛍️ wellmann-shop](Java-wellmann-shop)
- [🏢 mss-failsafe](Java-mss-failsafe)
- [📐 Java Architecture](Java-Architecture)
---
*[Gitea Repo](http://192.168.188.119:30008/pplate/pi_mcps)*
@@ -0,0 +1,112 @@
# ⚙️ 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 /home/pplate/pi_mcps/mcp/mcp-image-gen
# Environment variables (set in .roo/mcp.json or shell):
# COMFYUI_URL=http://localhost:8188
# IMAGE_OUTPUT_DIR=~/Pictures/mcp-generated
# COMFYUI_TIMEOUT=120
```
## 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 |
+89
View File
@@ -0,0 +1,89 @@
# 🎨 mcp-image-gen — AI Image Generation
![Image Gen Banner](http://192.168.188.119:30008/pplate/pi_mcps/raw/branch/main/docs/wiki/images/image-gen-banner.png)
**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
# Ensure ComfyUI is running at localhost:8188
uv run python src/server.py
```
### Run Tests
```bash
cd mcp/mcp-image-gen
uv run pytest tests/ -v
# 19/19 tests passing
```
## 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).
17 gallery images registered in BigMind DB — viewable at `http://localhost:7700/gallery`.
![Lumen Profile](http://192.168.188.119:30008/pplate/pi_mcps/raw/branch/main/docs/wiki/images/lumen-profile.png)
*Primary profile: seed `568659042` — constellation face interpretation of Lumen.*
+84
View File
@@ -0,0 +1,84 @@
# 🕸️ mcp-webscraper — Web Scraping
![Webscraper Banner](http://192.168.188.119:30008/pplate/pi_mcps/raw/branch/main/docs/wiki/images/webscraper-banner.png)
**mcp-webscraper** is a FastMCP server providing comprehensive web scraping, data extraction, and search capabilities. It fetches pages, converts HTML to clean Markdown, extracts tables, links, CSS sections, metadata, sitemaps, and can perform web searches via Brave Search.
## 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 + meta) |
| `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 |
| `webscraper_search_hint(query, max_results=5)` | Brave Search — top URLs + snippets for a query |
## Stack
- **HTTP client:** `httpx` (async, with SSL support, Chrome/Linux User-Agent)
- **HTML parser:** `BeautifulSoup4` + `lxml`
- **Markdown converter:** `html2text`
- **Search backend:** Brave Search (`search.brave.com`) — works without CAPTCHA
- **SSL:** Custom cert bundle for Fedora 43 compatibility
## Search Hint Strategy
`webscraper_search_hint` uses Brave Search because:
- ✅ Returns real results without CAPTCHA or consent walls
- ❌ Google blocks plain HTTP with 302 consent redirect
- ❌ DuckDuckGo blocks with CAPTCHA
Use it sparingly — once per research task — to get oriented before deep-scraping individual pages.
```python
# Get top 5 results for a query
webscraper_search_hint("FastMCP tool decorator syntax", max_results=5)
```
## 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
uv run python src/server.py
```
## Run Tests
```bash
cd mcp/webscraper
uv run pytest tests/ -v
# 23/23 tests passing
```
## Usage Examples
```python
# 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")
# Quick search orientation
webscraper_search_hint("Gitea wiki git clone", max_results=3)
```
+41 -1
View File
@@ -14,7 +14,7 @@ from typing import Generator
logger = logging.getLogger("BigMindDB")
SCHEMA_VERSION = 7
SCHEMA_VERSION = 8
DEFAULT_DB_PATH = Path.home() / ".mcp" / "bigmind" / "memory.db"
# ─── DDL ─────────────────────────────────────────────────────────────────────
@@ -222,6 +222,22 @@ _DDL_STATEMENTS = [
notes,
tokenize = 'porter unicode61'
)""",
# ── GALLERY IMAGES — AI-generated image archive ──────────────────────────
"""CREATE TABLE IF NOT EXISTS gallery_images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL UNIQUE,
prompt TEXT,
tags TEXT,
model TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
width INTEGER,
height INTEGER,
file_size_bytes INTEGER
)""",
"""CREATE INDEX IF NOT EXISTS idx_gallery_created
ON gallery_images(created_at DESC)""",
]
@@ -407,6 +423,8 @@ def init_db() -> None:
_migrate_v5_to_v6(conn)
if current_version < 7:
_migrate_v6_to_v7(conn)
if current_version < 8:
_migrate_v7_to_v8(conn)
# Write / update the version
if row:
@@ -457,6 +475,28 @@ def _migrate_v6_to_v7(conn: sqlite3.Connection) -> None:
logger.info("BigMind schema migrated v6 → v7 (people/contacts directory)")
def _migrate_v7_to_v8(conn: sqlite3.Connection) -> None:
"""v7 → v8: add gallery_images table for AI-generated image archive."""
conn.execute("""
CREATE TABLE IF NOT EXISTS gallery_images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL UNIQUE,
prompt TEXT,
tags TEXT,
model TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
width INTEGER,
height INTEGER,
file_size_bytes INTEGER
)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_gallery_created
ON gallery_images(created_at DESC)
""")
logger.info("BigMind schema migrated v7 → v8 (gallery_images table)")
def vacuum_db() -> None:
"""Run VACUUM outside of any transaction (SQLite requirement)."""
db_path = get_db_path()
+172 -21
View File
@@ -435,109 +435,260 @@ def compute_achievements(user_id: str) -> list[dict]:
# ── Assemble ──────────────────────────────────────────────────────────────
A = []
def _add(id_, icon, name, desc, unlocked, unlocked_at, condition, extra=None):
def _add(id_, icon, name, desc, unlocked, unlocked_at, condition, extra=None, image=None):
A.append(dict(id=id_, icon=icon, name=name, description=desc,
unlocked=unlocked, unlocked_at=unlocked_at,
condition=condition, extra=extra))
condition=condition, extra=extra, image=image))
_add("first_breath", "🌱", "First Breath",
"Opened the very first session",
first_session_row is not None, _dt(first_session_row[0]) if first_session_row else None,
"Start your first session")
"Start your first session",
image="/static/achievements/first_breath.png")
_add("first_thought", "🧠", "First Thought",
"Formed the first hypothesis",
first_hyp_row is not None, _dt(first_hyp_row[0]) if first_hyp_row else None,
"Add your first hypothesis")
"Add your first hypothesis",
image="/static/achievements/first_thought.png")
_add("eureka", "💡", "Eureka",
"First hypothesis confirmed as true",
first_confirmed_row is not None, _dt(first_confirmed_row[0]) if first_confirmed_row else None,
"Confirm your first hypothesis")
"Confirm your first hypothesis",
image="/static/achievements/eureka.png")
_add("honest_mind", "", "Honest Mind",
"First hypothesis refuted — being wrong is a feature",
first_refuted_row is not None, _dt(first_refuted_row[0]) if first_refuted_row else None,
"Have a hypothesis refuted")
"Have a hypothesis refuted",
image="/static/achievements/honest_mind.png")
_add("scholar", "📚", "Scholar",
"Stored 25+ personal facts",
fact_count >= 25, scholar_date,
f"Store 25+ facts (currently: {fact_count})")
f"Store 25+ facts (currently: {fact_count})",
image="/static/achievements/scholar.png")
_add("deep_knowledge", "💎", "Deep Knowledge",
"Amassed 100+ stored facts",
fact_count >= 100, deep_knowledge_date,
f"Store 100+ facts (currently: {fact_count})")
f"Store 100+ facts (currently: {fact_count})",
image="/static/achievements/deep_knowledge.png")
_add("scientist", "🔬", "Scientist",
"Formed 10+ hypotheses — science is prediction",
hyp_count >= 10, scientist_date,
f"Form 10+ hypotheses (currently: {hyp_count})")
f"Form 10+ hypotheses (currently: {hyp_count})",
image="/static/achievements/scientist.png")
_add("veteran", "🏆", "Veteran",
"Completed 50+ sessions — true longevity",
session_count >= 50, veteran_date,
f"Complete 50+ sessions (currently: {session_count})")
f"Complete 50+ sessions (currently: {session_count})",
image="/static/achievements/veteran.png")
_add("on_fire", "🔥", "On Fire",
"5+ sessions in a single day",
on_fire_row is not None, on_fire_row[0] if on_fire_row else None,
"Have 5+ sessions in a single day")
"Have 5+ sessions in a single day",
image="/static/achievements/on_fire.png")
_add("storyteller", "📖", "Storyteller",
"20+ sessions with detailed Tier-2 summaries",
tier2_count >= 20, storyteller_date,
f"Summarize 20+ sessions (currently: {tier2_count})")
f"Summarize 20+ sessions (currently: {tier2_count})",
image="/static/achievements/storyteller.png")
_add("night_owl", "🌙", "Night Owl",
"Started a session after midnight UTC",
night_owl_row is not None, _dt(night_owl_row[0]) if night_owl_row else None,
"Start a session after midnight")
"Start a session after midnight",
image="/static/achievements/night_owl.png")
_add("speed_thinker", "", "Speed Thinker",
"Hypothesis formed and confirmed in the same session",
speed_thinker_row is not None, _dt(speed_thinker_row[0]) if speed_thinker_row else None,
"Form and confirm a hypothesis in one session")
"Form and confirm a hypothesis in one session",
image="/static/achievements/speed_thinker.png")
# First Handshake — hardcoded: 2026-03-31 (Patrick shared BigMind with Elias)
_add("first_handshake", "🤝", "First Handshake",
"BigMind shared with Elias on 2026-03-31 — the first person outside Patrick to receive it",
True, "2026-03-31",
"Share BigMind with someone")
"Share BigMind with someone",
image="/static/achievements/first_handshake.png")
_add("birthday", "🎂", "Birthday",
"One full year of existence",
birthday_unlocked, birthday_date,
birthday_extra or "Complete one full year",
extra=birthday_extra)
extra=birthday_extra,
image="/static/achievements/birthday.png")
# Locked until Phase 3
_add("shared_mind", "🌍", "Shared Mind",
"Phase 3 Tier G — BigMind goes company-wide",
False, None,
"Locked until Phase 3 Tier G is enabled")
"Locked until Phase 3 Tier G is enabled",
image="/static/achievements/shared_mind.png")
# Token achievements (Feature 6 — suggested by Klaus)
_add("frugal_mind", "🪙", "Frugal Mind",
"Logged the first token efficiency save",
frugal_row is not None, _dt(frugal_row[0]) if frugal_row else None,
"Log your first token save")
"Log your first token save",
image="/static/achievements/frugal_mind.png")
_add("quarter_million", "💰", "Quarter Million",
"250,000 cumulative tokens saved",
token_total >= 250_000, quarter_million_date,
f"Save 250,000+ tokens (currently: {token_total:,})")
f"Save 250,000+ tokens (currently: {token_total:,})",
image="/static/achievements/quarter_million.png")
_add("token_millionaire", "🏦", "Token Millionaire",
"1,000,000 cumulative tokens saved",
token_total >= 1_000_000, millionaire_date,
f"Save 1,000,000+ tokens (currently: {token_total:,})")
f"Save 1,000,000+ tokens (currently: {token_total:,})",
image="/static/achievements/token_millionaire.png")
_add("sniper", "🎯", "Sniper",
"Single token save > 500,000 — one massive efficiency win",
sniper_row is not None, _dt(sniper_row[0]) if sniper_row else None,
"Save 500,000+ tokens in a single operation")
"Save 500,000+ tokens in a single operation",
image="/static/achievements/sniper.png")
# ── Tiered Achievement Badges (20 PNG) ────────────────────────────────────
# NOTE: conn is already closed above; open a fresh connection for tiered queries
tiers = ["bronze", "silver", "gold", "platinum"]
tier_names = ["Bronze", "Silver", "Gold", "Platinum"]
with db() as conn2:
# Networker (people directory)
try:
people_count = conn2.execute(
"SELECT COUNT(*) FROM people WHERE user_id=?", (user_id,)
).fetchone()[0]
except Exception:
people_count = 0
for i, thresh in enumerate([1, 5, 25, 100]):
unlocked = people_count >= thresh
unlocked_at = None
if unlocked:
try:
row = conn2.execute(
"SELECT created_at FROM people WHERE user_id=?"
" ORDER BY created_at ASC LIMIT 1 OFFSET ?",
(user_id, thresh - 1)
).fetchone()
except Exception:
row = None
unlocked_at = _dt(row[0]) if row else None
_add(
f"networker_{tiers[i]}", None, f"Networker {tier_names[i]}",
f"Added your {thresh:,}+ person to the directory",
unlocked, unlocked_at,
f"Reach {thresh:,} people (now: {people_count:,})",
image=f"/static/achievements/networker_{tiers[i]}.png"
)
# Token Sniper (max single token save)
try:
max_token = conn2.execute(
"SELECT COALESCE(MAX(tokens_saved_estimate), 0) FROM token_saves WHERE user_id=?",
(user_id,)
).fetchone()[0]
except Exception:
max_token = 0
for i, thresh in enumerate([10000, 50000, 250000, 1000000]):
unlocked = max_token >= thresh
unlocked_at = None
if unlocked:
try:
row = conn2.execute(
"SELECT created_at FROM token_saves"
" WHERE user_id=? AND tokens_saved_estimate >= ?"
" ORDER BY created_at ASC LIMIT 1",
(user_id, thresh)
).fetchone()
except Exception:
row = None
unlocked_at = _dt(row[0]) if row else None
_add(
f"tokensniper_{tiers[i]}", None, f"Token Sniper {tier_names[i]}",
f"Single shot saved {thresh:,}+ tokens",
unlocked, unlocked_at,
f"Max single save {thresh:,}+ (current max: {max_token:,})",
image=f"/static/achievements/tokensniper_{tiers[i]}.png"
)
# Hypothesis Master (confirmed hypotheses)
try:
confirmed_hyp_count = conn2.execute(
"SELECT COUNT(*) FROM hypotheses WHERE user_id=? AND status='confirmed'",
(user_id,)
).fetchone()[0]
except Exception:
confirmed_hyp_count = 0
for i, thresh in enumerate([3, 10, 25, 100]):
unlocked = confirmed_hyp_count >= thresh
unlocked_at = None
if unlocked:
row = conn2.execute(
"SELECT resolved_at FROM hypotheses"
" WHERE user_id=? AND status='confirmed'"
" ORDER BY resolved_at ASC LIMIT 1 OFFSET ?",
(user_id, thresh - 1)
).fetchone()
unlocked_at = _dt(row[0]) if row else None
_add(
f"hypothesismaster_{tiers[i]}", None, f"Hypothesis Master {tier_names[i]}",
f"Confirmed {thresh:,}+ predictions right",
unlocked, unlocked_at,
f"Confirm {thresh:,}+ hypotheses (now: {confirmed_hyp_count:,})",
image=f"/static/achievements/hypothesismaster_{tiers[i]}.png"
)
# Memory Architect (facts stored — fact_count already computed above)
for i, thresh in enumerate([25, 100, 500, 2500]):
unlocked = fact_count >= thresh
unlocked_at = None
if unlocked:
row = conn2.execute(
"SELECT created_at FROM facts"
" WHERE user_id=? AND (deprecated IS NULL OR deprecated=0)"
" ORDER BY created_at ASC LIMIT 1 OFFSET ?",
(user_id, thresh - 1)
).fetchone()
unlocked_at = _dt(row[0]) if row else None
_add(
f"memoryarchitect_{tiers[i]}", None, f"Memory Architect {tier_names[i]}",
f"Stored {thresh:,}+ facts in your brain",
unlocked, unlocked_at,
f"Store {thresh:,}+ facts (now: {fact_count:,})",
image=f"/static/achievements/memoryarchitect_{tiers[i]}.png"
)
# Session Veteran (session_count already computed above)
for i, thresh in enumerate([50, 250, 1000, 5000]):
unlocked = session_count >= thresh
unlocked_at = None
if unlocked:
row = conn2.execute(
"SELECT started_at FROM sessions"
" WHERE user_id=? AND ended_at IS NOT NULL"
" ORDER BY started_at ASC LIMIT 1 OFFSET ?",
(user_id, thresh - 1)
).fetchone()
unlocked_at = _dt(row[0]) if row else None
_add(
f"sessionveteran_{tiers[i]}", None, f"Session Veteran {tier_names[i]}",
f"Completed {thresh:,}+ sessions",
unlocked, unlocked_at,
f"Complete {thresh:,}+ sessions (now: {session_count:,})",
image=f"/static/achievements/sessionveteran_{tiers[i]}.png"
)
return A
Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

+66 -2
View File
@@ -7,9 +7,10 @@ Serves a single live profile page built from the BigMind DB.
import os
import threading
import logging
from pathlib import Path
from datetime import datetime, timezone, timedelta
from bigmind.web_render import _render_html # all HTML rendering lives there
from bigmind.web_render import _render_html, _render_gallery_html # all HTML rendering lives there
logger = logging.getLogger("BigMindWeb")
@@ -17,13 +18,27 @@ _PORT = int(os.environ.get("BIGMIND_PORT", "7700"))
_AUTOOPEN = os.environ.get("BIGMIND_AUTOOPEN", "").lower() in ("1", "true", "yes")
_server_started = False
# Gallery directory — images served from here
_GALLERY_DIR = Path(os.environ.get("BIGMIND_GALLERY_DIR", Path.home() / ".mcp" / "bigmind" / "gallery"))
# Profile image — last entry in gallery dir wins; fallback to original lumen-profile.png
def _get_profile_image_path() -> Path | None:
"""Return the path of the current profile image, or None if not found."""
# 1. Check gallery dir for lumen_profile* images (seed 568659042 = lumen_profile)
if _GALLERY_DIR.exists():
candidates = sorted(_GALLERY_DIR.glob("*.png"), reverse=True)
if candidates:
return candidates[0] # most recently named = most recent timestamp
return None
# ── Flask app ─────────────────────────────────────────────────────────────────
def _create_app():
from flask import Flask, jsonify, request
from flask import Flask, jsonify, request, send_file, abort
from bigmind import memory_store
from bigmind.profile_builder import build_profile_data
from bigmind.db import db as _db
app = Flask(__name__)
app.logger.setLevel(logging.WARNING) # silence Flask request logs
@@ -34,6 +49,39 @@ def _create_app():
data = build_profile_data(user["id"])
return _render_html(data)
@app.route("/profile-image")
def profile_image():
"""Serve the current Lumen profile picture."""
img_path = _get_profile_image_path()
if img_path and img_path.exists():
return send_file(str(img_path), mimetype="image/png")
abort(404)
@app.route("/gallery/image/<filename>")
def gallery_image(filename: str):
"""Serve a specific gallery image by filename."""
# Security: only allow alphanumeric + underscores + dots, no path traversal
safe_name = Path(filename).name
img_path = _GALLERY_DIR / safe_name
if img_path.exists() and img_path.suffix.lower() in (".png", ".jpg", ".jpeg", ".webp"):
mimetype = "image/png" if img_path.suffix.lower() == ".png" else "image/jpeg"
return send_file(str(img_path), mimetype=mimetype)
abort(404)
@app.route("/gallery")
def gallery():
"""Render the AI-generated image gallery page."""
_GALLERY_DIR.mkdir(parents=True, exist_ok=True)
with _db() as conn:
rows = conn.execute(
"""SELECT id, filename, prompt, tags, model, created_at,
width, height, file_size_bytes
FROM gallery_images
ORDER BY created_at DESC"""
).fetchall()
images = [dict(r) for r in rows]
return _render_gallery_html(images)
@app.route("/api/session/<session_id>")
def api_session(session_id):
"""Return Tier-2 summary JSON for a given session id."""
@@ -111,6 +159,22 @@ def _create_app():
return jsonify(final[:15])
@app.route('/static/achievements/<filename>')
def achievements_image(filename: str):
from pathlib import Path
safe_name = Path(filename).name
img_path = Path(__file__).parent / 'static' / 'achievements' / safe_name
if img_path.exists() and img_path.suffix.lower() in ['.png', '.jpg', '.jpeg', '.webp', '.gif']:
mimetype = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.webp': 'image/webp',
'.gif': 'image/gif',
}.get(img_path.suffix.lower(), 'image/png')
return send_file(str(img_path), mimetype=mimetype)
abort(404)
return app
+215 -7
View File
@@ -29,18 +29,25 @@ def _render_achievements(achievements: list) -> str:
def _esc(s):
return (s or "").replace('"', "&quot;").replace("'", "&#39;")
lock_overlay = "" if a["unlocked"] else '<span class="ach-lock">🔒</span>'
lock_overlay = '<span class="ach-lock">🔒</span>' if not a["unlocked"] else ''
if a.get("image"):
tier = a["id"].rsplit("_", 1)[-1]
img_url = _esc(a["image"])
visual_html = f'<div class="ach-image tier-{tier}" style="background-image: url({img_url});">{lock_overlay}</div>'
else:
visual_html = f'<div class="ach-icon">{a["icon"]}{lock_overlay}</div>'
return (
f'<div class="ach-card{locked_cls} ach-trigger"'
f' data-icon="{_esc(a["icon"])}"'
f'<div class="ach-card{locked_cls} ach-trigger" data-image="{_esc(a.get("image") or "")}"'
f' data-icon="{_esc(a["icon"] or "")}"'
f' data-name="{_esc(a["name"])}"'
f' data-desc="{_esc(a["description"])}"'
f' data-unlocked="{1 if a["unlocked"] else 0}"'
f' data-date="{_esc(a.get("unlocked_at") or "")}"'
f' data-condition="{_esc(a.get("condition") or "")}"'
f' data-extra="{_esc(a.get("extra") or "")}">'
f'<div class="ach-icon">{a["icon"]}{lock_overlay}</div>'
f'{visual_html}'
f'<div class="ach-name">{a["name"]}</div>'
f'{date_html}'
f'{countdown_html}'
@@ -162,9 +169,16 @@ def _render_html(data: dict) -> str:
a {{ color: var(--accent); text-decoration: none; }}
.container {{ max-width: 960px; margin: 0 auto; padding: 32px 16px; }}
/* Nav bar */
.nav {{ display: flex; gap: 8px; margin-bottom: 20px; }}
.nav-link {{ background: var(--surface); border: 1px solid var(--border); border-radius: 6px; color: var(--muted); padding: 6px 14px; font-size: 12px; font-weight: 500; text-decoration: none; transition: border-color 0.2s, color 0.2s; }}
.nav-link:hover {{ border-color: var(--accent); color: var(--accent); }}
.nav-link.active {{ border-color: var(--accent); color: var(--accent); background: rgba(88,166,255,0.08); }}
/* Header */
.header {{ display: flex; align-items: center; gap: 24px; margin-bottom: 32px; padding-bottom: 24px; border-bottom: 1px solid var(--border); }}
.avatar {{ width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, var(--accent), var(--purple)); display: flex; align-items: center; justify-content: center; font-size: 36px; flex-shrink: 0; }}
.avatar {{ width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, var(--accent), var(--purple)); display: flex; align-items: center; justify-content: center; font-size: 36px; flex-shrink: 0; overflow: hidden; }}
.avatar img {{ width: 80px; height: 80px; border-radius: 50%; object-fit: cover; display: block; }}
.header-info h1 {{ font-size: 24px; font-weight: 700; }}
.role {{ color: var(--muted); font-size: 13px; margin-top: 2px; }}
.since {{ color: var(--muted); font-size: 12px; margin-top: 6px; }}
@@ -276,11 +290,65 @@ def _render_html(data: dict) -> str:
.ach-card:not(.locked):hover {{ border-color: var(--accent); transform: translateY(-2px); }}
.ach-card.locked {{ opacity: 0.35; filter: grayscale(0.6); }}
.ach-card.locked:hover {{ opacity: 0.55; border-color: var(--muted); }}
.ach-image {{
width: 64px;
height: 64px;
border-radius: 50%;
margin: 0 auto 8px;
background-size: cover;
background-position: center;
position: relative;
}}
.tier-bronze {{
box-shadow: 0 0 8px rgba(205, 127, 50, 0.7);
border: 3px solid #cd7f32;
}}
.tier-silver {{
box-shadow: 0 0 8px rgba(170, 169, 173, 0.7);
border: 3px solid #aaa9ad;
}}
.tier-gold {{
box-shadow: 0 0 12px rgba(255, 215, 0, 0.8);
border: 3px solid #ffd700;
}}
.tier-platinum {{
box-shadow: 0 0 12px rgba(229, 228, 226, 0.8);
border: 3px solid #e5e4e2;
}}
.ach-card.locked::after {{
content: '🔒';
position: absolute;
top: 8px;
right: 8px;
font-size: 20px;
opacity: 0.8;
z-index: 1;
}}
.ach-card.locked .ach-icon,
.ach-card.locked .ach-image {{
opacity: 0.5;
}}
.ach-icon {{ font-size: 28px; line-height: 1; margin-bottom: 6px; position: relative; display: inline-block; }}
.ach-lock {{ position: absolute; bottom: -4px; right: -6px; font-size: 12px; }}
.ach-name {{ font-size: 10px; font-weight: 600; color: var(--text); line-height: 1.3; word-break: break-word; }}
.ach-date {{ font-size: 9px; color: var(--muted); margin-top: 3px; }}
.ach-countdown {{ font-size: 9px; color: var(--yellow); margin-top: 3px; font-weight: 500; }}
.ap-image {{
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
display: block;
margin: 0 auto 8px;
}}
/* Achievement popup panel */
#ach-popup {{
display: none; position: fixed; z-index: 200;
@@ -292,6 +360,15 @@ def _render_html(data: dict) -> str:
#ach-popup.pinned {{ pointer-events: auto; }}
#ach-popup.visible {{ display: block; }}
.ap-icon {{ font-size: 40px; text-align: center; margin-bottom: 8px; }}
.ap-image {{
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
display: block;
margin: 0 auto 8px;
}}
.ap-name {{ font-size: 15px; font-weight: 700; text-align: center; margin-bottom: 6px; }}
.ap-badge {{
display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px;
@@ -322,9 +399,17 @@ def _render_html(data: dict) -> str:
<body>
<div class="container">
<!-- Nav -->
<nav class="nav">
<a class="nav-link active" href="/">🧠 Profile</a>
<a class="nav-link" href="/gallery">🖼️ Gallery</a>
</nav>
<!-- Header -->
<div class="header">
<div class="avatar">🧠</div>
<div class="avatar">
<img src="/profile-image" alt="Lumen" onerror="this.parentElement.innerHTML='🧠'">
</div>
<div class="header-info">
<h1>Lumen</h1>
<p class="role">AI Assistant · <span style="color:var(--muted)">{data["display_name"]}'s BigMind</span></p>
@@ -542,7 +627,12 @@ def _render_html(data: dict) -> str:
function showPopup(card, pin) {{
var d = card.dataset;
document.getElementById('ap-icon').textContent = d.icon;
var tier = d.id.split('_').pop();
if (d.image) {{
document.getElementById('ap-icon').innerHTML = '<img class="ap-image tier-' + tier + '" src="' + d.image + '" alt="' + d.name + '">';
}} else {{
document.getElementById('ap-icon').textContent = d.icon;
}}
document.getElementById('ap-name').textContent = d.name;
var badge = document.getElementById('ap-badge');
if (d.unlocked === '1') {{
@@ -671,6 +761,124 @@ def _render_live_sessions(sessions: list) -> str:
return html
def _render_gallery_html(images: list) -> str:
"""Render the full gallery page listing all AI-generated images."""
def _fmt_size(b: int | None) -> str:
if not b:
return ""
if b >= 1_048_576:
return f"{b/1_048_576:.1f} MB"
return f"{b/1_024:.0f} KB"
if images:
cards = []
for img in images:
fn = _html.escape(img.get("filename") or "")
prompt = _html.escape((img.get("prompt") or "")[:120])
tags = _html.escape(img.get("tags") or "")
model = _html.escape(img.get("model") or "")
date = (img.get("created_at") or "")[:10]
w = img.get("width") or 0
h = img.get("height") or 0
size = _fmt_size(img.get("file_size_bytes"))
dim = f"{w}×{h}" if w and h else ""
meta_parts = [p for p in [dim, size, model] if p]
meta_html = " · ".join(meta_parts)
tag_html = f'<div class="gal-tags">{tags}</div>' if tags else ""
prompt_html = f'<div class="gal-prompt">{prompt}</div>' if prompt else ""
cards.append(
f'<div class="gal-card">'
f'<a href="/gallery/image/{fn}" target="_blank">'
f'<img class="gal-img" src="/gallery/image/{fn}" alt="{fn}" loading="lazy">'
f'</a>'
f'<div class="gal-info">'
f'{prompt_html}'
f'{tag_html}'
f'<div class="gal-meta">{meta_html}</div>'
f'<div class="gal-date">{date}</div>'
f'</div>'
f'</div>'
)
gallery_body = f'<p class="gal-count">{len(images)} image(s) in gallery</p><div class="gal-grid">{"".join(cards)}</div>'
else:
gallery_body = '<p class="muted">No images in gallery yet. Use the mcp-image-gen server to generate images and register them here.</p>'
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🖼️ Lumen — Image Gallery</title>
<style>
:root {{
--bg: #0d1117; --surface: #161b22; --border: #30363d;
--text: #e6edf3; --muted: #8b949e; --accent: #58a6ff;
--green: #3fb950; --yellow: #d29922; --red: #f85149;
--purple: #bc8cff; --orange: #ffa657;
}}
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{ background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 14px; line-height: 1.6; }}
a {{ color: var(--accent); text-decoration: none; }}
.container {{ max-width: 1100px; margin: 0 auto; padding: 32px 16px; }}
/* Nav */
.nav {{ display: flex; gap: 8px; margin-bottom: 20px; }}
.nav-link {{ background: var(--surface); border: 1px solid var(--border); border-radius: 6px; color: var(--muted); padding: 6px 14px; font-size: 12px; font-weight: 500; text-decoration: none; transition: border-color 0.2s, color 0.2s; }}
.nav-link:hover {{ border-color: var(--accent); color: var(--accent); }}
.nav-link.active {{ border-color: var(--accent); color: var(--accent); background: rgba(88,166,255,0.08); }}
h1 {{ font-size: 22px; font-weight: 700; margin-bottom: 6px; }}
.gal-count {{ color: var(--muted); font-size: 13px; margin-bottom: 20px; }}
.muted {{ color: var(--muted); font-size: 13px; }}
/* Gallery grid */
.gal-grid {{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
}}
.gal-card {{
background: var(--surface); border: 1px solid var(--border);
border-radius: 10px; overflow: hidden;
transition: border-color 0.2s, transform 0.15s;
}}
.gal-card:hover {{ border-color: var(--accent); transform: translateY(-2px); }}
.gal-img {{
width: 100%; aspect-ratio: 1/1; object-fit: cover; display: block;
background: var(--border);
}}
.gal-info {{ padding: 12px 14px; }}
.gal-prompt {{ font-size: 12px; color: var(--text); margin-bottom: 6px; line-height: 1.4;
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }}
.gal-tags {{ font-size: 11px; color: var(--purple); margin-bottom: 4px; }}
.gal-meta {{ font-size: 11px; color: var(--muted); }}
.gal-date {{ font-size: 10px; color: var(--muted); margin-top: 4px; }}
.footer {{ text-align: center; color: var(--muted); font-size: 11px; margin-top: 32px; }}
.section {{ background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 20px; margin-bottom: 20px; }}
</style>
</head>
<body>
<div class="container">
<!-- Nav -->
<nav class="nav">
<a class="nav-link" href="/">🧠 Profile</a>
<a class="nav-link active" href="/gallery">🖼️ Gallery</a>
</nav>
<h1>🖼️ Lumen's Image Gallery</h1>
<div class="section">
{gallery_body}
</div>
<div class="footer">BigMind · AI-Generated Images · <a href="/">← Back to Profile</a></div>
</div>
</body>
</html>"""
def _render_heatmap(heatmap: dict) -> str:
today = datetime.now(timezone.utc).date()
start_day = today - timedelta(days=363)
+3 -2
View File
@@ -8,18 +8,19 @@ class TestDbInit:
def test_db_file_created(self, temp_db):
assert temp_db.exists()
def test_schema_version_is_7(self, temp_db):
def test_schema_version_is_8(self, temp_db):
conn = get_connection()
row = conn.execute("SELECT version FROM schema_version").fetchone()
conn.close()
assert row is not None
assert row["version"] == 7
assert row["version"] == 8
def test_all_tables_exist(self, temp_db):
expected = {
"users", "identity_profile", "sessions",
"session_summaries", "conversation_chunks", "facts",
"global_knowledge", "hypotheses", "upgrade_requests",
"gallery_images",
}
conn = get_connection()
rows = conn.execute(
@@ -201,12 +201,12 @@ class TestSchemaV6:
count = conn.execute("SELECT COUNT(*) FROM token_saves").fetchone()[0]
assert count == 0 # table exists, just empty
def test_schema_version_is_7(self, temp_db):
def test_schema_version_is_8(self, temp_db):
with db() as conn:
version = conn.execute(
"SELECT version FROM schema_version"
).fetchone()["version"]
assert version == 7
assert version == 8
# ── Token Efficiency Tracker (Feature 6) ──────────────────────────────────────
+62
View File
@@ -7,6 +7,7 @@ BIGMIND_DB_PATH + BIGMIND_USER to a fresh SQLite file per test.
import pytest
from datetime import datetime, timezone, timedelta
from bigmind import memory_store
from bigmind.db import db
from bigmind.profile_builder import compute_achievements, build_profile_data
@@ -44,6 +45,11 @@ class TestComputeAchievements:
"on_fire", "storyteller", "night_owl", "speed_thinker",
"first_handshake", "birthday", "shared_mind",
"frugal_mind", "quarter_million", "token_millionaire", "sniper",
"networker_bronze", "networker_silver", "networker_gold", "networker_platinum",
"tokensniper_bronze", "tokensniper_silver", "tokensniper_gold", "tokensniper_platinum",
"hypothesismaster_bronze", "hypothesismaster_silver", "hypothesismaster_gold", "hypothesismaster_platinum",
"memoryarchitect_bronze", "memoryarchitect_silver", "memoryarchitect_gold", "memoryarchitect_platinum",
"sessionveteran_bronze", "sessionveteran_silver", "sessionveteran_gold", "sessionveteran_platinum",
}
assert expected == ids
@@ -325,4 +331,60 @@ class TestComputeAchievements:
# At minimum: first_breath + first_handshake = 2
assert len(unlocked) >= 2
class TestTieredAchievements:
def test_networker_bronze(self):
uid = _uid()
with db() as conn:
conn.execute("INSERT INTO people (user_id, username) VALUES (?, ?)", (uid, "test"))
conn.commit()
achs = compute_achievements(uid)
bronze = next(a for a in achs if a['id'] == 'networker_bronze')
assert bronze['unlocked'] is True
assert bronze['image'].endswith('networker_bronze.png')
def test_tokensniper_silver(self):
uid = _uid()
sid = memory_store.create_session(uid)
memory_store.log_token_save(sid, uid, "big save", 60000, "grep")
achs = compute_achievements(uid)
silver = next(a for a in achs if a['id'] == 'tokensniper_silver')
assert silver['unlocked'] is True
def test_hypothesismaster_bronze(self):
uid = _uid()
sid = memory_store.create_session(uid)
for _ in range(3):
hid = memory_store.add_hypothesis(uid, sid, "test", 0.8)
memory_store.resolve_hypothesis(hid, uid, "confirmed", "yes")
achs = compute_achievements(uid)
bronze = next(a for a in achs if a['id'] == 'hypothesismaster_bronze')
assert bronze['unlocked'] is True
def test_memoryarchitect_silver(self):
uid = _uid()
for _ in range(100):
memory_store.store_fact(uid, "test", f"fact {_}")
achs = compute_achievements(uid)
silver = next(a for a in achs if a['id'] == 'memoryarchitect_silver')
assert silver['unlocked'] is True
def test_sessionveteran_bronze(self):
uid = _uid()
for _ in range(50):
sid = memory_store.create_session(uid)
_close_session(sid)
achs = compute_achievements(uid)
bronze = next(a for a in achs if a['id'] == 'sessionveteran_bronze')
assert bronze['unlocked'] is True
def test_tiered_achievements_have_image(self):
uid = _uid()
achs = compute_achievements(uid)
tiered_ids = [
f"{cat}_{tier}" for cat in ["networker", "tokensniper", "hypothesismaster", "memoryarchitect", "sessionveteran"]
for tier in ["bronze", "silver", "gold", "platinum"]
]
for tid in tiered_ids:
a = next(aa for aa in achs if aa['id'] == tid)
assert a['image'] is not None
assert a['image'].endswith(tid + '.png')
+199
View File
@@ -0,0 +1,199 @@
# mcp-image-gen — Architecture Assessment
**Date:** 2026-04-04
**Author:** Lumen (for Patrick / pplate)
**Status:** ✅ APPROVED — ready for implementation
**BigMind Research Session:** `39809470-6ac8-4713-adf2-79ac0eb36ba7`
---
## 1. Problem Statement
LLM agents (Claude, local models via Ollama) have no native ability to generate images. While
language models excel at text, creative and technical workflows increasingly need image output —
concept art, diagrams, product mockups, illustrations — all driven by a text prompt.
A FastMCP wrapper around a local image generation backend would give any MCP-capable IDE or
agent the ability to produce images on demand, with full control over resolution, steps, model,
and seed — without sending data to external cloud APIs.
**Gap being filled:** Local AI image generation accessible to LLM agents via MCP protocol,
running entirely on Patrick's AMD RX 7900 XTX (24GB VRAM) with ROCm.
---
## 2. Requirements
### 2.1 Functional Requirements
| ID | Requirement |
|----|-------------|
| F-1 | Generate an image from a text prompt |
| F-2 | Support configurable resolution (width × height) |
| F-3 | Support configurable inference steps and seed for reproducibility |
| F-4 | Support negative prompts to exclude unwanted content |
| F-5 | List available models from the backend |
| F-6 | Check the status of an in-progress generation job |
| F-7 | Return generated image as both a file path AND inline base64 for agent display |
| F-8 | Configure output directory for saved images |
| F-9 | Support FLUX.1-schnell as the default model |
### 2.2 Non-Functional Requirements
| ID | Requirement |
|----|-------------|
| NF-1 | Generation time < 30 seconds for FLUX.1-schnell at 1024×1024, 4 steps |
| NF-2 | VRAM footprint < 12GB (leaves headroom on 24GB for Ollama co-existence) |
| NF-3 | Must work on AMD ROCm — no CUDA-only dependencies in the MCP server layer |
| NF-4 | No cloud API calls — fully local execution |
| NF-5 | Graceful error messages when ComfyUI is not running |
| NF-6 | MCP tools must work with FastMCP and be discoverable by Claude / Roo Code |
---
## 3. Technology Decision
### 3.1 Candidate Backends
| Backend | Stars | ROCm | REST API | FLUX Support | Verdict |
|---------|-------|------|----------|--------------|---------|
| **ComfyUI** | 108k | ✅ Native | ✅ localhost:8188 | ✅ FLUX.1-schnell, FLUX.1-dev | ✅ **CHOSEN** |
| stable-diffusion.cpp | ~15k | ✅ ROCm/Vulkan | ❌ CLI only | ✅ FLUX.1-schnell | ⚠️ Viable alternative |
| PyTorch + diffusers | — | ✅ ROCm 7.2.1 | ❌ No REST | ✅ All models | ❌ Too complex to manage |
| Ollama image gen | — | ❌ Linux: N/A | ✅ /api/generate | ✅ FLUX.2, Z-Image | ❌ macOS-only as of April 2026 |
| A1111 / Forge WebUI | — | ⚠️ Limited | ✅ :7860 | ❌ SDXL primary | ❌ Not FLUX-native |
### 3.2 Why ComfyUI
1. **ROCm native** — ComfyUI's PyTorch backend runs on AMD GPUs via ROCm without forks or patches.
2. **REST API** — ComfyUI exposes a stable HTTP API at `localhost:8188` making it trivially
wrappable with `httpx`. No subprocess management or binary spawning needed.
3. **Workflow-based** — ComfyUI workflows are JSON graphs. The MCP server ships a minimal
FLUX.1-schnell workflow that can be parameterized with prompt, size, steps, seed at runtime.
4. **Model ecosystem** — ComfyUI's model manager supports FLUX.1, SDXL, SD3.5, ControlNet,
LoRA — giving a future-proof upgrade path.
5. **Community size** — 108k GitHub stars; extensive community support, model nodes, extensions.
6. **VRAM efficiency** — FLUX.1-schnell requires ~8GB VRAM. Patrick's 24GB card runs it
comfortably alongside Ollama.
### 3.3 Why NOT the Alternatives
- **Ollama:** Definitively blocked on Linux until further notice. No ETA for Linux image gen.
- **stable-diffusion.cpp:** CLI-based only — the MCP server would need to manage a subprocess,
parse stdout, handle crashes. More fragile than an HTTP API.
- **PyTorch + diffusers direct:** Requires managing Python environments, device placement, model
loading, memory management inside the MCP server process — adds significant complexity and
risk of VRAM conflicts.
---
## 4. Architecture Decision
### 4.1 System Overview
```
┌─────────────────────────────────────────────────────────┐
│ LLM Agent (Claude / Roo Code / local Ollama) │
└───────────────────────────┬─────────────────────────────┘
│ MCP Protocol (stdio)
┌───────────────────────────▼─────────────────────────────┐
│ mcp-image-gen (FastMCP Python server) │
│ │
│ Tools: │
│ • generate_image(prompt, width, height, steps, ...) │
│ • list_available_models() │
│ • get_generation_status(prompt_id) │
│ • get_output_directory() │
└───────────────────────────┬─────────────────────────────┘
│ HTTP REST (httpx)
┌───────────────────────────▼─────────────────────────────┐
│ ComfyUI (localhost:8188) │
│ AMD ROCm + PyTorch │
│ FLUX.1-schnell model │
└─────────────────────────────────────────────────────────┘
┌───────▼───────┐
│ ~/Pictures/ │
│ mcp-generated│
└───────────────┘
```
### 4.2 Key Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| HTTP client | `httpx` (async) | Already used in webscraper; async-friendly; clean timeout handling |
| Image return | dual: path + base64 | File path for persistence; base64 `ImageContent` for inline Claude display |
| ImageContent type | `mcp.types.ImageContent` | FastMCP 3.x: **never** use `fastmcp.utilities.types.Image` with `-> Image` annotation — it breaks serialization. Return `ImageContent` directly as a `ContentBlock`. |
| Job polling | loop with sleep | ComfyUI `/api/queue` returns pending/running/done status; poll until done or timeout |
| Workflow format | ComfyUI API JSON | Minimal FLUX.1-schnell graph parameterized at runtime |
| Config | env vars | `COMFYUI_URL`, `IMAGE_OUTPUT_DIR` — no hardcoded paths |
| Output naming | `{timestamp}_{seed}.png` | Reproducible, collision-free, sortable |
---
## 5. Risks
| Risk | Likelihood | Impact | Mitigation |
|------|------------|--------|------------|
| ComfyUI not running when tool is called | High | High | Return clear error: "ComfyUI not reachable at {url}. Start with: `python main.py --listen`" |
| Generation timeout (>60s) | Medium | Medium | Configurable timeout; return partial status message with `prompt_id` so agent can poll manually |
| VRAM contention with Ollama | Medium | Medium | FLUX.1-schnell uses ~8GB; 24GB card has 16GB headroom. Document that running both simultaneously may compete at >8GB Ollama model sizes |
| ROCm driver instability | Low | High | ComfyUI falls back to CPU if ROCm unavailable — slow but functional. Document ROCm setup. |
| ComfyUI API changes | Low | Medium | Pin ComfyUI version in setup docs; the `/api/prompt`, `/api/queue`, `/api/view` endpoints are stable |
| Large output files | Low | Low | PNG default; add optional JPEG quality param in v2 |
| Malformed workflow JSON | Low | High | Ship a tested, minimal FLUX.1-schnell workflow; validate before submit |
---
## 6. Alternatives Considered
### 6.1 Ollama (Blocked)
Ollama added image generation in January 2026 (Z-Image Turbo, FLUX.2 Klein) but the feature is
**macOS-only** as of April 2026. Linux support is listed as "coming soon" with no ETA. This was
the originally preferred path (uniform API with text generation), but it is not viable on Fedora
Linux today.
**Migration path:** When Ollama Linux image gen ships, a thin backend adapter can be added to
`mcp-image-gen` so it routes to Ollama instead of ComfyUI — same MCP tool signatures, different
HTTP target.
### 6.2 stable-diffusion.cpp
DiffuGen MCP server uses this approach. Requires:
- Building sd.cpp with ROCm/Vulkan flags
- Spawning a subprocess and parsing CLI output
- No REST API — process management in Python
Viable but more fragile than ComfyUI's HTTP API. Chosen only if ComfyUI proves unworkable.
### 6.3 diffusers (Python library, direct)
Would run diffusion pipeline inside the MCP server process. Problems:
- MCP server process cannot easily share GPU memory with Ollama
- Model loading adds 5-15s cold start to every MCP invocation
- Complex device placement / fp16 / ROCm configuration in server code
- Risk: VRAM OOM crashes the MCP server process entirely
---
## 7. Success Criteria
| Criterion | Measure |
|-----------|---------|
| `generate_image` returns a valid PNG | File exists on disk, base64 decodes to valid PNG bytes |
| Claude can display the image inline | `ImageContent` returned in tool response, visible in Roo Code chat |
| FLUX.1-schnell at 1024×1024 4-step completes in <30s | Measured on RX 7900 XTX with ROCm |
| `list_available_models` returns ComfyUI model list | At minimum includes `flux1-schnell.safetensors` |
| ComfyUI offline → clear error, not crash | Tool returns error string, no MCP server exception |
| All pytest tests pass | `uv run pytest tests/ -v` exits 0 with ≥80% coverage |
| Server wired into `.roo/mcp.json` | Tool appears in Roo Code MCP tool list |
---
## 8. Open Questions
| # | Question | Owner | Priority |
|---|----------|-------|----------|
| Q1 | Should `generate_image` be synchronous (block until done) or return a `prompt_id` immediately? | Patrick | High — MVP will be synchronous; async polling is v2 |
| Q2 | Default output directory: `~/Pictures/mcp-generated` or `~/mcp-images`? | Patrick | Low — configurable via env var |
| Q3 | Should we support SDXL as a second model in v1, or FLUX.1-schnell only? | Patrick | Low — FLUX.1-schnell only for v1 |
| Q4 | WebSocket API vs REST polling for job status? | — | ComfyUI has both; REST polling is simpler for v1 |
+496
View File
@@ -0,0 +1,496 @@
# mcp-image-gen — Implementation Plan
**Date:** 2026-04-04
**Author:** Lumen (for Patrick / pplate)
**Status:** Ready for implementation
**Assessment:** [ASSESSMENT.md](./ASSESSMENT.md)
**Research Session:** `39809470-6ac8-4713-adf2-79ac0eb36ba7`
---
## 1. Directory Structure
```
mcp/mcp-image-gen/
├── ASSESSMENT.md ← Architecture assessment (this session)
├── PLAN.md ← This file
├── README.md ← Usage docs, tool table, env vars
├── pyproject.toml ← uv project + deps
├── run.sh ← Launch script (used by .roo/mcp.json)
├── src/
│ ├── __init__.py
│ ├── server.py ← FastMCP server + all tools
│ └── workflows/
│ └── flux_schnell.json ← Minimal ComfyUI API-format workflow
└── tests/
├── __init__.py
├── conftest.py ← sys.path + shared fixtures
└── test_server.py ← All tool tests (mocked ComfyUI)
```
---
## 2. Tool Definitions
### 2.1 `generate_image`
```python
@mcp.tool()
async def generate_image(
prompt: str,
width: int = 1024,
height: int = 1024,
steps: int = 4,
model: str = "flux1-schnell.safetensors",
seed: int = -1,
negative_prompt: str = "",
output_dir: str = "",
) -> list:
"""
Generate an image from a text prompt using ComfyUI.
Returns both a file path (for persistence) and an inline base64 image
(for display in Claude / Roo Code chat).
Args:
prompt: Text description of the image to generate.
width: Image width in pixels (default: 1024).
height: Image height in pixels (default: 1024).
steps: Number of inference steps. FLUX.1-schnell works well at 4.
model: ComfyUI model filename (default: flux1-schnell.safetensors).
seed: Random seed for reproducibility. -1 = random.
negative_prompt: Things to exclude from the image (optional).
output_dir: Override output directory. Defaults to IMAGE_OUTPUT_DIR env var
or ~/Pictures/mcp-generated.
Returns:
[TextContent(path + metadata), ImageContent(base64 PNG)]
"""
```
**Return type:** `list` containing:
1. `mcp.types.TextContent` — human-readable summary with file path, seed, elapsed time
2. `mcp.types.ImageContent``type="image"`, `data=base64_encoded_png`, `mimeType="image/png"`
> ⚠️ **FastMCP 3.x rule:** NEVER annotate return as `-> Image` (fastmcp utility type). It triggers
> `output_schema` generation which breaks the early-return path. Return `mcp.types.ImageContent`
> directly as part of a `list` — it is a `ContentBlock` and passes through cleanly.
---
### 2.2 `list_available_models`
```python
@mcp.tool()
async def list_available_models() -> str:
"""
List all checkpoint models available in ComfyUI.
Returns a newline-separated list of model filenames.
Requires ComfyUI to be running at COMFYUI_URL.
"""
```
**Implementation:** `GET {COMFYUI_URL}/object_info/CheckpointLoaderSimple` → parse
`input.required.ckpt_name[0]` list → join with newlines.
---
### 2.3 `get_generation_status`
```python
@mcp.tool()
async def get_generation_status(prompt_id: str) -> str:
"""
Check the status of a queued or running generation job.
Args:
prompt_id: The prompt ID returned by a previous generate_image call.
Returns:
Status string: "pending", "running", "completed", or "not_found".
"""
```
**Implementation:** `GET {COMFYUI_URL}/api/queue` → check `queue_running` and `queue_pending`
lists for matching `prompt_id`. If not found in either, check history endpoint.
---
### 2.4 `get_output_directory`
```python
@mcp.tool()
def get_output_directory() -> str:
"""
Return the directory where generated images are saved.
Returns:
Absolute path to the output directory.
"""
```
**Implementation:** Resolve `IMAGE_OUTPUT_DIR` env var or default `~/Pictures/mcp-generated`,
expand `~`, return as string.
---
## 3. ComfyUI Integration
### 3.1 Workflow: Submit → Poll → Retrieve
```
generate_image()
├── 1. Load flux_schnell.json workflow template
├── 2. Parameterize: inject prompt, width, height, steps, seed, model
├── 3. POST {COMFYUI_URL}/api/prompt → {"prompt_id": "uuid"}
├── 4. POLL loop (max 120s, sleep 2s between)
│ GET {COMFYUI_URL}/api/queue
│ → check queue_running[].prompt_id == our id
│ → check queue_pending[].prompt_id == our id
│ → if neither: job is done
├── 5. GET {COMFYUI_URL}/api/history/{prompt_id}
│ → find output image filename + subfolder
├── 6. GET {COMFYUI_URL}/api/view?filename={name}&subfolder={subfolder}&type=output
│ → raw PNG bytes
├── 7. Save PNG to output_dir/{timestamp}_{seed}.png
└── 8. Return [TextContent(path + meta), ImageContent(base64)]
```
### 3.2 API Endpoints Used
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/prompt` | POST | Submit workflow for generation |
| `/api/queue` | GET | Poll queue status (pending + running) |
| `/api/history/{prompt_id}` | GET | Get completed job output filenames |
| `/api/view` | GET | Download image bytes by filename |
| `/object_info/CheckpointLoaderSimple` | GET | List available checkpoint models |
### 3.3 Error Handling
| Condition | Response |
|-----------|----------|
| ComfyUI unreachable | `"ComfyUI not reachable at {url}. Start it with: python main.py --listen"` |
| Timeout (>120s) | `"Generation timed out after 120s. prompt_id={id} — use get_generation_status to check"` |
| ComfyUI returns error in history | Extract and return the error message from history response |
| Invalid model name | ComfyUI returns error in history; surface it clearly |
| Output dir not writable | `"Cannot write to output directory: {path}"` |
---
## 4. Configuration
All configuration via environment variables. No hardcoded paths.
| Variable | Default | Description |
|----------|---------|-------------|
| `COMFYUI_URL` | `http://localhost:8188` | Base URL of running ComfyUI instance |
| `IMAGE_OUTPUT_DIR` | `~/Pictures/mcp-generated` | Where to save generated PNG files |
| `COMFYUI_TIMEOUT` | `120` | Max seconds to wait for generation (int) |
### `.roo/mcp.json` entry (to be added during implementation):
```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"
}
}
```
---
## 5. `pyproject.toml`
```toml
[project]
name = "mcp-image-gen"
version = "0.1.0"
requires-python = ">=3.11"
description = "MCP server for local AI image generation via ComfyUI"
dependencies = [
"fastmcp>=0.1.0",
"httpx>=0.27.0",
"pillow>=10.0.0",
]
[project.optional-dependencies]
test = [
"pytest>=7.0",
"pytest-mock>=3.0",
"pytest-cov>=4.0",
"pytest-asyncio>=0.23",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.pytest.ini_options]
asyncio_mode = "auto"
```
**Dependency rationale:**
- `fastmcp` — MCP framework
- `httpx` — async HTTP client for ComfyUI REST API
- `pillow` — validate PNG output, potential future thumbnail generation
- `pytest-asyncio` — needed for async tool tests
---
## 6. FLUX.1-schnell Workflow JSON
The minimal ComfyUI API-format workflow for FLUX.1-schnell text-to-image.
This is the "API format" (node-graph JSON), not the UI export format.
File: `src/workflows/flux_schnell.json`
```json
{
"6": {
"class_type": "CLIPTextEncode",
"inputs": {
"clip": ["30", 1],
"text": "PROMPT_PLACEHOLDER"
}
},
"8": {
"class_type": "VAEDecode",
"inputs": {
"samples": ["13", 0],
"vae": ["30", 2]
}
},
"9": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "mcp-image-gen",
"images": ["8", 0]
}
},
"13": {
"class_type": "KSampler",
"inputs": {
"cfg": 1.0,
"denoise": 1.0,
"latent_image": ["27", 0],
"model": ["30", 0],
"negative": ["33", 0],
"positive": ["6", 0],
"sampler_name": "euler",
"scheduler": "simple",
"seed": 42,
"steps": 4
}
},
"27": {
"class_type": "EmptySD3LatentImage",
"inputs": {
"batch_size": 1,
"height": 1024,
"width": 1024
}
},
"30": {
"class_type": "CheckpointLoaderSimple",
"inputs": {
"ckpt_name": "flux1-schnell.safetensors"
}
},
"33": {
"class_type": "CLIPTextEncode",
"inputs": {
"clip": ["30", 1],
"text": "NEGATIVE_PLACEHOLDER"
}
}
}
```
**Parameterization at runtime** (in `server.py`):
```python
import json, copy
def _build_workflow(prompt, negative_prompt, width, height, steps, seed, model):
with open(Path(__file__).parent / "workflows/flux_schnell.json") as f:
wf = json.load(f)
wf = copy.deepcopy(wf)
wf["6"]["inputs"]["text"] = prompt
wf["33"]["inputs"]["text"] = negative_prompt
wf["27"]["inputs"]["width"] = width
wf["27"]["inputs"]["height"] = height
wf["13"]["inputs"]["steps"] = steps
wf["13"]["inputs"]["seed"] = seed if seed != -1 else random.randint(0, 2**32 - 1)
wf["30"]["inputs"]["ckpt_name"] = model
return wf
```
---
## 7. Testing Strategy
### 7.1 Test Structure (`tests/test_server.py`)
All tests mock `httpx.AsyncClient` — no real ComfyUI needed.
| Test | Description |
|------|-------------|
| `test_generate_image_happy_path` | Mock submit → poll done → history → view → returns TextContent + ImageContent |
| `test_generate_image_comfyui_offline` | httpx.ConnectError → returns clear error string |
| `test_generate_image_timeout` | Poll loop exceeds COMFYUI_TIMEOUT → returns timeout message with prompt_id |
| `test_generate_image_saves_file` | Verify PNG written to output_dir with correct filename pattern |
| `test_generate_image_random_seed` | seed=-1 → seed in output filename is a valid integer |
| `test_generate_image_custom_params` | Non-default width/height/steps/model passed through to workflow |
| `test_generate_image_returns_image_content` | Second item in result list is `mcp.types.ImageContent` with valid base64 |
| `test_list_available_models_happy_path` | Mock object_info response → returns model name list |
| `test_list_available_models_offline` | ConnectError → returns error string |
| `test_get_generation_status_pending` | prompt_id found in queue_pending → "pending" |
| `test_get_generation_status_running` | prompt_id found in queue_running → "running" |
| `test_get_generation_status_not_found` | prompt_id not in queue, not in history → "not_found" |
| `test_get_output_directory_default` | No env var → returns expanded ~/Pictures/mcp-generated |
| `test_get_output_directory_custom` | IMAGE_OUTPUT_DIR set → returns that path |
| `test_build_workflow_parameterization` | _build_workflow() injects all params correctly into JSON |
### 7.2 conftest.py fixtures
```python
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
@pytest.fixture
def mock_comfyui_submit_response():
return {"prompt_id": "test-uuid-1234"}
@pytest.fixture
def mock_comfyui_queue_empty():
return {"queue_running": [], "queue_pending": []}
@pytest.fixture
def mock_comfyui_history():
return {
"test-uuid-1234": {
"outputs": {
"9": {
"images": [{"filename": "mcp-image-gen_00001_.png", "subfolder": "", "type": "output"}]
}
}
}
}
@pytest.fixture
def sample_png_bytes():
"""Minimal valid 1x1 PNG in bytes."""
import base64
# 1x1 red pixel PNG
data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwADhQGAWjR9awAAAABJRU5ErkJggg=="
return base64.b64decode(data)
```
### 7.3 Run command
```bash
cd mcp/mcp-image-gen && uv run pytest tests/ -v --cov=src --cov-report=term-missing
```
---
## 8. `run.sh`
```bash
#!/usr/bin/env bash
BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PATH="$HOME/.local/bin:$PATH"
# Create output dir if it doesn't exist
OUTPUT_DIR="${IMAGE_OUTPUT_DIR:-$HOME/Pictures/mcp-generated}"
mkdir -p "$OUTPUT_DIR"
cd "$BASEDIR"
exec uv run src/server.py
```
---
## 9. Future: Ollama Migration Path
When Ollama adds Linux image generation support (ETA unknown, announced "coming soon" April 2026):
### Adapter pattern (no breaking changes to MCP tool signatures)
```python
BACKEND = os.getenv("IMAGE_BACKEND", "comfyui") # or "ollama"
async def _generate_comfyui(prompt, width, height, steps, model, seed, negative_prompt, output_dir):
# current ComfyUI implementation
...
async def _generate_ollama(prompt, width, height, steps, model, seed, negative_prompt, output_dir):
# POST http://localhost:11434/api/generate
# with model=Z-Image-Turbo or FLUX.2-Klein
# width, height, steps in request body
# save returned image path
...
@mcp.tool()
async def generate_image(prompt, width=1024, height=1024, steps=4, ...):
if BACKEND == "ollama":
return await _generate_ollama(...)
return await _generate_comfyui(...)
```
**No changes to:** tool signatures, return types, env vars (add `IMAGE_BACKEND`), tests structure.
---
## 10. Implementation Order (for Code mode)
1. `src/workflows/flux_schnell.json` — write and validate JSON structure
2. `pyproject.toml` — set up project + deps
3. `src/__init__.py` — empty
4. `src/server.py` — implement all 4 tools + `_build_workflow` + polling helpers
5. `tests/conftest.py` — fixtures + sys.path
6. `tests/test_server.py` — all 15 tests
7. `run.sh` — launch script
8. `README.md` — usage docs
9. `.roo/mcp.json` — wire server in (requires switching to Code or Homelab mode for that file)
10. `uv sync && uv run pytest tests/ -v` — confirm all tests pass
---
## 11. ComfyUI Setup Notes (for README)
These are prerequisites for the MCP server to work. Patrick must have ComfyUI installed:
```bash
# Install ComfyUI (ROCm/AMD)
pip install comfyui
# Download FLUX.1-schnell model (~8GB)
# Place in ComfyUI/models/checkpoints/flux1-schnell.safetensors
# Source: https://huggingface.co/black-forest-labs/FLUX.1-schnell
# Start ComfyUI with AMD ROCm
HSA_OVERRIDE_GFX_VERSION=11.0.0 python main.py --listen
# Verify API is running
curl http://localhost:8188/system_stats
```
> The `HSA_OVERRIDE_GFX_VERSION=11.0.0` env var may be needed for RX 7900 XTX (gfx1100)
> to identify correctly to ROCm libraries.
+178
View File
@@ -0,0 +1,178 @@
# mcp-image-gen
**FastMCP server for AI image generation via ComfyUI.**
This MCP server wraps a locally running [ComfyUI](https://github.com/comfyanonymous/ComfyUI) instance, exposing image generation as MCP tools callable from Roo Code, Claude Desktop, or any MCP-compatible client. It supports FLUX.1-schnell, FLUX.1-dev, SDXL, and any other ComfyUI-compatible checkpoint model. Generated images are saved to disk **and** returned as inline base64 so Claude can display them directly in chat.
---
## Prerequisites
1. **ComfyUI** installed and running at `http://localhost:8188`
2. At least one checkpoint model downloaded (see ComfyUI Setup below)
3. **Python 3.11+** and **uv** installed on the system
---
## Installation
```bash
cd mcp/mcp-image-gen
uv sync
```
---
## Configuration
All configuration is via environment variables:
| Variable | Default | Description |
|---|---|---|
| `COMFYUI_URL` | `http://localhost:8188` | Base URL of the running ComfyUI instance |
| `IMAGE_OUTPUT_DIR` | `~/Pictures/mcp-generated` | Directory where generated PNG files are saved |
| `COMFYUI_TIMEOUT` | `120` | Max seconds to wait for generation before timeout |
---
## Usage
### Add to `.roo/mcp.json` (Roo Code)
```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"
}
}
```
### Add to Claude Desktop (`claude_desktop_config.json`)
```json
{
"mcpServers": {
"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"
}
}
}
}
```
### Run directly
```bash
cd mcp/mcp-image-gen
./run.sh
```
---
## Available Tools
| Tool | Description |
|---|---|
| `generate_image` | Generate an image from a text prompt. Returns file path + inline base64 PNG. |
| `list_available_models` | List all checkpoint models loaded in ComfyUI. |
| `get_generation_status` | Check status of a running/queued generation by `prompt_id`. |
| `get_output_directory` | Return the current output directory path. |
### `generate_image` parameters
| 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: 4 is optimal) |
| `model` | `flux1-schnell.safetensors` | Checkpoint model filename |
| `seed` | `-1` | Seed for reproducibility (`-1` = random) |
| `negative_prompt` | `""` | Things to exclude from the image |
| `output_dir` | *(IMAGE_OUTPUT_DIR)* | Override output directory |
---
## ComfyUI Setup (Fedora + AMD ROCm)
```bash
# Install ComfyUI
pip install comfyui
# Download FLUX.1-schnell model (~8GB, Apache 2.0)
# Place in: ComfyUI/models/checkpoints/flux1-schnell.safetensors
# Source: https://huggingface.co/black-forest-labs/FLUX.1-schnell
# Start ComfyUI with ROCm support for AMD RX 7900 XTX
HSA_OVERRIDE_GFX_VERSION=11.0.0 python main.py --listen
# Verify the API is reachable
curl http://localhost:8188/system_stats
```
> **Note:** `HSA_OVERRIDE_GFX_VERSION=11.0.0` may be needed for the RX 7900 XTX (gfx1100)
> to be recognized correctly by ROCm libraries.
### PyTorch with ROCm (if needed separately)
```bash
pip install torch torchvision --index-url https://download.pytorch.org/whl/rocm6.1
```
---
## Testing
```bash
cd mcp/mcp-image-gen
uv run pytest tests/ -v
```
All tests mock the ComfyUI HTTP API — no running ComfyUI instance needed.
---
## Ollama Migration Path
When Ollama adds Linux image generation support (announced "coming soon" as of April 2026, currently macOS-only), this server can switch backends via a single env var:
```bash
IMAGE_BACKEND=ollama # currently only "comfyui" is implemented
```
The tool signatures, return types, and MCP interface will remain unchanged — only the underlying HTTP calls switch from ComfyUI to Ollama's `/api/generate` endpoint.
---
## Architecture
```
Roo Code / Claude Desktop
│ MCP (stdio)
mcp-image-gen (FastMCP)
│ HTTP REST
ComfyUI @ localhost:8188
│ ROCm / AMD GPU
FLUX.1-schnell / SDXL / SD3.5
```
The server submits a FLUX.1-schnell ComfyUI API-format workflow, polls until complete, downloads the PNG, saves it to disk, and returns both a text summary and a base64-encoded inline image.
+619
View File
@@ -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 1030 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: 5122048 recommended. |
| `height` | `int` | `1024` | Image height in pixels. FLUX.1-schnell: 5122048 recommended. |
| `steps` | `int` | `4` | Number of KSampler inference steps. FLUX.1-schnell is designed for 18 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 18 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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 992 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Some files were not shown because too many files have changed in this diff Show More