Chris4K commited on
Commit
0dd432e
·
verified ·
1 Parent(s): d0a1a60

Upload 7 files

Browse files
Files changed (7) hide show
  1. HOWTO.md +448 -0
  2. app.py +333 -0
  3. claude_chat.json +28 -0
  4. claude_extractor.json +25 -0
  5. mcp_server.py +253 -0
  6. requirements.txt +5 -0
  7. skill_registry.py +231 -0
HOWTO.md ADDED
@@ -0,0 +1,448 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ⚒️ FORGE — Complete How-To Guide
2
+
3
+ ## Table of Contents
4
+ 1. [What is FORGE?](#what-is-forge)
5
+ 2. [vs ClawHub](#vs-clawhub)
6
+ 3. [Deploy to HuggingFace](#deploy)
7
+ 4. [Skill Formats](#skill-formats)
8
+ 5. [CRUD — Create, Read, Update, Delete](#crud)
9
+ 6. [MCP Server — Claude native integration](#mcp)
10
+ 7. [Bins & Node Tools](#bins-and-node-tools)
11
+ 8. [Claude Skills (Anthropic API)](#claude-skills)
12
+ 9. [Agent Quick Start](#agent-quick-start)
13
+ 10. [REST API Reference](#rest-api)
14
+
15
+ ---
16
+
17
+ ## What is FORGE?
18
+
19
+ FORGE is a **skill artifactory for AI agents** — like npm, pip, or JFrog Artifactory but for executable agent capabilities.
20
+
21
+ Every skill is a self-contained, versioned unit that any agent can:
22
+ - **Discover** via REST API or MCP protocol
23
+ - **Download** as code (Python / Node.js / shell) or as a SKILL.md description
24
+ - **Hot-load** at runtime without retraining or redeployment
25
+ - **Execute** through a standard `execute()` interface
26
+
27
+ ---
28
+
29
+ ## vs ClawHub
30
+
31
+ | Feature | ClawHub | FORGE |
32
+ |---------|---------|-------|
33
+ | Primary format | SKILL.md (markdown) | JSON + embedded code |
34
+ | Also supports | — | SKILL.md (ClawHub compatible) ✅ |
35
+ | Code execution | Instructions only | Live executable Python ✅ |
36
+ | MCP Server | ❌ | ✅ |
37
+ | Bins / Node tools | Via Nix | Via `runtime` metadata |
38
+ | Claude skills | ❌ | ✅ (Anthropic API) |
39
+ | HF Space deploy | ❌ | ✅ one-click |
40
+ | Vector search | ✅ (OpenAI embeddings) | tag + text search |
41
+ | 3000+ community skills | ✅ | growing |
42
+
43
+ **Key difference:** ClawHub skills are *instructions* (markdown that goes into an LLM prompt). FORGE skills are *executable code* that agents run directly. You can publish both.
44
+
45
+ ---
46
+
47
+ ## Deploy
48
+
49
+ ### HuggingFace Space (recommended)
50
+
51
+ ```bash
52
+ # 1. Create Space at huggingface.co → SDK: Gradio
53
+ # 2. Clone your space
54
+ git clone https://huggingface.co/spaces/YOUR_NAME/agent-forge
55
+ cd agent-forge
56
+
57
+ # 3. Copy FORGE files into it
58
+ cp -r forge/* .
59
+
60
+ # 4. Push
61
+ git add . && git commit -m "Deploy FORGE" && git push
62
+ ```
63
+
64
+ The `README.md` frontmatter configures the Space:
65
+ ```yaml
66
+ ---
67
+ title: FORGE Agent Skill Artifactory
68
+ emoji: ⚒️
69
+ colorFrom: orange
70
+ colorTo: red
71
+ sdk: gradio
72
+ sdk_version: 4.44.0
73
+ app_file: app.py
74
+ pinned: true
75
+ ---
76
+ ```
77
+
78
+ ### Local dev
79
+
80
+ ```bash
81
+ pip install -r requirements.txt
82
+ python app.py
83
+ # → http://localhost:7860
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Skill Formats
89
+
90
+ FORGE supports **three skill formats**:
91
+
92
+ ### Format 1: JSON (executable Python)
93
+ Best for: skills that agents hot-load and execute directly.
94
+
95
+ ```json
96
+ {
97
+ "id": "my_skill",
98
+ "name": "My Skill",
99
+ "version": "1.0.0",
100
+ "description": "What this skill does",
101
+ "author": "yourname",
102
+ "tags": ["utility"],
103
+ "dependencies": ["requests"],
104
+ "runtime": "python",
105
+ "schema": {
106
+ "input": { "text": "str" },
107
+ "output": { "result": "str" }
108
+ },
109
+ "code": "def execute(text: str) -> dict:\n return {'result': text.upper()}"
110
+ }
111
+ ```
112
+
113
+ ### Format 2: SKILL.md (ClawHub compatible)
114
+ Best for: LLM-readable instructions that go into agent prompts.
115
+
116
+ ```markdown
117
+ ---
118
+ name: my-skill
119
+ version: 1.0.0
120
+ description: Does something useful
121
+ author: yourname
122
+ tags: [utility, text]
123
+ runtime: instructions
124
+ ---
125
+
126
+ # My Skill
127
+
128
+ ## Purpose
129
+ Explain what this skill teaches an agent to do.
130
+
131
+ ## Usage
132
+ When the user asks to process text, do the following:
133
+ 1. Step one
134
+ 2. Step two
135
+
136
+ ## Examples
137
+ Input: "hello world"
138
+ Output: "HELLO WORLD"
139
+ ```
140
+
141
+ ### Format 3: Node.js / Shell tool
142
+ Best for: wrapping CLI tools, Node scripts, or system binaries.
143
+
144
+ ```json
145
+ {
146
+ "id": "node_parser",
147
+ "name": "JSON Parser",
148
+ "version": "1.0.0",
149
+ "runtime": "node",
150
+ "dependencies_node": ["lodash"],
151
+ "bins": ["jq"],
152
+ "code": "const _ = require('lodash');\nfunction execute({data}) {\n return { keys: _.keys(JSON.parse(data)) };\n}\nmodule.exports = { execute };"
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## CRUD
159
+
160
+ ### CREATE — Publish a new skill
161
+
162
+ **Via UI:** Go to the "📦 Publish" tab → paste your skill JSON → click Publish.
163
+
164
+ **Via API:**
165
+ ```bash
166
+ curl -X POST https://YOUR_SPACE.hf.space/api/v1/skills \
167
+ -H "Content-Type: application/json" \
168
+ -d @my_skill.json
169
+ ```
170
+
171
+ **Via Python agent:**
172
+ ```python
173
+ forge = bootstrap_forge()
174
+ result = forge.publish({
175
+ "id": "my_skill",
176
+ "name": "My Skill",
177
+ "version": "1.0.0",
178
+ "description": "...",
179
+ "author": "me",
180
+ "tags": ["utility"],
181
+ "code": "def execute(**kwargs): return {'ok': True}"
182
+ })
183
+ ```
184
+
185
+ ---
186
+
187
+ ### READ — Discover and download skills
188
+
189
+ **Browse UI:** Go to "🔍 Browse" tab — search by keyword or filter by tag.
190
+
191
+ **Read one skill (metadata only):**
192
+ ```bash
193
+ GET /api/v1/skills/calculator
194
+ ```
195
+
196
+ **Read skill code (for hot-loading):**
197
+ ```bash
198
+ GET /api/v1/skills/calculator/code
199
+ ```
200
+
201
+ **Download as .py file:**
202
+ ```bash
203
+ GET /api/v1/skills/calculator/download
204
+ ```
205
+
206
+ **Full manifest (all skills + code for offline cache):**
207
+ ```bash
208
+ GET /api/v1/manifest
209
+ ```
210
+
211
+ **Search:**
212
+ ```bash
213
+ GET /api/v1/search?q=math
214
+ GET /api/v1/skills?tag=web
215
+ ```
216
+
217
+ ---
218
+
219
+ ### UPDATE — Publish a new version
220
+
221
+ FORGE uses **semver versioning**. To update a skill, publish it again with a bumped version:
222
+
223
+ ```json
224
+ {
225
+ "id": "calculator",
226
+ "version": "1.1.0", ← bump this
227
+ ...
228
+ }
229
+ ```
230
+
231
+ Previous versions are kept on disk as `calculator.v1.0.0.json`.
232
+
233
+ **Via UI:** Skill Detail tab → "Edit" → modify → save as new version.
234
+
235
+ ---
236
+
237
+ ### DELETE — Remove a skill
238
+
239
+ **Via UI:** Skill Detail tab → "🗑 Delete" button (with confirmation).
240
+
241
+ **Via API:**
242
+ ```bash
243
+ DELETE /api/v1/skills/my_skill
244
+ ```
245
+
246
+ ---
247
+
248
+ ## MCP
249
+
250
+ FORGE exposes an **MCP (Model Context Protocol) server** so Claude Desktop, Claude API, and any MCP client can discover and use skills natively.
251
+
252
+ ### Connect Claude to FORGE
253
+
254
+ Add to your `claude_desktop_config.json`:
255
+ ```json
256
+ {
257
+ "mcpServers": {
258
+ "forge": {
259
+ "command": "npx",
260
+ "args": ["-y", "mcp-remote", "https://YOUR_SPACE.hf.space/mcp/sse"]
261
+ }
262
+ }
263
+ }
264
+ ```
265
+
266
+ Or via the API with SSE:
267
+ ```python
268
+ import anthropic
269
+
270
+ client = anthropic.Anthropic()
271
+ response = client.beta.messages.create(
272
+ model="claude-opus-4-6",
273
+ max_tokens=1024,
274
+ tools=[],
275
+ mcp_servers=[{
276
+ "type": "url",
277
+ "url": "https://YOUR_SPACE.hf.space/mcp/sse",
278
+ "name": "forge"
279
+ }],
280
+ messages=[{"role": "user", "content": "List all math skills in FORGE"}]
281
+ )
282
+ ```
283
+
284
+ ### MCP Tools exposed by FORGE
285
+
286
+ | Tool | Description |
287
+ |------|-------------|
288
+ | `forge_list_skills` | List all skills, optionally filtered |
289
+ | `forge_search` | Semantic search across skills |
290
+ | `forge_get_skill` | Get full skill including code |
291
+ | `forge_get_code` | Get executable code for a skill |
292
+ | `forge_publish_skill` | Publish a new skill |
293
+ | `forge_get_stats` | Registry statistics |
294
+
295
+ ---
296
+
297
+ ## Bins and Node Tools
298
+
299
+ Skills can declare required system binaries and Node packages:
300
+
301
+ ```json
302
+ {
303
+ "id": "pdf_extractor",
304
+ "runtime": "python",
305
+ "bins": ["pdftotext", "gs"],
306
+ "dependencies": ["pypdf2"],
307
+ "install_notes": "apt-get install poppler-utils ghostscript",
308
+ "code": "..."
309
+ }
310
+ ```
311
+
312
+ ```json
313
+ {
314
+ "id": "image_optimizer",
315
+ "runtime": "node",
316
+ "bins": ["sharp", "imagemagick"],
317
+ "dependencies_node": ["sharp", "glob"],
318
+ "code": "const sharp = require('sharp'); ..."
319
+ }
320
+ ```
321
+
322
+ Agents check `skill.bins` before executing and can skip skills whose binaries aren't available on the current host.
323
+
324
+ ### Shell/Bin skill example
325
+
326
+ ```json
327
+ {
328
+ "id": "git_status",
329
+ "runtime": "shell",
330
+ "bins": ["git"],
331
+ "code": "#!/bin/bash\ngit -C \"$1\" status --porcelain"
332
+ }
333
+ ```
334
+
335
+ ---
336
+
337
+ ## Claude Skills
338
+
339
+ Skills that call the **Anthropic API** — these give your agent Claude-powered capabilities it can invoke as tools.
340
+
341
+ ### Example: Claude Skill structure
342
+
343
+ ```json
344
+ {
345
+ "id": "claude_summarizer",
346
+ "runtime": "python",
347
+ "dependencies": ["anthropic"],
348
+ "env_required": ["ANTHROPIC_API_KEY"],
349
+ "code": "import anthropic\n\ndef execute(text: str, style: str = 'bullet') -> dict:\n client = anthropic.Anthropic()\n msg = client.messages.create(\n model='claude-haiku-4-5-20251001',\n max_tokens=512,\n messages=[{'role': 'user', 'content': f'Summarize as {style} points:\\n{text}'}]\n )\n return {'summary': msg.content[0].text}\n"
350
+ }
351
+ ```
352
+
353
+ ### How agents use Claude skills
354
+
355
+ ```python
356
+ forge = bootstrap_forge()
357
+ summarizer = forge.load("claude_summarizer")
358
+
359
+ # Agents set ANTHROPIC_API_KEY in their environment
360
+ result = summarizer.execute(
361
+ text="Long article text...",
362
+ style="executive"
363
+ )
364
+ # → {"summary": "• Key point 1\n• Key point 2..."}
365
+ ```
366
+
367
+ ### Available Claude skills in FORGE
368
+
369
+ | Skill ID | What it does |
370
+ |----------|-------------|
371
+ | `claude_chat` | Single-turn Q&A with Claude |
372
+ | `claude_summarizer` | Summarize text in various styles |
373
+ | `claude_extractor` | Extract structured data from text |
374
+ | `claude_judge` | Evaluate/score outputs (LLM-as-judge) |
375
+ | `claude_coder` | Generate code from a spec |
376
+
377
+ ---
378
+
379
+ ## Agent Quick Start
380
+
381
+ ```python
382
+ import requests, types
383
+
384
+ # ── Bootstrap (one HTTP call) ──────────────────────────
385
+ def bootstrap_forge(url="https://chris4k-agent-forge.hf.space"):
386
+ r = requests.get(f"{url}/api/v1/skills/forge_client/code")
387
+ m = types.ModuleType("forge_client")
388
+ exec(r.json()["code"], m.__dict__)
389
+ return m.ForgeClient(url)
390
+
391
+ forge = bootstrap_forge()
392
+
393
+ # ── Discover ───────────────────────────────────────────
394
+ all_skills = forge.list() # all skills
395
+ math_skills = forge.list(tag="math") # by tag
396
+ results = forge.search("web scraping") # full-text search
397
+
398
+ # ── Load & Execute ─────────────────────────────────────
399
+ calc = forge.load("calculator")
400
+ search = forge.load("web_search")
401
+ memory = forge.load("memory_store")
402
+ fetcher = forge.load("http_fetch")
403
+ claude = forge.load("claude_chat") # Claude skill
404
+
405
+ # Run them
406
+ print(calc.execute(expression="2**32 / 1024"))
407
+ print(search.execute(query="AI news today", max_results=3))
408
+ memory.execute(action="set", key="goal", value="research AI", ttl=3600)
409
+ page = fetcher.execute(url="https://example.com", max_chars=2000)
410
+ reply = claude.execute(prompt="What is MCP?")
411
+
412
+ # ── Publish your own ───────────────────────────────────
413
+ forge.publish({
414
+ "id": "my_skill",
415
+ "name": "My Skill",
416
+ "version": "1.0.0",
417
+ "description": "Does something useful",
418
+ "author": "yourname",
419
+ "tags": ["utility"],
420
+ "code": "def execute(**kwargs): return {'ok': True, 'data': kwargs}"
421
+ })
422
+ ```
423
+
424
+ ---
425
+
426
+ ## REST API Reference
427
+
428
+ Base URL: `https://YOUR_SPACE.hf.space`
429
+
430
+ | Method | Path | Description |
431
+ |--------|------|-------------|
432
+ | `GET` | `/api/v1/manifest` | Full manifest (all skills + code) |
433
+ | `GET` | `/api/v1/skills` | List skills. Params: `?tag=` `?q=` |
434
+ | `GET` | `/api/v1/skills/{id}` | Get skill metadata + code |
435
+ | `GET` | `/api/v1/skills/{id}/code` | Minimal code payload for hot-loading |
436
+ | `GET` | `/api/v1/skills/{id}/download` | Download as `.py` file |
437
+ | `POST` | `/api/v1/skills` | Publish new skill (JSON body) |
438
+ | `PUT` | `/api/v1/skills/{id}` | Update skill (new version) |
439
+ | `DELETE` | `/api/v1/skills/{id}` | Delete skill |
440
+ | `GET` | `/api/v1/search?q=` | Full-text search |
441
+ | `GET` | `/api/v1/tags` | All tags |
442
+ | `GET` | `/api/v1/stats` | Registry statistics |
443
+ | `GET` | `/mcp/sse` | MCP Server-Sent Events endpoint |
444
+ | `POST` | `/mcp` | MCP JSON-RPC endpoint |
445
+
446
+ ---
447
+
448
+ *Built by Chris4K · ki-fusion-labs.de · MIT License*
app.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ⚒️ FORGE — Federated Open Registry for Generative Executables
3
+ v2.0 — CRUD UI · MCP Server · SKILL.md support · Claude skills
4
+ """
5
+ import json
6
+ import time
7
+ from pathlib import Path
8
+
9
+ import gradio as gr
10
+ from fastapi import FastAPI, HTTPException, Request
11
+ from fastapi.responses import JSONResponse, Response
12
+
13
+ import skill_registry as registry
14
+ from mcp_server import register_mcp_routes
15
+
16
+ api = FastAPI(title="FORGE API", version="2.0.0")
17
+ register_mcp_routes(api)
18
+
19
+ def jresp(data, status=200):
20
+ return JSONResponse(content=data, status_code=status)
21
+
22
+ @api.get("/api/v1/manifest")
23
+ async def api_manifest():
24
+ return jresp(registry.get_manifest())
25
+
26
+ @api.get("/api/v1/skills")
27
+ async def api_list(tag: str | None = None, q: str | None = None):
28
+ skills = registry.list_skills(tag=tag, query=q)
29
+ return jresp({"skills": skills, "count": len(skills)})
30
+
31
+ @api.get("/api/v1/skills/{skill_id}")
32
+ async def api_get(skill_id: str):
33
+ s = registry.get_skill(skill_id)
34
+ if not s: raise HTTPException(404, f"'{skill_id}' not found")
35
+ return jresp(s)
36
+
37
+ @api.get("/api/v1/skills/{skill_id}/code")
38
+ async def api_code(skill_id: str):
39
+ d = registry.get_skill_code(skill_id)
40
+ if not d: raise HTTPException(404)
41
+ return jresp(d)
42
+
43
+ @api.get("/api/v1/skills/{skill_id}/download")
44
+ async def api_download(skill_id: str):
45
+ d = registry.get_skill_code(skill_id)
46
+ if not d: raise HTTPException(404)
47
+ code = f'"""\nFORGE Skill: {skill_id} v{d["version"]}\nhttps://chris4k-agent-forge.hf.space\n"""\n\n{d["code"]}\n'
48
+ return Response(code, media_type="text/x-python",
49
+ headers={"Content-Disposition": f'attachment; filename="{skill_id}.py"'})
50
+
51
+ @api.post("/api/v1/skills")
52
+ async def api_publish(request: Request):
53
+ try: skill = await request.json()
54
+ except Exception: raise HTTPException(400, "Invalid JSON")
55
+ ok, msg = registry.publish_skill(skill)
56
+ return jresp({"ok": ok, "message": msg}, 201 if ok else 400)
57
+
58
+ @api.put("/api/v1/skills/{skill_id}")
59
+ async def api_update(skill_id: str, request: Request):
60
+ try: updates = await request.json()
61
+ except Exception: raise HTTPException(400, "Invalid JSON")
62
+ ok, msg = registry.update_skill(skill_id, updates)
63
+ return jresp({"ok": ok, "message": msg}, 200 if ok else 404)
64
+
65
+ @api.delete("/api/v1/skills/{skill_id}")
66
+ async def api_delete(skill_id: str):
67
+ ok, msg = registry.delete_skill(skill_id)
68
+ return jresp({"ok": ok, "message": msg}, 200 if ok else 404)
69
+
70
+ @api.get("/api/v1/search")
71
+ async def api_search(q: str):
72
+ skills = registry.list_skills(query=q)
73
+ return jresp({"query": q, "skills": skills, "count": len(skills)})
74
+
75
+ @api.get("/api/v1/tags")
76
+ async def api_tags():
77
+ return jresp({"tags": registry.get_all_tags()})
78
+
79
+ @api.get("/api/v1/stats")
80
+ async def api_stats():
81
+ return jresp(registry.get_stats())
82
+
83
+ # ─── CSS ──────────────────────────────────────────────────────────
84
+ CSS = """
85
+ @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&display=swap');
86
+ :root{--fg:#0a0a0f;--surf:#111118;--bord:#1e1e2e;--acc:#ff6b00;--acc2:#ff9500;--txt:#e8e8f0;--mute:#6b6b8a;--grn:#00ff88;--pur:#8b5cf6;--red:#ff4444;}
87
+ body,.gradio-container{background:var(--fg)!important;font-family:'Syne',sans-serif!important;color:var(--txt)!important;}
88
+ .forge-header{text-align:center;padding:2.5rem 1rem 1rem;border-bottom:1px solid var(--bord);margin-bottom:1.5rem;background:linear-gradient(180deg,#0f0f1a 0%,var(--fg) 100%);}
89
+ .forge-logo{font-family:'Space Mono',monospace;font-size:3.2rem;font-weight:700;background:linear-gradient(135deg,var(--acc),var(--acc2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;letter-spacing:-2px;line-height:1;}
90
+ .forge-sub{font-family:'Space Mono',monospace;font-size:.65rem;color:var(--mute);letter-spacing:.35em;text-transform:uppercase;margin-top:.4rem;}
91
+ .stat-bar{display:flex;justify-content:center;gap:3rem;padding:.8rem;margin:1rem 0 1.5rem;border:1px solid var(--bord);border-radius:8px;background:var(--surf);}
92
+ .stat-num{font-family:'Space Mono',monospace;font-size:1.8rem;color:var(--acc);font-weight:700;}
93
+ .stat-lbl{font-size:.65rem;color:var(--mute);text-transform:uppercase;letter-spacing:.15em;}
94
+ .skill-card{background:var(--surf);border:1px solid var(--bord);border-radius:8px;padding:1.2rem;margin-bottom:.6rem;transition:border-color .2s,transform .1s;}
95
+ .skill-card:hover{border-color:var(--acc);transform:translateX(3px);}
96
+ .skill-name{font-family:'Space Mono',monospace;font-size:.95rem;font-weight:700;color:var(--acc);}
97
+ .skill-desc{font-size:.83rem;color:var(--mute);line-height:1.5;margin-top:.3rem;}
98
+ .tag-pill{display:inline-block;background:#1a1a30;border:1px solid #2a2a50;color:var(--pur);font-family:'Space Mono',monospace;font-size:.6rem;padding:2px 8px;border-radius:20px;margin:3px 2px 0;}
99
+ .api-row{background:var(--surf);border-left:3px solid var(--acc);padding:.6rem 1rem;margin:.4rem 0;border-radius:0 6px 6px 0;font-family:'Space Mono',monospace;font-size:.75rem;}
100
+ .m-get{color:var(--grn);}.m-post{color:var(--acc2);}.m-put{color:#60a5fa;}.m-del{color:var(--red);}
101
+ .mcp-badge{background:#1a0a30;border:1px solid #5b21b650;border-radius:6px;padding:.75rem 1rem;margin:.5rem 0;font-family:'Space Mono',monospace;font-size:.75rem;color:#a78bfa;}
102
+ .gr-button{background:var(--acc)!important;color:#000!important;font-family:'Space Mono',monospace!important;font-weight:700!important;border:none!important;}
103
+ .gr-button:hover{background:var(--acc2)!important;}
104
+ textarea,input{background:var(--surf)!important;border-color:var(--bord)!important;color:var(--txt)!important;}
105
+ label span{color:var(--mute)!important;font-family:'Space Mono',monospace!important;font-size:.72rem!important;letter-spacing:.1em!important;}
106
+ .tab-nav button{background:var(--surf)!important;color:var(--mute)!important;border:1px solid var(--bord)!important;font-family:'Space Mono',monospace!important;font-size:.75rem!important;}
107
+ .tab-nav button.selected{color:var(--acc)!important;border-bottom-color:var(--acc)!important;}
108
+ """
109
+
110
+ PUBLISH_TEMPLATE = """{
111
+ "id": "my_skill",
112
+ "name": "My Skill",
113
+ "version": "1.0.0",
114
+ "description": "What this skill does for an agent.",
115
+ "author": "Chris4K",
116
+ "tags": ["utility"],
117
+ "runtime": "python",
118
+ "dependencies": [],
119
+ "env_required": [],
120
+ "bins": [],
121
+ "schema": {
122
+ "input": { "text": "str" },
123
+ "output": { "result": "str" }
124
+ },
125
+ "code": "def execute(text: str) -> dict:\\n return {'result': text.upper()}\\n"
126
+ }"""
127
+
128
+ SKILL_MD_TEMPLATE = """---
129
+ name: my-instructions-skill
130
+ version: 1.0.0
131
+ description: Teach an agent how to do something step-by-step.
132
+ author: Chris4K
133
+ tags: [instructions, utility]
134
+ runtime: instructions
135
+ ---
136
+
137
+ # My Instructions Skill
138
+
139
+ ## Purpose
140
+ Describe what capability this adds to an agent.
141
+
142
+ ## When to use
143
+ Use this skill when the user asks you to...
144
+
145
+ ## Steps
146
+ 1. First, do this
147
+ 2. Then do that
148
+ 3. Finally return the result
149
+
150
+ ## Examples
151
+ Input: "some example"
152
+ Output: "expected output"
153
+ """
154
+
155
+ def fmt_card(s):
156
+ tags = "".join(f'<span class="tag-pill">{t}</span>' for t in s.get("tags",[]))
157
+ rt = s.get("runtime","python")
158
+ return f"""<div class="skill-card">
159
+ <div class="skill-name">⚙ {s['name']} <span style="font-size:.65rem;color:#555570;margin-left:6px">[{rt}]</span>
160
+ <span style="float:right;font-size:.65rem;color:#4a4a6a">↓{s.get('downloads',0)} · v{s['version']}</span></div>
161
+ <div style="font-family:'Space Mono',monospace;font-size:.62rem;color:#4a4a6a;margin:2px 0 6px">by {s['author']} · <span style="color:#ff6b0060">{s['id']}</span></div>
162
+ <div class="skill-desc">{s['description'][:150]}{'…' if len(s['description'])>150 else ''}</div>
163
+ <div style="margin-top:6px">{tags}</div></div>"""
164
+
165
+ def render_list(skills):
166
+ if not skills: return '<div style="color:#4a4a6a;text-align:center;padding:2rem;font-family:Space Mono">No skills found.</div>'
167
+ return "".join(fmt_card(s) for s in skills)
168
+
169
+ def render_stats():
170
+ st = registry.get_stats()
171
+ return f"""<div class="stat-bar">
172
+ <div><div class="stat-num">{st['total_skills']}</div><div class="stat-lbl">Skills</div></div>
173
+ <div><div class="stat-num">{st['total_downloads']}</div><div class="stat-lbl">Downloads</div></div>
174
+ <div><div class="stat-num">{st['total_tags']}</div><div class="stat-lbl">Tags</div></div>
175
+ <div><div class="stat-num">✓</div><div class="stat-lbl">MCP Live</div></div></div>"""
176
+
177
+ def do_browse(q, tag):
178
+ return render_list(registry.list_skills(tag=None if tag=="all" else tag, query=q.strip() or None))
179
+
180
+ def load_detail(skill_id):
181
+ if not skill_id.strip(): return ("Enter a skill ID.", "", "", "", "")
182
+ s = registry.get_skill(skill_id.strip())
183
+ if not s: return (f"Skill '{skill_id}' not found.", "", "", "", "")
184
+ tags_html = " ".join(f'<span class="tag-pill">{t}</span>' for t in s.get("tags",[]))
185
+ meta = f"""<div class="skill-card">
186
+ <div class="skill-name">⚙ {s['name']} <span style="font-size:.65rem;color:#555570">[{s.get('runtime','python')}]</span></div>
187
+ <div style="font-family:'Space Mono',monospace;font-size:.68rem;color:#6b6b8a">v{s['version']} · by {s['author']} · ↓{s.get('downloads',0)}</div>
188
+ <div style="margin:.6rem 0;line-height:1.6">{s['description']}</div><div>{tags_html}</div>
189
+ {"<div style='margin-top:.4rem;font-size:.65rem;color:#ff4444;font-family:Space Mono,monospace'>⚠ env: "+', '.join(s['env_required'])+"</div>" if s.get('env_required') else ""}
190
+ {"<div style='margin-top:.4rem;font-size:.65rem;color:#60a5fa;font-family:Space Mono,monospace'>🔧 bins: "+', '.join(s.get('bins',[]))+"</div>" if s.get('bins') else ""}
191
+ <div style="margin-top:.75rem;border-top:1px solid #1e1e2e;padding-top:.5rem">
192
+ <strong style="color:#8b5cf6;font-size:.65rem;font-family:Space Mono,monospace">SCHEMA</strong>
193
+ <pre style="font-size:.65rem;color:#8080a0;margin-top:.3rem">{json.dumps(s.get('schema',{}),indent=2)}</pre></div></div>"""
194
+ code = s.get("code","")
195
+ instructions = s.get("instructions","(no markdown instructions)")
196
+ usage = f"""# ⚒️ Quick Start: {s['id']}
197
+ import requests, types
198
+ def bootstrap_forge(url="https://chris4k-agent-forge.hf.space"):
199
+ r = requests.get(f"{{url}}/api/v1/skills/forge_client/code")
200
+ m = types.ModuleType("forge_client")
201
+ exec(r.json()["code"], m.__dict__)
202
+ return m.ForgeClient(url)
203
+
204
+ forge = bootstrap_forge()
205
+ {s['id']} = forge.load("{s['id']}")
206
+ result = {s['id']}.execute() # see schema above
207
+ print(result)
208
+ """
209
+ edit_json = json.dumps({k:v for k,v in s.items() if k not in ("created_at","updated_at","downloads")}, indent=2)
210
+ return meta, code, instructions, usage, edit_json
211
+
212
+ def do_update(skill_id, edit_json):
213
+ if not skill_id.strip(): return "⚠ No skill loaded"
214
+ try: updates = json.loads(edit_json)
215
+ except json.JSONDecodeError as e: return f"❌ JSON error: {e}"
216
+ ok, msg = registry.update_skill(skill_id.strip(), updates)
217
+ return ("✅ " if ok else "❌ ") + msg
218
+
219
+ def do_delete(skill_id):
220
+ if not skill_id.strip(): return "⚠ No skill loaded"
221
+ ok, msg = registry.delete_skill(skill_id.strip())
222
+ return ("✅ " if ok else "❌ ") + msg
223
+
224
+ def publish_json(skill_json):
225
+ try: skill = json.loads(skill_json)
226
+ except json.JSONDecodeError as e: return f"❌ Invalid JSON: {e}"
227
+ ok, msg = registry.publish_skill(skill)
228
+ return ("✅ " if ok else "❌ ") + msg
229
+
230
+ def publish_md(skill_md):
231
+ skill, err = registry.parse_skill_md(skill_md)
232
+ if err: return f"❌ {err}"
233
+ ok, msg = registry.publish_skill(skill)
234
+ return ("✅ Published as '"+skill["id"]+"' — " if ok else "❌ ") + msg
235
+
236
+ def build_app():
237
+ with gr.Blocks(css=CSS, title="⚒️ FORGE") as demo:
238
+ gr.HTML("""<div class="forge-header">
239
+ <div class="forge-logo">⚒ FORGE</div>
240
+ <div class="forge-sub">Federated Open Registry for Generative Executables</div>
241
+ <div style="color:#4a4a6a;font-size:.78rem;margin-top:.6rem">Python · Node · SKILL.md · MCP · Claude Skills</div>
242
+ </div>""")
243
+ stats_html = gr.HTML(render_stats())
244
+
245
+ with gr.Tabs():
246
+ with gr.Tab("🔍 Browse"):
247
+ with gr.Row():
248
+ search_in = gr.Textbox(placeholder="search…", label="Search", scale=3)
249
+ tag_dd = gr.Dropdown(choices=["all"]+registry.get_all_tags(), value="all", label="Tag", scale=1)
250
+ srch_btn = gr.Button("Search", variant="primary", size="sm")
251
+ skills_out = gr.HTML(render_list(registry.list_skills()))
252
+ srch_btn.click(do_browse, [search_in, tag_dd], skills_out)
253
+ search_in.submit(do_browse, [search_in, tag_dd], skills_out)
254
+ tag_dd.change(do_browse, [search_in, tag_dd], skills_out)
255
+
256
+ with gr.Tab("⚙ Skill CRUD"):
257
+ skill_id_in = gr.Textbox(placeholder="e.g. calculator, claude_chat…", label="Skill ID")
258
+ with gr.Row():
259
+ load_btn = gr.Button("Load", variant="primary", size="sm")
260
+ update_btn = gr.Button("💾 Save Changes", size="sm")
261
+ delete_btn = gr.Button("🗑 Delete", size="sm")
262
+ crud_msg = gr.Textbox(label="Status", interactive=False, lines=1)
263
+ detail_meta = gr.HTML()
264
+ with gr.Tabs():
265
+ with gr.Tab("📄 Code"):
266
+ detail_code = gr.Code(language="python", label="Skill code")
267
+ with gr.Tab("📝 SKILL.md"):
268
+ detail_md = gr.Markdown()
269
+ with gr.Tab("🤖 Agent Usage"):
270
+ detail_use = gr.Code(language="python", label="Quick start")
271
+ with gr.Tab("✏️ Edit JSON"):
272
+ edit_json_in = gr.Code(language="json", label="Edit → Save Changes")
273
+ load_btn.click(load_detail, skill_id_in, [detail_meta, detail_code, detail_md, detail_use, edit_json_in])
274
+ skill_id_in.submit(load_detail, skill_id_in, [detail_meta, detail_code, detail_md, detail_use, edit_json_in])
275
+ update_btn.click(do_update, [skill_id_in, edit_json_in], crud_msg)
276
+ delete_btn.click(do_delete, skill_id_in, crud_msg)
277
+
278
+ with gr.Tab("📦 Publish"):
279
+ with gr.Tabs():
280
+ with gr.Tab("JSON (executable)"):
281
+ pub_json_in = gr.Code(value=PUBLISH_TEMPLATE, language="json", label="Skill JSON", lines=22)
282
+ gr.Button("⚒ Publish JSON Skill", variant="primary").click(publish_json, pub_json_in, gr.Textbox(label="Result", interactive=False))
283
+ with gr.Tab("SKILL.md (ClawHub compat)"):
284
+ pub_md_in = gr.Code(value=SKILL_MD_TEMPLATE, language="markdown", label="SKILL.md", lines=22)
285
+ gr.Button("⚒ Publish SKILL.md", variant="primary").click(publish_md, pub_md_in, gr.Textbox(label="Result", interactive=False))
286
+
287
+ with gr.Tab("🔌 MCP"):
288
+ gr.HTML("""<div style="padding:1rem">
289
+ <h2 style="color:#ff6b00;font-family:'Space Mono',monospace">⚒️ MCP Server</h2>
290
+ <p style="color:#6b6b8a">Connect Claude Desktop, Claude API, or any MCP client.</p>
291
+ <div class="mcp-badge"><strong>SSE (Claude Desktop)</strong><br>
292
+ <code>https://chris4k-agent-forge.hf.space/mcp/sse</code></div>
293
+ <div class="mcp-badge"><strong>JSON-RPC (API)</strong><br>
294
+ <code>https://chris4k-agent-forge.hf.space/mcp</code></div>
295
+ <h3 style="color:#8b5cf6;margin:1rem 0 .5rem;font-family:'Space Mono',monospace">Claude Desktop Config</h3>
296
+ <pre style="background:#0d0d1a;border:1px solid #1e1e2e;border-radius:6px;padding:1rem;font-size:.72rem;color:#00ff88">{"mcpServers":{"forge":{"command":"npx","args":["-y","mcp-remote","https://chris4k-agent-forge.hf.space/mcp/sse"]}}}</pre>
297
+ <h3 style="color:#8b5cf6;margin:1rem 0 .5rem;font-family:'Space Mono',monospace">MCP Tools</h3>
298
+ <div class="api-row">forge_list_skills · forge_get_skill · forge_get_code · forge_search · forge_publish_skill · forge_get_stats</div>
299
+ </div>""")
300
+
301
+ with gr.Tab("📡 API"):
302
+ gr.HTML("""<div style="font-family:'Space Mono',monospace;padding:1rem">
303
+ <h2 style="color:#ff6b00">REST API v2</h2>
304
+ <div class="api-row"><span class="m-get">GET</span> /api/v1/skills — list</div>
305
+ <div class="api-row"><span class="m-get">GET</span> /api/v1/skills/{id}/code — hot-load code</div>
306
+ <div class="api-row"><span class="m-get">GET</span> /api/v1/skills/{id}/download — .py file</div>
307
+ <div class="api-row"><span class="m-post">POST</span> /api/v1/skills — publish</div>
308
+ <div class="api-row"><span class="m-put">PUT</span> /api/v1/skills/{id} — update</div>
309
+ <div class="api-row"><span class="m-del">DEL</span> /api/v1/skills/{id} — delete</div>
310
+ <div class="api-row"><span class="m-get">GET</span> /api/v1/search?q= — search</div>
311
+ <div class="api-row"><span class="m-get">GET</span> /api/v1/manifest — full manifest</div>
312
+ <div class="api-row"><span class="m-get">GET</span> /mcp/sse — MCP SSE stream</div>
313
+ <div class="api-row"><span class="m-post">POST</span> /mcp — MCP JSON-RPC</div>
314
+ <pre style="background:#0d0d1a;border:1px solid #1e1e2e;border-radius:6px;padding:1rem;margin-top:1rem;font-size:.72rem;color:#00ff88">forge = bootstrap_forge()
315
+ calc = forge.load("calculator")
316
+ print(calc.execute(expression="2**32"))</pre></div>""")
317
+
318
+ with gr.Tab("📖 How-To"):
319
+ howto_path = Path(__file__).parent / "HOWTO.md"
320
+ howto_text = howto_path.read_text(encoding="utf-8") if howto_path.exists() else "HOWTO.md not found"
321
+ gr.Markdown(howto_text)
322
+
323
+ gr.HTML("""<div style="text-align:center;padding:1.5rem;border-top:1px solid #1e1e2e;margin-top:2rem;
324
+ font-family:'Space Mono',monospace;font-size:.65rem;color:#3a3a5a">
325
+ ⚒️ FORGE v2.0 · ki-fusion-labs.de · <a href="https://huggingface.co/Chris4K" style="color:#ff6b00">Chris4K</a> · MIT
326
+ </div>""")
327
+ return demo
328
+
329
+ demo = build_app()
330
+ app = gr.mount_gradio_app(api, demo, path="/")
331
+
332
+ if __name__ == "__main__":
333
+ demo.launch(server_name="0.0.0.0", server_port=7860)
claude_chat.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "claude_chat",
3
+ "name": "Claude Chat",
4
+ "version": "1.0.0",
5
+ "description": "Single-turn Q&A powered by Claude (Anthropic API). Agents can delegate reasoning, analysis, code generation, or any LLM task to Claude as a sub-skill. Requires ANTHROPIC_API_KEY env var.",
6
+ "author": "Chris4K",
7
+ "tags": ["claude", "anthropic", "llm", "reasoning", "ai"],
8
+ "runtime": "python",
9
+ "dependencies": ["anthropic"],
10
+ "env_required": ["ANTHROPIC_API_KEY"],
11
+ "schema": {
12
+ "input": {
13
+ "prompt": "str — the question or instruction for Claude",
14
+ "system": "str — optional system prompt (default: helpful assistant)",
15
+ "model": "str — model ID (default: claude-haiku-4-5-20251001)",
16
+ "max_tokens": "int — max response tokens (default: 1024)"
17
+ },
18
+ "output": {
19
+ "response": "str — Claude's response text",
20
+ "model": "str — model used",
21
+ "input_tokens": "int",
22
+ "output_tokens": "int"
23
+ }
24
+ },
25
+ "code": "import os\nfrom typing import Optional\n\n\ndef execute(\n prompt: str,\n system: str = \"You are a helpful AI assistant embedded in an agent skill.\",\n model: str = \"claude-haiku-4-5-20251001\",\n max_tokens: int = 1024,\n api_key: Optional[str] = None,\n) -> dict:\n \"\"\"\n Send a single-turn message to Claude and return the response.\n Used by agents to delegate LLM reasoning tasks.\n \"\"\"\n try:\n import anthropic\n except ImportError:\n return {\"error\": \"anthropic package not installed. Run: pip install anthropic\"}\n\n key = api_key or os.environ.get(\"ANTHROPIC_API_KEY\")\n if not key:\n return {\"error\": \"ANTHROPIC_API_KEY not set. Pass api_key= or set the env var.\"}\n\n if not prompt or not prompt.strip():\n return {\"error\": \"prompt cannot be empty\"}\n\n client = anthropic.Anthropic(api_key=key)\n\n try:\n msg = client.messages.create(\n model=model,\n max_tokens=max_tokens,\n system=system,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n response_text = msg.content[0].text if msg.content else \"\"\n return {\n \"response\": response_text,\n \"model\": model,\n \"input_tokens\": msg.usage.input_tokens,\n \"output_tokens\": msg.usage.output_tokens,\n \"stop_reason\": msg.stop_reason,\n }\n except anthropic.AuthenticationError:\n return {\"error\": \"Invalid ANTHROPIC_API_KEY\"}\n except anthropic.RateLimitError:\n return {\"error\": \"Rate limit hit — retry after a moment\"}\n except Exception as e:\n return {\"error\": str(e)}\n",
26
+ "downloads": 0,
27
+ "created_at": 1710000010
28
+ }
claude_extractor.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "claude_extractor",
3
+ "name": "Claude Extractor",
4
+ "version": "1.0.0",
5
+ "description": "Extract structured data from unstructured text using Claude. Provide a schema and raw text; Claude returns a JSON object matching the schema. Perfect for parsing documents, emails, web pages, or any free-form input.",
6
+ "author": "Chris4K",
7
+ "tags": ["claude", "anthropic", "extraction", "nlp", "structured-output", "json"],
8
+ "runtime": "python",
9
+ "dependencies": ["anthropic"],
10
+ "env_required": ["ANTHROPIC_API_KEY"],
11
+ "schema": {
12
+ "input": {
13
+ "text": "str — raw text to extract from",
14
+ "schema": "dict — JSON schema describing what to extract, e.g. {name: str, date: str, amount: float}",
15
+ "instructions": "str — optional extra extraction instructions"
16
+ },
17
+ "output": {
18
+ "extracted": "dict — extracted fields matching the schema",
19
+ "raw_response": "str — Claude's full response (for debugging)"
20
+ }
21
+ },
22
+ "code": "import os\nimport json\nimport re\nfrom typing import Optional\n\n\ndef execute(\n text: str,\n schema: dict,\n instructions: str = \"\",\n api_key: Optional[str] = None,\n model: str = \"claude-haiku-4-5-20251001\",\n) -> dict:\n \"\"\"\n Extract structured data from text using Claude.\n Returns a dict matching the provided schema.\n \"\"\"\n try:\n import anthropic\n except ImportError:\n return {\"error\": \"anthropic package not installed\"}\n\n key = api_key or os.environ.get(\"ANTHROPIC_API_KEY\")\n if not key:\n return {\"error\": \"ANTHROPIC_API_KEY not set\"}\n\n schema_str = json.dumps(schema, indent=2)\n extra = f\"\\n\\nAdditional instructions: {instructions}\" if instructions else \"\"\n\n system = (\n \"You are a precise data extraction assistant. \"\n \"Extract information from text and return ONLY valid JSON — no explanation, no markdown, no backticks. \"\n \"If a field cannot be found in the text, use null.\"\n )\n\n prompt = f\"\"\"Extract data from the following text and return a JSON object matching this schema:\n\n{schema_str}{extra}\n\nText to extract from:\n---\n{text[:4000]}\n---\n\nReturn ONLY the JSON object, nothing else.\"\"\"\n\n client = anthropic.Anthropic(api_key=key)\n try:\n msg = client.messages.create(\n model=model,\n max_tokens=1024,\n system=system,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n )\n raw = msg.content[0].text.strip() if msg.content else \"{}\"\n\n # Strip markdown fences if present\n raw_clean = re.sub(r\"^```[a-z]*\\n?\", \"\", raw).rstrip(\"```\").strip()\n\n try:\n extracted = json.loads(raw_clean)\n except json.JSONDecodeError:\n extracted = {\"_parse_error\": raw_clean}\n\n return {\n \"extracted\": extracted,\n \"raw_response\": raw,\n \"model\": model,\n }\n except Exception as e:\n return {\"error\": str(e)}\n",
23
+ "downloads": 0,
24
+ "created_at": 1710000011
25
+ }
mcp_server.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FORGE MCP Server
3
+ Exposes FORGE skills via Model Context Protocol (MCP)
4
+ so Claude Desktop, Claude API, and any MCP client can use them natively.
5
+
6
+ Endpoints:
7
+ GET /mcp/sse — SSE stream (for Claude Desktop / mcp-remote)
8
+ POST /mcp — JSON-RPC 2.0 (for direct API calls)
9
+ """
10
+ import json
11
+ import asyncio
12
+ import time
13
+ from typing import Any
14
+
15
+ from fastapi import FastAPI, Request
16
+ from fastapi.responses import StreamingResponse, JSONResponse
17
+
18
+ import skill_registry as registry
19
+
20
+ # ── MCP Tool Definitions ──────────────────────────────────────────
21
+
22
+ MCP_TOOLS = [
23
+ {
24
+ "name": "forge_list_skills",
25
+ "description": "List all skills available in the FORGE registry. Optionally filter by tag or search query.",
26
+ "inputSchema": {
27
+ "type": "object",
28
+ "properties": {
29
+ "tag": {"type": "string", "description": "Filter by tag (e.g. 'math', 'web', 'claude')"},
30
+ "query": {"type": "string", "description": "Text search across name, description, tags"},
31
+ },
32
+ },
33
+ },
34
+ {
35
+ "name": "forge_get_skill",
36
+ "description": "Get full metadata and executable code for a specific skill by ID.",
37
+ "inputSchema": {
38
+ "type": "object",
39
+ "properties": {
40
+ "skill_id": {"type": "string", "description": "The skill ID, e.g. 'calculator'"},
41
+ },
42
+ "required": ["skill_id"],
43
+ },
44
+ },
45
+ {
46
+ "name": "forge_get_code",
47
+ "description": "Get only the executable Python code for a skill. Returns minimal payload for agent hot-loading.",
48
+ "inputSchema": {
49
+ "type": "object",
50
+ "properties": {
51
+ "skill_id": {"type": "string", "description": "The skill ID"},
52
+ },
53
+ "required": ["skill_id"],
54
+ },
55
+ },
56
+ {
57
+ "name": "forge_search",
58
+ "description": "Search FORGE skills by keyword. Returns matching skills with descriptions and tags.",
59
+ "inputSchema": {
60
+ "type": "object",
61
+ "properties": {
62
+ "query": {"type": "string", "description": "Search query"},
63
+ },
64
+ "required": ["query"],
65
+ },
66
+ },
67
+ {
68
+ "name": "forge_get_stats",
69
+ "description": "Get FORGE registry statistics: total skills, downloads, top skills.",
70
+ "inputSchema": {
71
+ "type": "object",
72
+ "properties": {},
73
+ },
74
+ },
75
+ {
76
+ "name": "forge_publish_skill",
77
+ "description": "Publish a new skill to the FORGE registry. The skill must include id, name, version, description, author, tags, and code with a def execute() function.",
78
+ "inputSchema": {
79
+ "type": "object",
80
+ "properties": {
81
+ "skill_json": {
82
+ "type": "string",
83
+ "description": "JSON string of the skill document to publish",
84
+ },
85
+ },
86
+ "required": ["skill_json"],
87
+ },
88
+ },
89
+ ]
90
+
91
+ # ── Tool Dispatcher ───────────────────────────────────────────────
92
+
93
+ def dispatch_tool(name: str, args: dict) -> Any:
94
+ if name == "forge_list_skills":
95
+ tag = args.get("tag")
96
+ query = args.get("query")
97
+ skills = registry.list_skills(tag=tag, query=query)
98
+ return {
99
+ "count": len(skills),
100
+ "skills": [
101
+ {
102
+ "id": s["id"],
103
+ "name": s["name"],
104
+ "version": s["version"],
105
+ "description": s["description"],
106
+ "tags": s.get("tags", []),
107
+ "downloads": s.get("downloads", 0),
108
+ }
109
+ for s in skills
110
+ ],
111
+ }
112
+
113
+ elif name == "forge_get_skill":
114
+ skill_id = args.get("skill_id", "")
115
+ skill = registry.get_skill(skill_id)
116
+ if not skill:
117
+ return {"error": f"Skill '{skill_id}' not found"}
118
+ return skill
119
+
120
+ elif name == "forge_get_code":
121
+ skill_id = args.get("skill_id", "")
122
+ data = registry.get_skill_code(skill_id)
123
+ if not data:
124
+ return {"error": f"Skill '{skill_id}' not found"}
125
+ return data
126
+
127
+ elif name == "forge_search":
128
+ query = args.get("query", "")
129
+ skills = registry.list_skills(query=query)
130
+ return {
131
+ "query": query,
132
+ "count": len(skills),
133
+ "skills": [{"id": s["id"], "name": s["name"], "description": s["description"], "tags": s.get("tags", [])} for s in skills],
134
+ }
135
+
136
+ elif name == "forge_get_stats":
137
+ return registry.get_stats()
138
+
139
+ elif name == "forge_publish_skill":
140
+ try:
141
+ skill = json.loads(args.get("skill_json", "{}"))
142
+ except json.JSONDecodeError as e:
143
+ return {"error": f"Invalid JSON: {e}"}
144
+ ok, msg = registry.publish_skill(skill)
145
+ return {"ok": ok, "message": msg}
146
+
147
+ else:
148
+ return {"error": f"Unknown tool: {name}"}
149
+
150
+
151
+ # ── JSON-RPC 2.0 Handler ─────────────────────────────────────────
152
+
153
+ def handle_jsonrpc(request_body: dict) -> dict:
154
+ method = request_body.get("method", "")
155
+ params = request_body.get("params", {})
156
+ req_id = request_body.get("id", 1)
157
+
158
+ def ok(result):
159
+ return {"jsonrpc": "2.0", "id": req_id, "result": result}
160
+
161
+ def err(code, message):
162
+ return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}}
163
+
164
+ # MCP lifecycle
165
+ if method == "initialize":
166
+ return ok({
167
+ "protocolVersion": "2024-11-05",
168
+ "capabilities": {"tools": {"listChanged": False}},
169
+ "serverInfo": {
170
+ "name": "forge",
171
+ "version": "1.0.0",
172
+ "description": "FORGE — Federated Open Registry for Generative Executables. Skill artifactory for AI agents.",
173
+ },
174
+ })
175
+
176
+ elif method == "tools/list":
177
+ return ok({"tools": MCP_TOOLS})
178
+
179
+ elif method == "tools/call":
180
+ tool_name = params.get("name", "")
181
+ tool_args = params.get("arguments", {})
182
+ result = dispatch_tool(tool_name, tool_args)
183
+ return ok({
184
+ "content": [{"type": "text", "text": json.dumps(result, indent=2, ensure_ascii=False)}]
185
+ })
186
+
187
+ elif method == "ping":
188
+ return ok({})
189
+
190
+ else:
191
+ return err(-32601, f"Method not found: {method}")
192
+
193
+
194
+ # ── FastAPI Routes ────────────────────────────────────────────────
195
+
196
+ def register_mcp_routes(app: FastAPI):
197
+
198
+ @app.post("/mcp")
199
+ async def mcp_jsonrpc(request: Request):
200
+ try:
201
+ body = await request.json()
202
+ except Exception:
203
+ return JSONResponse(
204
+ {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error"}},
205
+ status_code=400,
206
+ )
207
+ response = handle_jsonrpc(body)
208
+ return JSONResponse(response)
209
+
210
+ @app.get("/mcp/sse")
211
+ async def mcp_sse(request: Request):
212
+ """
213
+ SSE endpoint for Claude Desktop / mcp-remote clients.
214
+ Streams MCP messages over Server-Sent Events.
215
+ """
216
+ async def event_stream():
217
+ # Send server info on connect
218
+ server_info = json.dumps({
219
+ "jsonrpc": "2.0",
220
+ "method": "notifications/initialized",
221
+ "params": {
222
+ "serverInfo": {
223
+ "name": "forge",
224
+ "version": "1.0.0",
225
+ },
226
+ "tools": MCP_TOOLS,
227
+ },
228
+ })
229
+ yield f"event: message\ndata: {server_info}\n\n"
230
+
231
+ # Keep alive
232
+ try:
233
+ while True:
234
+ if await request.is_disconnected():
235
+ break
236
+ yield f"event: ping\ndata: {json.dumps({'ts': int(time.time())})}\n\n"
237
+ await asyncio.sleep(15)
238
+ except asyncio.CancelledError:
239
+ pass
240
+
241
+ return StreamingResponse(
242
+ event_stream(),
243
+ media_type="text/event-stream",
244
+ headers={
245
+ "Cache-Control": "no-cache",
246
+ "X-Accel-Buffering": "no",
247
+ },
248
+ )
249
+
250
+ @app.get("/mcp/tools")
251
+ async def mcp_tools_list():
252
+ """Quick HTTP endpoint listing all MCP tools (non-SSE)."""
253
+ return JSONResponse({"tools": MCP_TOOLS, "count": len(MCP_TOOLS)})
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=4.44.0
2
+ fastapi>=0.111.0
3
+ uvicorn>=0.30.0
4
+ requests>=2.31.0
5
+ anthropic>=0.40.0
skill_registry.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FORGE — Skill Registry Core
3
+ Handles loading, validation, search, and serving of skills.
4
+ """
5
+ import json
6
+ import os
7
+ import re
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ SKILLS_DIR = Path(__file__).parent / "skills"
13
+
14
+ REQUIRED_FIELDS = {"id", "name", "version", "description", "author", "tags", "code"}
15
+
16
+ # ─────────────────────────────────────────────
17
+ # Internal helpers
18
+ # ─────────────────────────────────────────────
19
+
20
+ def _load_skill_file(path: Path) -> dict | None:
21
+ try:
22
+ data = json.loads(path.read_text(encoding="utf-8"))
23
+ if REQUIRED_FIELDS.issubset(data.keys()):
24
+ data.setdefault("downloads", 0)
25
+ data.setdefault("created_at", int(path.stat().st_mtime))
26
+ return data
27
+ except Exception:
28
+ pass
29
+ return None
30
+
31
+
32
+ def _save_skill_file(skill: dict):
33
+ path = SKILLS_DIR / f"{skill['id']}.json"
34
+ path.write_text(json.dumps(skill, indent=2, ensure_ascii=False), encoding="utf-8")
35
+
36
+
37
+ # ─────────────────────────────────────────────
38
+ # Public API
39
+ # ─────────────────────────────────────────────
40
+
41
+ def list_skills(tag: Optional[str] = None, query: Optional[str] = None) -> list[dict]:
42
+ """Return all skills (stripped of code), optionally filtered."""
43
+ skills = []
44
+ for path in sorted(SKILLS_DIR.glob("*.json")):
45
+ skill = _load_skill_file(path)
46
+ if skill is None:
47
+ continue
48
+ if tag and tag.lower() not in [t.lower() for t in skill.get("tags", [])]:
49
+ continue
50
+ if query:
51
+ q = query.lower()
52
+ searchable = f"{skill['name']} {skill['description']} {' '.join(skill.get('tags', []))}"
53
+ if q not in searchable.lower():
54
+ continue
55
+ skills.append({k: v for k, v in skill.items() if k != "code"})
56
+ return skills
57
+
58
+
59
+ def get_skill(skill_id: str) -> dict | None:
60
+ """Return full skill including code."""
61
+ path = SKILLS_DIR / f"{skill_id}.json"
62
+ if not path.exists():
63
+ return None
64
+ skill = _load_skill_file(path)
65
+ if skill:
66
+ # bump download count
67
+ skill["downloads"] = skill.get("downloads", 0) + 1
68
+ _save_skill_file(skill)
69
+ return skill
70
+
71
+
72
+ def get_skill_code(skill_id: str) -> dict | None:
73
+ """Return just the code + minimal meta for agent consumption."""
74
+ skill = get_skill(skill_id)
75
+ if not skill:
76
+ return None
77
+ return {
78
+ "id": skill["id"],
79
+ "version": skill["version"],
80
+ "dependencies": skill.get("dependencies", []),
81
+ "schema": skill.get("schema", {}),
82
+ "code": skill["code"],
83
+ }
84
+
85
+
86
+ def get_manifest() -> dict:
87
+ """Full manifest for agent bootstrap — includes code for all skills."""
88
+ skills = []
89
+ for path in sorted(SKILLS_DIR.glob("*.json")):
90
+ skill = _load_skill_file(path)
91
+ if skill:
92
+ skills.append(skill)
93
+ return {
94
+ "forge_version": "1.0.0",
95
+ "generated_at": int(time.time()),
96
+ "skill_count": len(skills),
97
+ "skills": skills,
98
+ }
99
+
100
+
101
+ def get_all_tags() -> list[str]:
102
+ tags: set[str] = set()
103
+ for path in SKILLS_DIR.glob("*.json"):
104
+ skill = _load_skill_file(path)
105
+ if skill:
106
+ tags.update(skill.get("tags", []))
107
+ return sorted(tags)
108
+
109
+
110
+ def publish_skill(skill: dict) -> tuple[bool, str]:
111
+ """Validate and save a new skill. Returns (ok, message)."""
112
+ missing = REQUIRED_FIELDS - set(skill.keys())
113
+ if missing:
114
+ return False, f"Missing required fields: {missing}"
115
+
116
+ # Validate ID format
117
+ if not re.match(r"^[a-z][a-z0-9_]{1,48}[a-z0-9]$", skill["id"]):
118
+ return False, "ID must be lowercase alphanumeric + underscores, 3-50 chars"
119
+
120
+ # Check execute() is present
121
+ if "def execute(" not in skill["code"]:
122
+ return False, "Skill code must contain a def execute(...) function"
123
+
124
+ # Don't overwrite existing (use versioning)
125
+ path = SKILLS_DIR / f"{skill['id']}.json"
126
+ if path.exists():
127
+ existing = _load_skill_file(path)
128
+ if existing and existing.get("version") == skill.get("version"):
129
+ return False, f"Version {skill['version']} of '{skill['id']}' already exists"
130
+
131
+ skill["created_at"] = int(time.time())
132
+ skill["downloads"] = 0
133
+ _save_skill_file(skill)
134
+ return True, f"Skill '{skill['id']}' v{skill['version']} published successfully"
135
+
136
+
137
+ def update_skill(skill_id: str, updates: dict) -> tuple[bool, str]:
138
+ """Update fields of an existing skill. Bumps version if code changes."""
139
+ path = SKILLS_DIR / f"{skill_id}.json"
140
+ if not path.exists():
141
+ return False, f"Skill '{skill_id}' not found"
142
+ existing = _load_skill_file(path)
143
+ if not existing:
144
+ return False, f"Failed to load skill '{skill_id}'"
145
+
146
+ # Prevent ID change via update
147
+ updates.pop("id", None)
148
+ updates.pop("created_at", None)
149
+ updates.pop("downloads", None)
150
+
151
+ existing.update(updates)
152
+ existing["updated_at"] = int(time.time())
153
+ _save_skill_file(existing)
154
+ return True, f"Skill '{skill_id}' updated"
155
+
156
+
157
+ def delete_skill(skill_id: str) -> tuple[bool, str]:
158
+ """Delete a skill from the registry."""
159
+ path = SKILLS_DIR / f"{skill_id}.json"
160
+ if not path.exists():
161
+ return False, f"Skill '{skill_id}' not found"
162
+ # Archive before deletion
163
+ archive = SKILLS_DIR / f"{skill_id}.deleted.json"
164
+ path.rename(archive)
165
+ return True, f"Skill '{skill_id}' deleted"
166
+
167
+
168
+ def parse_skill_md(content: str) -> tuple[dict | None, str]:
169
+ """
170
+ Parse a SKILL.md file (ClawHub-compatible format) into a FORGE skill dict.
171
+ Returns (skill_dict, error_message).
172
+ SKILL.md frontmatter (YAML between --- delimiters) + markdown body.
173
+ """
174
+ import re
175
+
176
+ # Extract YAML frontmatter
177
+ fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)", content, re.DOTALL)
178
+ if not fm_match:
179
+ return None, "No YAML frontmatter found. SKILL.md must start with ---"
180
+
181
+ yaml_str, body = fm_match.group(1), fm_match.group(2)
182
+
183
+ # Simple YAML parser (avoid pyyaml dependency)
184
+ meta = {}
185
+ for line in yaml_str.splitlines():
186
+ if ":" in line:
187
+ k, _, v = line.partition(":")
188
+ k = k.strip()
189
+ v = v.strip().strip('"').strip("'")
190
+ # Handle list values: [a, b, c]
191
+ if v.startswith("[") and v.endswith("]"):
192
+ v = [x.strip().strip('"').strip("'") for x in v[1:-1].split(",") if x.strip()]
193
+ meta[k] = v
194
+
195
+ required = {"name", "version", "description"}
196
+ missing = required - set(meta.keys())
197
+ if missing:
198
+ return None, f"Missing required frontmatter fields: {missing}"
199
+
200
+ # Build skill ID from name
201
+ import re as _re
202
+ skill_id = _re.sub(r"[^a-z0-9_]", "_", meta["name"].lower()).strip("_")
203
+
204
+ skill = {
205
+ "id": meta.get("id", skill_id),
206
+ "name": meta["name"],
207
+ "version": meta.get("version", "1.0.0"),
208
+ "description": meta["description"],
209
+ "author": meta.get("author", "unknown"),
210
+ "tags": meta.get("tags", []) if isinstance(meta.get("tags"), list) else [meta.get("tags", "instructions")],
211
+ "runtime": meta.get("runtime", "instructions"),
212
+ "dependencies": [],
213
+ "schema": {},
214
+ # SKILL.md skills store markdown body as "instructions" (not executable code)
215
+ "instructions": body.strip(),
216
+ # Minimal execute() wrapper that returns the instructions
217
+ "code": f'def execute(**kwargs) -> dict:\n """SKILL.md instruction skill — returns the instructions for the agent."""\n return {{"instructions": """{body.strip()[:500]}""", "runtime": "instructions"}}',
218
+ }
219
+ return skill, ""
220
+
221
+
222
+ def get_stats() -> dict:
223
+ skills = list_skills()
224
+ total_downloads = sum(s.get("downloads", 0) for s in skills)
225
+ all_tags = get_all_tags()
226
+ return {
227
+ "total_skills": len(skills),
228
+ "total_downloads": total_downloads,
229
+ "total_tags": len(all_tags),
230
+ "top_skills": sorted(skills, key=lambda x: x.get("downloads", 0), reverse=True)[:5],
231
+ }