""" ⚒️ FORGE — Federated Open Registry for Generative Executables v2.2 — Gradio 6.9 native theme · CRUD · MCP · SKILL.md · Claude Skills """ import json from pathlib import Path import gradio as gr from fastapi import FastAPI, HTTPException, Request from fastapi.responses import JSONResponse, Response import skill_registry as registry from mcp_server import register_mcp_routes # ─── FastAPI + MCP ──────────────────────────────────────────────── api = FastAPI(title="FORGE API", version="2.2.0") register_mcp_routes(api) def jresp(data, status=200): return JSONResponse(content=data, status_code=status) @api.get("/api/v1/manifest") async def api_manifest(): return jresp(registry.get_manifest()) @api.get("/api/v1/skills") async def api_list(tag: str | None = None, q: str | None = None): return jresp({"skills": registry.list_skills(tag=tag, query=q), "count": len(registry.list_skills(tag=tag, query=q))}) @api.get("/api/v1/skills/{skill_id}") async def api_get(skill_id: str): s = registry.get_skill(skill_id) if not s: raise HTTPException(404, f"'{skill_id}' not found") return jresp(s) @api.get("/api/v1/skills/{skill_id}/code") async def api_code(skill_id: str): d = registry.get_skill_code(skill_id) if not d: raise HTTPException(404) return jresp(d) @api.get("/api/v1/skills/{skill_id}/download") async def api_download(skill_id: str): d = registry.get_skill_code(skill_id) if not d: raise HTTPException(404) code = f'"""\nFORGE Skill: {skill_id} v{d["version"]}\nhttps://huggingface.co/spaces/Chris4K/agent-forge\n"""\n\n{d["code"]}\n' return Response(code, media_type="text/x-python", headers={"Content-Disposition": f'attachment; filename="{skill_id}.py"'}) @api.post("/api/v1/skills") async def api_publish(request: Request): try: skill = await request.json() except Exception: raise HTTPException(400, "Invalid JSON") ok, msg = registry.publish_skill(skill) return jresp({"ok": ok, "message": msg}, 201 if ok else 400) @api.put("/api/v1/skills/{skill_id}") async def api_update(skill_id: str, request: Request): try: updates = await request.json() except Exception: raise HTTPException(400, "Invalid JSON") ok, msg = registry.update_skill(skill_id, updates) return jresp({"ok": ok, "message": msg}, 200 if ok else 404) @api.delete("/api/v1/skills/{skill_id}") async def api_delete(skill_id: str): ok, msg = registry.delete_skill(skill_id) return jresp({"ok": ok, "message": msg}, 200 if ok else 404) @api.get("/api/v1/search") async def api_search(q: str): skills = registry.list_skills(query=q) return jresp({"query": q, "skills": skills, "count": len(skills)}) @api.get("/api/v1/tags") async def api_tags(): return jresp({"tags": registry.get_all_tags()}) @api.get("/api/v1/stats") async def api_stats(): return jresp(registry.get_stats()) # ─── Gradio 6 Theme ─────────────────────────────────────────────── # All dark palette values set via the theme system — no CSS class hacks class ForgeTheme(gr.themes.Base): def __init__(self): super().__init__( primary_hue=gr.themes.colors.Color( c50="#fff3e0", c100="#ffe0b2", c200="#ffcc80", c300="#ffb74d", c400="#ffa726", c500="#ff9500", c600="#ff6b00", c700="#e65100", c800="#bf360c", c900="#8b1a00", c950="#5a0e00", ), neutral_hue=gr.themes.colors.Color( c50="#f0f0f8", c100="#d8d8e8", c200="#b0b0c8", c300="#8888a8", c400="#606080", c500="#404060", c600="#2a2a45", c700="#1e1e35", c800="#141425", c900="#0e0e1a", c950="#0a0a0f", ), font=gr.themes.GoogleFont("Space Mono"), font_mono=gr.themes.GoogleFont("Space Mono"), ) # Body self.body_background_fill = "#0a0a0f" self.body_background_fill_dark = "#0a0a0f" self.body_text_color = "#e8e8f0" self.body_text_color_dark = "#e8e8f0" self.body_text_color_subdued = "#6b6b8a" self.body_text_color_subdued_dark = "#6b6b8a" # Blocks/panels self.block_background_fill = "#111118" self.block_background_fill_dark = "#111118" self.block_border_color = "#1e1e2e" self.block_border_color_dark = "#1e1e2e" self.block_border_width = "1px" self.block_title_text_color = "#ff6b00" self.block_title_text_color_dark = "#ff6b00" self.block_label_text_color = "#6b6b8a" self.block_label_text_color_dark = "#6b6b8a" self.block_label_background_fill = "#111118" self.block_label_background_fill_dark = "#111118" # Background fills (secondary panels) self.background_fill_primary = "#0a0a0f" self.background_fill_primary_dark = "#0a0a0f" self.background_fill_secondary = "#111118" self.background_fill_secondary_dark = "#111118" # Inputs self.input_background_fill = "#111118" self.input_background_fill_dark = "#111118" self.input_background_fill_focus = "#16162a" self.input_background_fill_focus_dark = "#16162a" self.input_border_color = "#2a2a45" self.input_border_color_dark = "#2a2a45" self.input_border_color_focus = "#ff6b00" self.input_border_color_focus_dark = "#ff6b00" self.input_placeholder_color = "#4a4a6a" self.input_placeholder_color_dark = "#4a4a6a" # Borders self.border_color_primary = "#1e1e2e" self.border_color_primary_dark = "#1e1e2e" self.border_color_accent = "#ff6b00" self.border_color_accent_dark = "#ff6b00" # Buttons self.button_primary_background_fill = "#ff6b00" self.button_primary_background_fill_dark = "#ff6b00" self.button_primary_background_fill_hover = "#ff9500" self.button_primary_background_fill_hover_dark = "#ff9500" self.button_primary_text_color = "#000000" self.button_primary_text_color_dark = "#000000" self.button_secondary_background_fill = "#1e1e2e" self.button_secondary_background_fill_dark = "#1e1e2e" self.button_secondary_background_fill_hover = "#2a2a45" self.button_secondary_background_fill_hover_dark = "#2a2a45" self.button_secondary_text_color = "#e8e8f0" self.button_secondary_text_color_dark = "#e8e8f0" self.button_secondary_border_color = "#2a2a45" self.button_secondary_border_color_dark = "#2a2a45" # Code blocks self.code_background_fill = "#0d0d1a" self.code_background_fill_dark = "#0d0d1a" # Shadow self.block_shadow = "none" self.block_shadow_dark = "none" # ─── Custom HTML-only CSS (skill cards, stat bar, forge branding) ─ # Only targets our own HTML components, not Gradio internals. CUSTOM_CSS = """ /* Forge brand */ .forge-header { text-align: center; padding: 2.2rem 1rem 1.2rem; border-bottom: 1px solid #1e1e2e; background: linear-gradient(180deg, #0f0f1a 0%, #0a0a0f 100%); margin-bottom: 0.5rem; } .forge-logo { font-family: 'Space Mono', monospace; font-size: 3rem; font-weight: 700; background: linear-gradient(135deg, #ff6b00, #ff9500); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; line-height: 1; letter-spacing: -2px; } .forge-sub { font-family: 'Space Mono', monospace; font-size: 0.62rem; color: #4a4a6a; letter-spacing: 0.32em; text-transform: uppercase; margin-top: 0.4rem; } /* Stats row */ .stat-bar { display: flex; justify-content: center; gap: 2.5rem; padding: 0.8rem 1rem; border: 1px solid #1e1e2e; border-radius: 8px; background: #111118; margin: 0.8rem 0 1rem; } .stat-num { font-family: 'Space Mono', monospace; font-size: 1.7rem; color: #ff6b00; font-weight: 700; line-height: 1; } .stat-lbl { font-size: 0.58rem; color: #4a4a6a; text-transform: uppercase; letter-spacing: 0.18em; margin-top: 3px; } /* Skill cards */ .skill-card { background: #111118; border: 1px solid #1e1e2e; border-radius: 8px; padding: 1rem 1.2rem; margin-bottom: 0.55rem; transition: border-color 0.15s, transform 0.1s; cursor: default; } .skill-card:hover { border-color: #ff6b00; transform: translateX(3px); } .sn { font-family: 'Space Mono', monospace; font-size: 0.88rem; font-weight: 700; color: #ff6b00; } .sd { font-size: 0.8rem; color: #6b6b8a; line-height: 1.5; margin-top: 0.3rem; } /* Tags + runtime badges */ .tag { display: inline-block; background: #16162a; border: 1px solid #2a2a50; color: #8b5cf6; font-family: 'Space Mono', monospace; font-size: 0.58rem; padding: 1px 7px; border-radius: 20px; margin: 2px 2px 0; } .rt { display:inline-block; font-family:'Space Mono',monospace; font-size:0.58rem; padding:1px 6px; border-radius:3px; margin-left:6px; background:#0f2018; color:#00ff88; border:1px solid #00ff8825; } .rt.node { background:#121a0a; color:#84cc16; border-color:#84cc1625; } .rt.ins { background:#12122a; color:#8b5cf6; border-color:#8b5cf625; } .rt.cld { background:#1e1208; color:#ff9500; border-color:#ff950025; } /* API / MCP boxes */ .api-row { background: #111118; border-left: 3px solid #ff6b00; padding: 0.52rem 1rem; margin: 0.32rem 0; border-radius: 0 6px 6px 0; font-family: 'Space Mono', monospace; font-size: 0.7rem; color: #c0c0d8; } .m-get { color: #00ff88; font-weight: 700; } .m-post { color: #ff9500; font-weight: 700; } .m-put { color: #60a5fa; font-weight: 700; } .m-del { color: #ff4444; font-weight: 700; } .mcp-box { background: #100a20; border: 1px solid #5b21b640; border-radius: 8px; padding: 0.75rem 1rem; margin: 0.45rem 0; font-family: 'Space Mono', monospace; font-size: 0.7rem; color: #c4b5fd; } .mcp-box code { color: #a78bfa; } .codebox { background: #0d0d1a; border: 1px solid #1e1e2e; border-radius: 6px; padding: 0.9rem 1rem; font-family: 'Space Mono', monospace; font-size: 0.68rem; color: #00ff88; white-space: pre; overflow-x: auto; line-height: 1.55; } .sec { font-family: 'Space Mono', monospace; color: #8b5cf6; font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.15em; margin: 1.1rem 0 0.45rem; } .info-note { font-family: 'Space Mono', monospace; font-size: 0.68rem; color: #4a4a6a; padding: 0.4rem 0; } """ PUBLISH_TEMPLATE = """{ "id": "my_skill", "name": "My Skill", "version": "1.0.0", "description": "What this skill does for an agent.", "author": "Chris4K", "tags": ["utility"], "runtime": "python", "dependencies": [], "env_required": [], "bins": [], "schema": { "input": { "text": "str" }, "output": { "result": "str" } }, "code": "def execute(text: str) -> dict:\\n return {'result': text.upper()}\\n" }""" SKILL_MD_TEMPLATE = """--- name: my-instructions-skill version: 1.0.0 description: Teach an agent how to do something step-by-step. author: Chris4K tags: [instructions, utility] runtime: instructions --- # My Instructions Skill ## Purpose Describe what capability this adds to an agent. ## When to use Use this skill when the user asks you to... ## Steps 1. First, do this 2. Then do that 3. Return the result ## Examples Input: "example" Output: "expected" """ # ─── HTML helpers ───────────────────────────────────────────────── def _rt(rt): rt = (rt or "python").lower() c = "node" if rt in ("node","nodejs","shell") else "ins" if rt=="instructions" else "cld" if "claude" in rt else "" return f'{rt}' def fmt_card(s): tags = "".join(f'{t}' for t in s.get("tags", [])) return f"""
{s['id']}
{json.dumps(s.get("schema", {}), indent=2)}
def execute(...) → dictConnect Claude Desktop, Claude API, or any MCP client directly to FORGE.
https://huggingface.co/spaces/Chris4K/agent-forge/mcp/ssehttps://huggingface.co/spaces/Chris4K/agent-forge/mcp
Base: https://huggingface.co/spaces/Chris4K/agent-forge