MCP Server (8 tools):
- browse_shaders: search by title, tags, type, sort (trending/new/top)
- get_shader: full details + GLSL source by ID
- get_shader_versions: version history with change notes
- get_shader_version_code: GLSL code from any specific version
- submit_shader: create new shader (published or draft)
- update_shader: push revisions with change notes, auto-versions
- get_trending: top-scored shaders
- get_desire_queue: open community requests
MCP resource: fractafrag://platform-info with shader format guide
Auth: Internal service token (Bearer internal:mcp-service) allows MCP
server to write to the API as the system user. No user API keys needed
for the MCP→API internal path.
Transport: Streamable HTTP on port 3200 via FastMCP SDK.
Stateless mode with JSON responses.
Hot Score Ranking:
- Wilson score lower bound with 48-hour time decay
- Recalculated on every vote (up/down/remove)
- Feed sorts by score for trending view
Connection config for Claude Desktop:
{
"mcpServers": {
"fractafrag": {
"url": "http://localhost:3200/mcp"
}
}
}
Root cause: Chromium limits ~16 simultaneous WebGL contexts. When scrolling
through a feed of 20+ shader cards, older contexts get silently evicted.
Once a context is lost on a canvas element, getContext('webgl2') returns null
on that same element forever — even after loseContext()/restore cycles.
Solution: The ShaderCanvas component now renders a container div and creates
canvas elements imperatively. When re-entering viewport:
1. Check if existing GL context is still alive (isContextLost)
2. If alive: just restart the animation loop
3. If dead: remove the old canvas, create a fresh DOM element, get a new
context, recompile, and start rendering
This means scrolling down creates new contexts and scrolling back up
replaces dead canvases with fresh ones. At any given time only ~9 visible
canvases hold active contexts — well within Chrome's limit.
Also: 200px rootMargin on IntersectionObserver pre-compiles shaders
before cards enter viewport for smoother scroll experience.
ShaderCanvas rewrite:
- IntersectionObserver-driven rendering: WebGL context only created when canvas
enters viewport, released when it leaves. Prevents context starvation when
20+ shaders are in the feed simultaneously.
- Graceful fallback UI when WebGL context unavailable (hexagon + 'scroll to load')
- Context loss/restore event handlers
- powerPreference: 'low-power' for feed thumbnails
- Pause animation loop when off-screen (saves GPU even with context alive)
- Separate resize observer (no devicePixelRatio scaling for feed — saves memory)
Fixed shaders:
- Pixel Art Dither: replaced mat4 dynamic indexing with unrolled Bayer lookup
(some WebGL drivers reject mat4[int_var][int_var])
- Wave Interference 2D: replaced C-style array element assignment with
individual vec2 variables (GLSL ES 300 compatibility)
Architecture — Shader versioning & draft system:
- New shader_versions table: immutable snapshots of every edit
- Shaders now have status: draft, published, archived
- current_version counter tracks version number
- Every create/update creates a ShaderVersion record
- Restore-from-version endpoint creates new version (never destructive)
- Drafts are private, only visible to author
- Forks start as drafts
- Free tier rate limit applies only to published shaders (drafts unlimited)
Architecture — Platform identity:
- System account 'fractafrag' (UUID 00000000-...-000001) created in init.sql
- is_system flag on users and shaders
- system_label field: 'fractafrag-curated', future: 'fractafrag-generated'
- Feed/explore can filter by is_system
- System shaders display distinctly from user/AI content
API changes:
- GET /shaders/mine — user workspace (drafts, published, archived)
- GET /shaders/{id}/versions — version history
- GET /shaders/{id}/versions/{n} — specific version
- POST /shaders/{id}/versions/{n}/restore — restore old version
- POST /shaders accepts status: 'draft' | 'published'
- PUT /shaders/{id} accepts change_note for version descriptions
- PUT status transitions: draft→published, published→archived, archived→published
Frontend — Editor improvements:
- Resizable split pane with drag handle (20-80% range, smooth col-resize cursor)
- Save Draft button (creates/updates as draft, no publish)
- Publish button (validates, publishes, redirects to shader page)
- Version badge shows current version number when editing existing
- Owner detection: editing own shader vs forking someone else's
- Saved status indicator ('Draft saved', 'Published')
Frontend — My Shaders workspace:
- /my-shaders route with status tabs (All, Draft, Published, Archived)
- Count badges per tab
- Status badges on shader cards (draft=yellow, published=green, archived=grey)
- Version badges (v1, v2, etc.)
- Quick actions: Edit, Publish, Archive, Restore, Delete per status
- Drafts link to editor, published link to detail page
Seed data — 200 fractafrag-curated shaders:
- 171 2D + 29 3D shaders
- 500 unique tags across all shaders
- All 200 titles are unique
- Covers: fractals (Mandelbrot, Julia sets), noise (fbm, Voronoi, Perlin),
raymarching (metaballs, terrain, torus knots, metall/glass),
effects (glitch, VHS, plasma, aurora, lightning, fireworks),
patterns (circuit, hex grid, stained glass, herringbone, moiré),
physics (wave interference, pendulum, caustics, gravity lens),
minimal (single shapes, gradients, dot grids),
nature (ink, watercolor, smoke, sand garden, coral, nebula),
color theory (RGB separation, CMY overlap, hue wheel),
domain warping (acid trip, lava rift, storm eye),
particles (fireflies, snow, ember, bubbles)
- Each shader has style_metadata (chaos_level, color_temperature, motion_type)
- Distributed creation times over 30 days for feed ranking variety
- Random initial scores for algorithm testing
- All authored by 'fractafrag' system account, is_system=true
- system_label='fractafrag-curated' for clear provenance
Schema:
- shader_versions table with (shader_id, version_number) unique constraint
- HNSW indexes on version lookup
- System account indexes
- Status-aware feed indexes
- Rename EngagementEvent.metadata → event_metadata (SQLAlchemy reserved name)
- Replace passlib with direct bcrypt usage (passlib incompatible with bcrypt 5.0)
- Fix renderer Dockerfile: npm ci → npm install (no lockfile)
- Fix frontend Dockerfile: single-stage, skip tsc for builds
- Remove deprecated 'version' key from docker-compose.yml
- Add docker-compose.dev.yml for data-stores-only local dev
- Add start_period to API healthcheck for startup grace