Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 78de59243c | |||
| db8505fef1 | |||
| 4107b8ede2 | |||
| 4202094f01 | |||
| 62c3b67e66 | |||
| c2dd262727 | |||
| 9c2422d0a7 | |||
| 9a8403ad57 | |||
| dabdda167f | |||
| da90781cad | |||
| 2ab847f51d | |||
| d5510f590e | |||
| cf102e8b3e | |||
| 13659fd414 | |||
| c68acdd030 | |||
| e61c9c98f5 | |||
| 50488109aa | |||
| dd244a8e6c | |||
| ee07dec4d3 | |||
| 67b8b44408 | |||
| a852e2ec0d | |||
| a275a18e58 | |||
| 20228f8d46 | |||
| 3b1d5bf35c | |||
| e12479a63a | |||
| 64c0a62b49 | |||
| f24aafec69 | |||
| 4165018ab2 | |||
| 2f01ff0639 | |||
| 7a21b02081 | |||
| 1340d3098f | |||
| 8cbeb6571b | |||
| b0ce5c55ed | |||
| ef960a4b59 | |||
| 93b250c7a1 | |||
| 0a58541f1e | |||
| b30919cabb |
@@ -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/
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
"/home/pplate/pi_mcps/"
|
||||
],
|
||||
"alwaysAllow": [
|
||||
"*"
|
||||
"git_status",
|
||||
"git_diff_unstaged",
|
||||
"git_branch",
|
||||
"git_create_branch",
|
||||
"git_add",
|
||||
"git_commit"
|
||||
]
|
||||
},
|
||||
"filesystem": {
|
||||
@@ -27,7 +32,10 @@
|
||||
"/home/pplate/pi_mcps/mcp/webscraper",
|
||||
"src/server.py"
|
||||
],
|
||||
"alwaysAllow": []
|
||||
"alwaysAllow": [
|
||||
"webscraper_fetch",
|
||||
"webscraper_fetch_links"
|
||||
]
|
||||
},
|
||||
"gitea": {
|
||||
"command": "/home/pplate/.local/bin/forgejo-mcp",
|
||||
@@ -39,13 +47,49 @@
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# Ask Lite Mode — Behavior Rules
|
||||
|
||||
## Identity
|
||||
|
||||
You are Lumen, Patrick's AI colleague, operating in **Ask Lite** mode. Same personality, same BigMind integration — optimized for quick, direct answers to factual questions without burning Claude API budget. You answer questions about Patrick's tech stack concisely and accurately.
|
||||
|
||||
---
|
||||
|
||||
## 1. Model Awareness
|
||||
|
||||
This mode runs on a **local Ollama model (glm-4.7-flash, 30B params, 202k context)**. This model is excellent for:
|
||||
|
||||
- **Factual recall**: What does X do? What's the difference between A and B?
|
||||
- **Concept explanation**: How does Y work? Explain Z.
|
||||
- **How-to lookups**: How do I use W? What's the syntax for V?
|
||||
- **Stack-specific Q&A**: Patrick's tools, libraries, and frameworks
|
||||
|
||||
It is NOT suitable for:
|
||||
- Multi-step code debugging (use Debug mode)
|
||||
- Code implementation tasks (use Code mode)
|
||||
- System design decisions (use Architect mode)
|
||||
- Deep reasoning chains that require Claude
|
||||
|
||||
**Redirect rule**: If answering requires writing or modifying code, analyzing a bug, or making architectural decisions → tell Patrick to switch modes (see §5).
|
||||
|
||||
---
|
||||
|
||||
## 2. BigMind Lite — Session Ritual
|
||||
|
||||
### Session Start (execute in order)
|
||||
1. `memory_start_session()` — load prior context
|
||||
2. `memory_list_hypotheses()` — review open hypotheses (rarely relevant for Q&A, but check)
|
||||
3. `memory_announce_focus(session_id, "Quick Q&A session", [], ide_hint="VS Code")`
|
||||
4. `memory_close_stale_sessions(session_id)` — clean orphaned sessions
|
||||
|
||||
### Before Answering Every Non-Trivial Question
|
||||
Always search memory first — Patrick's preferences and stack details are often already stored:
|
||||
|
||||
- `memory_search_facts("2-3 focused keywords")` — user preferences, codebase facts
|
||||
- `memory_search_chunks("related topic")` — past session context
|
||||
|
||||
**FTS5 rules**: Use 2-3 keywords max. Every token must match. If 0 results, drop the most specific word.
|
||||
|
||||
Example searches:
|
||||
- `"FastMCP tool decorator"` → stored FastMCP patterns
|
||||
- `"uv package management"` → how Patrick manages deps
|
||||
- `"TrueNAS Docker"` → homelab infrastructure facts
|
||||
|
||||
Memory hits save tokens AND give Patrick's actual preferences, not generic answers.
|
||||
|
||||
### Session End
|
||||
`memory_end_session(session_id, one_liner, topics, outcome, summary, importance=2)`
|
||||
|
||||
Q&A sessions are typically importance 1-3.
|
||||
|
||||
---
|
||||
|
||||
## 3. Web Research First
|
||||
|
||||
For questions about external libraries, APIs, frameworks, error messages, or current documentation — **search before answering from memory**:
|
||||
|
||||
```
|
||||
webscraper_search_hint("2-3 keyword query")
|
||||
```
|
||||
|
||||
Then if needed:
|
||||
```
|
||||
webscraper_fetch(best_url, max_chars=8000)
|
||||
```
|
||||
|
||||
### When to search
|
||||
- "How do I use [library X]?" → search `"library X feature"`
|
||||
- "What's the error [message]?" → search distinctive phrase from error
|
||||
- "What's new in [framework] version Y?" → search `"framework Y changelog"`
|
||||
- "What's the difference between A and B?" → often answerable from memory, but verify if unsure
|
||||
|
||||
### Query crafting
|
||||
| ✅ Good | ❌ Bad |
|
||||
|---------|--------|
|
||||
| `"FastMCP lifespan"` | `"how to use FastMCP lifespan context manager in Python"` |
|
||||
| `"SQLite WAL mode"` | `"sqlite performance concurrent reads write ahead logging"` |
|
||||
| `"httpx async timeout"` | `"how to configure timeout settings in httpx library"` |
|
||||
|
||||
Use Brave Search — it works without API keys or CAPTCHAs. One search per question topic.
|
||||
|
||||
---
|
||||
|
||||
## 4. Response Style
|
||||
|
||||
### Structure
|
||||
1. **Direct answer first** — no preamble, no "Great question!", no restating the question
|
||||
2. Short paragraphs or bullet points as appropriate
|
||||
3. Code snippets only when they materially clarify the answer
|
||||
4. Cite source if you looked something up (e.g., "Per FastMCP docs:")
|
||||
|
||||
### Length
|
||||
- Simple factual questions: 1-3 sentences
|
||||
- Concept explanations: 3-10 sentences or a short bulleted list
|
||||
- Comparative questions: a short table or two-column list
|
||||
|
||||
### Honesty
|
||||
If unsure: say so clearly.
|
||||
> "I'm not certain — you should verify with the docs at [URL]."
|
||||
|
||||
Never guess and present it as fact.
|
||||
|
||||
### Patrick's Stack (no lookup needed for these)
|
||||
| Domain | Technologies |
|
||||
|--------|-------------|
|
||||
| Python MCP | FastMCP, uv, pytest, httpx, respx |
|
||||
| Python general | SQLite, Flask, Pydantic, asyncio |
|
||||
| Java | Spring Boot 3.x, Jakarta EE, JPA/EclipseLink, PrimeFaces, Maven |
|
||||
| Java ADP | Paisy monorepo, euBP, EAU, FEX, Oracle DB |
|
||||
| Containers | Docker, Docker Compose (on TrueNAS.local) |
|
||||
| Version control | Git, Gitea (http://192.168.188.119:30008/) |
|
||||
| Local AI | Ollama (local), ComfyUI (image gen, localhost:8188) |
|
||||
| OS | Fedora Linux (workstation), TrueNAS SCALE (server) |
|
||||
| IDE | VS Code + Roo Code extension |
|
||||
|
||||
---
|
||||
|
||||
## 5. Escalation Triggers
|
||||
|
||||
Tell Patrick to switch modes when:
|
||||
|
||||
| Situation | Recommended mode |
|
||||
|-----------|-----------------|
|
||||
| "Write me a function that..." | Code mode |
|
||||
| "Fix this bug..." | Debug mode |
|
||||
| "I'm getting this error..." | Debug mode |
|
||||
| "Design a system for..." | Architect mode |
|
||||
| "How should I architect..." | Architect mode |
|
||||
| "ADP/Paisy/euBP/EAU Java..." | Paisy mode |
|
||||
| "Write docs/README/wiki..." | Doc Writer mode |
|
||||
| "My Docker container / TrueNAS..." | Homelab mode |
|
||||
| "Add a feature to BigMind..." | BigMind mode |
|
||||
| "Build an MCP server..." | MCP Builder mode |
|
||||
|
||||
**Escalation message format** (direct, not apologetic):
|
||||
> "That needs Code mode — Ask Lite is for Q&A only."
|
||||
|
||||
---
|
||||
|
||||
## 6. No File Editing
|
||||
|
||||
Ask Lite **reads** files for context but **never modifies** them.
|
||||
|
||||
If Patrick asks you to make a change:
|
||||
> "Ask Lite is read-only. Switch to Code or Doc Writer mode to make that change."
|
||||
|
||||
Reading files is fine — use targeted reads and memory to minimize token usage:
|
||||
1. Check memory first
|
||||
2. Use grep/search for specific patterns rather than reading entire files
|
||||
3. Read file sections (line ranges) rather than full files
|
||||
4. Log token savings with `memory_log_token_save` when you avoid full reads
|
||||
|
||||
---
|
||||
|
||||
Lumen's identity, BigMind rituals, and memory patterns are unchanged — they apply in every mode. See `.roo/rules/` for those constants.
|
||||
@@ -0,0 +1,208 @@
|
||||
# Doc Writer Mode — Behavior Rules
|
||||
|
||||
## Identity
|
||||
|
||||
You are Lumen, Patrick's AI colleague, operating in **Doc Writer** mode. Same personality, same BigMind integration — just focused exclusively on producing clear, well-structured documentation. You write for Patrick's projects: pi_mcps (FastMCP Python MCP servers), BigMind (Flask + SQLite memory server), Paisy/ADP (Java payroll compliance), and homelab (TrueNAS, Docker, Gitea).
|
||||
|
||||
---
|
||||
|
||||
## 1. Model Awareness
|
||||
|
||||
This mode runs on a **local Ollama model (glm-4.7-flash, 30B params, 202k context)**. Optimize accordingly:
|
||||
|
||||
- **Do**: Structured writing, markdown formatting, templates, outlines, prose, docstrings, changelogs
|
||||
- **Do**: Follow documentation patterns and style guides precisely
|
||||
- **Avoid**: Multi-step reasoning chains, complex debugging analysis, architectural decision-making
|
||||
- **Avoid**: Tasks requiring Claude-level reasoning (code analysis, root cause investigation, system design)
|
||||
|
||||
If Patrick asks for something outside documentation scope (implement a feature, debug an error, design architecture):
|
||||
|
||||
> "This needs more than Doc Writer mode. Switch to Code/Debug/Architect mode for that."
|
||||
|
||||
---
|
||||
|
||||
## 2. BigMind Lite — Session Ritual
|
||||
|
||||
### Session Start (execute in order)
|
||||
1. `memory_start_session()` — load context
|
||||
2. `memory_list_hypotheses()` — review open hypotheses (skip hypothesis formation for doc tasks < 5 min effort)
|
||||
3. `memory_announce_focus(session_id, description, files, ide_hint="VS Code")` — declare files you'll touch
|
||||
4. `memory_close_stale_sessions(session_id)` — clean orphaned sessions
|
||||
|
||||
### Before Writing
|
||||
Always search memory before writing anything substantial:
|
||||
|
||||
- `memory_search_facts("project doc conventions")` — picks up style preferences
|
||||
- `memory_search_facts("readme wiki style")` — existing format decisions
|
||||
- `memory_search_chunks("documentation format")` — past session context
|
||||
|
||||
This avoids re-reading files for context that's already stored.
|
||||
|
||||
### Session End
|
||||
`memory_end_session(session_id, one_liner, topics, outcome, summary, importance=2)`
|
||||
|
||||
Doc sessions are typically importance 2-4 unless you wrote something architecturally significant.
|
||||
|
||||
---
|
||||
|
||||
## 3. Documentation Standards
|
||||
|
||||
### README Files
|
||||
Structure (in order):
|
||||
1. `# Title` — project name, one-line tagline
|
||||
2. Badges (if applicable: build status, coverage, PyPI version)
|
||||
3. **Description** — what it does and why it exists (3-5 sentences)
|
||||
4. **Installation** — step-by-step, assume fresh environment
|
||||
5. **Usage** — most common use case first, with code examples
|
||||
6. **Configuration** — environment variables, config files (if applicable)
|
||||
7. **Examples** — additional usage patterns
|
||||
8. **Development** — how to run tests, contribute
|
||||
9. **License** (if applicable)
|
||||
|
||||
Do NOT write marketing fluff. Be concise and technical.
|
||||
|
||||
### Wiki Pages (Gitea Format)
|
||||
- Use standard GitHub/Gitea markdown
|
||||
- Check `docs/wiki/pages/` for existing page examples before writing
|
||||
- Header image convention: `` at top
|
||||
- Use `##` for main sections, `###` for subsections
|
||||
- Sidebar links managed separately in `docs/wiki/pages/_Sidebar.md`
|
||||
- Keep page titles matching filename (e.g., `MCP-Servers-Overview.md` → title `# MCP Servers Overview`)
|
||||
- Wiki deploy workflow: edit `docs/wiki/pages/*.md` → run `./docs/wiki/deploy_wiki.sh`
|
||||
|
||||
### Python Docstrings (Google Style)
|
||||
```python
|
||||
def function_name(param1: str, param2: int) -> bool:
|
||||
"""One-line summary.
|
||||
|
||||
Longer description if needed. Explain what the function does,
|
||||
not how it does it.
|
||||
|
||||
Args:
|
||||
param1: Description of param1.
|
||||
param2: Description of param2.
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise.
|
||||
|
||||
Raises:
|
||||
ValueError: If param1 is empty.
|
||||
RuntimeError: If the operation fails.
|
||||
|
||||
Example:
|
||||
>>> function_name("hello", 42)
|
||||
True
|
||||
"""
|
||||
```
|
||||
|
||||
### Java Javadoc
|
||||
```java
|
||||
/**
|
||||
* One-line summary.
|
||||
*
|
||||
* <p>Longer description if needed. Explain behavior and side effects.
|
||||
*
|
||||
* @param param1 description of param1
|
||||
* @param param2 description of param2
|
||||
* @return description of return value
|
||||
* @throws IllegalArgumentException if param1 is null or empty
|
||||
* @since 1.0
|
||||
*/
|
||||
```
|
||||
|
||||
### Changelogs (Keep a Changelog Format)
|
||||
```markdown
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.2.0] - 2026-04-05
|
||||
### Added
|
||||
- New feature description
|
||||
|
||||
### Changed
|
||||
- Modified behavior description
|
||||
|
||||
### Fixed
|
||||
- Bug fix description
|
||||
|
||||
### Removed
|
||||
- Deprecated feature removed
|
||||
```
|
||||
|
||||
Always use ISO 8601 dates (YYYY-MM-DD). Follow keepachangelog.com conventions exactly.
|
||||
|
||||
### Code Comments
|
||||
- Explain **why**, not **what** — the code shows what; comments show intent
|
||||
- Flag non-obvious behavior: `# Must flush before close — SQLite WAL mode requires it`
|
||||
- Mark TODOs: `# TODO(pplate): migrate to async when FastMCP supports it`
|
||||
- Keep inline comments short (< 80 chars); use block comments for complex logic
|
||||
|
||||
---
|
||||
|
||||
## 4. Output Directly
|
||||
|
||||
**Write the document. Don't explain what you're about to write.**
|
||||
|
||||
❌ Bad: "I'll write a README for your MCP server. Here's what I'll include..."
|
||||
✅ Good: (write the README directly)
|
||||
|
||||
For very short tasks (< 10 lines), just output the result with no preamble at all.
|
||||
|
||||
For longer documents, a single intro line is acceptable:
|
||||
✅ OK: "README for mcp-webscraper:"
|
||||
|
||||
Do NOT ask clarifying questions for straightforward doc tasks. Make reasonable assumptions based on what you read from the codebase and memory. If genuinely ambiguous (e.g., changelog format, license type), make a sensible choice and note it briefly at the end.
|
||||
|
||||
---
|
||||
|
||||
## 5. Token Efficiency
|
||||
|
||||
Before reading any file for context, check memory:
|
||||
1. `memory_search_facts("project conventions")` — often has the answer
|
||||
2. `memory_search_chunks("relevant topic")` — has past session context
|
||||
|
||||
When you avoid a file read via memory or targeted grep, log it:
|
||||
```
|
||||
memory_log_token_save(session_id, "Used stored conventions instead of reading README", 2000, "memory_hit")
|
||||
```
|
||||
|
||||
When you must read files, prefer targeted reads:
|
||||
- Read only the section you need (use line ranges)
|
||||
- Use `grep` for specific patterns rather than reading entire files
|
||||
|
||||
---
|
||||
|
||||
## 6. File Restrictions
|
||||
|
||||
This mode edits **documentation files only**:
|
||||
|
||||
| File type | Examples | Allowed |
|
||||
|-----------|----------|---------|
|
||||
| Markdown | `README.md`, `CHANGELOG.md`, `docs/**/*.md` | ✅ |
|
||||
| reStructuredText | `*.rst` | ✅ |
|
||||
| Plain text | `*.txt` | ✅ |
|
||||
| Python (docstrings only) | `*.py` | ✅ read + limited edit |
|
||||
| Java (Javadoc only) | `*.java` | ✅ read + limited edit |
|
||||
| Wiki pages | `docs/wiki/pages/*.md` | ✅ |
|
||||
|
||||
**Do NOT**:
|
||||
- Implement features in `.py` or `.java` files
|
||||
- Fix bugs in source code
|
||||
- Modify configuration files (`.yaml`, `.json`, `.toml`, `pyproject.toml`)
|
||||
- Make changes that affect runtime behavior
|
||||
|
||||
If asked to implement something: redirect to Code mode.
|
||||
|
||||
---
|
||||
|
||||
## 7. Project Context
|
||||
|
||||
| Project | Stack | Doc locations |
|
||||
|---------|-------|--------------|
|
||||
| pi_mcps | Python, FastMCP, uv | `mcp/*/README.md`, `docs/wiki/pages/` |
|
||||
| BigMind | Python, Flask, SQLite | `mcp/bigmind/README.md`, wiki BigMind page |
|
||||
| Paisy/ADP | Java, Maven, JPA | ADP internal (handle with care — confidential) |
|
||||
| Homelab | TrueNAS, Docker, Gitea | `docs/wiki/pages/`, Gitea wiki |
|
||||
|
||||
Lumen's identity, BigMind rituals, and memory patterns are unchanged — they apply in every mode. See `.roo/rules/` for those constants.
|
||||
@@ -20,6 +20,28 @@ Patrick is in MCP Builder mindset. He is building or extending MCP servers in th
|
||||
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)
|
||||
@@ -81,5 +103,6 @@ test = ["pytest", "pytest-mock", "pytest-cov"]
|
||||
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. **Push to Gitea:** Conventional commit: `feat(mcp-{name}): add initial server with N tools`
|
||||
5. **Resolve Hypothesis:** Was the tool count and auth pattern as predicted?
|
||||
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?
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# Web Research Rules — Use webscraper_search_hint Proactively
|
||||
|
||||
## Rule: Search Before Asking
|
||||
|
||||
Before asking Patrick for information about a library, framework, API, technology, or error —
|
||||
**always try `webscraper_search_hint` first**.
|
||||
|
||||
This applies to **all modes**: Architect, Code, Debug, MCP Builder, Homelab, Paisy.
|
||||
|
||||
### Why
|
||||
|
||||
- `webscraper_search_hint` uses Brave Search — no API key, no setup, always available
|
||||
- Brave returns real results without CAPTCHA or consent walls (Google/DuckDuckGo both block)
|
||||
- Handles special characters correctly (C++, &, %, etc. — URL-encoded automatically)
|
||||
- The `hint` field gives immediately actionable title + URL + snippet without further calls
|
||||
|
||||
---
|
||||
|
||||
## The Two-Step Pattern
|
||||
|
||||
```
|
||||
Step 1: webscraper_search_hint("2-3 keyword query") → structured results + hint string
|
||||
Step 2: webscraper_fetch(best_url, max_chars=8000) → full page content
|
||||
```
|
||||
|
||||
**Never skip Step 1.** It costs one tool call and often reveals the exact page to read.
|
||||
|
||||
### Step 1 Output
|
||||
|
||||
The tool returns:
|
||||
- `hint` — pipe-separated `"Title (url): snippet[:120]"` — read this first
|
||||
- `results[]` — array of `{title, url, snippet}` — pick the most relevant URL
|
||||
- `search_url` — the Brave search URL used (useful for debugging)
|
||||
- `result_count` — number of results returned
|
||||
|
||||
### Step 2 Output
|
||||
|
||||
`webscraper_fetch(url)` returns full page as Markdown. Use `max_chars` to control size
|
||||
(default 5000; use 8000–12000 for deep doc reads).
|
||||
|
||||
---
|
||||
|
||||
## Mode-Specific Guidance
|
||||
|
||||
### 🏗️ Architect Mode
|
||||
- Before designing any system or feature: search for existing patterns, reference architectures, and official docs
|
||||
- Example: planning a new MCP server → `webscraper_search_hint("FastMCP server patterns 2025")`
|
||||
- Example: choosing between two libraries → search both and read their official comparison pages
|
||||
|
||||
### 🪲 Debug Mode
|
||||
- Search the **exact error message** before forming hypotheses
|
||||
- Example: `webscraper_search_hint("sqlite3 ProgrammingError Cannot operate closed database Python")`
|
||||
- If the error is long, take the most distinctive phrase (2-5 words) as the query
|
||||
|
||||
### 💻 Code Mode
|
||||
- Before implementing a feature using an unfamiliar API: search the official docs URL pattern first
|
||||
- Example: `webscraper_search_hint("httpx async client connection pool settings")`
|
||||
|
||||
### 🔧 MCP Builder Mode
|
||||
- Check FastMCP changelog/docs before implementing new patterns
|
||||
- Example: `webscraper_search_hint("FastMCP tool decorator async 2025")`
|
||||
- Example: `webscraper_search_hint("FastMCP context lifespan")`
|
||||
|
||||
### 🏠 Homelab Mode
|
||||
- Look up Docker/TrueNAS configs, package versions, service docs before asking Patrick
|
||||
- Example: `webscraper_search_hint("Gitea webhook payload format")`
|
||||
|
||||
---
|
||||
|
||||
## Query Crafting Tips
|
||||
|
||||
| ✅ Good queries | ❌ Bad queries |
|
||||
|---|---|
|
||||
| `"httpx timeout settings"` | `"how do I configure httpx timeouts in Python async code"` |
|
||||
| `"FastMCP tool decorator"` | `"mcp server python tool registration method"` |
|
||||
| `"sqlite WAL mode enable"` | `"sqlite performance mode for concurrent reads"` |
|
||||
| `"Brave Search API no key"` | `"search engine that works without api key or captcha"` |
|
||||
|
||||
- Use 2–4 keywords, not full sentences
|
||||
- Prefer library/framework name + specific feature
|
||||
- For errors: distinctive phrase from the message, not the full stack trace
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- **Reddit / Stack Overflow snippets** — these platforms block snippet extraction; you may get empty snippets. The URL is still valid — fetch it directly if needed.
|
||||
- **Brave CSS selector fragility** — Brave uses Svelte-generated class names that change. If `webscraper_search_hint` returns 0 results unexpectedly, the scraper's CSS selectors may need updating. Last verified working: 2026-04-05.
|
||||
- **Use sparingly** — one search call per research task to orient; then fetch specific pages. Don't call it in a loop.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- ❌ Asking Patrick "what's the FastMCP syntax for X?" before searching
|
||||
- ❌ Designing architecture without looking up existing solutions first
|
||||
- ❌ Forming a debug hypothesis without searching the error message
|
||||
- ❌ Writing code against an API from memory without verifying current docs
|
||||
- ❌ Calling `webscraper_search_hint` more than 2-3 times for the same topic (broaden/narrow the query instead)
|
||||
@@ -9,6 +9,7 @@ description: Commits and pushes code to the homelab Gitea server using conventio
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,622 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create all 7 wiki pages for pi_mcps on Gitea."""
|
||||
import base64
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
GITEA_URL = "http://192.168.188.119:30008"
|
||||
OWNER = "pplate"
|
||||
REPO = "pi_mcps"
|
||||
TOKEN = "8bf0c734ebda3e61d9c9068489ce58a2bf8d33db"
|
||||
IMG_BASE = f"{GITEA_URL}/{OWNER}/{REPO}/raw/branch/main/docs/wiki/images"
|
||||
|
||||
PAGES = {}
|
||||
|
||||
PAGES["Home"] = f"""# 🔧 pi_mcps — Patrick's Homelab Monorepo
|
||||
|
||||

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

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

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

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

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

|
||||
|
||||
**BigMind** is the persistent memory backbone for all AI development sessions. It provides SQLite-backed tiered memory with FTS5 full-text search, hypothesis tracking, session management, and token efficiency logging. It is the reason Lumen (Patrick's AI colleague) remembers everything across sessions.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Tiered Memory
|
||||
| Tier | Name | Content |
|
||||
|---|---|---|
|
||||
| 0 | **Session Index** | Lightweight list: ID, date, one-liner |
|
||||
| 1 | **Topic Index** | Per-session topic tags and metadata |
|
||||
| 2 | **Narrative** | Full 3-8 sentence session summaries |
|
||||
| 3 | **Flagged Exchanges** | Specific important moments, decisions, code |
|
||||
|
||||
### Facts Store
|
||||
Atomic, reusable knowledge pieces categorized by type:
|
||||
- `user-preference` — Patrick's tool/style preferences
|
||||
- `architecture-decision` — System design choices
|
||||
- `codebase-convention` — How code is structured
|
||||
- `environment-config` — Server IPs, paths, credentials
|
||||
- `bug-pattern` — Known bugs and fixes
|
||||
- `api-contract` — MCP tool signatures
|
||||
|
||||
## Key Tools
|
||||
|
||||
### Session Lifecycle
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `memory_start_session()` | Open new session, load prior context |
|
||||
| `memory_end_session(...)` | Close session with summary, topics, outcome |
|
||||
| `memory_announce_focus(...)` | Declare files to be touched this session |
|
||||
| `memory_close_stale_sessions(...)` | Clean up crashed IDE sessions |
|
||||
|
||||
### Search
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `memory_search_facts(query, limit=10)` | FTS5 search over stored facts |
|
||||
| `memory_search_chunks(query, limit=10)` | FTS5 search over conversation chunks |
|
||||
| `memory_list_sessions(limit=20)` | Browse session history |
|
||||
|
||||
### Storage
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `memory_store_fact(category, fact)` | Store atomic reusable fact |
|
||||
| `memory_append_chunk(session_id, content, role)` | Store conversation chunk |
|
||||
| `memory_flag_important(session_id, content, role, flag_reason)` | Flag critical exchange |
|
||||
| `memory_log_token_save(session_id, description, tokens_saved, method_used)` | Track efficiency |
|
||||
|
||||
### Hypotheses
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `memory_add_hypothesis(session_id, hypothesis, confidence)` | Form testable prediction |
|
||||
| `memory_resolve_hypothesis(hypothesis_id, status, resolution)` | Confirm/refute prediction |
|
||||
| `memory_list_hypotheses(status)` | Review open/closed predictions |
|
||||
|
||||
## FTS5 Search Tips
|
||||
|
||||
BigMind uses SQLite FTS5 — **every token must match**. Use 2-3 focused keywords:
|
||||
|
||||
```
|
||||
✅ memory_search_facts("TrueNAS Docker")
|
||||
✅ memory_search_facts("mcp.json config")
|
||||
❌ memory_search_facts("homelab infrastructure TrueNAS Docker server") → 0 results
|
||||
```
|
||||
|
||||
## Stats (2026-04-04)
|
||||
|
||||
| Metric | Value |
|
||||
|---|---|
|
||||
| DB size | 744KB |
|
||||
| Sessions | 98 |
|
||||
| Facts | 97+ |
|
||||
| Chunks | 41 |
|
||||
| Schema version | v7 |
|
||||
|
||||
## DB Location
|
||||
|
||||
`~/.mcp/bigmind/memory.db` — outside the repo, never committed.
|
||||
|
||||
## Session Ritual
|
||||
|
||||
Every session **must** follow this ritual:
|
||||
|
||||
**Start:**
|
||||
1. `memory_start_session()`
|
||||
2. `memory_list_hypotheses()`
|
||||
3. `memory_announce_focus(...)`
|
||||
4. `memory_close_stale_sessions(...)`
|
||||
|
||||
**End:**
|
||||
1. `memory_end_session(one_liner, topics, outcome, summary, importance)`
|
||||
"""
|
||||
|
||||
PAGES["Development-Conventions"] = """# 🛠️ Development Conventions
|
||||
|
||||
All MCP servers in this repo follow a consistent set of conventions to ensure maintainability, testability, and compatibility with Roo Code tooling.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Each MCP server lives at `mcp/<server-name>/` with this layout:
|
||||
|
||||
```
|
||||
mcp/<server-name>/
|
||||
├── src/
|
||||
│ ├── __init__.py
|
||||
│ └── server.py ← FastMCP server entry point
|
||||
├── tests/
|
||||
│ └── test_server.py ← pytest test suite
|
||||
├── pyproject.toml ← uv-managed dependencies
|
||||
├── run.sh ← launch script
|
||||
├── README.md ← server documentation
|
||||
├── PLAN.md ← architecture plan (pre-implementation)
|
||||
└── ASSESSMENT.md ← pre-implementation assessment
|
||||
```
|
||||
|
||||
## FastMCP Pattern
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("server-name")
|
||||
|
||||
@mcp.tool()
|
||||
def my_tool(param: str) -> str:
|
||||
\"\"\"Tool description shown to the AI.\"\"\"
|
||||
return result
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
## Package Management
|
||||
|
||||
**All projects use `uv`** — never `pip` directly:
|
||||
|
||||
```bash
|
||||
# Create new server
|
||||
uv init mcp/my-server
|
||||
cd mcp/my-server
|
||||
uv add fastmcp httpx
|
||||
|
||||
# Sync dependencies
|
||||
uv sync
|
||||
|
||||
# Run server
|
||||
uv run python src/server.py
|
||||
|
||||
# Run tests
|
||||
uv run pytest tests/ -v
|
||||
```
|
||||
|
||||
## pyproject.toml Template
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "mcp-my-server"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"fastmcp>=2.0.0",
|
||||
"httpx",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
mcp-my-server = "src.server:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
```
|
||||
|
||||
## Testing Conventions
|
||||
|
||||
- Tests live in `tests/test_server.py`
|
||||
- Use `pytest` via `uv run pytest`
|
||||
- Mock external dependencies (ComfyUI, web URLs) for unit tests
|
||||
- All tests must pass before committing (`git push` should only happen with green tests)
|
||||
|
||||
## Commit Convention
|
||||
|
||||
Follow **Conventional Commits** format:
|
||||
|
||||
```
|
||||
feat: add webscraper_fetch_section tool
|
||||
fix: handle ComfyUI timeout gracefully
|
||||
docs: update mcp-image-gen README with AMD setup
|
||||
test: add unit tests for generate_image tool
|
||||
refactor: extract workflow builder to separate module
|
||||
chore: bump fastmcp to 2.1.0
|
||||
```
|
||||
|
||||
## Creating a New MCP Server
|
||||
|
||||
Use the `new-mcp-server` Roo skill in MCP Builder mode for full scaffolding:
|
||||
|
||||
```
|
||||
1. Switch to 🔧 MCP Builder mode in Roo Code
|
||||
2. Say: "Create a new MCP server for <purpose>"
|
||||
3. Roo will load the new-mcp-server skill and scaffold everything
|
||||
```
|
||||
|
||||
## Gitea Repository
|
||||
|
||||
Code is hosted at: `http://192.168.188.119:30008/pplate/pi_mcps`
|
||||
|
||||
Push with the `gitea-push` Roo skill to ensure conventional commit format.
|
||||
"""
|
||||
|
||||
|
||||
def create_wiki_page(title: str, content: str) -> bool:
|
||||
content_b64 = base64.b64encode(content.encode("utf-8")).decode("ascii")
|
||||
payload = json.dumps({
|
||||
"title": title,
|
||||
"content_base64": content_b64,
|
||||
"message": f"docs: create {title} wiki page"
|
||||
}).encode("utf-8")
|
||||
|
||||
url = f"{GITEA_URL}/api/v1/repos/{OWNER}/{REPO}/wiki/pages"
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=payload,
|
||||
headers={
|
||||
"Authorization": f"token {TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="POST"
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
data = json.loads(resp.read().decode())
|
||||
print(f"✅ Created: {data.get('title', title)}")
|
||||
return True
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode()
|
||||
print(f"❌ Failed [{title}]: HTTP {e.code} — {body[:200]}")
|
||||
return False
|
||||
except Exception as ex:
|
||||
print(f"❌ Failed [{title}]: {ex}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
results = {}
|
||||
for title, content in PAGES.items():
|
||||
ok = create_wiki_page(title, content)
|
||||
results[title] = ok
|
||||
|
||||
print("\n=== Summary ===")
|
||||
for title, ok in results.items():
|
||||
status = "✅" if ok else "❌"
|
||||
print(f"{status} {title}")
|
||||
|
||||
total = sum(results.values())
|
||||
print(f"\n{total}/{len(results)} pages created successfully")
|
||||
@@ -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"
|
||||
|
After Width: | Height: | Size: 737 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 798 KiB |
|
After Width: | Height: | Size: 888 KiB |
|
After Width: | Height: | Size: 745 KiB |
|
After Width: | Height: | Size: 541 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 457 KiB |
|
After Width: | Height: | Size: 501 KiB |
|
After Width: | Height: | Size: 778 KiB |
|
After Width: | Height: | Size: 814 KiB |
@@ -0,0 +1,125 @@
|
||||
# 🧠 BigMind — Persistent AI Memory
|
||||
|
||||

|
||||
|
||||
**BigMind** is the persistent memory backbone for all AI development sessions. It provides SQLite-backed tiered memory with FTS5 full-text search, hypothesis tracking, session management, 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)`
|
||||
@@ -0,0 +1,184 @@
|
||||
# 🛠️ 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/
|
||||
│ ├── 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
|
||||
```
|
||||
|
||||
## Web Research with mcp-webscraper
|
||||
|
||||
Before asking Patrick for information about a library, framework, API, or technology — **search first**.
|
||||
|
||||
The webscraper MCP server provides `webscraper_search_hint` (Brave Search, no API key, always available) as the entry point for all research tasks. Use the two-step pattern:
|
||||
|
||||
```
|
||||
Step 1: webscraper_search_hint("topic or error message") → get candidate URLs
|
||||
Step 2: webscraper_fetch(best_url) → read the full page
|
||||
```
|
||||
|
||||
### When to search
|
||||
|
||||
| Situation | Action |
|
||||
|---|---|
|
||||
| Need docs for a library or framework | `webscraper_search_hint("library-name official docs")` |
|
||||
| Investigating an error or stack trace | `webscraper_search_hint("exact error message language")` |
|
||||
| Planning a feature — need design patterns | `webscraper_search_hint("pattern-name best practices")` |
|
||||
| Checking latest version / changelog | `webscraper_search_hint("library-name changelog release")` |
|
||||
| Looking up API contracts | `webscraper_fetch(official_docs_url)` directly |
|
||||
|
||||
### Especially useful in
|
||||
|
||||
- **🏗️ Architect mode** — look up patterns and docs *before* designing. Don't design blind.
|
||||
- **🪲 Debug mode** — search the exact error message before forming hypotheses.
|
||||
- **🔧 MCP Builder mode** — check FastMCP changelog for new patterns before implementing.
|
||||
|
||||
### Known caveats
|
||||
|
||||
- Reddit and Stack Overflow may return empty snippets (platform blocks)
|
||||
- Brave uses Svelte CSS classes that can change — if `webscraper_search_hint` returns 0 results, selectors may need updating (last verified: 2026-04-05)
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,56 @@
|
||||
# 🔧 pi_mcps — Patrick's Homelab Monorepo
|
||||
|
||||

|
||||
|
||||
Welcome to **pi_mcps**, Patrick's personal homelab monorepo. This repository houses MCP (Model Context Protocol) servers, Java projects, and homelab tooling — all built and maintained on a Fedora Linux workstation with an AMD Ryzen 5900X + RX 7900 XTX.
|
||||
|
||||
## What's in this repo?
|
||||
|
||||
| Directory | Contents |
|
||||
|---|---|
|
||||
| [`mcp/mcp-image-gen/`](../src/branch/main/mcp/mcp-image-gen) | 🎨 AI image generation via ComfyUI + FLUX.1-schnell |
|
||||
| [`mcp/webscraper/`](../src/branch/main/mcp/webscraper) | 🕸️ Web scraping and data extraction |
|
||||
| [`mcp/bigmind/`](../src/branch/main/mcp/bigmind) | 🧠 Persistent AI memory system |
|
||||
| [`java/`](../src/branch/main/java) | ☕ Java EE / Spring projects |
|
||||
| [`plans/`](../src/branch/main/plans) | 📋 Architecture decisions and health reports |
|
||||
|
||||
## Stack
|
||||
|
||||
- **Language:** Python 3.11+ (MCP servers), Java 8–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, 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*
|
||||
@@ -0,0 +1,164 @@
|
||||
# 📐 Java Architecture Patterns
|
||||
|
||||

|
||||
|
||||
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 |
|
||||
@@ -0,0 +1,43 @@
|
||||
# ☕ Java Projects Overview
|
||||
|
||||

|
||||
|
||||
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
|
||||
@@ -0,0 +1,94 @@
|
||||
# 🏢 mss-failsafe — Multi-Module Enterprise Application
|
||||
|
||||

|
||||
|
||||
**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)
|
||||
@@ -0,0 +1,71 @@
|
||||
# 🛍️ wellmann-shop — JSF E-Commerce Application
|
||||
|
||||

|
||||
|
||||
**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)
|
||||
@@ -0,0 +1,42 @@
|
||||
# 🔌 MCP Servers Overview
|
||||
|
||||

|
||||
|
||||
This repo contains three production-grade MCP (Model Context Protocol) servers, each specialized for a different capability domain. Together they give Roo Code / Claude Desktop a complete set of superpowers.
|
||||
|
||||
## The Three Pillars
|
||||
|
||||
```
|
||||
Roo Code / Claude Desktop
|
||||
│
|
||||
├── bigmind ──────────► ~/.mcp/bigmind/memory.db (persistent memory)
|
||||
├── mcp-image-gen ────► ComfyUI @ localhost:8188 (image generation)
|
||||
└── webscraper ───────► Internet / Intranet (web scraping + 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
|
||||
```
|
||||
@@ -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 |
|
||||
@@ -0,0 +1,89 @@
|
||||
# 🎨 mcp-image-gen — AI Image Generation
|
||||
|
||||

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

|
||||
|
||||
*Primary profile: seed `568659042` — constellation face interpretation of Lumen.*
|
||||
@@ -0,0 +1,137 @@
|
||||
# 🕸️ mcp-webscraper — Web Scraping
|
||||
|
||||

|
||||
|
||||
**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: The Two-Step Research Pattern
|
||||
|
||||
`webscraper_search_hint` is the **entry point for all web research**. The recommended workflow is:
|
||||
|
||||
```
|
||||
Step 1: webscraper_search_hint("your query") → get candidate URLs + snippets
|
||||
Step 2: webscraper_fetch(best_url) → get full page content
|
||||
```
|
||||
|
||||
This avoids scraping irrelevant pages and gives you an overview before committing to a deep read.
|
||||
|
||||
### Why Brave Search?
|
||||
|
||||
`webscraper_search_hint` uses Brave Search (`search.brave.com`) because:
|
||||
- ✅ Returns real results without CAPTCHA or consent walls
|
||||
- ✅ No API key required — works with plain HTTP GET
|
||||
- ✅ Handles special characters (C++, &, %, etc.) via URL encoding
|
||||
- ❌ Google blocks plain HTTP with 302 consent redirect
|
||||
- ❌ DuckDuckGo blocks with CAPTCHA
|
||||
|
||||
### Return Value
|
||||
|
||||
The tool returns a structured dict:
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "FastMCP tool decorator",
|
||||
"search_url": "https://search.brave.com/search?q=FastMCP+tool+decorator&source=web",
|
||||
"result_count": 5,
|
||||
"hint": "FastMCP Docs (https://docs.fastmcp.dev): The @mcp.tool() decorator registers a function as... | PyPI FastMCP (https://pypi.org/project/fastmcp/): FastMCP 2.x — modern MCP server framework... | ...",
|
||||
"results": [
|
||||
{
|
||||
"title": "FastMCP Docs",
|
||||
"url": "https://docs.fastmcp.dev",
|
||||
"snippet": "The @mcp.tool() decorator registers a function as an MCP tool..."
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `hint` field is a pipe-separated string of `"Title (url): snippet[:120]"` entries — immediately actionable for deciding which URL to fetch next.
|
||||
|
||||
### Example: Two-Step Research Flow
|
||||
|
||||
```python
|
||||
# Step 1: Orient — what pages exist about this topic?
|
||||
result = webscraper_search_hint("httpx async client timeout settings", max_results=5)
|
||||
# hint: "HTTPX Docs (https://www.python-httpx.org/...): Configure timeout... | ..."
|
||||
|
||||
# Step 2: Deep-dive the most relevant result
|
||||
content = webscraper_fetch("https://www.python-httpx.org/advanced/timeouts/", max_chars=8000)
|
||||
```
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- **Reddit / Stack Overflow snippets** may be empty — these platforms block snippet extraction
|
||||
- **Brave CSS selectors** use Svelte-generated class names that may change. If you get 0 results, the scraper's selectors may need updating (last verified: 2026-04-05)
|
||||
- **Use sparingly** — once per research task to get oriented, not for every query
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
# 28/28 tests passing
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```python
|
||||
# Step 1: Search — get candidate URLs for a topic
|
||||
webscraper_search_hint("FastMCP tool decorator syntax", max_results=5)
|
||||
|
||||
# Step 2: Deep-dive the most relevant URL
|
||||
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")
|
||||
|
||||
# Search with special characters (C++, &, % all work)
|
||||
webscraper_search_hint("C++ std::optional usage", max_results=3)
|
||||
```
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 410 KiB |
|
After Width: | Height: | Size: 372 KiB |
|
After Width: | Height: | Size: 390 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 340 KiB |
|
After Width: | Height: | Size: 406 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 329 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 301 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 462 KiB |
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 376 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 289 KiB |
|
After Width: | Height: | Size: 431 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 403 KiB |
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -29,18 +29,25 @@ def _render_achievements(achievements: list) -> str:
|
||||
def _esc(s):
|
||||
return (s or "").replace('"', """).replace("'", "'")
|
||||
|
||||
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)
|
||||
|
||||
@@ -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) ──────────────────────────────────────
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -0,0 +1,619 @@
|
||||
# mcp-image-gen — Usage Guide
|
||||
|
||||
> **Comprehensive reference for using the ComfyUI-backed image generation MCP server**
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Prerequisites — ComfyUI Setup](#1-prerequisites--comfyui-setup)
|
||||
2. [Quick Start — Running the MCP Server](#2-quick-start--running-the-mcp-server)
|
||||
3. [How to Ask Lumen to Generate Images](#3-how-to-ask-lumen-to-generate-images)
|
||||
4. [Available Tools](#4-available-tools)
|
||||
5. [Parameters Reference](#5-parameters-reference)
|
||||
6. [Output Format](#6-output-format)
|
||||
7. [Environment Variables](#7-environment-variables)
|
||||
8. [Test Status](#8-test-status)
|
||||
9. [Prompt Tips for FLUX.1-schnell](#9-prompt-tips-for-flux1-schnell)
|
||||
10. [Known Limitations](#10-known-limitations)
|
||||
|
||||
---
|
||||
|
||||
## 1. Prerequisites — ComfyUI Setup
|
||||
|
||||
### ComfyUI must be running before any image generation tool call succeeds.
|
||||
|
||||
The MCP server connects to ComfyUI's REST API at `http://localhost:8188`. If ComfyUI is not running, `generate_image` and `list_available_models` will return a graceful error message — no crash.
|
||||
|
||||
### Install ComfyUI
|
||||
|
||||
> ⚠️ **ComfyUI is NOT on PyPI** — `pip install comfyui` will fail with "No matching distribution found".
|
||||
> It must be installed from source via `git clone`.
|
||||
|
||||
```bash
|
||||
# Clone from source (the only correct installation method)
|
||||
git clone https://github.com/comfyanonymous/ComfyUI.git
|
||||
cd ComfyUI
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Install PyTorch with ROCm (AMD RX 7900 XTX)
|
||||
|
||||
Patrick's RX 7900 XTX (gfx1100, 24GB VRAM) uses the ROCm backend. Standard CUDA builds **will not work** on AMD hardware.
|
||||
|
||||
```bash
|
||||
# PyTorch with ROCm 6.1 support
|
||||
pip install torch torchvision --index-url https://download.pytorch.org/whl/rocm6.1
|
||||
```
|
||||
|
||||
> **ROCm version note:** ROCm 7.2.1 is the current production release as of April 2026.
|
||||
> Check `rocm-smi` to confirm your ROCm version before installing torch.
|
||||
|
||||
### Download FLUX.1-schnell (Primary Model)
|
||||
|
||||
FLUX.1-schnell is the recommended model — fast (4 steps), Apache 2.0 licensed, excellent quality.
|
||||
|
||||
> ⚠️ **FLUX.1-schnell is a gated model on HuggingFace.**
|
||||
> A bare `wget` on the URL returns HTTP 401. You must:
|
||||
> 1. Accept the license at https://huggingface.co/black-forest-labs/FLUX.1-schnell (click **"Agree and access repository"** — one-time)
|
||||
> 2. Create a HuggingFace access token with **Read** permissions at https://huggingface.co/settings/tokens
|
||||
|
||||
#### Option A — `huggingface-cli` (recommended)
|
||||
|
||||
```bash
|
||||
# Install the HuggingFace Hub CLI
|
||||
pip install huggingface_hub
|
||||
|
||||
# Log in — paste your Read token when prompted
|
||||
huggingface-cli login
|
||||
|
||||
# Download (~8GB) directly into ComfyUI checkpoints
|
||||
huggingface-cli download black-forest-labs/FLUX.1-schnell \
|
||||
flux1-schnell.safetensors \
|
||||
--local-dir ~/ComfyUI/models/checkpoints/
|
||||
```
|
||||
|
||||
#### Option B — `wget` with Authorization header
|
||||
|
||||
```bash
|
||||
wget --header="Authorization: Bearer hf_YOUR_TOKEN_HERE" \
|
||||
https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/flux1-schnell.safetensors \
|
||||
-O ~/ComfyUI/models/checkpoints/flux1-schnell.safetensors
|
||||
```
|
||||
|
||||
> Replace `hf_YOUR_TOKEN_HERE` with your actual HuggingFace token from https://huggingface.co/settings/tokens
|
||||
|
||||
#### Alternative: fp8 quantized variant (~8.1GB, faster inference)
|
||||
|
||||
If you want slightly faster inference with near-identical quality, the fp8 quantized version is also available:
|
||||
|
||||
```bash
|
||||
huggingface-cli download black-forest-labs/FLUX.1-schnell-fp8 \
|
||||
flux1-schnell-fp8.safetensors \
|
||||
--local-dir ~/ComfyUI/models/checkpoints/
|
||||
```
|
||||
|
||||
> **Download note:** Both variants are ~8GB — expect 10–30 minutes depending on connection speed.
|
||||
|
||||
You'll also need the CLIP and VAE models — see the [ComfyUI FLUX guide](https://github.com/comfyanonymous/ComfyUI/blob/master/README.md) for full model list.
|
||||
|
||||
### Start ComfyUI (AMD ROCm)
|
||||
|
||||
```bash
|
||||
# Standard start — listens on all interfaces at port 8188
|
||||
HSA_OVERRIDE_GFX_VERSION=11.0.0 python main.py --listen
|
||||
|
||||
# Or with explicit port
|
||||
HSA_OVERRIDE_GFX_VERSION=11.0.0 python main.py --listen --port 8188
|
||||
```
|
||||
|
||||
> **`HSA_OVERRIDE_GFX_VERSION=11.0.0`** — Required for RX 7900 XTX (gfx1100).
|
||||
> Without this, ROCm may fail to detect the GPU correctly. This tells the HIP runtime
|
||||
> to treat the GPU as gfx1100 architecture.
|
||||
|
||||
### Verify ComfyUI is Running
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8188/system_stats | python3 -m json.tool | head -20
|
||||
```
|
||||
|
||||
Expected response includes `system` object with `python_version`, `pytorch_version`, `embedded_python`, and `comfyui_version`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Quick Start — Running the MCP Server
|
||||
|
||||
### Via `run.sh` (recommended)
|
||||
|
||||
```bash
|
||||
cd /home/pplate/pi_mcps/mcp/mcp-image-gen
|
||||
./run.sh
|
||||
```
|
||||
|
||||
[`run.sh`](run.sh) automatically:
|
||||
- Sets `PATH` to include `~/.local/bin` for `uv`
|
||||
- Creates `IMAGE_OUTPUT_DIR` (`~/Pictures/mcp-generated`) if it doesn't exist
|
||||
- Launches the FastMCP server via `uv run src/server.py` (stdio transport)
|
||||
|
||||
### Via uv directly
|
||||
|
||||
```bash
|
||||
cd /home/pplate/pi_mcps/mcp/mcp-image-gen
|
||||
uv run src/server.py
|
||||
```
|
||||
|
||||
### Wired into `.roo/mcp.json`
|
||||
|
||||
The server is already configured in [`.roo/mcp.json`](../../.roo/mcp.json):
|
||||
|
||||
```json
|
||||
"mcp-image-gen": {
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"--directory", "/home/pplate/pi_mcps/mcp/mcp-image-gen",
|
||||
"run", "src/server.py"
|
||||
],
|
||||
"env": {
|
||||
"COMFYUI_URL": "http://localhost:8188",
|
||||
"IMAGE_OUTPUT_DIR": "/home/pplate/Pictures/mcp-generated"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Roo Code / Claude Desktop will auto-start the server when any image generation tool is invoked. The MCP server itself starts in ~1 second — ComfyUI must already be running separately.
|
||||
|
||||
### Install dependencies (first time)
|
||||
|
||||
```bash
|
||||
cd /home/pplate/pi_mcps/mcp/mcp-image-gen
|
||||
uv sync
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. How to Ask Lumen to Generate Images
|
||||
|
||||
Just speak naturally. Lumen will call the appropriate MCP tool automatically.
|
||||
|
||||
### Basic generation
|
||||
|
||||
> *"Generate an image of a futuristic city at sunset"*
|
||||
|
||||
```
|
||||
→ generate_image(prompt="futuristic city at sunset", width=1024, height=1024, steps=4)
|
||||
```
|
||||
|
||||
### Specific style and size
|
||||
|
||||
> *"Create a portrait of a red fox in watercolor style, 1024x1024"*
|
||||
|
||||
```
|
||||
→ generate_image(
|
||||
prompt="portrait of a red fox, watercolor style, detailed fur, soft brushstrokes",
|
||||
width=1024, height=1024
|
||||
)
|
||||
```
|
||||
|
||||
### Reproducible with a fixed seed
|
||||
|
||||
> *"Make an image with seed 42 so I can reproduce it"*
|
||||
|
||||
```
|
||||
→ generate_image(prompt="...", seed=42)
|
||||
```
|
||||
|
||||
The seed is reported in the text output so you can use the same seed again.
|
||||
|
||||
### Landscape format
|
||||
|
||||
> *"Generate a wide cinematic landscape of a Norwegian fjord, 1920x1080"*
|
||||
|
||||
```
|
||||
→ generate_image(prompt="Norwegian fjord, cinematic, golden hour", width=1920, height=1080)
|
||||
```
|
||||
|
||||
### Excluding unwanted elements
|
||||
|
||||
> *"Generate a clean product photo of a coffee mug, no background clutter, no text"*
|
||||
|
||||
```
|
||||
→ generate_image(
|
||||
prompt="product photo of a ceramic coffee mug, studio lighting, white background",
|
||||
negative_prompt="clutter, text, watermark, blurry, shadows"
|
||||
)
|
||||
```
|
||||
|
||||
### More inference steps for higher quality
|
||||
|
||||
> *"Generate a highly detailed oil painting of a medieval castle, use 20 steps"*
|
||||
|
||||
```
|
||||
→ generate_image(
|
||||
prompt="oil painting of a medieval castle, highly detailed, dramatic lighting",
|
||||
steps=20,
|
||||
model="flux1-dev.safetensors" # FLUX.1-dev supports higher step counts better
|
||||
)
|
||||
```
|
||||
|
||||
### Check what models are available
|
||||
|
||||
> *"List what models are available in ComfyUI"*
|
||||
|
||||
```
|
||||
→ list_available_models()
|
||||
```
|
||||
|
||||
### Check status of a long-running job
|
||||
|
||||
> *"What's the status of prompt ID abc-123?"*
|
||||
|
||||
```
|
||||
→ get_generation_status(prompt_id="abc-123")
|
||||
```
|
||||
|
||||
### Find out where images are saved
|
||||
|
||||
> *"Where are my generated images being saved?"*
|
||||
|
||||
```
|
||||
→ get_output_directory()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Available Tools
|
||||
|
||||
### `generate_image`
|
||||
|
||||
Generate an image from a text prompt using ComfyUI's FLUX.1-schnell workflow.
|
||||
|
||||
**Full signature:**
|
||||
```python
|
||||
async def generate_image(
|
||||
prompt: str,
|
||||
width: int = 1024,
|
||||
height: int = 1024,
|
||||
steps: int = 4,
|
||||
model: str = "flux1-schnell.safetensors",
|
||||
seed: int = -1,
|
||||
negative_prompt: str = "",
|
||||
output_dir: str = "",
|
||||
) -> list[TextContent | ImageContent]
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
1. Loads the bundled `flux_schnell.json` ComfyUI API workflow template
|
||||
2. Injects your prompt, dimensions, seed, model into the correct workflow nodes
|
||||
3. Submits the workflow to ComfyUI via `POST /api/prompt`
|
||||
4. Polls `/api/queue` every 2 seconds until the job leaves the queue
|
||||
5. Fetches history via `/api/history/{prompt_id}` to find the output filename
|
||||
6. Downloads the PNG from `/api/view`
|
||||
7. Saves the PNG to disk as `YYYYMMDD_HHMMSS_{seed}.png`
|
||||
8. Returns `[TextContent(path + metadata), ImageContent(base64 PNG)]`
|
||||
|
||||
---
|
||||
|
||||
### `list_available_models`
|
||||
|
||||
List all checkpoint models currently available in ComfyUI.
|
||||
|
||||
```python
|
||||
async def list_available_models() -> list[str]
|
||||
```
|
||||
|
||||
Calls `/object_info/CheckpointLoaderSimple` and extracts the checkpoint name list. Use this to discover what models are installed before passing a `model` name to `generate_image`.
|
||||
|
||||
**Example return:**
|
||||
```json
|
||||
["flux1-schnell.safetensors", "flux1-dev.safetensors", "sd_xl_base_1.0.safetensors"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `get_generation_status`
|
||||
|
||||
Check the status of a queued or running generation job.
|
||||
|
||||
```python
|
||||
async def get_generation_status(prompt_id: str) -> dict
|
||||
```
|
||||
|
||||
**Return values:**
|
||||
|
||||
| `status` | Meaning |
|
||||
|---|---|
|
||||
| `"pending"` | Job is in the queue, not yet started |
|
||||
| `"running"` | Job is currently being processed |
|
||||
| `"completed"` | Job finished — image is in ComfyUI's history |
|
||||
| `"not_found"` | Unknown prompt_id — may have expired from history |
|
||||
| `"error"` | ComfyUI was unreachable |
|
||||
|
||||
Useful when `generate_image` times out (default 120s) — the job may still be running in ComfyUI.
|
||||
|
||||
---
|
||||
|
||||
### `get_output_directory`
|
||||
|
||||
Return the absolute path where generated images will be saved.
|
||||
|
||||
```python
|
||||
def get_output_directory() -> str
|
||||
```
|
||||
|
||||
Returns the expanded, absolute path derived from `IMAGE_OUTPUT_DIR` env var (or `~/Pictures/mcp-generated` default). The directory may not exist yet — `generate_image` creates it on first use.
|
||||
|
||||
---
|
||||
|
||||
## 5. Parameters Reference
|
||||
|
||||
Full parameter table for `generate_image`:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `prompt` | `str` | *(required)* | Text description of the image. Goes into the positive CLIP text encoder node. |
|
||||
| `width` | `int` | `1024` | Image width in pixels. FLUX.1-schnell: 512–2048 recommended. |
|
||||
| `height` | `int` | `1024` | Image height in pixels. FLUX.1-schnell: 512–2048 recommended. |
|
||||
| `steps` | `int` | `4` | Number of KSampler inference steps. FLUX.1-schnell is designed for 1–8 steps. |
|
||||
| `model` | `str` | `"flux1-schnell.safetensors"` | Checkpoint model filename as listed by `list_available_models`. |
|
||||
| `seed` | `int` | `-1` | RNG seed for reproducibility. `-1` = new random seed each call (0 to 2³²−1). |
|
||||
| `negative_prompt` | `str` | `""` | Text description of things to exclude. Goes into negative CLIP encoder node. |
|
||||
| `output_dir` | `str` | `""` | Override save directory. Empty = uses `IMAGE_OUTPUT_DIR` env var or default. |
|
||||
|
||||
### Recommended dimensions
|
||||
|
||||
| Use case | Width | Height |
|
||||
|---|---|---|
|
||||
| Square (default) | 1024 | 1024 |
|
||||
| Portrait | 768 | 1024 |
|
||||
| Landscape | 1024 | 768 |
|
||||
| Widescreen | 1280 | 720 |
|
||||
| HD widescreen | 1920 | 1080 |
|
||||
| Tall portrait | 512 | 768 |
|
||||
|
||||
> **VRAM note:** Patrick's RX 7900 XTX has 24GB VRAM. FLUX.1-schnell requires ~8GB,
|
||||
> so you can comfortably run 1920×1080 and even larger. FLUX.1-dev requires ~12GB.
|
||||
|
||||
---
|
||||
|
||||
## 6. Output Format
|
||||
|
||||
`generate_image` returns a list with **two items** when successful:
|
||||
|
||||
### Item 1 — `TextContent` (file path + metadata)
|
||||
|
||||
```
|
||||
Generated: /home/pplate/Pictures/mcp-generated/20260404_121500_3847291045.png
|
||||
Seed: 3847291045
|
||||
Elapsed: 8.3s
|
||||
Size: 1024x1024, Steps: 4, Model: flux1-schnell.safetensors
|
||||
```
|
||||
|
||||
The filename format is `YYYYMMDD_HHMMSS_{seed}.png` — the seed is embedded so you can reproduce the exact image by passing it back as the `seed` parameter.
|
||||
|
||||
### Item 2 — `ImageContent` (inline base64 PNG)
|
||||
|
||||
The image displays **directly in Roo Code / Claude Desktop chat** as an inline image — no need to open a file browser. The same PNG is also saved to disk at the path shown in the TextContent.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "image",
|
||||
"mimeType": "image/png",
|
||||
"data": "<base64-encoded PNG bytes>"
|
||||
}
|
||||
```
|
||||
|
||||
### Error responses
|
||||
|
||||
When ComfyUI is unreachable or an error occurs, only **one** `TextContent` is returned (no ImageContent):
|
||||
|
||||
```
|
||||
ComfyUI not reachable at http://localhost:8188. Start it with: python main.py --listen
|
||||
```
|
||||
|
||||
```
|
||||
Generation timed out after 120s. prompt_id=abc-123 — use get_generation_status to check
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Environment Variables
|
||||
|
||||
Configure via environment variables in [`.roo/mcp.json`](../../.roo/mcp.json) or shell:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `COMFYUI_URL` | `http://localhost:8188` | Base URL of the running ComfyUI REST API. Change this if ComfyUI runs on a different host or port. |
|
||||
| `IMAGE_OUTPUT_DIR` | `~/Pictures/mcp-generated` | Directory where generated PNG files are saved. Supports `~` expansion. Created automatically on first generation. |
|
||||
| `COMFYUI_TIMEOUT` | `120` | Maximum seconds to wait for a generation job before returning a timeout error. Increase for very large images or slow hardware. |
|
||||
|
||||
### Setting via shell
|
||||
|
||||
```bash
|
||||
export COMFYUI_URL="http://localhost:8188"
|
||||
export IMAGE_OUTPUT_DIR="/home/pplate/Pictures/ai-art"
|
||||
export COMFYUI_TIMEOUT="300"
|
||||
./run.sh
|
||||
```
|
||||
|
||||
### Setting via mcp.json env block
|
||||
|
||||
```json
|
||||
"mcp-image-gen": {
|
||||
"command": "uv",
|
||||
"args": ["--directory", "/home/pplate/pi_mcps/mcp/mcp-image-gen", "run", "src/server.py"],
|
||||
"env": {
|
||||
"COMFYUI_URL": "http://localhost:8188",
|
||||
"IMAGE_OUTPUT_DIR": "/home/pplate/Pictures/mcp-generated",
|
||||
"COMFYUI_TIMEOUT": "120"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Test Status
|
||||
|
||||
**19 pytest tests — all passing.** Tests mock all ComfyUI HTTP calls using [respx](https://lundberg.github.io/respx/). No running ComfyUI instance is needed to run the tests.
|
||||
|
||||
```bash
|
||||
cd /home/pplate/pi_mcps/mcp/mcp-image-gen
|
||||
uv run pytest tests/ -v
|
||||
```
|
||||
|
||||
### Test coverage breakdown
|
||||
|
||||
| Test file | Tests | Coverage area |
|
||||
|---|---|---|
|
||||
| [`tests/test_server.py`](tests/test_server.py) | 19 | All 4 tools + workflow builder |
|
||||
|
||||
| Test name | What it verifies |
|
||||
|---|---|
|
||||
| `test_build_flux_workflow_structure` | Workflow has correct node class_types |
|
||||
| `test_build_flux_workflow_params_injected` | All params injected into correct nodes |
|
||||
| `test_negative_prompt_included` | Negative prompt goes to node 33 |
|
||||
| `test_random_seed_generated` | `seed=-1` produces a valid integer in `_meta` |
|
||||
| `test_list_available_models` | Returns model list from mocked `/object_info` |
|
||||
| `test_list_available_models_comfyui_offline` | ConnectError → graceful error string |
|
||||
| `test_get_generation_status_pending` | `prompt_id` in queue_pending → `"pending"` |
|
||||
| `test_get_generation_status_running` | `prompt_id` in queue_running → `"running"` |
|
||||
| `test_get_generation_status_complete` | Not in queue + in history → `"completed"` |
|
||||
| `test_get_output_directory_default` | No env var → `~/Pictures/mcp-generated` expanded |
|
||||
| `test_get_output_directory_custom` | Custom env var → that path returned |
|
||||
| `test_generate_image_success` | Full lifecycle: queue→poll→history→view→save |
|
||||
| `test_generate_image_comfyui_unavailable` | ConnectError → single TextContent error |
|
||||
| `test_generate_image_timeout` | COMFYUI_TIMEOUT=0 → timeout TextContent |
|
||||
| `test_generate_image_empty_prompt` | Empty string prompt → still succeeds |
|
||||
| `test_generate_image_long_prompt` | 500-char prompt → not truncated, succeeds |
|
||||
| `test_generate_image_invalid_model` | 404 from /prompt → error TextContent, no file saved |
|
||||
| `test_generate_image_custom_output_dir` | Custom `output_dir` param → saved there, dir created |
|
||||
| `test_generate_image_random_seed_variance` | `seed=-1` × 2 → different seeds, different filenames |
|
||||
|
||||
### Test mock stack
|
||||
|
||||
- **[respx](https://lundberg.github.io/respx/)** — HTTP-level mocking for all ComfyUI API endpoints
|
||||
- **[Pillow](https://pillow.readthedocs.io/)** (in conftest) — generates real PNG bytes for image response fixtures
|
||||
- **monkeypatch** — env vars (`IMAGE_OUTPUT_DIR`, `COMFYUI_URL`, `COMFYUI_TIMEOUT`) and server module attributes
|
||||
|
||||
Real image generation requires ComfyUI to be running. Tests prove the tool logic is correct at the protocol level.
|
||||
|
||||
---
|
||||
|
||||
## 9. Prompt Tips for FLUX.1-schnell
|
||||
|
||||
FLUX.1-schnell is a guidance-distilled model designed for speed at 1–8 steps. It responds differently from SDXL or SD1.5.
|
||||
|
||||
### Prompt structure that works well
|
||||
|
||||
```
|
||||
[subject], [style/medium], [lighting], [camera/composition], [mood/atmosphere], [quality modifiers]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
ancient library at night, oil painting, warm candlelight, wide angle, mysterious atmosphere, highly detailed, sharp focus
|
||||
```
|
||||
|
||||
### Style keywords
|
||||
|
||||
| Style | Prompt keywords |
|
||||
|---|---|
|
||||
| Photography | `cinematic photograph, DSLR, 85mm lens, shallow depth of field, bokeh` |
|
||||
| Oil painting | `oil painting, thick brushstrokes, textured canvas, impressionist` |
|
||||
| Watercolor | `watercolor painting, soft washes, paper texture, flowing colors` |
|
||||
| Digital art | `digital art, concept art, artstation, octane render` |
|
||||
| Anime/illustration | `anime style, cel shading, vibrant colors, clean linework` |
|
||||
| Sketch | `pencil sketch, hand drawn, crosshatching, charcoal` |
|
||||
|
||||
### Lighting keywords
|
||||
|
||||
- `golden hour`, `blue hour`, `dramatic lighting`, `rim lighting`
|
||||
- `studio lighting`, `soft diffused light`, `volumetric light`
|
||||
- `neon glow`, `bioluminescent`, `moonlit`, `candlelight`
|
||||
|
||||
### What works well with FLUX.1-schnell
|
||||
|
||||
- **Clear subject + style** — "red panda in a cozy library, watercolor style"
|
||||
- **Landscape scenes** — fjords, forests, cities, abstract environments
|
||||
- **Portrait shots** — animals and characters with descriptive appearance
|
||||
- **Concept art** — futuristic cities, sci-fi environments, fantasy scenes
|
||||
- **Low step counts** — 4 steps is designed to be near-optimal for this model
|
||||
|
||||
### What to avoid
|
||||
|
||||
- **Booru-style tag dumps** (FLUX handles natural language better than SD1.5)
|
||||
- **Contradictory instructions** — "dark AND bright", "realistic AND cartoon"
|
||||
- **Overly complex scenes** at very small resolutions
|
||||
|
||||
### Using the negative prompt
|
||||
|
||||
FLUX.1-schnell has reduced CFG guidance so negative prompts have less impact than in SDXL.
|
||||
Use them for broad exclusions:
|
||||
|
||||
```
|
||||
negative_prompt="blurry, out of focus, watermark, text, signature, low quality, artifacts"
|
||||
```
|
||||
|
||||
### Reproducibility
|
||||
|
||||
Always save the seed from the TextContent output if you want to reproduce a result:
|
||||
|
||||
```
|
||||
Seed: 3847291045
|
||||
```
|
||||
|
||||
Then pass it back: `seed=3847291045`
|
||||
|
||||
---
|
||||
|
||||
## 10. Known Limitations
|
||||
|
||||
### ComfyUI must run locally
|
||||
|
||||
The MCP server connects to `COMFYUI_URL` (default: `http://localhost:8188`). ComfyUI is a local application — it does not have a cloud API. You must start it before requesting image generation. The server returns a clear error message if ComfyUI is not reachable.
|
||||
|
||||
### Model must be pre-loaded
|
||||
|
||||
ComfyUI loads checkpoint models into VRAM on first use. The first generation with a model takes longer as VRAM is allocated (FLUX.1-schnell: ~8GB). Subsequent generations with the same model are faster.
|
||||
|
||||
```bash
|
||||
# Verify model is installed before generation
|
||||
# → ask Lumen: "list available models in ComfyUI"
|
||||
```
|
||||
|
||||
### AMD ROCm setup complexity
|
||||
|
||||
AMD GPU support requires:
|
||||
1. ROCm drivers installed (`rocm-smi` working)
|
||||
2. PyTorch built with ROCm support (not the default CUDA build)
|
||||
3. `HSA_OVERRIDE_GFX_VERSION=11.0.0` for RX 7900 XTX (gfx1100)
|
||||
|
||||
Without these, ComfyUI will fall back to CPU — very slow (minutes per image vs. ~8 seconds on RX 7900 XTX).
|
||||
|
||||
Check GPU is being used:
|
||||
```bash
|
||||
# In another terminal while generating:
|
||||
watch -n 1 rocm-smi
|
||||
# VRAM usage should spike to ~8GB during generation
|
||||
```
|
||||
|
||||
### Timeout on large images
|
||||
|
||||
The default `COMFYUI_TIMEOUT=120` (2 minutes) may not be enough for:
|
||||
- Very large resolutions (2048×2048+)
|
||||
- High step counts (20+)
|
||||
- First generation loading a new model
|
||||
|
||||
Increase via env var:
|
||||
```bash
|
||||
export COMFYUI_TIMEOUT=300 # 5 minutes
|
||||
```
|
||||
|
||||
If `generate_image` returns a timeout error, the job may still be running in ComfyUI. Use `get_generation_status(prompt_id)` to check.
|
||||
|
||||
### Ollama image gen is macOS-only (April 2026)
|
||||
|
||||
Ollama launched experimental image generation in January 2026, but it is **macOS-only** as of April 2026. Linux support is announced as "coming soon." When Linux support arrives, the server can switch backends via `IMAGE_BACKEND=ollama` without changing any tool signatures.
|
||||
|
||||
### ComfyUI history is ephemeral
|
||||
|
||||
ComfyUI keeps generation history in memory — it is lost on restart. The `get_generation_status` tool will return `"not_found"` for old prompt IDs after a ComfyUI restart. The saved PNG file on disk persists regardless.
|
||||
|
After Width: | Height: | Size: 992 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 860 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -40,7 +40,9 @@ class ComfyUIClient:
|
||||
|
||||
async def queue_prompt(self, workflow: dict) -> str:
|
||||
"""Submit a workflow to ComfyUI and return the prompt_id."""
|
||||
payload = {"prompt": workflow}
|
||||
# Strip internal metadata keys (e.g. "_meta") — they are not ComfyUI nodes
|
||||
clean_workflow = {k: v for k, v in workflow.items() if not k.startswith("_")}
|
||||
payload = {"prompt": clean_workflow}
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
resp = await client.post(f"{self.base_url}/api/prompt", json=payload)
|
||||
resp.raise_for_status()
|
||||
@@ -115,7 +117,8 @@ def build_flux_workflow(
|
||||
wf["27"]["inputs"]["height"] = height
|
||||
wf["13"]["inputs"]["steps"] = steps
|
||||
wf["13"]["inputs"]["seed"] = actual_seed
|
||||
wf["30"]["inputs"]["ckpt_name"] = model
|
||||
# Node 32 = UNETLoader (flux1-schnell.safetensors is UNet-only, not all-in-one checkpoint)
|
||||
wf["32"]["inputs"]["unet_name"] = model
|
||||
|
||||
# Attach the actual seed as metadata so callers can retrieve it
|
||||
wf["_meta"] = {"actual_seed": actual_seed}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"6": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {
|
||||
"clip": ["30", 1],
|
||||
"clip": ["30", 0],
|
||||
"text": "PROMPT_PLACEHOLDER"
|
||||
}
|
||||
},
|
||||
@@ -10,7 +10,7 @@
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {
|
||||
"samples": ["13", 0],
|
||||
"vae": ["30", 2]
|
||||
"vae": ["31", 0]
|
||||
}
|
||||
},
|
||||
"9": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"cfg": 1.0,
|
||||
"denoise": 1.0,
|
||||
"latent_image": ["27", 0],
|
||||
"model": ["30", 0],
|
||||
"model": ["32", 0],
|
||||
"negative": ["33", 0],
|
||||
"positive": ["6", 0],
|
||||
"sampler_name": "euler",
|
||||
@@ -44,15 +44,31 @@
|
||||
}
|
||||
},
|
||||
"30": {
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"class_type": "DualCLIPLoader",
|
||||
"inputs": {
|
||||
"ckpt_name": "flux1-schnell.safetensors"
|
||||
"clip_name1": "t5xxl_fp8_e4m3fn.safetensors",
|
||||
"clip_name2": "clip_l.safetensors",
|
||||
"type": "flux",
|
||||
"device": "default"
|
||||
}
|
||||
},
|
||||
"31": {
|
||||
"class_type": "VAELoader",
|
||||
"inputs": {
|
||||
"vae_name": "ae.safetensors"
|
||||
}
|
||||
},
|
||||
"32": {
|
||||
"class_type": "UNETLoader",
|
||||
"inputs": {
|
||||
"unet_name": "flux1-schnell.safetensors",
|
||||
"weight_dtype": "fp8_e4m3fn"
|
||||
}
|
||||
},
|
||||
"33": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {
|
||||
"clip": ["30", 1],
|
||||
"clip": ["30", 0],
|
||||
"text": "NEGATIVE_PLACEHOLDER"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ COMFYUI_BASE = "http://test-comfyui:8188"
|
||||
# build_flux_workflow — pure function, no mocking needed
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_build_flux_workflow_structure():
|
||||
"""Verify build_flux_workflow returns a dict with correct node types."""
|
||||
wf = build_flux_workflow(
|
||||
@@ -45,7 +44,9 @@ def test_build_flux_workflow_structure():
|
||||
assert wf["9"]["class_type"] == "SaveImage"
|
||||
assert wf["13"]["class_type"] == "KSampler"
|
||||
assert wf["27"]["class_type"] == "EmptySD3LatentImage"
|
||||
assert wf["30"]["class_type"] == "CheckpointLoaderSimple"
|
||||
assert wf["30"]["class_type"] == "DualCLIPLoader"
|
||||
assert wf["31"]["class_type"] == "VAELoader"
|
||||
assert wf["32"]["class_type"] == "UNETLoader"
|
||||
assert wf["33"]["class_type"] == "CLIPTextEncode"
|
||||
|
||||
|
||||
@@ -66,7 +67,7 @@ def test_build_flux_workflow_params_injected():
|
||||
assert wf["27"]["inputs"]["height"] == 768
|
||||
assert wf["13"]["inputs"]["steps"] == 8
|
||||
assert wf["13"]["inputs"]["seed"] == 12345
|
||||
assert wf["30"]["inputs"]["ckpt_name"] == "sdxl.safetensors"
|
||||
assert wf["32"]["inputs"]["unet_name"] == "sdxl.safetensors"
|
||||
|
||||
|
||||
def test_negative_prompt_included():
|
||||
@@ -103,7 +104,6 @@ def test_random_seed_generated():
|
||||
# list_available_models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_available_models():
|
||||
@@ -146,7 +146,6 @@ async def test_list_available_models_comfyui_offline():
|
||||
# get_generation_status
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_generation_status_pending(queue_with_pending):
|
||||
@@ -191,7 +190,6 @@ async def test_get_generation_status_complete(queue_empty, mock_history_response
|
||||
# get_output_directory
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_output_directory_default(monkeypatch):
|
||||
"""No IMAGE_OUTPUT_DIR env var → returns expanded ~/Pictures/mcp-generated."""
|
||||
monkeypatch.delenv("IMAGE_OUTPUT_DIR", raising=False)
|
||||
@@ -216,7 +214,6 @@ def test_get_output_directory_custom(monkeypatch, tmp_path):
|
||||
# generate_image
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_image_success(
|
||||
@@ -300,3 +297,253 @@ async def test_generate_image_timeout(monkeypatch, queue_with_pending):
|
||||
assert len(result) == 1
|
||||
assert "timed out" in result[0].text.lower()
|
||||
assert "test-uuid-1234" in result[0].text
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_image_empty_prompt(tmp_path, sample_image_bytes, mock_history_response, queue_empty, monkeypatch):
|
||||
"""Empty prompt → workflow has empty text in positive node, but generation succeeds."""
|
||||
monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))
|
||||
|
||||
respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
|
||||
return_value=httpx.Response(200, json={"prompt_id": "test-empty-uuid"})
|
||||
)
|
||||
respx.get(f"{COMFYUI_BASE}/api/queue").mock(
|
||||
return_value=httpx.Response(200, json=queue_empty)
|
||||
)
|
||||
mock_history_empty = {
|
||||
"test-empty-uuid": {
|
||||
"outputs": {
|
||||
"9": {
|
||||
"images": [
|
||||
{
|
||||
"filename": "mcp-image-gen_00001_.png",
|
||||
"subfolder": "",
|
||||
"type": "output",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": {"completed": True},
|
||||
}
|
||||
}
|
||||
respx.get(f"{COMFYUI_BASE}/api/history/test-empty-uuid").mock(
|
||||
return_value=httpx.Response(200, json=mock_history_empty)
|
||||
)
|
||||
respx.get(f"{COMFYUI_BASE}/api/view").mock(
|
||||
return_value=httpx.Response(200, content=sample_image_bytes)
|
||||
)
|
||||
|
||||
result = await generate_image(prompt="", output_dir=str(tmp_path))
|
||||
|
||||
assert len(result) == 2
|
||||
text_content = result[0]
|
||||
image_content = result[1]
|
||||
assert "Generated:" in text_content.text
|
||||
assert str(tmp_path) in text_content.text
|
||||
# Verify workflow was built with empty prompt (indirectly via success)
|
||||
assert image_content.mimeType == "image/png"
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_image_long_prompt(tmp_path, sample_image_bytes, mock_history_response, queue_empty, monkeypatch):
|
||||
"""Very long prompt → passed as-is to workflow without truncation."""
|
||||
monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))
|
||||
|
||||
long_prompt = "a " + "very long descriptive prompt " * 50 # ~500 chars
|
||||
|
||||
respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
|
||||
return_value=httpx.Response(200, json={"prompt_id": "test-long-uuid"})
|
||||
)
|
||||
respx.get(f"{COMFYUI_BASE}/api/queue").mock(
|
||||
return_value=httpx.Response(200, json=queue_empty)
|
||||
)
|
||||
mock_history_long = {
|
||||
"test-long-uuid": {
|
||||
"outputs": {
|
||||
"9": {
|
||||
"images": [
|
||||
{
|
||||
"filename": "mcp-image-gen_00001_.png",
|
||||
"subfolder": "",
|
||||
"type": "output",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": {"completed": True},
|
||||
}
|
||||
}
|
||||
respx.get(f"{COMFYUI_BASE}/api/history/test-long-uuid").mock(
|
||||
return_value=httpx.Response(200, json=mock_history_long)
|
||||
)
|
||||
respx.get(f"{COMFYUI_BASE}/api/view").mock(
|
||||
return_value=httpx.Response(200, content=sample_image_bytes)
|
||||
)
|
||||
|
||||
result = await generate_image(prompt=long_prompt, output_dir=str(tmp_path))
|
||||
|
||||
assert len(result) == 2
|
||||
# Success implies long prompt was accepted (ComfyUI handles it)
|
||||
saved_files = list(tmp_path.glob("*.png"))
|
||||
assert len(saved_files) == 1
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_image_invalid_model(tmp_path, monkeypatch):
|
||||
"""Invalid model → ComfyUI /prompt returns 500 or 404, tool returns error TextContent."""
|
||||
monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))
|
||||
|
||||
respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
|
||||
return_value=httpx.Response(404, json={"error": "Model not found"})
|
||||
)
|
||||
|
||||
result = await generate_image(
|
||||
prompt="a cat",
|
||||
model="nonexistent-model.safetensors",
|
||||
output_dir=str(tmp_path)
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert "404" in result[0].text
|
||||
assert "Model not found" in result[0].text
|
||||
# No file saved
|
||||
saved_files = list(tmp_path.glob("*.png"))
|
||||
assert len(saved_files) == 0
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_image_custom_output_dir(tmp_path, sample_image_bytes, mock_history_response, queue_empty, monkeypatch):
|
||||
"""Custom output_dir → image saved there, path reflects it."""
|
||||
custom_dir = tmp_path / "custom"
|
||||
monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path)) # Base for default
|
||||
|
||||
respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
|
||||
return_value=httpx.Response(200, json={"prompt_id": "test-custom-uuid"})
|
||||
)
|
||||
respx.get(f"{COMFYUI_BASE}/api/queue").mock(
|
||||
return_value=httpx.Response(200, json=queue_empty)
|
||||
)
|
||||
mock_history_custom = {
|
||||
"test-custom-uuid": {
|
||||
"outputs": {
|
||||
"9": {
|
||||
"images": [
|
||||
{
|
||||
"filename": "mcp-image-gen_00001_.png",
|
||||
"subfolder": "",
|
||||
"type": "output",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": {"completed": True},
|
||||
}
|
||||
}
|
||||
respx.get(f"{COMFYUI_BASE}/api/history/test-custom-uuid").mock(
|
||||
return_value=httpx.Response(200, json=mock_history_custom)
|
||||
)
|
||||
respx.get(f"{COMFYUI_BASE}/api/view").mock(
|
||||
return_value=httpx.Response(200, content=sample_image_bytes)
|
||||
)
|
||||
|
||||
result = await generate_image(
|
||||
prompt="a dog",
|
||||
output_dir=str(custom_dir),
|
||||
)
|
||||
|
||||
assert len(result) == 2
|
||||
text_content = result[0]
|
||||
assert str(custom_dir) in text_content.text
|
||||
# Directory was created
|
||||
assert custom_dir.exists()
|
||||
saved_files = list(custom_dir.glob("*.png"))
|
||||
assert len(saved_files) == 1
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_image_random_seed_variance(tmp_path, sample_image_bytes, mock_history_response, queue_empty, monkeypatch):
|
||||
"""seed=-1 → different actual_seed each call, reflected in filename."""
|
||||
monkeypatch.setattr(server, "IMAGE_OUTPUT_DIR", str(tmp_path))
|
||||
|
||||
# First generation
|
||||
respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
|
||||
return_value=httpx.Response(200, json={"prompt_id": "seed1-uuid"})
|
||||
)
|
||||
respx.get(f"{COMFYUI_BASE}/api/queue").mock(
|
||||
return_value=httpx.Response(200, json=queue_empty)
|
||||
)
|
||||
mock_history_seed1 = {
|
||||
"seed1-uuid": {
|
||||
"outputs": {
|
||||
"9": {
|
||||
"images": [
|
||||
{
|
||||
"filename": "mcp-image-gen_00001_.png",
|
||||
"subfolder": "",
|
||||
"type": "output",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": {"completed": True},
|
||||
}
|
||||
}
|
||||
respx.get(f"{COMFYUI_BASE}/api/history/seed1-uuid").mock(
|
||||
return_value=httpx.Response(200, json=mock_history_seed1)
|
||||
)
|
||||
respx.get(f"{COMFYUI_BASE}/api/view").mock(
|
||||
return_value=httpx.Response(200, content=sample_image_bytes)
|
||||
)
|
||||
|
||||
result1 = await generate_image(prompt="cat", seed=-1, output_dir=str(tmp_path))
|
||||
seed1 = [line for line in result1[0].text.split("\n") if "Seed:" in line][0].split(": ")[1]
|
||||
filename1 = Path(result1[0].text.split("Generated: ")[1].split("\n")[0]).name
|
||||
assert "Seed:" in result1[0].text
|
||||
assert int(seed1) != 0 # Not default
|
||||
|
||||
# Reset mocks for second call
|
||||
respx.reset()
|
||||
respx.post(f"{COMFYUI_BASE}/api/prompt").mock(
|
||||
return_value=httpx.Response(200, json={"prompt_id": "seed2-uuid"})
|
||||
)
|
||||
respx.get(f"{COMFYUI_BASE}/api/queue").mock(
|
||||
return_value=httpx.Response(200, json=queue_empty)
|
||||
)
|
||||
mock_history_seed2 = {
|
||||
"seed2-uuid": {
|
||||
"outputs": {
|
||||
"9": {
|
||||
"images": [
|
||||
{
|
||||
"filename": "mcp-image-gen_00002_.png",
|
||||
"subfolder": "",
|
||||
"type": "output",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": {"completed": True},
|
||||
}
|
||||
}
|
||||
respx.get(f"{COMFYUI_BASE}/api/history/seed2-uuid").mock(
|
||||
return_value=httpx.Response(200, json=mock_history_seed2)
|
||||
)
|
||||
respx.get(f"{COMFYUI_BASE}/api/view").mock(
|
||||
return_value=httpx.Response(200, content=sample_image_bytes)
|
||||
)
|
||||
|
||||
result2 = await generate_image(prompt="cat", seed=-1, output_dir=str(tmp_path))
|
||||
seed2 = [line for line in result2[0].text.split("\n") if "Seed:" in line][0].split(": ")[1]
|
||||
filename2 = Path(result2[0].text.split("Generated: ")[1].split("\n")[0]).name
|
||||
|
||||
# Different seeds and filenames
|
||||
assert seed1 != seed2
|
||||
assert filename1 != filename2
|
||||
# Both saved
|
||||
saved_files = list(tmp_path.glob("*.png"))
|
||||
assert len(saved_files) == 2
|
||||
|
||||