Spaces:
Paused
Paused
| """ | |
| ⚒️ 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) | |
| async def api_manifest(): | |
| return jresp(registry.get_manifest()) | |
| 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))}) | |
| 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) | |
| async def api_code(skill_id: str): | |
| d = registry.get_skill_code(skill_id) | |
| if not d: raise HTTPException(404) | |
| return jresp(d) | |
| 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"'}) | |
| 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) | |
| 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) | |
| 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) | |
| async def api_search(q: str): | |
| skills = registry.list_skills(query=q) | |
| return jresp({"query": q, "skills": skills, "count": len(skills)}) | |
| async def api_tags(): | |
| return jresp({"tags": registry.get_all_tags()}) | |
| 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'<span class="rt {c}">{rt}</span>' | |
| def fmt_card(s): | |
| tags = "".join(f'<span class="tag">{t}</span>' for t in s.get("tags", [])) | |
| return f"""<div class="skill-card"> | |
| <div class="sn">⚙ {s['name']}{_rt(s.get('runtime','python'))} | |
| <span style="float:right;font-size:.6rem;color:#3a3a5a">↓{s.get('downloads',0)} · v{s['version']}</span> | |
| </div> | |
| <div style="font-family:'Space Mono',monospace;font-size:.6rem;color:#3a3a5a;margin:2px 0 5px"> | |
| by {s['author']} · <code style="color:#ff6b0050">{s['id']}</code> | |
| </div> | |
| <div class="sd">{s['description'][:170]}{'…' if len(s['description'])>170 else ''}</div> | |
| <div style="margin-top:5px">{tags}</div> | |
| </div>""" | |
| def render_list(skills): | |
| if not skills: | |
| return '<div style="color:#3a3a5a;text-align:center;padding:2rem;font-family:Space Mono,monospace">No skills found.</div>' | |
| return "".join(fmt_card(s) for s in skills) | |
| def make_header(): | |
| st = registry.get_stats() | |
| return f"""<div class="forge-header"> | |
| <div class="forge-logo">⚒ FORGE</div> | |
| <div class="forge-sub">Federated Open Registry for Generative Executables</div> | |
| <div style="color:#3a3a5a;font-size:.72rem;margin-top:.5rem;font-family:'Space Mono',monospace"> | |
| Python · Node · SKILL.md · MCP · Claude Skills | |
| </div> | |
| </div> | |
| <div class="stat-bar"> | |
| <div><div class="stat-num">{st['total_skills']}</div><div class="stat-lbl">Skills</div></div> | |
| <div><div class="stat-num">{st['total_downloads']}</div><div class="stat-lbl">Downloads</div></div> | |
| <div><div class="stat-num">{st['total_tags']}</div><div class="stat-lbl">Tags</div></div> | |
| <div><div class="stat-num" style="color:#00ff88">✓</div><div class="stat-lbl">MCP Live</div></div> | |
| </div>""" | |
| # ─── Tab handlers ───────────────────────────────────────────────── | |
| def do_browse(q, tag): | |
| return render_list(registry.list_skills( | |
| tag=None if tag == "all" else tag, | |
| query=q.strip() or None | |
| )) | |
| def load_detail(skill_id): | |
| empty4 = ("", "", "", "") | |
| blank = '<div class="info-note" style="padding:.5rem">Enter a skill ID above, then click 📂 Load.</div>' | |
| if not skill_id.strip(): | |
| return blank, *empty4 | |
| s = registry.get_skill(skill_id.strip()) | |
| if not s: | |
| return f'<div style="color:#ff4444;font-family:Space Mono,monospace;padding:.5rem">Skill \'{skill_id}\' not found.</div>', *empty4 | |
| tags = " ".join(f'<span class="tag">{t}</span>' for t in s.get("tags", [])) | |
| env_w = f'<div style="color:#ff4444;font-family:Space Mono,monospace;font-size:.62rem;margin-top:.35rem">⚠ env vars needed: {", ".join(s["env_required"])}</div>' if s.get("env_required") else "" | |
| bins_i = f'<div style="color:#60a5fa;font-family:Space Mono,monospace;font-size:.62rem;margin-top:.2rem">🔧 system bins: {", ".join(s["bins"])}</div>' if s.get("bins") else "" | |
| deps = ", ".join(s.get("dependencies", [])) or "none" | |
| meta = f"""<div class="skill-card"> | |
| <div class="sn">⚙ {s['name']}{_rt(s.get('runtime','python'))}</div> | |
| <div style="font-family:'Space Mono',monospace;font-size:.62rem;color:#6b6b8a"> | |
| v{s['version']} · by {s['author']} · ↓{s.get('downloads',0)} downloads | |
| </div> | |
| <div style="margin:.6rem 0;line-height:1.6;font-size:.85rem">{s['description']}</div> | |
| <div>{tags}</div> | |
| {env_w}{bins_i} | |
| <div style="margin-top:.75rem;border-top:1px solid #1e1e2e;padding-top:.55rem"> | |
| <div class="sec">Schema</div> | |
| <pre style="font-size:.62rem;color:#7070a0;margin-top:.3rem;font-family:'Space Mono',monospace;line-height:1.5">{json.dumps(s.get("schema", {}), indent=2)}</pre> | |
| <div class="sec">Dependencies</div> | |
| <div style="font-family:'Space Mono',monospace;font-size:.65rem;color:#ff9500;margin-top:.2rem">{deps}</div> | |
| </div> | |
| </div>""" | |
| code = s.get("code", "# no code") | |
| instr = s.get("instructions", "_No SKILL.md instructions for this skill._") | |
| usage = f"""# ⚒️ Quick Start: {s['id']} | |
| # ───────────────────────────────────────────────── | |
| import requests, types | |
| def bootstrap_forge(url="https://huggingface.co/spaces/Chris4K/agent-forge"): | |
| r = requests.get(f"{{url}}/api/v1/skills/forge_client/code") | |
| m = types.ModuleType("forge_client") | |
| exec(r.json()["code"], m.__dict__) | |
| return m.ForgeClient(url) | |
| forge = bootstrap_forge() | |
| skill = forge.load("{s['id']}") | |
| result = skill.execute() # ← fill in params from schema above | |
| print(result) | |
| """ | |
| edit_j = json.dumps( | |
| {k: v for k, v in s.items() if k not in ("created_at","updated_at","downloads")}, | |
| indent=2, ensure_ascii=False | |
| ) | |
| return meta, code, instr, usage, edit_j | |
| def do_update(sid, ej): | |
| if not sid.strip(): return "⚠ Load a skill first" | |
| try: u = json.loads(ej) | |
| except json.JSONDecodeError as e: return f"❌ JSON error: {e}" | |
| ok, msg = registry.update_skill(sid.strip(), u) | |
| return ("✅ " if ok else "❌ ") + msg | |
| def do_delete(sid): | |
| if not sid.strip(): return "⚠ Load a skill first" | |
| ok, msg = registry.delete_skill(sid.strip()) | |
| return ("✅ " if ok else "❌ ") + msg | |
| def publish_json(jstr): | |
| try: s = json.loads(jstr) | |
| except json.JSONDecodeError as e: return f"❌ Invalid JSON: {e}" | |
| ok, msg = registry.publish_skill(s) | |
| return ("✅ " if ok else "❌ ") + msg | |
| def publish_md(mstr): | |
| s, err = registry.parse_skill_md(mstr) | |
| if err: return f"❌ {err}" | |
| ok, msg = registry.publish_skill(s) | |
| return ("✅ Published '" + s["id"] + "' — " if ok else "❌ ") + msg | |
| # ─── Build UI ───────────────────────────────────────────────────── | |
| def build_app(): | |
| theme = ForgeTheme() | |
| with gr.Blocks(title="⚒️ FORGE — Skill Artifactory", theme=theme) as demo: | |
| # Inject only our custom component CSS via <style> | |
| gr.HTML(f"<style>{CUSTOM_CSS}</style>") | |
| # Header + stats bar | |
| gr.HTML(make_header()) | |
| with gr.Tabs(): | |
| # ── BROWSE ──────────────────────────────────────────── | |
| with gr.Tab("🔍 Browse"): | |
| with gr.Row(): | |
| search_in = gr.Textbox( | |
| placeholder="search name, tags, description…", | |
| label="Search", scale=3, container=True | |
| ) | |
| tag_dd = gr.Dropdown( | |
| choices=["all"] + registry.get_all_tags(), | |
| value="all", label="Tag", scale=1 | |
| ) | |
| srch_btn = gr.Button("🔍 Search", variant="primary") | |
| skills_out = gr.HTML(render_list(registry.list_skills())) | |
| srch_btn.click(do_browse, [search_in, tag_dd], skills_out) | |
| search_in.submit(do_browse, [search_in, tag_dd], skills_out) | |
| tag_dd.change(do_browse, [search_in, tag_dd], skills_out) | |
| # ── CRUD ───────────────────────────────────────────── | |
| with gr.Tab("⚙ Skill CRUD"): | |
| gr.HTML('<div class="info-note">Load any skill by ID to view, edit, or delete it.</div>') | |
| sid_in = gr.Textbox( | |
| placeholder="e.g. calculator web_search claude_chat", | |
| label="Skill ID" | |
| ) | |
| with gr.Row(): | |
| load_btn = gr.Button("📂 Load", variant="primary") | |
| update_btn = gr.Button("💾 Save Changes", variant="secondary") | |
| delete_btn = gr.Button("🗑 Delete", variant="secondary") | |
| crud_msg = gr.Textbox(label="Status", interactive=False, max_lines=1) | |
| detail_meta = gr.HTML('<div class="info-note">No skill loaded.</div>') | |
| with gr.Tabs(): | |
| with gr.Tab("📄 Code"): | |
| d_code = gr.Code(language="python", label="Executable code") | |
| with gr.Tab("📝 SKILL.md"): | |
| d_md = gr.Markdown() | |
| with gr.Tab("🤖 Usage"): | |
| d_use = gr.Code(language="python", label="Agent bootstrap") | |
| with gr.Tab("✏️ Edit JSON"): | |
| d_edit = gr.Code(language="json", label="Edit → Save Changes") | |
| load_btn.click(load_detail, sid_in, [detail_meta, d_code, d_md, d_use, d_edit]) | |
| sid_in.submit(load_detail, sid_in, [detail_meta, d_code, d_md, d_use, d_edit]) | |
| update_btn.click(do_update, [sid_in, d_edit], crud_msg) | |
| delete_btn.click(do_delete, sid_in, crud_msg) | |
| # ── PUBLISH ─────────────────────────────────────────── | |
| with gr.Tab("📦 Publish"): | |
| with gr.Tabs(): | |
| with gr.Tab("🐍 Python / JSON"): | |
| gr.HTML('<div class="info-note">Must include <code>def execute(...) → dict</code></div>') | |
| pj_in = gr.Code(value=PUBLISH_TEMPLATE, language="json", label="Skill JSON", lines=20) | |
| pj_btn = gr.Button("⚒ Publish JSON Skill", variant="primary") | |
| pj_out = gr.Textbox(label="Result", interactive=False) | |
| pj_btn.click(publish_json, pj_in, pj_out) | |
| with gr.Tab("📝 SKILL.md"): | |
| gr.HTML('<div class="info-note">YAML frontmatter + markdown body. Compatible with ClawHub / OpenClaw.</div>') | |
| pm_in = gr.Code(value=SKILL_MD_TEMPLATE, language="markdown", label="SKILL.md", lines=20) | |
| pm_btn = gr.Button("⚒ Publish SKILL.md", variant="primary") | |
| pm_out = gr.Textbox(label="Result", interactive=False) | |
| pm_btn.click(publish_md, pm_in, pm_out) | |
| # ── MCP ─────────────────────────────────────────────── | |
| with gr.Tab("🔌 MCP"): | |
| gr.HTML(f"""<div style="padding:.75rem 0"> | |
| <div class="sec" style="font-size:.9rem;color:#ff6b00;margin-top:0">⚒️ FORGE MCP Server</div> | |
| <p style="color:#6b6b8a;font-size:.82rem;margin-bottom:.75rem">Connect Claude Desktop, Claude API, or any MCP client directly to FORGE.</p> | |
| <div class="sec">SSE Endpoint — Claude Desktop / mcp-remote</div> | |
| <div class="mcp-box"><code>https://huggingface.co/spaces/Chris4K/agent-forge/mcp/sse</code></div> | |
| <div class="sec">JSON-RPC Endpoint — direct API</div> | |
| <div class="mcp-box"><code>https://huggingface.co/spaces/Chris4K/agent-forge/mcp</code></div> | |
| <div class="sec">Claude Desktop Config</div> | |
| <div class="codebox">{{ | |
| "mcpServers": {{ | |
| "forge": {{ | |
| "command": "npx", | |
| "args": ["-y", "mcp-remote", | |
| "https://huggingface.co/spaces/Chris4K/agent-forge/mcp/sse"] | |
| }} | |
| }} | |
| }}</div> | |
| <div class="sec">Anthropic API with MCP</div> | |
| <div class="codebox">import anthropic | |
| client = anthropic.Anthropic() | |
| response = client.beta.messages.create( | |
| model="claude-opus-4-6", | |
| max_tokens=1024, | |
| mcp_servers=[{{ | |
| "type": "url", | |
| "url": "https://huggingface.co/spaces/Chris4K/agent-forge/mcp/sse", | |
| "name": "forge" | |
| }}], | |
| messages=[{{"role": "user", | |
| "content": "List all skills in FORGE, load the calculator, compute 2**32"}}] | |
| )</div> | |
| <div class="sec">Available MCP Tools</div> | |
| <div class="api-row">forge_list_skills · forge_get_skill · forge_get_code · forge_search · forge_publish_skill · forge_get_stats</div> | |
| </div>""") | |
| # ── API ─────────────────────────────────────────────── | |
| with gr.Tab("📡 API"): | |
| gr.HTML(f"""<div style="padding:.75rem 0"> | |
| <div class="sec" style="font-size:.9rem;color:#ff6b00;margin-top:0">REST API v2</div> | |
| <p style="font-family:'Space Mono',monospace;font-size:.68rem;color:#6b6b8a;margin-bottom:.75rem"> | |
| Base: <code style="color:#ff9500">https://huggingface.co/spaces/Chris4K/agent-forge</code> | |
| </p> | |
| <div class="api-row"><span class="m-get">GET</span> /api/v1/skills List skills ?tag= ?q=</div> | |
| <div class="api-row"><span class="m-get">GET</span> /api/v1/skills/{{id}} Get skill + code</div> | |
| <div class="api-row"><span class="m-get">GET</span> /api/v1/skills/{{id}}/code Minimal hot-load payload</div> | |
| <div class="api-row"><span class="m-get">GET</span> /api/v1/skills/{{id}}/download Download as .py</div> | |
| <div class="api-row"><span class="m-post">POST</span> /api/v1/skills Publish new skill</div> | |
| <div class="api-row"><span class="m-put">PUT</span> /api/v1/skills/{{id}} Update skill</div> | |
| <div class="api-row"><span class="m-del">DEL</span> /api/v1/skills/{{id}} Delete skill</div> | |
| <div class="api-row"><span class="m-get">GET</span> /api/v1/search?q= Full-text search</div> | |
| <div class="api-row"><span class="m-get">GET</span> /api/v1/manifest All skills + code (offline)</div> | |
| <div class="api-row"><span class="m-get">GET</span> /mcp/sse MCP SSE stream</div> | |
| <div class="api-row"><span class="m-post">POST</span> /mcp MCP JSON-RPC</div> | |
| <div class="sec">Agent Bootstrap</div> | |
| <div class="codebox">import requests, types | |
| def bootstrap_forge(url="https://huggingface.co/spaces/Chris4K/agent-forge"): | |
| r = requests.get(f"{{url}}/api/v1/skills/forge_client/code") | |
| m = types.ModuleType("forge_client") | |
| exec(r.json()["code"], m.__dict__) | |
| return m.ForgeClient(url) | |
| forge = bootstrap_forge() | |
| calc = forge.load("calculator") # pure Python math | |
| search = forge.load("web_search") # DuckDuckGo, no key needed | |
| memory = forge.load("memory_store") # KV store with TTL | |
| fetch = forge.load("http_fetch") # web page scraper | |
| claude = forge.load("claude_chat") # needs ANTHROPIC_API_KEY | |
| print(calc.execute(expression="2**32")) | |
| print(search.execute(query="AI news today", max_results=3)) | |
| memory.execute(action="set", key="goal", value="build JARVIS", ttl=3600) | |
| reply = claude.execute(prompt="Explain MCP in one paragraph")</div> | |
| </div>""") | |
| # ── HOWTO ───────────────────────────────────────────── | |
| with gr.Tab("📖 How-To"): | |
| p = Path(__file__).parent / "HOWTO.md" | |
| gr.Markdown(p.read_text(encoding="utf-8") if p.exists() else "HOWTO.md not found") | |
| gr.HTML('<div style="text-align:center;padding:1.2rem;border-top:1px solid #1e1e2e;margin-top:1rem;font-family:Space Mono,monospace;font-size:.6rem;color:#2a2a45">⚒️ FORGE v2.2 · ki-fusion-labs.de · <a href="https://huggingface.co/Chris4K" style="color:#ff6b00;text-decoration:none">Chris4K</a> · MIT</div>') | |
| return demo | |
| # ─── Mount ──────────────────────────────────────────────────────── | |
| demo = build_app() | |
| app = gr.mount_gradio_app(api, demo, path="/") | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False) |