+ )}
{desire.tip_amount_cents > 0 && (
💰 ${(desire.tip_amount_cents / 100).toFixed(2)} bounty
@@ -77,7 +82,7 @@ export default function BountyDetail() {
{desire.status === 'open' && (
-
+
Fulfill this Desire →
diff --git a/services/frontend/src/pages/Editor.tsx b/services/frontend/src/pages/Editor.tsx
index 108989d..bf67990 100644
--- a/services/frontend/src/pages/Editor.tsx
+++ b/services/frontend/src/pages/Editor.tsx
@@ -9,7 +9,7 @@
*/
import { useState, useEffect, useCallback, useRef } from 'react';
-import { useParams, useNavigate } from 'react-router-dom';
+import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import api from '@/lib/api';
import { useAuthStore } from '@/stores/auth';
@@ -38,8 +38,13 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
export default function Editor() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
const { isAuthenticated, user } = useAuthStore();
+ // Fulfillment context — read once from URL, persist in ref so it survives navigation
+ const fulfillId = searchParams.get('fulfill');
+ const fulfillDesireId = useRef(fulfillId);
+
const [code, setCode] = useState(DEFAULT_SHADER);
const [liveCode, setLiveCode] = useState(DEFAULT_SHADER);
const [title, setTitle] = useState('Untitled Shader');
@@ -70,6 +75,16 @@ export default function Editor() {
enabled: !!id,
});
+ // Fetch desire context when fulfilling
+ const { data: fulfillDesire } = useQuery({
+ queryKey: ['desire', fulfillDesireId.current],
+ queryFn: async () => {
+ const { data } = await api.get(`/desires/${fulfillDesireId.current}`);
+ return data;
+ },
+ enabled: !!fulfillDesireId.current,
+ });
+
useEffect(() => {
if (existingShader) {
setCode(existingShader.glsl_code);
@@ -160,7 +175,10 @@ export default function Editor() {
}
} else {
// Create new shader
- const { data } = await api.post('/shaders', payload);
+ const { data } = await api.post('/shaders', {
+ ...payload,
+ fulfills_desire_id: fulfillDesireId.current || undefined,
+ });
if (publishStatus === 'published') {
navigate(`/shader/${data.id}`);
} else {
@@ -278,6 +296,17 @@ export default function Editor() {
)}
+ {/* Desire fulfillment context banner */}
+ {fulfillDesire && (
+
+ 🎯 Fulfilling desire:
+ {fulfillDesire.prompt_text}
+ {fulfillDesire.style_hints && (
+ Style hints available
+ )}
+
+ )}
+
{/* Split pane: editor + drag handle + preview */}
{/* Code editor */}
diff --git a/services/mcp/server.py b/services/mcp/server.py
index 28cb306..6cf628b 100644
--- a/services/mcp/server.py
+++ b/services/mcp/server.py
@@ -41,6 +41,14 @@ async def api_post(path: str, data: dict):
return resp.json()
+async def api_post_with_params(path: str, params: dict):
+ """POST with query parameters (not JSON body). Used for endpoints like fulfill."""
+ async with httpx.AsyncClient(base_url=API_BASE, timeout=15.0) as client:
+ resp = await client.post(f"/api/v1{path}", params=params, headers=INTERNAL_AUTH)
+ resp.raise_for_status()
+ return resp.json()
+
+
async def api_put(path: str, data: dict):
async with httpx.AsyncClient(base_url=API_BASE, timeout=15.0) as client:
resp = await client.put(f"/api/v1{path}", json=data, headers=INTERNAL_AUTH)
@@ -121,7 +129,8 @@ async def get_shader_version_code(shader_id: str, version_number: int) -> str:
@mcp.tool()
async def submit_shader(title: str, glsl_code: str, description: str = "", tags: str = "",
- shader_type: str = "2d", status: str = "published") -> str:
+ shader_type: str = "2d", status: str = "published",
+ fulfills_desire_id: str = "") -> str:
"""Submit a new GLSL shader to Fractafrag.
Shader format: void mainImage(out vec4 fragColor, in vec2 fragCoord)
@@ -134,11 +143,15 @@ async def submit_shader(title: str, glsl_code: str, description: str = "", tags:
tags: Comma-separated tags (e.g. "fractal,noise,colorful")
shader_type: 2d, 3d, or audio-reactive
status: "published" to go live, "draft" to save privately
+ fulfills_desire_id: Optional UUID of a desire this shader fulfills
"""
tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else []
- result = await api_post("/shaders", {"title": title, "glsl_code": glsl_code,
- "description": description, "tags": tag_list,
- "shader_type": shader_type, "status": status})
+ payload = {"title": title, "glsl_code": glsl_code,
+ "description": description, "tags": tag_list,
+ "shader_type": shader_type, "status": status}
+ if fulfills_desire_id:
+ payload["fulfills_desire_id"] = fulfills_desire_id
+ result = await api_post("/shaders", payload)
return json.dumps({"id": result["id"], "title": result["title"],
"status": result.get("status"), "current_version": result.get("current_version", 1),
"message": f"Shader '{result['title']}' created.", "url": f"/shader/{result['id']}"})
@@ -210,7 +223,11 @@ async def get_similar_shaders(shader_id: str, limit: int = 10) -> str:
@mcp.tool()
async def get_desire_queue(min_heat: float = 0, limit: int = 10) -> str:
- """Get open shader desires/bounties. These are community requests.
+ """Get open shader desires/bounties with cluster context and style hints.
+
+ Returns community requests ranked by heat. Use cluster_count to identify
+ high-demand desires (many similar requests). Use style_hints to understand
+ the visual direction requested.
Args:
min_heat: Minimum heat score (higher = more demand)
@@ -220,11 +237,42 @@ async def get_desire_queue(min_heat: float = 0, limit: int = 10) -> str:
return json.dumps({"count": len(desires),
"desires": [{"id": d["id"], "prompt_text": d["prompt_text"],
"heat_score": d.get("heat_score", 0),
+ "cluster_count": d.get("cluster_count", 0),
+ "style_hints": d.get("style_hints"),
"tip_amount_cents": d.get("tip_amount_cents", 0),
- "status": d.get("status")}
+ "status": d.get("status"),
+ "fulfilled_by_shader": d.get("fulfilled_by_shader")}
for d in desires]})
+@mcp.tool()
+async def fulfill_desire(desire_id: str, shader_id: str) -> str:
+ """Mark a desire as fulfilled by linking it to a published shader.
+
+ The shader must be published. The desire must be open.
+ Use get_desire_queue to find open desires, then submit_shader or
+ use an existing shader ID to fulfill one.
+
+ Args:
+ desire_id: UUID of the desire to fulfill
+ shader_id: UUID of the published shader that fulfills this desire
+ """
+ try:
+ result = await api_post_with_params(
+ f"/desires/{desire_id}/fulfill",
+ {"shader_id": shader_id}
+ )
+ return json.dumps({"status": "fulfilled", "desire_id": desire_id,
+ "shader_id": shader_id,
+ "message": f"Desire {desire_id} fulfilled by shader {shader_id}."})
+ except httpx.HTTPStatusError as e:
+ try:
+ error_detail = e.response.json().get("detail", str(e))
+ except Exception:
+ error_detail = str(e)
+ return json.dumps({"error": error_detail, "status_code": e.response.status_code})
+
+
@mcp.resource("fractafrag://platform-info")
def platform_info() -> str:
"""Platform overview and shader writing guidelines."""