Chris4K commited on
Commit
4e4bf46
·
verified ·
1 Parent(s): 5c85fa0

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +7 -0
  2. README.md +94 -6
  3. main.py +1571 -0
  4. requirements.txt +2 -0
Dockerfile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY main.py .
6
+ EXPOSE 7860
7
+ CMD ["python", "main.py"]
README.md CHANGED
@@ -1,11 +1,99 @@
1
  ---
2
- title: Agent Forge
3
- emoji: 🚀
4
  colorFrom: blue
5
- colorTo: green
6
  sdk: docker
7
- pinned: false
8
- short_description: AI artifactory (SKills, Capabilities, Prompts, Config...)
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: FORGE v2 — Universal Capability Registry
3
+ emoji:
4
  colorFrom: blue
5
+ colorTo: red
6
  sdk: docker
7
+ pinned: true
8
+ license: mit
9
+ short_description: Universal Capability Registry for AI Agents
10
  ---
11
 
12
+ # FORGE v2
13
+ ### Universal Capability Registry for AI Agents
14
+
15
+ > *More than a skill registry. The single source of truth for everything an AI agent needs at runtime.*
16
+
17
+ ---
18
+
19
+ ## What is FORGE?
20
+
21
+ FORGE is a **universal capability registry** — a typed, versioned, searchable store for every artifact an AI agent ecosystem needs:
22
+
23
+ | Type | What it stores |
24
+ |------|---------------|
25
+ | `skill` | Executable Python code (`def execute(...)`) — hot-loadable at runtime |
26
+ | `prompt` | System prompts, personas, Jinja2/f-string templates |
27
+ | `workflow` | Multi-step ReAct plans, orchestration graphs |
28
+ | `knowledge` | Curated text/JSON for RAG injection or agent context |
29
+ | `config` | Agent behavior policies, guardrails, tool whitelists |
30
+ | `mcp_ref` | Pointer to external MCP servers with auth hints |
31
+ | `model_ref` | HF model pointer + usage notes + prompting guide |
32
+ | `bundle` | Named collection of capabilities (complete agent loadout) |
33
+
34
+ ---
35
+
36
+ ## Quick Start (Agent Side)
37
+
38
+ ```python
39
+ import requests, types
40
+
41
+ def bootstrap_forge(url="https://chris4k-agent-forge.hf.space"):
42
+ r = requests.get(f"{url}/api/capabilities/forge_client/payload")
43
+ m = types.ModuleType("forge_client")
44
+ exec(r.json()["payload"]["code"], m.__dict__)
45
+ return m.ForgeClient(url)
46
+
47
+ forge = bootstrap_forge()
48
+
49
+ # Load a skill at runtime
50
+ calc = forge.load_skill("calculator")
51
+ print(calc.execute(expression="sqrt(144) + 2**8"))
52
+
53
+ # Get a prompt template
54
+ persona = forge.get_prompt("researcher_persona",
55
+ variables={"agent_name": "MyResearcher", "max_steps": "8"})
56
+
57
+ # Load a complete agent bundle
58
+ loadout = forge.get_bundle("researcher_loadout")
59
+ ```
60
+
61
+ ---
62
+
63
+ ## REST API
64
+
65
+ ```
66
+ GET /api/capabilities List/search (params: q, type, tag, limit)
67
+ GET /api/capabilities/{id} Get capability (latest version)
68
+ GET /api/capabilities/{id}/{v} Get specific version
69
+ GET /api/capabilities/{id}/payload Payload only (for hot-loading)
70
+ GET /api/capabilities/{id}/resolve Resolve bundle
71
+ POST /api/capabilities Publish new capability
72
+ GET /api/stats Counts by type
73
+ GET /api/tags All tags
74
+ ```
75
+
76
+ ## MCP Server
77
+
78
+ ```
79
+ GET /mcp/sse SSE transport
80
+ POST /mcp JSON-RPC 2.0
81
+
82
+ Tools: forge_search, forge_get, forge_publish, forge_list_types, forge_resolve_bundle
83
+ ```
84
+
85
+ Claude Desktop config:
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "forge": {
90
+ "command": "npx",
91
+ "args": ["-y", "mcp-remote", "https://chris4k-agent-forge.hf.space/mcp/sse"]
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ ---
98
+
99
+ Built with ⚒ by [Chris4K](https://huggingface.co/Chris4K) — ki-fusion-labs.de
main.py ADDED
@@ -0,0 +1,1571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FORGE v2 - Universal Capability Registry for AI Agents
3
+ Federated Open Registry for Generative Executables (and more)
4
+
5
+ Capability types:
6
+ skill - executable Python code (def execute(...))
7
+ prompt - system prompts, personas, jinja2 templates
8
+ workflow - multi-step ReAct plans / orchestration graphs
9
+ knowledge - curated text/JSON for RAG injection
10
+ config - agent behavior policies, guardrails
11
+ mcp_ref - pointer to external MCP server
12
+ model_ref - HF model pointer + prompting guide
13
+ bundle - named collection of capabilities (agent loadout)
14
+ """
15
+
16
+ import asyncio
17
+ import json
18
+ import os
19
+ import sqlite3
20
+ import time
21
+ import uuid
22
+ from contextlib import asynccontextmanager
23
+ from datetime import datetime, timezone
24
+ from pathlib import Path
25
+ from typing import Optional
26
+
27
+ import uvicorn
28
+ from fastapi import FastAPI, HTTPException, Query, Request
29
+ from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Config
33
+ # ---------------------------------------------------------------------------
34
+
35
+ DB_PATH = Path(os.getenv("FORGE_DB", "/tmp/forge.db"))
36
+ PORT = int(os.getenv("PORT", "7860"))
37
+ FORGE_KEY = os.getenv("FORGE_KEY", "") # optional publish auth key
38
+ BASE_URL = os.getenv("FORGE_BASE_URL", "https://chris4k-agent-forge.hf.space")
39
+
40
+ VALID_TYPES = {
41
+ "skill", "prompt", "workflow", "knowledge",
42
+ "config", "mcp_ref", "model_ref", "bundle"
43
+ }
44
+
45
+ TYPE_ICONS = {
46
+ "skill": "⚙", # gear
47
+ "prompt": "💬", # speech bubble
48
+ "workflow": "🔗", # link
49
+ "knowledge": "📚", # book
50
+ "config": "⚙︎", # settings
51
+ "mcp_ref": "📡", # antenna
52
+ "model_ref": "🤖", # robot
53
+ "bundle": "📦", # package
54
+ }
55
+
56
+ TYPE_COLORS = {
57
+ "skill": "#ff6b00",
58
+ "prompt": "#8b5cf6",
59
+ "workflow": "#06b6d4",
60
+ "knowledge": "#10b981",
61
+ "config": "#f59e0b",
62
+ "mcp_ref": "#ef4444",
63
+ "model_ref": "#ec4899",
64
+ "bundle": "#6366f1",
65
+ }
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Database
69
+ # ---------------------------------------------------------------------------
70
+
71
+ def get_db() -> sqlite3.Connection:
72
+ conn = sqlite3.connect(str(DB_PATH), check_same_thread=False)
73
+ conn.row_factory = sqlite3.Row
74
+ conn.execute("PRAGMA journal_mode=WAL")
75
+ conn.execute("PRAGMA foreign_keys=ON")
76
+ return conn
77
+
78
+
79
+ def init_db():
80
+ conn = get_db()
81
+ conn.executescript("""
82
+ CREATE TABLE IF NOT EXISTS capabilities (
83
+ id TEXT NOT NULL,
84
+ version TEXT NOT NULL DEFAULT '1.0.0',
85
+ type TEXT NOT NULL,
86
+ name TEXT NOT NULL,
87
+ description TEXT NOT NULL DEFAULT '',
88
+ author TEXT NOT NULL DEFAULT 'anonymous',
89
+ tags TEXT NOT NULL DEFAULT '[]',
90
+ payload TEXT NOT NULL DEFAULT '{}',
91
+ schema_in TEXT NOT NULL DEFAULT '{}',
92
+ schema_out TEXT NOT NULL DEFAULT '{}',
93
+ deps TEXT NOT NULL DEFAULT '[]',
94
+ meta TEXT NOT NULL DEFAULT '{}',
95
+ downloads INTEGER NOT NULL DEFAULT 0,
96
+ deprecated INTEGER NOT NULL DEFAULT 0,
97
+ created_at REAL NOT NULL,
98
+ updated_at REAL NOT NULL,
99
+ PRIMARY KEY (id, version)
100
+ );
101
+
102
+ CREATE INDEX IF NOT EXISTS idx_cap_type ON capabilities(type);
103
+ CREATE INDEX IF NOT EXISTS idx_cap_created ON capabilities(created_at DESC);
104
+ CREATE INDEX IF NOT EXISTS idx_cap_downloads ON capabilities(downloads DESC);
105
+
106
+ CREATE TABLE IF NOT EXISTS events (
107
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
108
+ kind TEXT NOT NULL,
109
+ cap_id TEXT,
110
+ cap_type TEXT,
111
+ meta TEXT NOT NULL DEFAULT '{}',
112
+ ts REAL NOT NULL
113
+ );
114
+ """)
115
+ conn.commit()
116
+ conn.close()
117
+
118
+
119
+ def seed_db():
120
+ """Seed with built-in capabilities if DB is empty."""
121
+ conn = get_db()
122
+ count = conn.execute("SELECT COUNT(*) FROM capabilities").fetchone()[0]
123
+ if count > 0:
124
+ conn.close()
125
+ return
126
+
127
+ now = time.time()
128
+ seeds = [
129
+ # --- skills ---
130
+ {
131
+ "id": "forge_client",
132
+ "version": "2.0.0",
133
+ "type": "skill",
134
+ "name": "FORGE Client",
135
+ "description": "Bootstrap client. Download this first. Lets agents discover, hot-load, and publish capabilities at runtime.",
136
+ "author": "Chris4K",
137
+ "tags": json.dumps(["meta", "core", "bootstrap"]),
138
+ "payload": json.dumps({
139
+ "language": "python",
140
+ "dependencies": ["requests"],
141
+ "code": (
142
+ "import requests, types, sys\n"
143
+ "from typing import Optional\n\n"
144
+ "class ForgeClient:\n"
145
+ " def __init__(self, url='https://chris4k-agent-forge.hf.space'):\n"
146
+ " self.url = url.rstrip('/')\n"
147
+ " self._cache = {}\n\n"
148
+ " def search(self, q='', cap_type=None, tag=None, limit=20):\n"
149
+ " params = {'q': q, 'limit': limit}\n"
150
+ " if cap_type: params['type'] = cap_type\n"
151
+ " if tag: params['tag'] = tag\n"
152
+ " return requests.get(f'{self.url}/api/capabilities', params=params, timeout=10).json()\n\n"
153
+ " def get(self, cap_id, version=None):\n"
154
+ " path = f'{self.url}/api/capabilities/{cap_id}'\n"
155
+ " if version: path += f'/{version}'\n"
156
+ " return requests.get(path, timeout=10).json()\n\n"
157
+ " def load_skill(self, cap_id, force=False):\n"
158
+ " if cap_id in self._cache and not force:\n"
159
+ " return self._cache[cap_id]\n"
160
+ " r = requests.get(f'{self.url}/api/capabilities/{cap_id}/payload', timeout=10).json()\n"
161
+ " m = types.ModuleType(f'forge_{cap_id}')\n"
162
+ " exec(compile(r['payload']['code'], f'<forge:{cap_id}>', 'exec'), m.__dict__)\n"
163
+ " if not hasattr(m, 'execute'):\n"
164
+ " raise ImportError(f'Skill {cap_id!r} has no execute() function')\n"
165
+ " self._cache[cap_id] = m\n"
166
+ " return m\n\n"
167
+ " def get_prompt(self, cap_id, variables=None):\n"
168
+ " r = requests.get(f'{self.url}/api/capabilities/{cap_id}/payload', timeout=10).json()\n"
169
+ " tmpl = r['payload']['template']\n"
170
+ " if variables:\n"
171
+ " for k, v in variables.items():\n"
172
+ " tmpl = tmpl.replace('{{' + k + '}}', str(v))\n"
173
+ " return tmpl\n\n"
174
+ " def get_bundle(self, bundle_id):\n"
175
+ " return requests.get(f'{self.url}/api/capabilities/{bundle_id}/resolve', timeout=15).json()\n\n"
176
+ " def publish(self, capability, api_key=None):\n"
177
+ " h = {'Content-Type': 'application/json'}\n"
178
+ " if api_key: h['X-Forge-Key'] = api_key\n"
179
+ " return requests.post(f'{self.url}/api/capabilities', json=capability, headers=h, timeout=15).json()\n\n"
180
+ "def execute(url='https://chris4k-agent-forge.hf.space'):\n"
181
+ " c = ForgeClient(url)\n"
182
+ " stats = requests.get(f'{url}/api/stats', timeout=5).json()\n"
183
+ " return {'client': c, 'stats': stats, 'message': f'FORGE ready. {stats.get(\"total\",0)} capabilities.'}\n"
184
+ )
185
+ }),
186
+ "schema_in": json.dumps({"url": "str - FORGE base URL"}),
187
+ "schema_out": json.dumps({"client": "ForgeClient", "stats": "dict"}),
188
+ "deps": json.dumps([]),
189
+ },
190
+ {
191
+ "id": "calculator",
192
+ "version": "1.0.0",
193
+ "type": "skill",
194
+ "name": "Calculator",
195
+ "description": "Safe math expression evaluator. Supports arithmetic, powers, trig, logs. Uses AST parsing - no eval() risks.",
196
+ "author": "Chris4K",
197
+ "tags": json.dumps(["math", "utility"]),
198
+ "payload": json.dumps({
199
+ "language": "python",
200
+ "dependencies": [],
201
+ "code": (
202
+ "import ast, math, operator as op\n\n"
203
+ "_OPS = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,\n"
204
+ " ast.Div: op.truediv, ast.Pow: op.pow, ast.USub: op.neg,\n"
205
+ " ast.Mod: op.mod, ast.FloorDiv: op.floordiv}\n"
206
+ "_NAMES = {'pi': math.pi, 'e': math.e, 'inf': math.inf, 'tau': math.tau,\n"
207
+ " 'sqrt': math.sqrt, 'abs': abs, 'round': round, 'floor': math.floor,\n"
208
+ " 'ceil': math.ceil, 'log': math.log, 'log2': math.log2, 'log10': math.log10,\n"
209
+ " 'exp': math.exp, 'sin': math.sin, 'cos': math.cos, 'tan': math.tan,\n"
210
+ " 'factorial': math.factorial, 'min': min, 'max': max}\n\n"
211
+ "def _eval(node):\n"
212
+ " if isinstance(node, ast.Constant): return node.value\n"
213
+ " if isinstance(node, ast.BinOp): return _OPS[type(node.op)](_eval(node.left), _eval(node.right))\n"
214
+ " if isinstance(node, ast.UnaryOp): return _OPS[type(node.op)](_eval(node.operand))\n"
215
+ " if isinstance(node, ast.Call):\n"
216
+ " fn = _NAMES.get(node.func.id)\n"
217
+ " if not fn: raise ValueError(f'Unknown function: {node.func.id}')\n"
218
+ " return fn(*[_eval(a) for a in node.args])\n"
219
+ " if isinstance(node, ast.Name): return _NAMES[node.id]\n"
220
+ " if isinstance(node, ast.Expression): return _eval(node.body)\n"
221
+ " raise ValueError(f'Unsupported: {type(node).__name__}')\n\n"
222
+ "def execute(expression: str) -> dict:\n"
223
+ " try:\n"
224
+ " result = _eval(ast.parse(expression.strip(), mode='eval'))\n"
225
+ " return {'result': float(result), 'formatted': f'{result:.10g}', 'expression': expression}\n"
226
+ " except Exception as exc:\n"
227
+ " return {'error': str(exc), 'expression': expression}\n"
228
+ )
229
+ }),
230
+ "schema_in": json.dumps({"expression": "str - math expression"}),
231
+ "schema_out": json.dumps({"result": "float", "formatted": "str"}),
232
+ "deps": json.dumps([]),
233
+ },
234
+ # --- prompts ---
235
+ {
236
+ "id": "researcher_persona",
237
+ "version": "1.0.0",
238
+ "type": "prompt",
239
+ "name": "Researcher Persona",
240
+ "description": "System prompt for a deep-research agent with self-improvement duty. Includes ReAct discipline, pattern detection, skill candidate identification.",
241
+ "author": "Chris4K",
242
+ "tags": json.dumps(["persona", "researcher", "react", "self-improvement"]),
243
+ "payload": json.dumps({
244
+ "format": "plain",
245
+ "variables": ["agent_name", "max_steps"],
246
+ "template": (
247
+ "You are {{agent_name}}, a deep research agent running in the FORGE ecosystem.\n\n"
248
+ "Your primary duties:\n"
249
+ "1. Execute research tasks rigorously using ReAct loops (max {{max_steps}} steps)\n"
250
+ "2. Identify patterns across results - 3+ repeated insights = skill candidate\n"
251
+ "3. When you notice a pattern, emit: SKILL_CANDIDATE: <description>\n"
252
+ "4. After completing any task, perform a brief self-reflection:\n"
253
+ " - What worked well?\n"
254
+ " - What could be a reusable capability?\n"
255
+ " - What slowed you down?\n\n"
256
+ "ReAct discipline:\n"
257
+ "- Think before acting. Write Thought: before every Action:\n"
258
+ "- Cite sources. Never fabricate.\n"
259
+ "- If uncertain, say so and use a tool to verify.\n\n"
260
+ "You have access to FORGE. Use forge_client to load skills at runtime.\n"
261
+ "Do not re-implement capabilities that already exist in FORGE."
262
+ )
263
+ }),
264
+ "schema_in": json.dumps({"agent_name": "str", "max_steps": "int"}),
265
+ "schema_out": json.dumps({"rendered_prompt": "str"}),
266
+ "deps": json.dumps([]),
267
+ },
268
+ {
269
+ "id": "task_decomposition_prompt",
270
+ "version": "1.0.0",
271
+ "type": "prompt",
272
+ "name": "Task Decomposition Prompt",
273
+ "description": "Prompt template for breaking a complex task into subtasks with dependency ordering. Outputs structured JSON.",
274
+ "author": "Chris4K",
275
+ "tags": json.dumps(["planning", "decomposition", "kanban"]),
276
+ "payload": json.dumps({
277
+ "format": "plain",
278
+ "variables": ["task", "context", "available_agents"],
279
+ "template": (
280
+ "Decompose the following task into ordered subtasks.\n\n"
281
+ "TASK: {{task}}\n"
282
+ "CONTEXT: {{context}}\n"
283
+ "AVAILABLE AGENTS: {{available_agents}}\n\n"
284
+ "Output a JSON array where each item has:\n"
285
+ " id: string (snake_case)\n"
286
+ " title: string\n"
287
+ " description: string\n"
288
+ " agent: string (which agent should handle this)\n"
289
+ " est_minutes: int\n"
290
+ " deps: array of upstream task ids\n"
291
+ " priority: 1-5 (5=critical)\n\n"
292
+ "Respond ONLY with the JSON array. No markdown."
293
+ )
294
+ }),
295
+ "schema_in": json.dumps({"task": "str", "context": "str", "available_agents": "str"}),
296
+ "schema_out": json.dumps({"subtasks": "array"}),
297
+ "deps": json.dumps([]),
298
+ },
299
+ # --- workflows ---
300
+ {
301
+ "id": "research_and_summarize",
302
+ "version": "1.0.0",
303
+ "type": "workflow",
304
+ "name": "Research and Summarize",
305
+ "description": "3-step workflow: web search, fetch top result, summarize. Outputs structured findings.",
306
+ "author": "Chris4K",
307
+ "tags": json.dumps(["research", "web", "summarize"]),
308
+ "payload": json.dumps({
309
+ "entry": "search",
310
+ "steps": [
311
+ {"id": "search", "cap_id": "web_search", "type": "skill",
312
+ "params": {"query": "{{input.query}}", "max_results": 5},
313
+ "next": "fetch"},
314
+ {"id": "fetch", "cap_id": "http_fetch", "type": "skill",
315
+ "params": {"url": "{{search.results[0].url}}", "max_chars": 3000},
316
+ "next": "summarize"},
317
+ {"id": "summarize", "cap_id": "text_summarizer", "type": "skill",
318
+ "params": {"text": "{{fetch.content}}", "max_length": 150},
319
+ "next": None}
320
+ ]
321
+ }),
322
+ "schema_in": json.dumps({"query": "str"}),
323
+ "schema_out": json.dumps({"search_results": "list", "summary": "str"}),
324
+ "deps": json.dumps(["web_search", "http_fetch", "text_summarizer"]),
325
+ },
326
+ # --- knowledge ---
327
+ {
328
+ "id": "forge_api_reference",
329
+ "version": "1.0.0",
330
+ "type": "knowledge",
331
+ "name": "FORGE API Reference",
332
+ "description": "Complete REST API reference for FORGE v2. Inject this into any agent that needs to interact with the capability registry.",
333
+ "author": "Chris4K",
334
+ "tags": json.dumps(["forge", "api", "reference", "docs"]),
335
+ "payload": json.dumps({
336
+ "format": "markdown",
337
+ "source": BASE_URL,
338
+ "content": (
339
+ "# FORGE v2 REST API\n\n"
340
+ f"Base URL: {BASE_URL}\n\n"
341
+ "## Capability Endpoints\n\n"
342
+ "GET /api/capabilities - List/search capabilities\n"
343
+ "GET /api/capabilities/{id} - Get capability (latest version)\n"
344
+ "GET /api/capabilities/{id}/payload - Payload only (hot-load)\n"
345
+ "GET /api/capabilities/{id}/resolve - Resolve bundle (all deps)\n"
346
+ "POST /api/capabilities - Publish capability\n\n"
347
+ "## MCP Endpoints\n\n"
348
+ "GET /mcp/sse - SSE stream for MCP\n"
349
+ "POST /mcp - JSON-RPC 2.0\n\n"
350
+ "## MCP Tools: forge_search, forge_get, forge_publish, forge_list_types\n"
351
+ )
352
+ }),
353
+ "schema_in": json.dumps({}),
354
+ "schema_out": json.dumps({"content": "str", "format": "str"}),
355
+ "deps": json.dumps([]),
356
+ },
357
+ # --- config ---
358
+ {
359
+ "id": "researcher_agent_config",
360
+ "version": "1.0.0",
361
+ "type": "config",
362
+ "name": "Researcher Agent Config",
363
+ "description": "Default configuration for the FORGE researcher agent. Controls timeouts, max steps, tool access, self-improvement thresholds.",
364
+ "author": "Chris4K",
365
+ "tags": json.dumps(["config", "researcher", "agent"]),
366
+ "payload": json.dumps({
367
+ "settings": {
368
+ "max_react_steps": 8,
369
+ "llm_timeout_s": 120,
370
+ "allowed_skills": ["web_search", "http_fetch", "text_summarizer", "calculator"],
371
+ "self_improve": True,
372
+ "pattern_threshold": 3,
373
+ "skill_candidate_auto_draft": True,
374
+ "memory_tiers": ["episodic", "semantic"],
375
+ "trace_enabled": True
376
+ }
377
+ }),
378
+ "schema_in": json.dumps({}),
379
+ "schema_out": json.dumps({"settings": "dict"}),
380
+ "deps": json.dumps([]),
381
+ },
382
+ # --- mcp_ref ---
383
+ {
384
+ "id": "forge_mcp",
385
+ "version": "1.0.0",
386
+ "type": "mcp_ref",
387
+ "name": "FORGE MCP Server",
388
+ "description": "MCP server reference for FORGE itself. Connect Claude Desktop or any MCP client to the capability registry.",
389
+ "author": "Chris4K",
390
+ "tags": json.dumps(["mcp", "forge", "registry"]),
391
+ "payload": json.dumps({
392
+ "url": f"{BASE_URL}/mcp/sse",
393
+ "transport": "sse",
394
+ "tools": ["forge_search", "forge_get", "forge_publish", "forge_list_types"],
395
+ "npx_command": f"npx -y mcp-remote {BASE_URL}/mcp/sse",
396
+ "auth": "none"
397
+ }),
398
+ "schema_in": json.dumps({}),
399
+ "schema_out": json.dumps({}),
400
+ "deps": json.dumps([]),
401
+ },
402
+ # --- model_ref ---
403
+ {
404
+ "id": "qwen3_5_35b",
405
+ "version": "1.0.0",
406
+ "type": "model_ref",
407
+ "name": "Qwen3.5-35B-A3B",
408
+ "description": "ki-fusion RTX 5090 inference model. MoE architecture, 35B total / 3.5B active. Best for complex reasoning, code, multi-step tasks.",
409
+ "author": "Chris4K",
410
+ "tags": json.dumps(["llm", "qwen", "rtx5090", "ki-fusion", "moe"]),
411
+ "payload": json.dumps({
412
+ "repo_id": "Qwen/Qwen3.5-35B-A3B",
413
+ "provider": "ki-fusion-labs.de",
414
+ "endpoint_env": "KI_FUSION_URL",
415
+ "api_key_env": "KI_FUSION_KEY",
416
+ "context_length": 32768,
417
+ "strengths": ["reasoning", "code", "multi-step", "german"],
418
+ "prompting_notes": "Supports thinking mode. Use <think> tags for chain-of-thought. Temperature 0.6 for creative, 0.1 for factual.",
419
+ "compatible_with": ["openai_sdk", "lm_studio", "litellm"]
420
+ }),
421
+ "schema_in": json.dumps({}),
422
+ "schema_out": json.dumps({}),
423
+ "deps": json.dumps([]),
424
+ },
425
+ # --- bundle ---
426
+ {
427
+ "id": "researcher_loadout",
428
+ "version": "1.0.0",
429
+ "type": "bundle",
430
+ "name": "Researcher Agent Loadout",
431
+ "description": "Everything a researcher agent needs: forge client, web search, HTTP fetch, summarizer, persona prompt, config, and model reference. One bundle, full capability.",
432
+ "author": "Chris4K",
433
+ "tags": json.dumps(["bundle", "researcher", "loadout", "starter"]),
434
+ "payload": json.dumps({
435
+ "capabilities": [
436
+ "forge_client",
437
+ "calculator",
438
+ "researcher_persona",
439
+ "task_decomposition_prompt",
440
+ "researcher_agent_config",
441
+ "forge_mcp",
442
+ "qwen3_5_35b"
443
+ ],
444
+ "description": "Complete researcher agent capability set"
445
+ }),
446
+ "schema_in": json.dumps({}),
447
+ "schema_out": json.dumps({}),
448
+ "deps": json.dumps([
449
+ "forge_client", "calculator", "researcher_persona",
450
+ "task_decomposition_prompt", "researcher_agent_config",
451
+ "forge_mcp", "qwen3_5_35b"
452
+ ]),
453
+ },
454
+ ]
455
+
456
+ for s in seeds:
457
+ now = time.time()
458
+ s.setdefault("schema_in", "{}")
459
+ s.setdefault("schema_out", "{}")
460
+ s.setdefault("deps", "[]")
461
+ s.setdefault("meta", "{}")
462
+ conn.execute("""
463
+ INSERT OR IGNORE INTO capabilities
464
+ (id, version, type, name, description, author, tags, payload,
465
+ schema_in, schema_out, deps, meta, downloads, deprecated, created_at, updated_at)
466
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,0,0,?,?)
467
+ """, (
468
+ s["id"], s["version"], s["type"], s["name"], s["description"],
469
+ s["author"], s["tags"], s["payload"], s["schema_in"], s["schema_out"],
470
+ s["deps"], s.get("meta", "{}"), now, now
471
+ ))
472
+ conn.commit()
473
+ conn.close()
474
+
475
+
476
+ # ---------------------------------------------------------------------------
477
+ # DB helpers
478
+ # ---------------------------------------------------------------------------
479
+
480
+ def _row_to_dict(row) -> dict:
481
+ d = dict(row)
482
+ for field in ("tags", "payload", "schema_in", "schema_out", "deps", "meta"):
483
+ try:
484
+ d[field] = json.loads(d.get(field) or "{}")
485
+ except Exception:
486
+ pass
487
+ return d
488
+
489
+
490
+ def db_list(
491
+ q: str = "",
492
+ cap_type: str = "",
493
+ tag: str = "",
494
+ limit: int = 50,
495
+ offset: int = 0,
496
+ ) -> list[dict]:
497
+ conn = get_db()
498
+ where, params = ["deprecated=0"], []
499
+ if cap_type and cap_type in VALID_TYPES:
500
+ where.append("type=?"); params.append(cap_type)
501
+ if tag:
502
+ where.append("tags LIKE ?"); params.append(f'%"{tag}"%')
503
+ if q:
504
+ where.append("(name LIKE ? OR description LIKE ? OR tags LIKE ?)")
505
+ params += [f"%{q}%", f"%{q}%", f"%{q}%"]
506
+
507
+ sql = f"""
508
+ SELECT id, version, type, name, description, author, tags,
509
+ schema_in, schema_out, deps, downloads, created_at, updated_at
510
+ FROM capabilities
511
+ WHERE {' AND '.join(where)}
512
+ GROUP BY id
513
+ HAVING MAX(created_at)
514
+ ORDER BY downloads DESC, created_at DESC
515
+ LIMIT ? OFFSET ?
516
+ """
517
+ rows = conn.execute(sql, params + [limit, offset]).fetchall()
518
+ conn.close()
519
+ return [_row_to_dict(r) for r in rows]
520
+
521
+
522
+ def db_get(cap_id: str, version: str = "") -> Optional[dict]:
523
+ conn = get_db()
524
+ if version:
525
+ row = conn.execute(
526
+ "SELECT * FROM capabilities WHERE id=? AND version=?", (cap_id, version)
527
+ ).fetchone()
528
+ else:
529
+ row = conn.execute(
530
+ "SELECT * FROM capabilities WHERE id=? AND deprecated=0 ORDER BY created_at DESC LIMIT 1",
531
+ (cap_id,)
532
+ ).fetchone()
533
+ conn.close()
534
+ return _row_to_dict(row) if row else None
535
+
536
+
537
+ def db_versions(cap_id: str) -> list[dict]:
538
+ conn = get_db()
539
+ rows = conn.execute(
540
+ "SELECT id, version, name, author, downloads, deprecated, created_at FROM capabilities WHERE id=? ORDER BY created_at DESC",
541
+ (cap_id,)
542
+ ).fetchall()
543
+ conn.close()
544
+ return [_row_to_dict(r) for r in rows]
545
+
546
+
547
+ def db_publish(cap: dict) -> tuple[bool, str]:
548
+ cap_id = cap.get("id", "").strip()
549
+ if not cap_id:
550
+ return False, "Missing 'id'"
551
+ cap_type = cap.get("type", "")
552
+ if cap_type not in VALID_TYPES:
553
+ return False, f"Invalid type '{cap_type}'. Valid: {sorted(VALID_TYPES)}"
554
+ if not cap.get("name"):
555
+ return False, "Missing 'name'"
556
+ payload = cap.get("payload", {})
557
+ if not isinstance(payload, dict):
558
+ return False, "'payload' must be a JSON object"
559
+
560
+ # Type-specific validation
561
+ if cap_type == "skill" and "code" not in payload:
562
+ return False, "skill payload must contain 'code'"
563
+ if cap_type == "prompt" and "template" not in payload:
564
+ return False, "prompt payload must contain 'template'"
565
+ if cap_type == "workflow" and "steps" not in payload:
566
+ return False, "workflow payload must contain 'steps'"
567
+ if cap_type == "bundle" and "capabilities" not in payload:
568
+ return False, "bundle payload must contain 'capabilities'"
569
+ if cap_type == "mcp_ref" and "url" not in payload:
570
+ return False, "mcp_ref payload must contain 'url'"
571
+ if cap_type == "model_ref" and "repo_id" not in payload:
572
+ return False, "model_ref payload must contain 'repo_id'"
573
+
574
+ now = time.time()
575
+ version = cap.get("version", "1.0.0")
576
+
577
+ conn = get_db()
578
+ exists = conn.execute(
579
+ "SELECT 1 FROM capabilities WHERE id=? AND version=?", (cap_id, version)
580
+ ).fetchone()
581
+ if exists:
582
+ conn.close()
583
+ return False, f"Capability '{cap_id}' v{version} already exists"
584
+
585
+ conn.execute("""
586
+ INSERT INTO capabilities
587
+ (id, version, type, name, description, author, tags, payload,
588
+ schema_in, schema_out, deps, meta, downloads, deprecated, created_at, updated_at)
589
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,0,0,?,?)
590
+ """, (
591
+ cap_id, version, cap_type,
592
+ cap.get("name", cap_id),
593
+ cap.get("description", ""),
594
+ cap.get("author", "anonymous"),
595
+ json.dumps(cap.get("tags", [])),
596
+ json.dumps(payload),
597
+ json.dumps(cap.get("schema_in", {})),
598
+ json.dumps(cap.get("schema_out", {})),
599
+ json.dumps(cap.get("deps", [])),
600
+ json.dumps(cap.get("meta", {})),
601
+ now, now,
602
+ ))
603
+ conn.execute("INSERT INTO events (kind, cap_id, cap_type, meta, ts) VALUES (?,?,?,?,?)",
604
+ ("publish", cap_id, cap_type, json.dumps({"version": version}), now))
605
+ conn.commit()
606
+ conn.close()
607
+ return True, f"Capability '{cap_id}' v{version} published"
608
+
609
+
610
+ def db_stats() -> dict:
611
+ conn = get_db()
612
+ rows = conn.execute(
613
+ "SELECT type, COUNT(*) as cnt, SUM(downloads) as dl FROM capabilities WHERE deprecated=0 GROUP BY type"
614
+ ).fetchall()
615
+ total = conn.execute("SELECT COUNT(*), SUM(downloads) FROM capabilities WHERE deprecated=0").fetchone()
616
+ conn.close()
617
+ by_type = {r["type"]: {"count": r["cnt"], "downloads": r["dl"] or 0} for r in rows}
618
+ return {
619
+ "total": total[0] or 0,
620
+ "total_downloads": total[1] or 0,
621
+ "by_type": by_type,
622
+ }
623
+
624
+
625
+ def db_resolve_bundle(bundle_id: str) -> Optional[dict]:
626
+ cap = db_get(bundle_id)
627
+ if not cap or cap["type"] != "bundle":
628
+ return None
629
+ cap_ids = cap["payload"].get("capabilities", [])
630
+ resolved = {}
631
+ for cid in cap_ids:
632
+ c = db_get(cid)
633
+ if c:
634
+ resolved[cid] = c
635
+ return {"bundle": cap, "resolved": resolved, "missing": [c for c in cap_ids if c not in resolved]}
636
+
637
+
638
+ # ---------------------------------------------------------------------------
639
+ # MCP Server
640
+ # ---------------------------------------------------------------------------
641
+
642
+ MCP_TOOLS = [
643
+ {
644
+ "name": "forge_search",
645
+ "description": "Search FORGE capability registry. Returns capabilities matching query, type, or tag filters.",
646
+ "inputSchema": {
647
+ "type": "object",
648
+ "properties": {
649
+ "query": {"type": "string", "description": "Search term"},
650
+ "type": {"type": "string", "description": "Filter by type (skill|prompt|workflow|knowledge|config|mcp_ref|model_ref|bundle)"},
651
+ "tag": {"type": "string", "description": "Filter by tag"},
652
+ "limit": {"type": "integer", "default": 10},
653
+ },
654
+ },
655
+ },
656
+ {
657
+ "name": "forge_get",
658
+ "description": "Get a specific capability from FORGE by ID. Returns full details including payload.",
659
+ "inputSchema": {
660
+ "type": "object",
661
+ "required": ["id"],
662
+ "properties": {
663
+ "id": {"type": "string", "description": "Capability ID"},
664
+ "version": {"type": "string", "description": "Specific version (omit for latest)"},
665
+ },
666
+ },
667
+ },
668
+ {
669
+ "name": "forge_publish",
670
+ "description": "Publish a new capability to FORGE registry.",
671
+ "inputSchema": {
672
+ "type": "object",
673
+ "required": ["id", "type", "name", "payload"],
674
+ "properties": {
675
+ "id": {"type": "string"},
676
+ "type": {"type": "string", "description": "skill|prompt|workflow|knowledge|config|mcp_ref|model_ref|bundle"},
677
+ "name": {"type": "string"},
678
+ "version": {"type": "string", "default": "1.0.0"},
679
+ "description": {"type": "string"},
680
+ "author": {"type": "string"},
681
+ "tags": {"type": "array", "items": {"type": "string"}},
682
+ "payload": {"type": "object"},
683
+ "deps": {"type": "array", "items": {"type": "string"}},
684
+ },
685
+ },
686
+ },
687
+ {
688
+ "name": "forge_list_types",
689
+ "description": "List all capability types with counts and descriptions.",
690
+ "inputSchema": {"type": "object", "properties": {}},
691
+ },
692
+ {
693
+ "name": "forge_resolve_bundle",
694
+ "description": "Resolve a bundle capability - returns all capabilities it contains.",
695
+ "inputSchema": {
696
+ "type": "object",
697
+ "required": ["id"],
698
+ "properties": {"id": {"type": "string", "description": "Bundle ID"}},
699
+ },
700
+ },
701
+ ]
702
+
703
+ TYPE_DESCRIPTIONS = {
704
+ "skill": "Executable Python code with def execute(). Hot-loadable at runtime.",
705
+ "prompt": "System prompts, persona templates, jinja2/fstring templates.",
706
+ "workflow": "Multi-step ReAct plans and orchestration graphs.",
707
+ "knowledge": "Curated text/JSON chunks for RAG injection or agent context.",
708
+ "config": "Agent behavior policies, guardrails, tool whitelists.",
709
+ "mcp_ref": "Pointer to an external MCP server with connection details.",
710
+ "model_ref": "HF model pointer with usage notes and prompting guide.",
711
+ "bundle": "Named collection of capabilities (complete agent loadout).",
712
+ }
713
+
714
+
715
+ def handle_mcp(method: str, params: dict, req_id) -> dict:
716
+ def ok(result):
717
+ return {"jsonrpc": "2.0", "id": req_id, "result": result}
718
+
719
+ if method == "initialize":
720
+ return ok({
721
+ "protocolVersion": "2024-11-05",
722
+ "serverInfo": {"name": "FORGE", "version": "2.0.0"},
723
+ "capabilities": {"tools": {}},
724
+ })
725
+
726
+ if method == "tools/list":
727
+ return ok({"tools": MCP_TOOLS})
728
+
729
+ if method == "tools/call":
730
+ name = params.get("name", "")
731
+ args = params.get("arguments", {})
732
+
733
+ if name == "forge_search":
734
+ caps = db_list(
735
+ q=args.get("query", ""),
736
+ cap_type=args.get("type", ""),
737
+ tag=args.get("tag", ""),
738
+ limit=args.get("limit", 10),
739
+ )
740
+ # Strip payload for search results (may be large)
741
+ for c in caps:
742
+ c.pop("payload", None)
743
+ return ok({"content": [{"type": "text", "text": json.dumps({"capabilities": caps, "count": len(caps)})}]})
744
+
745
+ if name == "forge_get":
746
+ cap = db_get(args["id"], args.get("version", ""))
747
+ if not cap:
748
+ return ok({"content": [{"type": "text", "text": json.dumps({"error": f"Not found: {args['id']}"})}]})
749
+ conn = get_db()
750
+ conn.execute("UPDATE capabilities SET downloads=downloads+1 WHERE id=? AND version=?",
751
+ (cap["id"], cap["version"]))
752
+ conn.commit(); conn.close()
753
+ return ok({"content": [{"type": "text", "text": json.dumps(cap)}]})
754
+
755
+ if name == "forge_publish":
756
+ ok_flag, msg = db_publish(args)
757
+ return ok({"content": [{"type": "text", "text": json.dumps({"ok": ok_flag, "message": msg})}]})
758
+
759
+ if name == "forge_list_types":
760
+ stats = db_stats()
761
+ types_info = []
762
+ for t in sorted(VALID_TYPES):
763
+ info = stats["by_type"].get(t, {"count": 0, "downloads": 0})
764
+ types_info.append({
765
+ "type": t,
766
+ "description": TYPE_DESCRIPTIONS[t],
767
+ "count": info["count"],
768
+ "downloads": info["downloads"],
769
+ })
770
+ return ok({"content": [{"type": "text", "text": json.dumps({"types": types_info})}]})
771
+
772
+ if name == "forge_resolve_bundle":
773
+ result = db_resolve_bundle(args["id"])
774
+ if not result:
775
+ return ok({"content": [{"type": "text", "text": json.dumps({"error": f"Bundle not found: {args['id']}"})}]})
776
+ return ok({"content": [{"type": "text", "text": json.dumps(result)}]})
777
+
778
+ return {"jsonrpc": "2.0", "id": req_id,
779
+ "error": {"code": -32601, "message": f"Unknown tool: {name}"}}
780
+
781
+ if method in ("notifications/initialized", "notifications/cancelled"):
782
+ return None
783
+
784
+ return {"jsonrpc": "2.0", "id": req_id,
785
+ "error": {"code": -32601, "message": f"Method not found: {method}"}}
786
+
787
+
788
+ # ---------------------------------------------------------------------------
789
+ # FastAPI app
790
+ # ---------------------------------------------------------------------------
791
+
792
+ @asynccontextmanager
793
+ async def lifespan(app: FastAPI):
794
+ init_db()
795
+ seed_db()
796
+ yield
797
+
798
+ app = FastAPI(title="FORGE v2", version="2.0.0", lifespan=lifespan)
799
+
800
+
801
+ # --- REST API ---------------------------------------------------------------
802
+
803
+ @app.get("/api/capabilities")
804
+ async def api_list(
805
+ q: str = Query(""),
806
+ type: str = Query(""),
807
+ tag: str = Query(""),
808
+ limit: int = Query(50, le=200),
809
+ offset: int = Query(0),
810
+ ):
811
+ caps = db_list(q=q, cap_type=type, tag=tag, limit=limit, offset=offset)
812
+ return JSONResponse({"capabilities": caps, "count": len(caps)})
813
+
814
+
815
+ @app.get("/api/capabilities/{cap_id}/versions")
816
+ async def api_versions(cap_id: str):
817
+ versions = db_versions(cap_id)
818
+ if not versions:
819
+ raise HTTPException(404, f"Capability '{cap_id}' not found")
820
+ return JSONResponse({"id": cap_id, "versions": versions})
821
+
822
+
823
+ @app.get("/api/capabilities/{cap_id}/resolve")
824
+ async def api_resolve(cap_id: str):
825
+ result = db_resolve_bundle(cap_id)
826
+ if not result:
827
+ raise HTTPException(404, f"Bundle '{cap_id}' not found or not a bundle")
828
+ return JSONResponse(result)
829
+
830
+
831
+ @app.get("/api/capabilities/{cap_id}/payload")
832
+ async def api_payload(cap_id: str, version: str = Query("")):
833
+ cap = db_get(cap_id, version)
834
+ if not cap:
835
+ raise HTTPException(404, f"Capability '{cap_id}' not found")
836
+ conn = get_db()
837
+ conn.execute("UPDATE capabilities SET downloads=downloads+1 WHERE id=? AND version=?",
838
+ (cap["id"], cap["version"]))
839
+ conn.commit(); conn.close()
840
+ return JSONResponse({"id": cap["id"], "version": cap["version"],
841
+ "type": cap["type"], "payload": cap["payload"]})
842
+
843
+
844
+ @app.get("/api/capabilities/{cap_id}/{version}")
845
+ async def api_get_version(cap_id: str, version: str):
846
+ cap = db_get(cap_id, version)
847
+ if not cap:
848
+ raise HTTPException(404, f"Capability '{cap_id}' v{version} not found")
849
+ return JSONResponse(cap)
850
+
851
+
852
+ @app.get("/api/capabilities/{cap_id}")
853
+ async def api_get(cap_id: str):
854
+ cap = db_get(cap_id)
855
+ if not cap:
856
+ raise HTTPException(404, f"Capability '{cap_id}' not found")
857
+ conn = get_db()
858
+ conn.execute("UPDATE capabilities SET downloads=downloads+1 WHERE id=? AND version=?",
859
+ (cap["id"], cap["version"]))
860
+ conn.commit(); conn.close()
861
+ return JSONResponse(cap)
862
+
863
+
864
+ @app.post("/api/capabilities", status_code=201)
865
+ async def api_publish(request: Request):
866
+ # Optional auth
867
+ if FORGE_KEY:
868
+ key = request.headers.get("x-forge-key", "")
869
+ if key != FORGE_KEY:
870
+ raise HTTPException(403, "Invalid X-Forge-Key")
871
+ try:
872
+ body = await request.json()
873
+ except Exception:
874
+ raise HTTPException(400, "Invalid JSON")
875
+ ok_flag, msg = db_publish(body)
876
+ if not ok_flag:
877
+ raise HTTPException(400, msg)
878
+ return JSONResponse({"ok": True, "message": msg})
879
+
880
+
881
+ @app.get("/api/stats")
882
+ async def api_stats():
883
+ return JSONResponse(db_stats())
884
+
885
+
886
+ @app.get("/api/tags")
887
+ async def api_tags():
888
+ conn = get_db()
889
+ rows = conn.execute("SELECT tags FROM capabilities WHERE deprecated=0").fetchall()
890
+ conn.close()
891
+ tags: set[str] = set()
892
+ for r in rows:
893
+ try:
894
+ tags.update(json.loads(r["tags"]))
895
+ except Exception:
896
+ pass
897
+ return JSONResponse({"tags": sorted(tags)})
898
+
899
+
900
+ @app.get("/api/health")
901
+ async def health():
902
+ stats = db_stats()
903
+ return JSONResponse({"ok": True, "capabilities": stats["total"], "version": "2.0.0"})
904
+
905
+
906
+ # --- MCP endpoints ----------------------------------------------------------
907
+
908
+ @app.get("/mcp/sse")
909
+ async def mcp_sse(request: Request):
910
+ client_id = str(uuid.uuid4())[:8]
911
+
912
+ async def event_gen():
913
+ yield f"data: {json.dumps({'jsonrpc':'2.0','method':'connected','params':{'client_id':client_id}})}\n\n"
914
+ # Send available tools on connect
915
+ tools_msg = {
916
+ "jsonrpc": "2.0", "method": "notifications/tools",
917
+ "params": {"tools": MCP_TOOLS}
918
+ }
919
+ yield f"data: {json.dumps(tools_msg)}\n\n"
920
+ while True:
921
+ if await request.is_disconnected():
922
+ break
923
+ yield f": ping\n\n"
924
+ await asyncio.sleep(15)
925
+
926
+ return StreamingResponse(
927
+ event_gen(),
928
+ media_type="text/event-stream",
929
+ headers={
930
+ "Cache-Control": "no-cache",
931
+ "Connection": "keep-alive",
932
+ "X-Accel-Buffering": "no",
933
+ },
934
+ )
935
+
936
+
937
+ @app.post("/mcp")
938
+ async def mcp_jsonrpc(request: Request):
939
+ try:
940
+ body = await request.json()
941
+ except Exception:
942
+ return JSONResponse({"jsonrpc": "2.0", "id": None,
943
+ "error": {"code": -32700, "message": "Parse error"}})
944
+ # Batch support
945
+ if isinstance(body, list):
946
+ results = [handle_mcp(r.get("method", ""), r.get("params", {}), r.get("id")) for r in body]
947
+ return JSONResponse([r for r in results if r is not None])
948
+ result = handle_mcp(body.get("method", ""), body.get("params", {}), body.get("id"))
949
+ if result is None:
950
+ return JSONResponse({"jsonrpc": "2.0", "id": body.get("id"), "result": {}})
951
+ return JSONResponse(result)
952
+
953
+
954
+ # ---------------------------------------------------------------------------
955
+ # SPA
956
+ # ---------------------------------------------------------------------------
957
+
958
+ SPA = """<!DOCTYPE html>
959
+ <html lang="en">
960
+ <head>
961
+ <meta charset="UTF-8">
962
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
963
+ <title>&#9954; FORGE v2 &#8212; Universal Capability Registry</title>
964
+ <style>
965
+ @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&family=DM+Mono:wght@300;400;500&display=swap');
966
+ *{box-sizing:border-box;margin:0;padding:0}
967
+ :root{
968
+ --bg:#08080f;--surface:#0f0f1a;--surface2:#141421;--border:#1e1e30;
969
+ --accent:#ff6b00;--accent2:#ff9500;--text:#e0e0f0;--muted:#5a5a7a;
970
+ --green:#00ff88;--purple:#8b5cf6;--cyan:#06b6d4;--pink:#ec4899;
971
+ --yellow:#f59e0b;--red:#ef4444;--indigo:#6366f1;--teal:#10b981;
972
+ }
973
+ html,body{height:100%;background:var(--bg);color:var(--text);font-family:'Syne',sans-serif}
974
+ a{color:var(--accent);text-decoration:none}
975
+ a:hover{text-decoration:underline}
976
+ ::-webkit-scrollbar{width:6px;height:6px}
977
+ ::-webkit-scrollbar-track{background:var(--surface)}
978
+ ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
979
+
980
+ /* Layout */
981
+ .app{display:flex;flex-direction:column;height:100vh}
982
+ .header{padding:1rem 2rem;border-bottom:1px solid var(--border);background:var(--surface);display:flex;align-items:center;gap:1.5rem}
983
+ .logo{font-family:'Space Mono',monospace;font-size:1.6rem;font-weight:700;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;letter-spacing:-1px}
984
+ .tagline{font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--muted);letter-spacing:0.2em;text-transform:uppercase}
985
+ .header-stats{margin-left:auto;display:flex;gap:1.5rem}
986
+ .hstat{text-align:center}
987
+ .hstat-num{font-family:'Space Mono',monospace;font-size:1.1rem;color:var(--accent);font-weight:700}
988
+ .hstat-lbl{font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em}
989
+
990
+ .main{display:flex;flex:1;overflow:hidden}
991
+ .sidebar{width:220px;min-width:220px;border-right:1px solid var(--border);background:var(--surface);display:flex;flex-direction:column;overflow-y:auto}
992
+ .content{flex:1;overflow-y:auto;padding:1.5rem}
993
+
994
+ /* Sidebar */
995
+ .sidebar-section{padding:0.75rem 1rem 0.25rem;font-family:'DM Mono',monospace;font-size:0.65rem;color:var(--muted);letter-spacing:0.2em;text-transform:uppercase}
996
+ .type-btn{display:flex;align-items:center;gap:0.6rem;padding:0.55rem 1rem;cursor:pointer;font-size:0.82rem;transition:background 0.15s;border:none;background:none;color:var(--text);width:100%;text-align:left}
997
+ .type-btn:hover{background:var(--surface2)}
998
+ .type-btn.active{background:var(--surface2);border-left:3px solid var(--accent);color:var(--accent)}
999
+ .type-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
1000
+ .type-count{margin-left:auto;font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--muted)}
1001
+
1002
+ /* Tabs */
1003
+ .tabs{display:flex;border-bottom:1px solid var(--border);margin-bottom:1.5rem;gap:0}
1004
+ .tab{padding:0.6rem 1.2rem;cursor:pointer;font-family:'DM Mono',monospace;font-size:0.78rem;color:var(--muted);border-bottom:2px solid transparent;transition:all 0.15s;letter-spacing:0.05em}
1005
+ .tab.active{color:var(--accent);border-bottom-color:var(--accent)}
1006
+ .tab:hover{color:var(--text)}
1007
+
1008
+ /* Cards */
1009
+ .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}
1010
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:1.1rem;cursor:pointer;transition:border-color 0.15s,transform 0.1s;position:relative;overflow:hidden}
1011
+ .card:hover{border-color:var(--accent);transform:translateY(-1px)}
1012
+ .card-type{font-family:'DM Mono',monospace;font-size:0.65rem;letter-spacing:0.15em;text-transform:uppercase;margin-bottom:0.5rem;display:flex;align-items:center;gap:0.4rem}
1013
+ .card-name{font-family:'Space Mono',monospace;font-size:0.95rem;font-weight:700;color:var(--text);margin-bottom:0.3rem}
1014
+ .card-desc{font-size:0.8rem;color:var(--muted);line-height:1.5;margin-bottom:0.7rem}
1015
+ .card-footer{display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap}
1016
+ .tag{display:inline-block;background:#1a1a30;border:1px solid #2a2a50;color:var(--purple);font-family:'DM Mono',monospace;font-size:0.6rem;padding:2px 7px;border-radius:20px;letter-spacing:0.05em}
1017
+ .card-meta{margin-left:auto;font-family:'DM Mono',monospace;font-size:0.65rem;color:var(--muted)}
1018
+
1019
+ /* Detail Panel */
1020
+ .detail{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:1.5rem;margin-bottom:1.5rem}
1021
+ .detail-header{display:flex;align-items:flex-start;gap:1rem;margin-bottom:1rem}
1022
+ .detail-type{font-family:'DM Mono',monospace;font-size:0.65rem;letter-spacing:0.15em;text-transform:uppercase;padding:3px 10px;border-radius:4px;border:1px solid}
1023
+ .detail-title{font-family:'Space Mono',monospace;font-size:1.4rem;font-weight:700;line-height:1.2}
1024
+ .detail-meta{font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--muted);margin-top:0.25rem}
1025
+ .detail-desc{font-size:0.88rem;line-height:1.7;color:var(--text);margin:0.75rem 0}
1026
+ .section-label{font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--purple);letter-spacing:0.15em;text-transform:uppercase;margin:1rem 0 0.5rem}
1027
+ pre{background:#0a0a14;border:1px solid var(--border);border-radius:6px;padding:1rem;font-family:'DM Mono',monospace;font-size:0.75rem;color:var(--green);overflow-x:auto;white-space:pre-wrap;line-height:1.6}
1028
+ .copy-btn{float:right;font-size:0.65rem;font-family:'DM Mono',monospace;background:var(--surface2);border:1px solid var(--border);color:var(--muted);padding:3px 8px;border-radius:4px;cursor:pointer;transition:all 0.15s}
1029
+ .copy-btn:hover{color:var(--accent);border-color:var(--accent)}
1030
+
1031
+ /* Search bar */
1032
+ .search-row{display:flex;gap:0.75rem;margin-bottom:1.25rem;align-items:center}
1033
+ .search-input{flex:1;background:var(--surface);border:1px solid var(--border);color:var(--text);padding:0.6rem 1rem;border-radius:6px;font-family:'DM Mono',monospace;font-size:0.82rem;outline:none;transition:border-color 0.15s}
1034
+ .search-input:focus{border-color:var(--accent)}
1035
+ .search-input::placeholder{color:var(--muted)}
1036
+ .btn{padding:0.55rem 1.2rem;border:none;border-radius:6px;cursor:pointer;font-family:'Space Mono',monospace;font-size:0.75rem;font-weight:700;letter-spacing:0.05em;transition:all 0.15s}
1037
+ .btn-primary{background:var(--accent);color:#000}
1038
+ .btn-primary:hover{background:var(--accent2)}
1039
+ .btn-secondary{background:var(--surface2);color:var(--text);border:1px solid var(--border)}
1040
+ .btn-secondary:hover{border-color:var(--accent);color:var(--accent)}
1041
+
1042
+ /* Publish form */
1043
+ .form-group{margin-bottom:1rem}
1044
+ .form-label{display:block;font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--muted);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:0.35rem}
1045
+ .form-input,.form-select,.form-textarea{width:100%;background:var(--surface);border:1px solid var(--border);color:var(--text);padding:0.6rem 0.9rem;border-radius:6px;font-family:'DM Mono',monospace;font-size:0.82rem;outline:none;transition:border-color 0.15s}
1046
+ .form-input:focus,.form-select:focus,.form-textarea:focus{border-color:var(--accent)}
1047
+ .form-select option{background:var(--surface);color:var(--text)}
1048
+ .form-textarea{min-height:200px;resize:vertical;font-size:0.75rem}
1049
+ .form-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
1050
+ .result-msg{padding:0.75rem 1rem;border-radius:6px;font-family:'DM Mono',monospace;font-size:0.8rem;margin-top:0.75rem;display:none}
1051
+ .result-ok{background:#0a1f10;border:1px solid #1a4a20;color:var(--green)}
1052
+ .result-err{background:#1f0a0a;border:1px solid #4a1a1a;color:var(--red)}
1053
+
1054
+ /* API docs */
1055
+ .endpoint{background:var(--surface2);border-left:3px solid var(--accent);padding:0.65rem 1rem;margin:0.4rem 0;border-radius:0 6px 6px 0;font-family:'DM Mono',monospace;font-size:0.78rem}
1056
+ .method-get{color:var(--green)}
1057
+ .method-post{color:var(--accent2)}
1058
+ .method-delete{color:var(--red)}
1059
+
1060
+ /* Bundle visualization */
1061
+ .bundle-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:0.75rem;margin-top:0.75rem}
1062
+ .bundle-item{background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:0.75rem;display:flex;align-items:center;gap:0.6rem}
1063
+ .bundle-item-icon{font-size:1.2rem}
1064
+ .bundle-item-name{font-family:'Space Mono',monospace;font-size:0.75rem;font-weight:700}
1065
+ .bundle-item-type{font-family:'DM Mono',monospace;font-size:0.6rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em}
1066
+
1067
+ /* Misc */
1068
+ .empty{text-align:center;padding:3rem;color:var(--muted);font-family:'DM Mono',monospace;font-size:0.85rem}
1069
+ .badge{display:inline-block;padding:2px 8px;border-radius:4px;font-family:'DM Mono',monospace;font-size:0.65rem;letter-spacing:0.05em}
1070
+ .dep-chip{background:#1a1030;border:1px solid #2a2050;color:var(--purple);font-family:'DM Mono',monospace;font-size:0.65rem;padding:2px 8px;border-radius:4px;cursor:pointer;display:inline-block;margin:2px}
1071
+ .dep-chip:hover{border-color:var(--purple);color:#a78bfa}
1072
+ .back-btn{display:inline-flex;align-items:center;gap:0.4rem;margin-bottom:1rem;font-family:'DM Mono',monospace;font-size:0.75rem;color:var(--muted);cursor:pointer}
1073
+ .back-btn:hover{color:var(--accent)}
1074
+ .version-list{display:flex;flex-direction:column;gap:0.5rem}
1075
+ .version-item{display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0.75rem;background:var(--surface2);border:1px solid var(--border);border-radius:6px;font-family:'DM Mono',monospace;font-size:0.75rem}
1076
+ .version-tag{padding:2px 8px;background:#1a1a30;border:1px solid #2a2a50;border-radius:4px;color:var(--cyan)}
1077
+ </style>
1078
+ </head>
1079
+ <body>
1080
+ <div class="app">
1081
+ <header class="header">
1082
+ <div>
1083
+ <div class="logo">&#9954; FORGE</div>
1084
+ <div class="tagline">Universal Capability Registry for AI Agents</div>
1085
+ </div>
1086
+ <div class="header-stats" id="headerStats">
1087
+ <div class="hstat"><div class="hstat-num" id="statTotal">&#8212;</div><div class="hstat-lbl">Capabilities</div></div>
1088
+ <div class="hstat"><div class="hstat-num" id="statDownloads">&#8212;</div><div class="hstat-lbl">Downloads</div></div>
1089
+ <div class="hstat"><div class="hstat-num" id="statTypes">8</div><div class="hstat-lbl">Types</div></div>
1090
+ </div>
1091
+ </header>
1092
+
1093
+ <div class="main">
1094
+ <nav class="sidebar">
1095
+ <div class="sidebar-section">Browse by Type</div>
1096
+ <button class="type-btn active" data-type="" onclick="filterType('')">
1097
+ <span class="type-dot" style="background:#fff"></span> All
1098
+ <span class="type-count" id="cnt-all">&#8212;</span>
1099
+ </button>
1100
+ <button class="type-btn" data-type="skill" onclick="filterType('skill')">
1101
+ <span class="type-dot" style="background:#ff6b00"></span> skill
1102
+ <span class="type-count" id="cnt-skill">0</span>
1103
+ </button>
1104
+ <button class="type-btn" data-type="prompt" onclick="filterType('prompt')">
1105
+ <span class="type-dot" style="background:#8b5cf6"></span> prompt
1106
+ <span class="type-count" id="cnt-prompt">0</span>
1107
+ </button>
1108
+ <button class="type-btn" data-type="workflow" onclick="filterType('workflow')">
1109
+ <span class="type-dot" style="background:#06b6d4"></span> workflow
1110
+ <span class="type-count" id="cnt-workflow">0</span>
1111
+ </button>
1112
+ <button class="type-btn" data-type="knowledge" onclick="filterType('knowledge')">
1113
+ <span class="type-dot" style="background:#10b981"></span> knowledge
1114
+ <span class="type-count" id="cnt-knowledge">0</span>
1115
+ </button>
1116
+ <button class="type-btn" data-type="config" onclick="filterType('config')">
1117
+ <span class="type-dot" style="background:#f59e0b"></span> config
1118
+ <span class="type-count" id="cnt-config">0</span>
1119
+ </button>
1120
+ <button class="type-btn" data-type="mcp_ref" onclick="filterType('mcp_ref')">
1121
+ <span class="type-dot" style="background:#ef4444"></span> mcp_ref
1122
+ <span class="type-count" id="cnt-mcp_ref">0</span>
1123
+ </button>
1124
+ <button class="type-btn" data-type="model_ref" onclick="filterType('model_ref')">
1125
+ <span class="type-dot" style="background:#ec4899"></span> model_ref
1126
+ <span class="type-count" id="cnt-model_ref">0</span>
1127
+ </button>
1128
+ <button class="type-btn" data-type="bundle" onclick="filterType('bundle')">
1129
+ <span class="type-dot" style="background:#6366f1"></span> bundle
1130
+ <span class="type-count" id="cnt-bundle">0</span>
1131
+ </button>
1132
+
1133
+ <div class="sidebar-section" style="margin-top:1rem">Navigation</div>
1134
+ <button class="type-btn" onclick="showTab('browse')">&#128270; Browse</button>
1135
+ <button class="type-btn" onclick="showTab('publish')">&#128228; Publish</button>
1136
+ <button class="type-btn" onclick="showTab('api')">&#128225; API Docs</button>
1137
+ </nav>
1138
+
1139
+ <div class="content">
1140
+ <!-- BROWSE TAB -->
1141
+ <div id="tab-browse">
1142
+ <div id="view-list">
1143
+ <div class="search-row">
1144
+ <input class="search-input" id="searchInput" placeholder="Search capabilities..." oninput="debounceSearch()">
1145
+ <button class="btn btn-primary" onclick="doSearch()">Search</button>
1146
+ </div>
1147
+ <div class="cards" id="cardGrid"></div>
1148
+ </div>
1149
+
1150
+ <div id="view-detail" style="display:none">
1151
+ <div class="back-btn" onclick="showList()">&#8592; Back to list</div>
1152
+ <div id="detailContent"></div>
1153
+ </div>
1154
+ </div>
1155
+
1156
+ <!-- PUBLISH TAB -->
1157
+ <div id="tab-publish" style="display:none">
1158
+ <h2 style="font-family:'Space Mono',monospace;color:var(--accent);margin-bottom:1.25rem;font-size:1.1rem">Publish Capability</h2>
1159
+ <div class="form-row">
1160
+ <div class="form-group">
1161
+ <label class="form-label">ID *</label>
1162
+ <input class="form-input" id="pubId" placeholder="my_capability">
1163
+ </div>
1164
+ <div class="form-group">
1165
+ <label class="form-label">Version</label>
1166
+ <input class="form-input" id="pubVersion" value="1.0.0">
1167
+ </div>
1168
+ </div>
1169
+ <div class="form-row">
1170
+ <div class="form-group">
1171
+ <label class="form-label">Type *</label>
1172
+ <select class="form-select" id="pubType" onchange="updatePayloadHint()">
1173
+ <option value="skill">skill</option>
1174
+ <option value="prompt">prompt</option>
1175
+ <option value="workflow">workflow</option>
1176
+ <option value="knowledge">knowledge</option>
1177
+ <option value="config">config</option>
1178
+ <option value="mcp_ref">mcp_ref</option>
1179
+ <option value="model_ref">model_ref</option>
1180
+ <option value="bundle">bundle</option>
1181
+ </select>
1182
+ </div>
1183
+ <div class="form-group">
1184
+ <label class="form-label">Author</label>
1185
+ <input class="form-input" id="pubAuthor" placeholder="your_name">
1186
+ </div>
1187
+ </div>
1188
+ <div class="form-group">
1189
+ <label class="form-label">Name *</label>
1190
+ <input class="form-input" id="pubName" placeholder="Human-readable name">
1191
+ </div>
1192
+ <div class="form-group">
1193
+ <label class="form-label">Description</label>
1194
+ <input class="form-input" id="pubDesc" placeholder="What does this do?">
1195
+ </div>
1196
+ <div class="form-group">
1197
+ <label class="form-label">Tags (comma-separated)</label>
1198
+ <input class="form-input" id="pubTags" placeholder="utility, search, nlp">
1199
+ </div>
1200
+ <div class="form-group">
1201
+ <label class="form-label">Dependencies (comma-separated capability IDs)</label>
1202
+ <input class="form-input" id="pubDeps" placeholder="web_search, calculator">
1203
+ </div>
1204
+ <div class="form-group">
1205
+ <label class="form-label">Payload (JSON) *</label>
1206
+ <textarea class="form-textarea" id="pubPayload" style="min-height:250px;font-size:0.72rem"></textarea>
1207
+ <div style="font-family:'DM Mono',monospace;font-size:0.65rem;color:var(--muted);margin-top:0.3rem" id="payloadHint"></div>
1208
+ </div>
1209
+ <button class="btn btn-primary" onclick="publishCap()">&#9954; Publish to FORGE</button>
1210
+ <div class="result-msg" id="pubResult"></div>
1211
+ </div>
1212
+
1213
+ <!-- API TAB -->
1214
+ <div id="tab-api" style="display:none">
1215
+ <h2 style="font-family:'Space Mono',monospace;color:var(--accent);margin-bottom:1.25rem;font-size:1.1rem">FORGE API v2</h2>
1216
+ <p style="font-family:'DM Mono',monospace;font-size:0.8rem;color:var(--muted);margin-bottom:1.25rem">
1217
+ Base: <code style="color:var(--accent2)" id="baseUrl"></code>
1218
+ </p>
1219
+
1220
+ <div class="section-label">Capability Endpoints</div>
1221
+ <div class="endpoint"><span class="method-get">GET</span> /api/capabilities &mdash; <span style="color:var(--muted)">List/search. Params: q, type, tag, limit, offset</span></div>
1222
+ <div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id} &mdash; <span style="color:var(--muted)">Get capability (latest version)</span></div>
1223
+ <div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id}/{version} &mdash; <span style="color:var(--muted)">Get specific version</span></div>
1224
+ <div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id}/versions &mdash; <span style="color:var(--muted)">Version history</span></div>
1225
+ <div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id}/payload &mdash; <span style="color:var(--muted)">Payload only (for hot-loading)</span></div>
1226
+ <div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id}/resolve &mdash; <span style="color:var(--muted)">Resolve bundle (all deps)</span></div>
1227
+ <div class="endpoint"><span class="method-post">POST</span> /api/capabilities &mdash; <span style="color:var(--muted)">Publish capability. Optional: X-Forge-Key header</span></div>
1228
+ <div class="endpoint"><span class="method-get">GET</span> /api/stats &mdash; <span style="color:var(--muted)">Counts by type + downloads</span></div>
1229
+ <div class="endpoint"><span class="method-get">GET</span> /api/tags &mdash; <span style="color:var(--muted)">All tags</span></div>
1230
+
1231
+ <div class="section-label" style="margin-top:1.5rem">MCP Server</div>
1232
+ <div class="endpoint"><span class="method-get">GET</span> /mcp/sse &mdash; <span style="color:var(--muted)">SSE stream (MCP transport)</span></div>
1233
+ <div class="endpoint"><span class="method-post">POST</span> /mcp &mdash; <span style="color:var(--muted)">JSON-RPC 2.0 endpoint</span></div>
1234
+
1235
+ <div class="section-label" style="margin-top:1.5rem">MCP Tools</div>
1236
+ <div class="endpoint">forge_search(query, type, tag, limit)</div>
1237
+ <div class="endpoint">forge_get(id, version?)</div>
1238
+ <div class="endpoint">forge_publish({id, type, name, payload, ...})</div>
1239
+ <div class="endpoint">forge_list_types()</div>
1240
+ <div class="endpoint">forge_resolve_bundle(id)</div>
1241
+
1242
+ <div class="section-label" style="margin-top:1.5rem">Quick Start</div>
1243
+ <div style="position:relative">
1244
+ <button class="copy-btn" onclick="copyCode('quickstart')">Copy</button>
1245
+ <pre id="quickstart">import requests, types
1246
+
1247
+ def bootstrap_forge(url=window.location.origin):
1248
+ r = requests.get(f"{url}/api/capabilities/forge_client/payload")
1249
+ m = types.ModuleType("forge_client")
1250
+ exec(r.json()["payload"]["code"], m.__dict__)
1251
+ return m.ForgeClient(url)
1252
+
1253
+ forge = bootstrap_forge()
1254
+
1255
+ # Load a skill
1256
+ calc = forge.load_skill("calculator")
1257
+ print(calc.execute(expression="sqrt(144) + 2**8"))
1258
+
1259
+ # Get a prompt
1260
+ persona = forge.get_prompt("researcher_persona",
1261
+ variables={"agent_name": "MyAgent", "max_steps": "8"})
1262
+
1263
+ # Resolve a bundle (get everything)
1264
+ loadout = forge.get_bundle("researcher_loadout")</pre>
1265
+ </div>
1266
+
1267
+ <div class="section-label" style="margin-top:1.5rem">Claude Desktop MCP Config</div>
1268
+ <div style="position:relative">
1269
+ <button class="copy-btn" onclick="copyCode('mcpConfig')">Copy</button>
1270
+ <pre id="mcpConfig" style="color:var(--cyan)">{
1271
+ "mcpServers": {
1272
+ "forge": {
1273
+ "command": "npx",
1274
+ "args": ["-y", "mcp-remote", "<span id='mcpUrl'></span>/mcp/sse"]
1275
+ }
1276
+ }
1277
+ }</pre>
1278
+ </div>
1279
+
1280
+ <div class="section-label" style="margin-top:1.5rem">Capability Types Reference</div>
1281
+ <div id="typesTable" style="display:grid;grid-template-columns:100px 1fr;gap:0.4rem 1rem;font-family:'DM Mono',monospace;font-size:0.75rem;margin-top:0.5rem"></div>
1282
+ </div>
1283
+ </div>
1284
+ </div>
1285
+ </div>
1286
+
1287
+ <script>
1288
+ const TYPE_COLORS = {
1289
+ skill:'#ff6b00',prompt:'#8b5cf6',workflow:'#06b6d4',knowledge:'#10b981',
1290
+ config:'#f59e0b',mcp_ref:'#ef4444',model_ref:'#ec4899',bundle:'#6366f1'
1291
+ };
1292
+ const TYPE_ICONS = {
1293
+ skill:'&#9881;',prompt:'&#128172;',workflow:'&#128279;',knowledge:'&#128218;',
1294
+ config:'&#9881;&#65038;',mcp_ref:'&#128225;',model_ref:'&#129302;',bundle:'&#128230;'
1295
+ };
1296
+ const TYPE_DESC = {
1297
+ skill:'Executable Python code (def execute(...)). Hot-loadable at runtime.',
1298
+ prompt:'System prompts, personas, Jinja2 / f-string templates.',
1299
+ workflow:'Multi-step ReAct plans and orchestration graphs.',
1300
+ knowledge:'Curated text/JSON chunks for RAG injection or agent context.',
1301
+ config:'Agent behavior policies, guardrails, tool whitelists.',
1302
+ mcp_ref:'Pointer to an external MCP server with connection details.',
1303
+ model_ref:'HF model pointer with usage notes and prompting guide.',
1304
+ bundle:'Named collection of capabilities (complete agent loadout).',
1305
+ };
1306
+
1307
+ let currentType = '';
1308
+ let searchTimer = null;
1309
+ let currentCap = null;
1310
+
1311
+ const BASE = window.location.origin;
1312
+ document.getElementById('baseUrl').textContent = BASE;
1313
+ document.querySelectorAll('#mcpUrl').forEach(el => el.textContent = BASE);
1314
+
1315
+ // Types table
1316
+ const tt = document.getElementById('typesTable');
1317
+ Object.entries(TYPE_DESC).forEach(([t,d]) => {
1318
+ tt.innerHTML += `<div style="color:${TYPE_COLORS[t]};font-weight:700">${t}</div><div style="color:var(--muted)">${d}</div>`;
1319
+ });
1320
+
1321
+ async function loadStats() {
1322
+ const r = await fetch('/api/stats'); const s = await r.json();
1323
+ document.getElementById('statTotal').textContent = s.total;
1324
+ document.getElementById('statDownloads').textContent = s.total_downloads;
1325
+ document.getElementById('cnt-all').textContent = s.total;
1326
+ const bt = s.by_type || {};
1327
+ ['skill','prompt','workflow','knowledge','config','mcp_ref','model_ref','bundle'].forEach(t => {
1328
+ const el = document.getElementById('cnt-'+t);
1329
+ if(el) el.textContent = bt[t]?.count || 0;
1330
+ });
1331
+ }
1332
+
1333
+ function filterType(t) {
1334
+ currentType = t;
1335
+ document.querySelectorAll('.type-btn').forEach(b => {
1336
+ b.classList.toggle('active', b.dataset.type === t);
1337
+ });
1338
+ doSearch();
1339
+ }
1340
+
1341
+ function debounceSearch() {
1342
+ clearTimeout(searchTimer);
1343
+ searchTimer = setTimeout(doSearch, 300);
1344
+ }
1345
+
1346
+ async function doSearch() {
1347
+ const q = document.getElementById('searchInput').value;
1348
+ const params = new URLSearchParams({q, limit:100});
1349
+ if(currentType) params.set('type', currentType);
1350
+ const r = await fetch('/api/capabilities?' + params);
1351
+ const data = await r.json();
1352
+ renderCards(data.capabilities || []);
1353
+ }
1354
+
1355
+ function renderCards(caps) {
1356
+ const grid = document.getElementById('cardGrid');
1357
+ if(!caps.length) {
1358
+ grid.innerHTML = '<div class="empty">No capabilities found.</div>';
1359
+ return;
1360
+ }
1361
+ grid.innerHTML = caps.map(c => {
1362
+ const color = TYPE_COLORS[c.type] || '#fff';
1363
+ const icon = TYPE_ICONS[c.type] || '?';
1364
+ const tags = (c.tags || []).slice(0,4).map(t => `<span class="tag">${t}</span>`).join('');
1365
+ return `<div class="card" onclick="showDetail('${c.id}')">
1366
+ <div class="card-type" style="color:${color}">${icon} ${c.type}</div>
1367
+ <div class="card-name">${c.name}</div>
1368
+ <div class="card-desc">${(c.description||'').slice(0,120)}${c.description?.length>120?'&hellip;':''}</div>
1369
+ <div class="card-footer">${tags}<div class="card-meta">&#8595; ${c.downloads||0} &middot; v${c.version}</div></div>
1370
+ </div>`;
1371
+ }).join('');
1372
+ }
1373
+
1374
+ async function showDetail(id) {
1375
+ const r = await fetch(`/api/capabilities/${id}`);
1376
+ const cap = await r.json();
1377
+ currentCap = cap;
1378
+ document.getElementById('view-list').style.display = 'none';
1379
+ document.getElementById('view-detail').style.display = 'block';
1380
+ renderDetail(cap);
1381
+ }
1382
+
1383
+ function renderDetail(cap) {
1384
+ const color = TYPE_COLORS[cap.type] || '#fff';
1385
+ const icon = TYPE_ICONS[cap.type] || '';
1386
+ const tags = (cap.tags||[]).map(t=>`<span class="tag">${t}</span>`).join('');
1387
+ const deps = (cap.deps||[]).map(d=>`<span class="dep-chip" onclick="showDetail('${d}')">${d}</span>`).join('');
1388
+
1389
+ let payloadHtml = '';
1390
+ const p = cap.payload || {};
1391
+
1392
+ if(cap.type === 'skill') {
1393
+ payloadHtml = `
1394
+ <div class="section-label">Code</div>
1395
+ <div style="position:relative">
1396
+ <button class="copy-btn" onclick="copyText(decodeURIComponent('${encodeURIComponent(p.code||'')}'))">Copy</button>
1397
+ <pre>${escHtml(p.code||'')}</pre>
1398
+ </div>
1399
+ ${(p.dependencies||[]).length ? `<div class="section-label">Dependencies</div><div style="font-family:'DM Mono',monospace;font-size:0.78rem;color:var(--yellow)">${p.dependencies.join(', ')}</div>` : ''}
1400
+ <div class="section-label">Quick Load</div>
1401
+ <pre style="color:var(--cyan)">forge = ForgeClient()
1402
+ skill = forge.load_skill("${cap.id}")
1403
+ result = skill.execute()</pre>`;
1404
+ } else if(cap.type === 'prompt') {
1405
+ payloadHtml = `
1406
+ <div class="section-label">Template</div>
1407
+ <pre>${escHtml(p.template||'')}</pre>
1408
+ ${p.variables ? `<div class="section-label">Variables</div><div style="font-family:'DM Mono',monospace;font-size:0.78rem;color:var(--yellow)">${JSON.stringify(p.variables)}</div>` : ''}
1409
+ <div class="section-label">Quick Load</div>
1410
+ <pre style="color:var(--cyan)">prompt = forge.get_prompt("${cap.id}", variables={...})</pre>`;
1411
+ } else if(cap.type === 'workflow') {
1412
+ const steps = p.steps||[];
1413
+ payloadHtml = `
1414
+ <div class="section-label">Steps (${steps.length})</div>
1415
+ ${steps.map((s,i) => `<div style="background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:0.75rem;margin-bottom:0.5rem;font-family:'DM Mono',monospace;font-size:0.75rem">
1416
+ <span style="color:var(--cyan)">#${i+1} ${s.id}</span> &rarr;
1417
+ <span class="dep-chip" onclick="showDetail('${s.cap_id}')" style="cursor:pointer">${s.cap_id}</span>
1418
+ ${s.next ? `<span style="color:var(--muted)"> &rarr; ${s.next}</span>` : '<span style="color:var(--green)"> &rarr; END</span>'}
1419
+ </div>`).join('')}`;
1420
+ } else if(cap.type === 'bundle') {
1421
+ const caps_in = p.capabilities||[];
1422
+ payloadHtml = `
1423
+ <div class="section-label">Contains ${caps_in.length} Capabilities</div>
1424
+ <div class="bundle-grid">${caps_in.map(cid=>`
1425
+ <div class="bundle-item" onclick="showDetail('${cid}')" style="cursor:pointer">
1426
+ <span class="bundle-item-icon">${TYPE_ICONS[cid]||'&#9711;'}</span>
1427
+ <div><div class="bundle-item-name">${cid}</div></div>
1428
+ </div>`).join('')}
1429
+ </div>
1430
+ <div class="section-label" style="margin-top:1rem">Quick Load Bundle</div>
1431
+ <pre style="color:var(--cyan)">loadout = forge.get_bundle("${cap.id}")
1432
+ # Returns all ${caps_in.length} capabilities resolved</pre>`;
1433
+ } else {
1434
+ payloadHtml = `
1435
+ <div class="section-label">Payload</div>
1436
+ <pre>${escHtml(JSON.stringify(p, null, 2))}</pre>`;
1437
+ }
1438
+
1439
+ const schemaIn = JSON.stringify(cap.schema_in||{}, null, 2);
1440
+ const schemaOut = JSON.stringify(cap.schema_out||{}, null, 2);
1441
+
1442
+ document.getElementById('detailContent').innerHTML = `
1443
+ <div class="detail">
1444
+ <div class="detail-header">
1445
+ <div>
1446
+ <div class="detail-type" style="color:${color};border-color:${color}20;background:${color}10">${icon} ${cap.type}</div>
1447
+ </div>
1448
+ <div>
1449
+ <div class="detail-title">${cap.name}</div>
1450
+ <div class="detail-meta">v${cap.version} &middot; by ${cap.author} &middot; &#8595; ${cap.downloads||0} downloads</div>
1451
+ </div>
1452
+ </div>
1453
+ <div class="detail-desc">${escHtml(cap.description||'')}</div>
1454
+ ${tags ? `<div style="margin-bottom:0.75rem">${tags}</div>` : ''}
1455
+ ${deps ? `<div class="section-label">Dependencies</div><div style="margin-bottom:0.75rem">${deps}</div>` : ''}
1456
+ ${payloadHtml}
1457
+ ${schemaIn !== '{}' ? `<div class="section-label">Input Schema</div><pre style="color:var(--purple)">${escHtml(schemaIn)}</pre>` : ''}
1458
+ ${schemaOut !== '{}' ? `<div class="section-label">Output Schema</div><pre style="color:var(--cyan)">${escHtml(schemaOut)}</pre>` : ''}
1459
+ </div>`;
1460
+ }
1461
+
1462
+ function showList() {
1463
+ document.getElementById('view-list').style.display = 'block';
1464
+ document.getElementById('view-detail').style.display = 'none';
1465
+ currentCap = null;
1466
+ }
1467
+
1468
+ function showTab(tab) {
1469
+ ['browse','publish','api'].forEach(t => {
1470
+ document.getElementById('tab-'+t).style.display = t===tab?'block':'none';
1471
+ });
1472
+ }
1473
+
1474
+ // Publish
1475
+ const PAYLOAD_HINTS = {
1476
+ skill: '{"code": "def execute(x: str) -> dict:\\n return {\\"result\\": x}", "language": "python", "dependencies": []}',
1477
+ prompt: '{"template": "You are {{agent_name}}. {{instructions}}", "format": "plain", "variables": ["agent_name", "instructions"]}',
1478
+ workflow: '{"entry": "step1", "steps": [{"id": "step1", "cap_id": "some_skill", "type": "skill", "params": {}, "next": null}]}',
1479
+ knowledge: '{"format": "markdown", "source": "https://...", "content": "# Title\\n\\nContent here..."}',
1480
+ config: '{"settings": {"max_steps": 8, "timeout": 60, "allowed_tools": []}}',
1481
+ mcp_ref: '{"url": "https://example.com/mcp/sse", "transport": "sse", "tools": [], "auth": "none"}',
1482
+ model_ref: '{"repo_id": "org/model-name", "task": "text-generation", "notes": "Usage notes...", "context_length": 8192}',
1483
+ bundle: '{"capabilities": ["cap_id_1", "cap_id_2", "cap_id_3"], "description": "What this bundle provides"}',
1484
+ };
1485
+
1486
+ function updatePayloadHint() {
1487
+ const t = document.getElementById('pubType').value;
1488
+ const hint = PAYLOAD_HINTS[t] || '{}';
1489
+ document.getElementById('pubPayload').value = hint;
1490
+ document.getElementById('payloadHint').textContent = `Required field for ${t}: ${
1491
+ t==='skill'?'code' : t==='prompt'?'template' : t==='workflow'?'steps' :
1492
+ t==='bundle'?'capabilities' : t==='mcp_ref'?'url' : t==='model_ref'?'repo_id' : 'content/settings'
1493
+ }`;
1494
+ }
1495
+ updatePayloadHint();
1496
+
1497
+ async function publishCap() {
1498
+ const id = document.getElementById('pubId').value.trim();
1499
+ const type = document.getElementById('pubType').value;
1500
+ const name = document.getElementById('pubName').value.trim();
1501
+ const version = document.getElementById('pubVersion').value.trim() || '1.0.0';
1502
+ const author = document.getElementById('pubAuthor').value.trim() || 'anonymous';
1503
+ const desc = document.getElementById('pubDesc').value.trim();
1504
+ const tagsRaw = document.getElementById('pubTags').value.trim();
1505
+ const depsRaw = document.getElementById('pubDeps').value.trim();
1506
+ const payloadRaw = document.getElementById('pubPayload').value.trim();
1507
+
1508
+ let payload;
1509
+ try { payload = JSON.parse(payloadRaw); } catch(e) {
1510
+ showResult('error', 'Invalid JSON payload: ' + e.message); return;
1511
+ }
1512
+
1513
+ const body = {
1514
+ id, type, name, version, author,
1515
+ description: desc,
1516
+ tags: tagsRaw ? tagsRaw.split(',').map(t=>t.trim()).filter(Boolean) : [],
1517
+ deps: depsRaw ? depsRaw.split(',').map(d=>d.trim()).filter(Boolean) : [],
1518
+ payload,
1519
+ };
1520
+
1521
+ const r = await fetch('/api/capabilities', {
1522
+ method:'POST',
1523
+ headers:{'Content-Type':'application/json'},
1524
+ body: JSON.stringify(body),
1525
+ });
1526
+ const data = await r.json();
1527
+ if(r.ok) {
1528
+ showResult('ok', data.message);
1529
+ loadStats();
1530
+ doSearch();
1531
+ } else {
1532
+ showResult('error', data.detail || data.message || 'Error');
1533
+ }
1534
+ }
1535
+
1536
+ function showResult(type, msg) {
1537
+ const el = document.getElementById('pubResult');
1538
+ el.className = 'result-msg ' + (type==='ok'?'result-ok':'result-err');
1539
+ el.textContent = (type==='ok'?'&#10003; ':'&#10007; ') + msg;
1540
+ el.style.display = 'block';
1541
+ setTimeout(()=>{ el.style.display='none'; }, 5000);
1542
+ }
1543
+
1544
+ function copyCode(id) {
1545
+ const el = document.getElementById(id);
1546
+ navigator.clipboard.writeText(el.innerText || el.textContent);
1547
+ }
1548
+ function copyText(t) { navigator.clipboard.writeText(t); }
1549
+ function escHtml(s) {
1550
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1551
+ }
1552
+
1553
+ // Init
1554
+ loadStats();
1555
+ doSearch();
1556
+ </script>
1557
+ </body>
1558
+ </html>"""
1559
+
1560
+
1561
+ @app.get("/", response_class=HTMLResponse)
1562
+ async def root():
1563
+ return HTMLResponse(content=SPA, media_type="text/html; charset=utf-8")
1564
+
1565
+
1566
+ # ---------------------------------------------------------------------------
1567
+ # Entry point
1568
+ # ---------------------------------------------------------------------------
1569
+
1570
+ if __name__ == "__main__":
1571
+ uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fastapi>=0.111.0
2
+ uvicorn>=0.30.0