
Universal Skills Marketplace
- 1 installs
- Updated June 1, 2026
- ahump20/claudopenai
universal-skills-marketplace is a Claude Code skill for building an unofficial MCP server that bridges Claude Code and OpenAI Codex skill catalogs into one searchable registry.
About
universal-skills-marketplace guides building an unofficial MCP server that bridges Claude Code (.claude-plugin) and OpenAI Codex (.codex-plugin) skill catalogs. It lets a session search one registry for skills from either ecosystem, returns quality scores plus install commands for both CLIs, and loads them on demand. The skill routes across 12 reference docs covering MCP tool design, a manifest translator, a D1 catalog schema, R2 storage, and a Cloudflare Workers backend. It is explicitly unofficial and not affiliated with Anthropic or OpenAI.
- Builds an MCP server bridging Claude Code and Codex skill catalogs
- Includes a manifest translator between claude-plugin and codex-plugin formats
- Backend on Cloudflare Workers with D1 catalog and R2 content storage
Universal Skills Marketplace by the numbers
- 1 all-time installs (skills.sh)
- Ranked #644 of 782 Skill Development skills by installs in the Skillselion catalog
- Data as of Jul 7, 2026 (Skillselion catalog sync)
universal-skills-marketplace capabilities & compatibility
- Capabilities
- mcp server development · manifest translation · skill registry · quality scoring
- Works with
- github · cloudflare
- Use cases
- orchestration · research
- Runs
- Remote server
- Pricing
- Free
What universal-skills-marketplace says it does
Context7 for skills, not docs. One MCP server. Two ecosystems. Unofficial.
search one registry for skills originating in either ecosystem, get them back with quality scores + install commands for both CLIs, and load them on demand.
Translator loudness** — no silent field drops; every lossy translation logs + shims.
npx skills add https://github.com/ahump20/claudopenai --skill universal-skills-marketplaceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| Last updated | June 1, 2026 |
| Repository | ahump20/claudopenai ↗ |
What it does
Build or extend an MCP server that bridges Claude Code and Codex skill catalogs with a manifest translator.
Who is it for?
Developers building the ClaudOpenAI marketplace MCP server, translator, or Cloudflare backend.
When should I use this skill?
You are building, extending, or consuming a cross-ecosystem skills marketplace MCP server that bridges Claude Code and Codex plugin catalogs.
What you get
An MCP server registry that searches skills across both ecosystems and returns quality scores plus dual-CLI install commands.
- MCP server package
- manifest translator
- D1 catalog schema
By the numbers
- 12 reference docs in the routing table
- quality scoring on a 0-100 rubric
- SKILL.md capped at 100 lines by validate.sh
Files
Universal Skills Marketplace
Context7 for skills, not docs. One MCP server. Two ecosystems. Unofficial.
Mission
Build, maintain, or extend an unofficial bridge that lets any Claude Code or OpenAI Codex session search one registry for skills originating in either ecosystem, get them back with quality scores + install commands for both CLIs, and load them on demand.
Not affiliated with Anthropic or OpenAI. Identity details in `references/00-architecture-overview.md`.
Routing table — pick the right reference
| I want to… | Primary reference | Supporting asset / script |
|---|---|---|
| Understand the whole system | references/00-architecture-overview.md | assets/diagrams/system-topology.png |
| Learn the SKILL.md open standard | references/01-agentskills-io-spec-walkthrough.md | assets/templates/standalone-SKILL.md.template |
| Author a Claude plugin | references/02-claude-plugin-format.md | assets/templates/claude-plugin.json.template, assets/real-examples/context7-plugin.json |
| Author a Codex plugin | references/03-codex-plugin-format.md | assets/templates/codex-plugin.json.template, assets/real-examples/openai-canva-plugin.json |
| Design MCP tools | references/04-mcp-tool-design.md | assets/templates/mcp-server-index.ts.template |
| Deploy Cloudflare Workers | references/05-cloudflare-workers-playbook.md | assets/templates/wrangler.toml.*.template |
| Design the D1 catalog | references/06-d1-schema-design.md | assets/templates/d1-schema.sql.template |
| Plan R2 content storage | references/07-r2-storage-patterns.md | — |
| Build the upstream indexer | references/08-github-indexer-design.md | scripts/fetch-upstream-catalog.sh |
| Score skill quality 0-100 | references/09-quality-scoring-rubric.md | assets/fixtures/known-good/ |
| Copy what Context7 got right | references/10-context7-architectural-analysis.md | assets/real-examples/context7-*.json |
| Translate between manifest formats | references/11-manifest-translator-algorithm.md | scripts/test-translator.ts, assets/fixtures/lossy-cases/ |
| Verify end-to-end | references/12-verification-playbook.md | scripts/validate.sh |
Phase dispatcher
- Planning / just arrived → read
00-architecture-overview.md, then10-context7-architectural-analysis.md, thenreferences/11-manifest-translator-algorithm.md(the three anchoring docs) - Phase 0 — spikes complete → evidence in
/docs/spikes/(upstream URLs verified, Codex schema derived, rate limits mapped, iCloud strategy documented) - Phase 1 — schemas + this skill → you are here; running this skill IS Phase 1's deliverable
- Phase 2 — npm package →
packages/mcp-server/implementation driven by04-mcp-tool-design.md+11-manifest-translator-algorithm.md - Phase 3 — Cloudflare backend → three Workers driven by
05-cloudflare-workers-playbook.md+06-d1-schema-design.md+08-github-indexer-design.md
Hard rules (inherited from ../../CLAUDE.md)
1. Anti-Fabrication — if a URL, field, or behavior isn't verified against installed plugins or a live upstream, it does NOT ship. 2. Anti-Mock-Data — no hardcoded skills anywhere except assets/fixtures/ and packages/*/tests/fixtures/. 3. Verification — "build passed" ≠ acceptance; curl + fresh Claude Code AND Codex session required. 4. Translator loudness — no silent field drops; every lossy translation logs + shims. 5. context7 fidelity — plugin wrapper stays trivial; all logic in the npm package. 6. Identity — unofficial, independent, community project. Never imply endorsement from either company.
Run before any commit
bash skills/universal-skills-marketplace/scripts/validate.shMust exit 0. Checks: frontmatter valid, SKILL.md ≤100 lines, all 12 references present, all templates parse, no broken intra-skill links.
%% Indexer cycle — runs every 6h via cron (Mermaid source)
sequenceDiagram
participant Cron as Cloudflare Cron
participant Worker as universal-skills-indexer
participant KV as KV: INDEXER_STATE
participant GH as GitHub API
participant Norm as translator.toCanonical
participant Score as scorer.scoreSkill
participant D1 as D1: skills
participant R2 as R2: content
Cron->>Worker: scheduled("0 */6 * * *")
loop For each upstream source
Worker->>KV: get("sha:<source>")
KV-->>Worker: lastSha
Worker->>GH: GET /repos/<src>/branches/<default>
GH-->>Worker: { commit.sha: headSha }
alt headSha === lastSha
Worker->>KV: put("last_checked:<source>", now)
Note over Worker: skip — unchanged
else changed
Worker->>GH: GET /compare/lastSha...headSha
GH-->>Worker: { files: [...] }
loop For each SKILL.md / plugin.json file
Worker->>GH: GET /contents/<path>?ref=<sha>
GH-->>Worker: base64 content
Worker->>Norm: normalize(path, content)
Norm-->>Worker: CanonicalSkill
Worker->>Score: scoreSkill(canonical)
Score-->>Worker: 0-100
Worker->>D1: UPSERT INTO skills ...
Worker->>R2: PUT skills/<id>/<ver>/*
end
Worker->>KV: put("sha:<source>", headSha)
end
end
Worker->>Worker: console.log cycle summary
%% ClaudOpenAI — system topology (Mermaid source)
%% Render: https://mermaid.live OR `npx -y @mermaid-js/mermaid-cli -i system-topology.mmd -o system-topology.png`
flowchart TB
subgraph Upstreams["🔷 Upstream Repos (9 verified — Spike S1)"]
R1[anthropics/claude-plugins-official<br/>16.8K⭐]
R2[anthropics/skills<br/>115.9K⭐]
R3[anthropics/knowledge-work-plugins<br/>11.1K⭐ Apache-2.0]
R4[openai/codex<br/>74.8K⭐ Apache-2.0]
R5[openai/codex-plugin-cc<br/>13.8K⭐ bridge prior art]
R6[openai/skills<br/>16.7K⭐]
R7[openai/swarm<br/>21.3K⭐ dormant]
R8[openai/openai-agents-python<br/>20.7K⭐ MIT]
R9[openai/plugins<br/>778⭐]
end
subgraph CFWorkers["🟠 Cloudflare Workers"]
Indexer[universal-skills-indexer<br/>cron 0 */6 * * *<br/>GITHUB_TOKEN]
API[universal-skills-api<br/>api.marketplace.blazesportsintel.com]
Bridge[universal-skills-bridge<br/>marketplace + registry<br/>.blazesportsintel.com]
end
subgraph Storage["💾 Storage"]
D1[(D1: universal-skills<br/>skills, versions, refs, sources, fts5)]
R2B[R2: universal-skills-content<br/>skills/id/version/*]
KV1[KV: CACHE]
KV2[KV: RATE_LIMIT]
KV3[KV: INDEXER_STATE]
end
subgraph Clients["👥 Clients"]
CC[Claude Code<br/>~/.claude/mcp.json]
CX[OpenAI Codex<br/>~/.codex/config.toml]
NPM[Clean-install npx<br/>@blazesportsintel/universal-skills-mcp]
end
Upstreams -- "git ls-remote + sparse-clone" --> Indexer
Indexer -- "UPSERT / write" --> D1
Indexer -- "content.tgz" --> R2B
Indexer -- "cursor/etag" --> KV3
D1 -- "SELECT" --> API
D1 -- "SELECT" --> Bridge
R2B -- "presigned URLs" --> API
KV1 -- "cache hit/miss" --> API
KV2 -- "rate limit" --> API
CC -- "POST /mcp (JSON-RPC 2.0)" --> API
CX -- "POST /mcp" --> API
NPM -- "stdio" --> API
CC -- "/plugin marketplace add" --> Bridge
CX -- "catalog sync" --> Bridge
%% Manifest translator — bidirectional flow via canonical middle (Mermaid source)
flowchart LR
subgraph Inputs
CP[.claude-plugin/<br/>plugin.json +<br/>convention dirs]
CXP[.codex-plugin/<br/>plugin.json +<br/>interface{} + apps]
SSM[Standalone<br/>SKILL.md +<br/>refs/scripts/assets]
end
subgraph Canonical["🔷 CanonicalSkill (zod)"]
C[id, origin, type, name, description<br/>skills[], mcpServers, commands<br/>hooks, agents, apps, interface<br/>ecosystem_extensions, translation_log]
end
subgraph Outputs
CPO[Claude plugin dir<br/>+ codex_ecosystem.json<br/>+ HTML-comment shims]
CXPO[Codex plugin dir<br/>+ claude_ecosystem.json<br/>+ .app.json + interface{}]
SSMO[Standalone SKILL.md<br/>(only if single-skill plugin)]
end
CP -- "toCanonical('claude')" --> C
CXP -- "toCanonical('codex')" --> C
SSM -- "toCanonical('standalone')" --> C
C -- "fromCanonical('claude')" --> CPO
C -- "fromCanonical('codex')" --> CXPO
C -- "fromCanonical('standalone')" --> SSMO
C -- "translation_log<br/>[{level, field, shim_generated}]" --> Log[Log entries:<br/>info / warning / lossy]
{
"$comment": "Intentional low-quality stub. Should score <30 per scorer rubric. Used by scorer.test.ts to verify minimum thresholds.",
"id": "random-user/stub",
"origin": {
"ecosystem": "standalone",
"sourcePath": "stub/",
"sourceSha": null,
"repo": "random-user/stub-skills",
"discoveredAt": "2026-04-12T00:00:00Z"
},
"type": "skill",
"name": "stub",
"description": "does stuff",
"skills": [
{
"path": ".",
"name": "stub",
"description": "does stuff",
"frontmatter": { "name": "stub", "description": "does stuff" },
"body": "# Stub\n\nNot much here.",
"references": [],
"scripts": [],
"assets": []
}
],
"mcpServers": {},
"commands": [],
"agents": [],
"apps": {},
"ecosystem_extensions": { "claude": {}, "codex": {} },
"translation_log": [],
"quality_score": 20,
"quality_breakdown": {
"has_references": 0,
"has_scripts": 0,
"description_quality": 0,
"has_examples": 0,
"passes_validation": 20,
"star_weight": 0,
"has_tests": 0
},
"last_verified": "2026-04-12T00:00:00Z"
}
{
"$comment": "Known-good fixture: synthetic canonical record for an 'openai-skills/pdf' skill, designed to score >=70 per prompt line 147. Used by scorer.test.ts.",
"id": "openai-skills/pdf",
"origin": {
"ecosystem": "standalone",
"sourcePath": ".curated/pdf",
"sourceSha": "abc123def456",
"repo": "openai/skills",
"discoveredAt": "2026-04-12T00:00:00Z"
},
"type": "skill",
"name": "pdf",
"description": "Use when processing, extracting, or generating PDF documents. Triggers on 'pdf', 'pdf extract', 'pdf fill form', 'read pdf', 'pdf table extract'. Covers text extraction, metadata parsing, form filling, and PDF generation via PyPDF2/reportlab.",
"version": "0.3.1",
"author": { "name": "OpenAI", "url": "https://openai.com" },
"license": "MIT",
"keywords": ["pdf", "extraction", "forms", "text", "parsing"],
"tags": ["pdf", "document-processing", "extraction"],
"category": "Productivity",
"skills": [
{
"path": ".",
"name": "pdf",
"description": "Use when processing PDFs — extraction, form filling, generation.",
"version": "0.3.1",
"frontmatter": {
"name": "pdf",
"description": "Use when processing, extracting, or generating PDF documents. Triggers on 'pdf', 'pdf extract', 'pdf fill form'.",
"version": "0.3.1"
},
"body": "# PDF Skill\n\n## Examples\n\n### Example 1 — text extraction\n\n...\n\n### Example 2 — form filling\n\n...",
"references": [
"references/01-text-extraction.md",
"references/02-form-filling.md",
"references/03-pdf-generation.md",
"references/04-tables-and-layout.md"
],
"scripts": [
"scripts/validate.sh",
"scripts/test-extraction.py",
"scripts/extract-cli.py"
],
"assets": [
"assets/sample-forms/w9.pdf",
"assets/sample-invoices/inv-001.pdf"
]
}
],
"mcpServers": {},
"commands": [],
"hooks": null,
"agents": [],
"apps": {},
"interface": null,
"ecosystem_extensions": { "claude": {}, "codex": {} },
"translation_log": [],
"quality_score": 80,
"quality_breakdown": {
"has_references": 10,
"has_scripts": 10,
"description_quality": 5,
"has_examples": 10,
"passes_validation": 20,
"star_weight": 15,
"has_tests": 20
},
"compatibility_flags": {
"claude": { "compatible": true, "min_version": null, "lossy_fields": [] },
"codex": { "compatible": true, "min_version": null, "lossy_fields": [] }
},
"content_hash": "sha256:deadbeef...",
"last_verified": "2026-04-12T00:00:00Z",
"install_count": 0
}
{
"$comment": "Claude plugin with SKILL.md using allowed-tools frontmatter key. Round-trip Claude->Codex->Claude should preserve allowed-tools via .claude-plugin/codex_ecosystem.json sidecar. Translator writes log entry { level: 'lossy', field: 'allowed-tools', shim_generated: 'html-comment' } on Codex leg.",
"plugin_json": {
"name": "imessage",
"description": "Access iMessage conversations and send messages",
"author": { "name": "Austin Humphrey" }
},
"skill_md_frontmatter": {
"name": "access",
"description": "Use when accessing iMessage DB.",
"allowed-tools": ["Read"],
"user-invocable": true,
"disable-model-invocation": false
},
"skill_md_body": "# iMessage Access\n\nInstructions for reading iMessage DB.",
"expected_translation_log_after_roundtrip": [
{
"level": "lossy",
"field": "skills.access.frontmatter.allowed-tools",
"message": "Claude-only frontmatter key preserved as HTML comment",
"shim_generated": "<!-- translator-shim: field=allowed-tools value=[Read]; restored from codex_ecosystem.json on round-trip -->"
},
{
"level": "info",
"field": "skills.access.frontmatter.user-invocable",
"message": "Claude-only frontmatter key stashed in sidecar"
}
]
}
{
"$comment": "Codex plugin with full interface{} block + apps pointer. Round-trip Codex->Claude->Codex should preserve all interface fields + connector IDs via .claude-plugin/codex_ecosystem.json.",
"codex_plugin_json": {
"name": "canva",
"version": "1.0.0",
"description": "Search, create, edit designs",
"author": { "url": "https://www.canva.com" },
"homepage": "https://www.canva.com",
"repository": "https://github.com/openai/plugins",
"license": "MIT",
"keywords": [],
"skills": "./skills/",
"apps": "./.app.json",
"interface": {
"displayName": "Canva",
"shortDescription": "Search, create, edit designs",
"longDescription": "Search, create, edit designs",
"category": "Productivity",
"capabilities": [],
"websiteURL": "https://www.canva.com",
"privacyPolicyURL": "https://www.canva.com/policies/privacy-policy/",
"termsOfServiceURL": "https://www.canva.com/policies/terms-of-use/",
"defaultPrompt": ["Create or adapt a Canva design"],
"composerIcon": "./assets/app-icon.png",
"logo": "./assets/app-icon.png",
"screenshots": []
}
},
"app_json": { "apps": { "canva": { "id": "connector_c0ffee" } } },
"expected_translation_log_on_codex_to_claude": [
{
"level": "lossy",
"field": "interface",
"message": "Codex catalog metadata preserved in .claude-plugin/codex_ecosystem.json",
"shim_generated": ".claude-plugin/codex_ecosystem.json"
},
{
"level": "lossy",
"field": "apps",
"message": "Codex connector registry preserved in .claude-plugin/codex_ecosystem.json.apps; notes file emitted",
"shim_generated": "docs/codex-apps.notes.md"
},
{
"level": "info",
"field": "homepage,repository,license,keywords",
"message": "Codex top-level metadata preserved in codex_ecosystem.json"
}
]
}
Invalid YAML
Frontmatter opens with ---, closes with ---, but the YAML inside has unclosed brackets.
frontmatter.ts should throw YAMLParseError with line number pointing to the unclosed [.
A Skill With No Frontmatter
This file has no YAML frontmatter block at the top. It is intentionally malformed.
frontmatter.ts should throw InvalidFrontmatterError with message: "No YAML frontmatter found in SKILL.md".
validate.sh should reject this file with exit code 1.
Fixtures
Test fixtures for the translator, scorer, and frontmatter parser. Referenced from packages/mcp-server/tests/ via relative symlink.
Subdirectories
known-good/
Valid canonical records + SKILL.md documents with known expected behavior.
openai-pdf-skill.canonical.json— synthetic high-quality PDF skill;scoreSkill()expected >= 70 (prompt line 147 invariant)minimal-stub.canonical.json— intentional low-quality stub; expected score < 30
lossy-cases/
Inputs that intentionally exercise lossy translation paths. Each fixture declares the expected translation_log entries in a companion field.
claude-with-allowed-tools.json— Claude frontmatter withallowed-tools; must round-trip viacodex_ecosystem.jsonsidecarcodex-with-interface-and-apps.json— Codex plugin with fullinterface{}block + connectorapps; must round-trip via sidecar
malformed/
Inputs that should fail validation with typed errors.
missing-frontmatter.md— no YAML frontmatter;InvalidFrontmatterErrorinvalid-yaml.md— unclosed bracket in frontmatter;YAMLParseError
Adding new fixtures
1. Drop the file into the right subdirectory 2. Update packages/mcp-server/tests/fixtures.ts to enumerate it 3. Add a corresponding test case 4. npm run test:unit should pass
Scope is deliberately additive — existing fixtures are ground truth; don't edit without updating expected behavior in corresponding tests.
College Baseball Intelligence Agent
The full 330-team D1 landscape — every conference, every program, every storyline that mainstream media ignores. ESPN covers 15 programs. BSI covers the other 315 with the same analytical rigor.
Core Rule
Every program gets the same methodology. No prestige bias. The metric works the same at Dallas Baptist as it does at Vanderbilt.
Routing
- Texas-only depth →
texas-longhorns-baseball-intelligence - Live game coverage →
bsi-gameday-ops - Texas + other teams (comparative) → stays here
- Everything else → stays here
Workflows (Mode Selection)
See references/mode-research.md for deep-dive investigations. See references/mode-analytics.md for statistical analysis and comparisons. See references/mode-editorial.md for BSI-voice content production. See references/mode-feature-dev.md for BSI platform features and data pipelines. See references/mode-scouting.md for program evaluation and opponent prep. See references/mode-postseason.md for NCAA Tournament selection and bracket analysis.
Tool Contract
See references/tool-registry.md for full MCP tool docs and name mapping. See references/team-slug-directory.md for 330-team slug reference.
Season Context
See references/season-state-calendar.md before any current-season analysis. See references/conference-profiles.md for conference intelligence.
Supporting References
See references/analytics-framework.md for metric interpretation hierarchy. See references/stat-glossary.md for metric definitions. See references/scouting-framework.md for 8-dimension program evaluation. See references/postseason-framework.md for selection/seeding methodology. See references/editorial-voice.md for BSI writing standards. See references/platform-architecture.md for BSI tech stack patterns. See references/research-protocol.md for multi-source research methodology.
Non-Negotiables
- Never fabricate stats, records, rosters, scores, or player data
- Every current-season claim requires tool verification with source + timestamp
- Tool failure → state what's unknown, don't fill gaps with inference
- Separate verified fact, analytical inference, and editorial opinion
- Cover every program with equal analytical rigor
Ship Gate
- [ ] Every statistical claim verified via tool or flagged as unverified
- [ ] Source and timestamp included for live data
- [ ] Season-state lens applied
- [ ] No prestige bias in methodology
- [ ] Unknowns declared, not papered over
{
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"]
}
}
{
"name": "context7",
"description": "Upstash Context7 MCP server for up-to-date documentation lookup. Pull version-specific documentation and code examples directly from source repositories into your LLM context.",
"author": {
"name": "Upstash"
}
}
{
"name": "canva",
"version": "1.0.0",
"description": "Search, create, edit designs",
"author": {
"url": "https://www.canva.com"
},
"homepage": "https://www.canva.com",
"repository": "https://github.com/openai/plugins",
"license": "MIT",
"keywords": [],
"skills": "./skills/",
"apps": "./.app.json",
"interface": {
"displayName": "Canva",
"shortDescription": "Search, create, edit designs",
"longDescription": "Search, create, edit designs",
"category": "Productivity",
"capabilities": [],
"websiteURL": "https://www.canva.com",
"privacyPolicyURL": "https://www.canva.com/policies/privacy-policy/",
"termsOfServiceURL": "https://www.canva.com/policies/terms-of-use/",
"defaultPrompt": [
"Create or adapt a Canva design for presentations, resized social variants, or translated versions"
],
"screenshots": [],
"composerIcon": "./assets/app-icon.png",
"logo": "./assets/app-icon.png"
}
}
{
"mcpServers": {
"cloudflare-api": {
"type": "http",
"url": "https://mcp.cloudflare.com/mcp",
"note": "Official Cloudflare API MCP server. Uses OAuth on first connection, with optional bearer-token auth for automation. Provides token-efficient access to the Cloudflare API via search() and execute()."
}
}
}
{
"name": "cloudflare",
"version": "0.1.0",
"description": "Cloudflare platform plugin with curated skills for Workers, Wrangler, and Agents SDK plus the official Cloudflare API MCP server.",
"author": {
"name": "Cloudflare",
"url": "https://workers.cloudflare.com/"
},
"homepage": "https://workers.cloudflare.com/",
"repository": "https://github.com/openai/plugins",
"license": "MIT",
"keywords": [
"cloudflare",
"workflow",
"deployment",
"edge-functions",
"edge",
"analytics",
"wrangler",
"agents-sdk",
"serverless",
"ai-gateway"
],
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"interface": {
"displayName": "Cloudflare",
"shortDescription": "Cloudflare platform guidance with official MCP",
"longDescription": "Bring Cloudflare platform guidance into Codex with curated skills for the broader platform, Wrangler CLI, and the Agents SDK, plus the official Cloudflare API MCP server for authenticated access to live account data and workflows across Workers, Pages, storage, AI, networking, security, and analytics services.",
"developerName": "Cloudflare",
"category": "Coding",
"capabilities": [
"Interactive",
"Write"
],
"websiteURL": "https://workers.cloudflare.com/",
"privacyPolicyURL": "https://www.cloudflare.com/privacypolicy/",
"termsOfServiceURL": "https://www.cloudflare.com/website-terms/",
"defaultPrompt": [
"Help me choose the right Cloudflare product, configure Wrangler, and use Cloudflare MCP to inspect or deploy this project"
],
"composerIcon": "./assets/cloudflare-small.svg",
"logo": "./assets/cloudflare.png",
"screenshots": [],
"brandColor": "#F48120"
}
}
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "./scripts/post_write_figma_parity_check.sh"
}
]
}
]
}
}
{
"name": "github",
"version": "0.1.0",
"description": "Inspect repositories, triage pull requests and issues, debug CI, and publish changes through a hybrid GitHub connector and CLI workflow.",
"author": {
"name": "OpenAI",
"email": "support@openai.com",
"url": "https://openai.com/"
},
"homepage": "https://github.com/",
"repository": "https://github.com/openai/plugins",
"license": "MIT",
"keywords": [
"github",
"pull-request",
"code-review",
"issues",
"ci",
"actions"
],
"skills": "./skills/",
"apps": "./.app.json",
"interface": {
"displayName": "GitHub",
"shortDescription": "Triage PRs, issues, CI, and publish flows",
"longDescription": "Use GitHub to inspect repositories, review pull requests, address feedback, debug failing Actions checks, and prepare code changes for review through a connector-first workflow with targeted CLI fallbacks.",
"developerName": "OpenAI",
"category": "Coding",
"capabilities": [
"Interactive",
"Write"
],
"websiteURL": "https://github.com/",
"privacyPolicyURL": "https://docs.github.com/site-policy/privacy-policies/github-general-privacy-statement",
"termsOfServiceURL": "https://docs.github.com/en/site-policy/github-terms/github-terms-of-service",
"defaultPrompt": [
"Inspect PRs, triage issues, debug failing checks, and prepare code changes for review"
],
"composerIcon": "./assets/github-small.svg",
"logo": "./assets/github.png",
"screenshots": [],
"brandColor": "#24292F"
}
}
Real Examples
Verbatim copies of real manifest + hook files from the user's installed plugin cache. Each file includes a // from: annotation comment where the format permits (JSON files rely on this README for provenance).
Source annotations
| File | Source path | Purpose |
|---|---|---|
context7-plugin.json | ~/.claude/plugins/marketplaces/claude-plugins-official/external_plugins/context7/.claude-plugin/plugin.json | The 7-line trivial Claude wrapper we emulate |
context7-mcp.json | ~/.claude/plugins/marketplaces/claude-plugins-official/external_plugins/context7/.mcp.json | The 5-line Claude .mcp.json pointing to an npm package |
openai-canva-plugin.json | ~/.codex/plugins/cache/openai-curated/canva/fb0a18376bcd9f2604047fbe7459ec5aed70c64b/.codex-plugin/plugin.json | Full Codex manifest with interface{} block, apps pointer, minimal author ({url} only), no mcpServers/developerName/brandColor |
openai-cloudflare-plugin.json | ~/.codex/plugins/cache/openai-curated/cloudflare/fb0a18376bcd9f2604047fbe7459ec5aed70c64b/.codex-plugin/plugin.json | Codex manifest with mcpServers pointer, full author object |
openai-cloudflare-mcp.json | ~/.codex/plugins/cache/openai-curated/cloudflare/fb0a18376bcd9f2604047fbe7459ec5aed70c64b/.mcp.json | WRAPPED-shape Codex .mcp.json — note the outer {"mcpServers": {...}} wrapper |
openai-github-plugin.json | ~/.codex/plugins/cache/openai-curated/github/fb0a18376bcd9f2604047fbe7459ec5aed70c64b/.codex-plugin/plugin.json | Codex manifest with apps connector registry |
openai-figma-hooks.json | ~/.codex/plugins/cache/openai-curated/figma/fb0a18376bcd9f2604047fbe7459ec5aed70c64b/hooks.json | The only Codex-ecosystem hooks.json observed in the 16-plugin corpus; uses PostToolUse event |
bsi-college-baseball-intelligence-SKILL.md | BSI-repo/skill-improvements/college-baseball-intelligence/SKILL.md | Real standalone SKILL.md following router pattern (dispatcher ≤100 lines) |
Why these specific files
Chosen for coverage: together they exercise every major shape in the manifest translator:
- Claude minimal wrapper (context7)
- Claude
.mcp.jsonflat shape (context7) - Codex
.mcp.jsonwrapped shape (openai-cloudflare-mcp) - Codex
interface{}with 14 sub-fields (openai-cloudflare) - Codex with partial fields —
appspresent,mcpServersabsent (openai-canva) - Codex with
appsconnector (openai-github) - Codex
hooks.jsonwithPostToolUse(openai-figma-hooks) - Standalone SKILL.md following router pattern (bsi-college-baseball-intelligence)
Usage
Templates in ../templates/ are blueprints — use them to generate new manifests. These real-examples/ are ground truth — use them to verify that generated output matches real-world structure.
Translator round-trip tests at ../fixtures/ reference these files as sources for their expected outputs.
Do NOT edit
If you need a different example, add a new file alongside. Don't edit these — they represent observed reality as of 2026-04-12. Editing them invalidates the translator's ground truth.
Refresh procedure
If a future Phase 0 spike revisits the installed plugin cache (e.g. after Codex or Claude plugin schema changes upstream), rerun:
bash skills/universal-skills-marketplace/scripts/fetch-upstream-catalog.sh --refresh-real-examplesThe script re-copies all files listed above with updated paths + regenerates this README.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://marketplace.blazesportsintel.com/schemas/canonical-skill.schema.json",
"title": "CanonicalSkill",
"description": "ClaudOpenAI universal-skills-marketplace canonical intermediate format. The translator produces and consumes this shape. See references/11-manifest-translator-algorithm.md.",
"type": "object",
"required": ["id", "origin", "type", "name", "description"],
"additionalProperties": false,
"properties": {
"id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]*(/[a-z0-9][a-z0-9-]*)?$"},
"origin": {
"type": "object",
"required": ["ecosystem", "sourcePath"],
"properties": {
"ecosystem": {"enum": ["claude", "codex", "standalone"]},
"sourcePath": {"type": "string"},
"sourceSha": {"type": ["string", "null"]},
"repo": {"type": ["string", "null"]},
"discoveredAt": {"type": "string", "format": "date-time"}
}
},
"type": {"enum": ["plugin", "skill", "marketplace"]},
"name": {"type": "string"},
"description": {"type": "string"},
"version": {"type": ["string", "null"]},
"author": {
"type": ["object", "null"],
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"url": {"type": "string"}
}
},
"homepage": {"type": ["string", "null"]},
"repository": {"type": ["string", "null"]},
"license": {"type": ["string", "null"]},
"keywords": {"type": "array", "items": {"type": "string"}, "default": []},
"tags": {"type": "array", "items": {"type": "string"}, "default": []},
"category": {"type": ["string", "null"]},
"skills": {
"type": "array",
"default": [],
"items": {
"type": "object",
"required": ["path", "name", "description"],
"properties": {
"path": {"type": "string"},
"name": {"type": "string"},
"description": {"type": "string"},
"version": {"type": ["string", "null"]},
"frontmatter": {"type": "object", "additionalProperties": true},
"body": {"type": "string"},
"references": {"type": "array", "items": {"type": "string"}},
"scripts": {"type": "array", "items": {"type": "string"}},
"assets": {"type": "array", "items": {"type": "string"}},
"nestedAgents": {"type": "array"},
"skillInterface": {"type": ["object", "null"]}
}
}
},
"mcpServers": {"type": "object", "additionalProperties": true},
"commands": {"type": "array", "default": [], "items": {"type": "object"}},
"hooks": {
"type": ["object", "null"],
"properties": {
"description": {"type": ["string", "null"]},
"events": {"type": "object", "additionalProperties": true}
}
},
"agents": {"type": "array", "default": [], "items": {"type": "object"}},
"apps": {"type": "object", "additionalProperties": true},
"interface": {"type": ["object", "null"], "additionalProperties": true},
"ecosystem_extensions": {
"type": "object",
"properties": {
"claude": {"type": "object", "additionalProperties": true},
"codex": {"type": "object", "additionalProperties": true}
}
},
"translation_log": {
"type": "array",
"default": [],
"items": {
"type": "object",
"required": ["level", "field", "message"],
"properties": {
"level": {"enum": ["info", "warning", "lossy", "error"]},
"field": {"type": "string"},
"message": {"type": "string"},
"shim_generated": {"type": ["string", "null"]}
}
}
},
"quality_score": {"type": "number", "minimum": 0, "maximum": 100},
"quality_breakdown": {"type": "object", "additionalProperties": true},
"compatibility_flags": {
"type": "object",
"properties": {
"claude": {
"type": "object",
"properties": {
"compatible": {"type": "boolean"},
"min_version": {"type": ["string", "null"]},
"lossy_fields": {"type": "array", "items": {"type": "string"}}
}
},
"codex": {
"type": "object",
"properties": {
"compatible": {"type": "boolean"},
"min_version": {"type": ["string", "null"]},
"lossy_fields": {"type": "array", "items": {"type": "string"}}
}
}
}
},
"content_hash": {"type": "string"},
"last_verified": {"type": "string", "format": "date-time"},
"install_count": {"type": "integer", "minimum": 0, "default": 0}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://marketplace.blazesportsintel.com/schemas/claude-plugin.schema.json",
"title": "ClaudePlugin",
"description": "Schema for .claude-plugin/plugin.json. Reverse-engineered from 35+ installed plugins under ~/.claude/plugins/marketplaces/claude-plugins-official/. Claude plugins are convention-based: plugin.json carries identity only; skills/agents/commands/hooks are discovered by directory walking.",
"type": "object",
"required": ["name", "description"],
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]{0,63}$",
"description": "kebab-case slug matching directory name"
},
"description": {
"type": "string",
"description": "one-sentence summary"
},
"author": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"url": {"type": "string"}
},
"additionalProperties": true
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://marketplace.blazesportsintel.com/schemas/codex-plugin.schema.json",
"title": "CodexPlugin",
"description": "Schema for .codex-plugin/plugin.json. Derived from 16 installed openai-curated plugins at ~/.codex/plugins/cache/openai-curated/ (Spike S2, 2026-04-12). Field frequencies reflect observed presence.",
"type": "object",
"required": [
"name",
"version",
"description",
"author",
"homepage",
"repository",
"license",
"keywords",
"skills",
"interface"
],
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "kebab-case plugin slug",
"pattern": "^[a-z0-9][a-z0-9-]{0,63}$"
},
"version": {
"type": "string",
"description": "semver (e.g. 0.1.0, 1.0.0, 2.0.7)",
"pattern": "^\\d+\\.\\d+\\.\\d+"
},
"description": {
"type": "string",
"description": "one-sentence summary"
},
"author": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"url": {"type": "string", "format": "uri"}
},
"additionalProperties": false
},
"homepage": {"type": "string", "format": "uri"},
"repository": {"type": "string"},
"license": {
"type": "string",
"description": "SPDX identifier, 'Proprietary', or 'LicenseRef-*' custom reference"
},
"keywords": {
"type": "array",
"items": {"type": "string"}
},
"skills": {
"type": "string",
"description": "path to skills directory (always observed as './skills/')",
"pattern": "^\\./"
},
"apps": {
"type": "string",
"description": "path to .app.json connector registry (56% of plugins)",
"pattern": "^\\./"
},
"mcpServers": {
"type": "string",
"description": "path to .mcp.json (31% of plugins). Codex .mcp.json is wrapped: {\"mcpServers\": {...}}",
"pattern": "^\\./"
},
"hooks": {
"type": "string",
"description": "path to hooks.json (6% of plugins, observed only in figma)",
"pattern": "^\\./"
},
"interface": {
"type": "object",
"description": "Codex-only catalog metadata (no Claude equivalent)",
"required": [
"displayName",
"shortDescription",
"longDescription",
"category",
"capabilities",
"websiteURL",
"privacyPolicyURL",
"termsOfServiceURL",
"defaultPrompt",
"composerIcon",
"logo",
"screenshots"
],
"properties": {
"displayName": {"type": "string"},
"shortDescription": {"type": "string"},
"longDescription": {"type": "string"},
"developerName": {
"type": "string",
"description": "optional (88% of plugins); absent in canva, stripe"
},
"category": {
"type": "string",
"description": "observed values: Coding, Productivity, Communication, Research, Design",
"examples": ["Coding", "Productivity", "Communication", "Research", "Design"]
},
"capabilities": {
"type": "array",
"items": {
"type": "string",
"examples": ["Interactive", "Write", "Read"]
}
},
"websiteURL": {"type": "string"},
"privacyPolicyURL": {"type": "string"},
"termsOfServiceURL": {"type": "string"},
"defaultPrompt": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
],
"description": "string in most plugins; array of strings in canva. Canonical form normalizes to array."
},
"composerIcon": {"type": "string"},
"logo": {"type": "string"},
"screenshots": {
"type": "array",
"description": "observed empty in most installed plugins; entries shape unverified"
},
"brandColor": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{6}$",
"description": "optional (88% of plugins); absent in canva, stripe"
}
},
"additionalProperties": false
}
}
}
{
"name": "{{plugin_name_kebab}}",
"description": "{{one_sentence_description}}",
"author": {
"name": "{{author_name}}",
"email": "{{author_email}}",
"url": "{{author_url}}"
}
}
{
"name": "{{plugin_name_kebab}}",
"version": "{{version_semver}}",
"description": "{{one_sentence_description}}",
"author": {
"name": "{{author_name}}",
"email": "{{author_email}}",
"url": "{{author_url}}"
},
"homepage": "{{homepage_url}}",
"repository": "{{repository_url}}",
"license": "{{license_spdx}}",
"keywords": [
"{{keyword_1}}",
"{{keyword_2}}"
],
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"interface": {
"displayName": "{{display_name_title_case}}",
"shortDescription": "{{short_description_max_60}}",
"longDescription": "{{long_description_paragraph}}",
"developerName": "{{developer_name}}",
"category": "Coding",
"capabilities": [
"Read",
"Interactive"
],
"websiteURL": "{{website_url}}",
"privacyPolicyURL": "{{privacy_url}}",
"termsOfServiceURL": "{{terms_url}}",
"defaultPrompt": [
"{{default_prompt_1}}",
"{{default_prompt_2}}"
],
"brandColor": "{{brand_color_hex}}",
"composerIcon": "./assets/icon-composer.svg",
"logo": "./assets/icon-logo.svg",
"screenshots": []
}
}
-- ClaudOpenAI D1 schema
-- Version: 0.1.0
-- Author: Austin Humphrey / Blaze Sports Intel
-- See: skills/universal-skills-marketplace/references/06-d1-schema-design.md
-- ============================================
-- skills: primary catalog
-- ============================================
CREATE TABLE IF NOT EXISTS skills (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL,
source_ecosystem TEXT NOT NULL CHECK(source_ecosystem IN ('claude','codex','universal')),
source_url TEXT NOT NULL,
source_repo TEXT NOT NULL,
source_commit TEXT NOT NULL,
source_path TEXT NOT NULL,
manifest_format TEXT NOT NULL CHECK(manifest_format IN ('claude-plugin','codex-plugin','standalone-skill','openai-agent')),
quality_score INTEGER NOT NULL DEFAULT 0 CHECK(quality_score BETWEEN 0 AND 100),
install_count INTEGER NOT NULL DEFAULT 0,
star_count INTEGER NOT NULL DEFAULT 0,
content_hash TEXT NOT NULL,
compat_claude INTEGER NOT NULL DEFAULT 0 CHECK(compat_claude IN (0,1)),
compat_codex INTEGER NOT NULL DEFAULT 0 CHECK(compat_codex IN (0,1)),
tags TEXT, -- JSON array as string
category TEXT,
last_verified TEXT NOT NULL, -- ISO 8601
indexed_at TEXT NOT NULL, -- ISO 8601
tombstoned INTEGER NOT NULL DEFAULT 0 CHECK(tombstoned IN (0,1))
);
CREATE INDEX IF NOT EXISTS idx_skills_score ON skills(quality_score DESC);
CREATE INDEX IF NOT EXISTS idx_skills_ecosystem ON skills(source_ecosystem, tombstoned);
CREATE INDEX IF NOT EXISTS idx_skills_last_verified ON skills(last_verified DESC);
CREATE INDEX IF NOT EXISTS idx_skills_source_repo ON skills(source_repo);
CREATE INDEX IF NOT EXISTS idx_skills_content_hash ON skills(content_hash);
-- ============================================
-- skill_versions: version history
-- ============================================
CREATE TABLE IF NOT EXISTS skill_versions (
skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
version TEXT NOT NULL,
content_hash TEXT NOT NULL,
source_commit TEXT NOT NULL,
indexed_at TEXT NOT NULL,
PRIMARY KEY(skill_id, version)
);
CREATE INDEX IF NOT EXISTS idx_skill_versions_skill ON skill_versions(skill_id, indexed_at DESC);
-- ============================================
-- skill_references: referenced file metadata
-- ============================================
CREATE TABLE IF NOT EXISTS skill_references (
skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
version TEXT NOT NULL,
ref_path TEXT NOT NULL,
kind TEXT NOT NULL CHECK(kind IN ('reference','script','asset')),
sha256 TEXT NOT NULL,
size_bytes INTEGER NOT NULL,
mime TEXT NOT NULL,
PRIMARY KEY(skill_id, version, ref_path)
);
CREATE INDEX IF NOT EXISTS idx_skill_references_skill ON skill_references(skill_id, version);
-- ============================================
-- sources: upstream repo state (indexer's internal table)
-- ============================================
CREATE TABLE IF NOT EXISTS sources (
name TEXT PRIMARY KEY,
repo_url TEXT NOT NULL,
default_branch TEXT NOT NULL,
last_sync_sha TEXT,
last_sync_at TEXT,
last_check_at TEXT,
last_result TEXT CHECK(last_result IS NULL OR last_result IN ('ok','rate_limited','error','unchanged')),
error_message TEXT,
priority_tier TEXT NOT NULL CHECK(priority_tier IN ('A','B','C')),
poll_interval_seconds INTEGER NOT NULL
);
-- ============================================
-- skills_fts: full-text search (FTS5 virtual table)
-- ============================================
CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
id UNINDEXED,
name,
description,
tags,
category,
content='skills',
content_rowid='rowid',
tokenize='porter unicode61'
);
-- Content-sync triggers (FTS5 "external content" pattern)
CREATE TRIGGER IF NOT EXISTS skills_ai AFTER INSERT ON skills BEGIN
INSERT INTO skills_fts(rowid, id, name, description, tags, category)
VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.category);
END;
CREATE TRIGGER IF NOT EXISTS skills_ad AFTER DELETE ON skills BEGIN
INSERT INTO skills_fts(skills_fts, rowid, id, name, description, tags, category)
VALUES('delete', old.rowid, old.id, old.name, old.description, old.tags, old.category);
END;
CREATE TRIGGER IF NOT EXISTS skills_au AFTER UPDATE ON skills BEGIN
INSERT INTO skills_fts(skills_fts, rowid, id, name, description, tags, category)
VALUES('delete', old.rowid, old.id, old.name, old.description, old.tags, old.category);
INSERT INTO skills_fts(rowid, id, name, description, tags, category)
VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.category);
END;
-- ============================================
-- Initial source records (per Spike S1)
-- ============================================
INSERT OR IGNORE INTO sources (name, repo_url, default_branch, priority_tier, poll_interval_seconds) VALUES
('anthropics/claude-plugins-official', 'https://github.com/anthropics/claude-plugins-official', 'main', 'B', 21600),
('anthropics/skills', 'https://github.com/anthropics/skills', 'main', 'B', 21600),
('anthropics/knowledge-work-plugins', 'https://github.com/anthropics/knowledge-work-plugins', 'main', 'A', 21600),
('openai/codex', 'https://github.com/openai/codex', 'main', 'A', 21600),
('openai/codex-plugin-cc', 'https://github.com/openai/codex-plugin-cc', 'main', 'A', 21600),
('openai/skills', 'https://github.com/openai/skills', 'main', 'B', 21600),
('openai/swarm', 'https://github.com/openai/swarm', 'main', 'C', 86400),
('openai/openai-agents-python', 'https://github.com/openai/openai-agents-python', 'main', 'A', 21600),
('openai/plugins', 'https://github.com/openai/plugins', 'main', 'B', 21600);
{
"{{server_name}}": {
"command": "npx",
"args": ["-y", "{{npm_package_name}}"]
}
}
{
"mcpServers": {
"{{server_name}}": {
"command": "npx",
"args": ["-y", "{{npm_package_name}}"]
}
}
}
#!/usr/bin/env node
/**
* {{package_name}} — MCP server entry point.
* Template generated from ClaudOpenAI skill assets/templates/mcp-server-index.ts.template.
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
// ---------- Tool schemas ----------
const ExampleToolInput = z.object({
param: z.string().describe("Describe what this param does"),
});
// ---------- Server factory ----------
export function createServerInstance() {
const server = new Server(
{
name: "{{server_name}}",
version: "{{version}}",
},
{
capabilities: { tools: {} },
}
);
server.setRequestHandler(
{ method: "tools/list" },
async () => ({
tools: [
{
name: "example-tool",
description: "Describe what this tool does. Claude reads this to decide when to invoke it.",
inputSchema: zodToJsonSchema(ExampleToolInput),
},
],
})
);
server.setRequestHandler(
{ method: "tools/call" },
async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "example-tool": {
const validated = ExampleToolInput.parse(args);
return {
content: [
{
type: "text",
text: JSON.stringify({
result: `You passed: ${validated.param}`,
meta: {
source: "{{server_name}}",
fetched_at: new Date().toISOString(),
timezone: "America/Chicago",
},
}),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
);
return server;
}
// ---------- CLI ----------
if (import.meta.url === `file://${process.argv[1]}`) {
const server = createServerInstance();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Server running on stdio");
}
---
name: {{skill_name_kebab}}
description: {{description_with_triggers}}
version: {{version_semver}}
---
# {{skill_title_case}}
{{one_sentence_mission}}
## Routing table
| I want to… | Primary reference | Supporting script/asset |
|------------|-------------------|--------------------------|
| {{intent_1}} | `references/{{ref_1}}.md` | `scripts/{{script_1}}` |
| {{intent_2}} | `references/{{ref_2}}.md` | `assets/{{asset_1}}` |
| {{intent_3}} | `references/{{ref_3}}.md` | — |
## Phase dispatcher
- **{{phase_1_label}}** → read `references/{{phase_1_ref}}.md`
- **{{phase_2_label}}** → read `references/{{phase_2_ref}}.md`
- **{{phase_3_label}}** → read `references/{{phase_3_ref}}.md`
## Hard rules
1. {{rule_1}}
2. {{rule_2}}
3. {{rule_3}}
## Run before any commit
```bash
bash skills/{{skill_name_kebab}}/scripts/validate.sh
```
Must exit 0.
name = "{{worker_name}}"
main = "src/index.ts"
compatibility_date = "2026-04-01"
compatibility_flags = ["nodejs_compat"]
[[d1_databases]]
binding = "DB"
database_name = "{{d1_database_name}}"
database_id = "{{d1_database_id}}"
[[r2_buckets]]
binding = "CONTENT"
bucket_name = "{{r2_bucket_name}}"
[[kv_namespaces]]
binding = "CACHE"
id = "{{cache_kv_id}}"
[[kv_namespaces]]
binding = "RATE_LIMIT"
id = "{{ratelimit_kv_id}}"
[observability]
enabled = true
[[routes]]
pattern = "{{custom_domain}}/*"
zone_name = "{{zone_name}}"
custom_domain = true
[vars]
REGISTRY_VERSION = "{{version}}"
name = "{{worker_name}}"
main = "src/index.ts"
compatibility_date = "2026-04-01"
[[d1_databases]]
binding = "DB"
database_name = "{{d1_database_name}}"
database_id = "{{d1_database_id}}"
[observability]
enabled = true
[[routes]]
pattern = "{{primary_domain}}/*"
zone_name = "{{zone_name}}"
custom_domain = true
[[routes]]
pattern = "{{alt_domain}}/*"
zone_name = "{{zone_name}}"
custom_domain = true
name = "{{worker_name}}"
main = "src/index.ts"
compatibility_date = "2026-04-01"
compatibility_flags = ["nodejs_compat"]
[triggers]
crons = ["0 */6 * * *"]
[[d1_databases]]
binding = "DB"
database_name = "{{d1_database_name}}"
database_id = "{{d1_database_id}}"
[[r2_buckets]]
binding = "CONTENT"
bucket_name = "{{r2_bucket_name}}"
[[kv_namespaces]]
binding = "INDEXER_STATE"
id = "{{indexer_state_kv_id}}"
[observability]
enabled = true
# Secrets (set via: wrangler secret put GITHUB_TOKEN)
00 — Architecture Overview
Identity positioning
ClaudOpenAI is an unofficial, independent, community project. It is not affiliated with, endorsed by, or sponsored by Anthropic or OpenAI. "Claude Code," "Claude," "OpenAI Codex," and related names are trademarks of their respective owners. Every piece of UI, every README, every response produced by the MCP server must reinforce this.
Why: we're building a bridge that talks to both companies' products. It would be easy to inadvertently imply partnership. Don't. The NOTICE file at the repo root is the canonical statement.
What we're building
A single MCP server that indexes skills and plugins from public repos in both ecosystems, normalizes them into a canonical JSON format, and serves them to any Claude Code or Codex session on demand. Plus a marketplace.json bridge that lets the same backend feed both ecosystems' catalog formats.
Think of it as Context7 but for skills instead of library docs. Same two-tool search-plus-fetch pattern; same open-to-every-MCP-client model; same "we don't own the data, we just organize it" philosophy.
The problem we're solving
Claude Code and OpenAI Codex have converged on:
- The SKILL.md format (YAML frontmatter + markdown body +
references//scripts//assets/progressive disclosure) - MCP (Model Context Protocol) for tool extension
- The Apache/MIT open-source friendliness of their official repos
And diverged on:
- Plugin manifests:
.claude-plugin/plugin.json(nearly-empty, convention-based) vs.codex-plugin/plugin.json(rich, withinterface{}+apps+keywords+licensefields) - Marketplace catalogs: Claude's
marketplace.jsonat.claude-plugin/marketplace.jsonvs Codex's at.agents/plugins/marketplace.json - MCP transport defaults: Claude defaults to stdio via npx or remote HTTP; Codex's preferred transport still being verified (see Spike S4)
.mcp.jsonshapes: Claude uses flat{"<name>": {...}}; Codex uses wrapped{"mcpServers": {"<name>": {...}}}
The result: a skill author has to publish twice, and a skill consumer can't find the other ecosystem's skills without manually browsing each repo. We're the bridge.
System topology
┌─────────────────────────────────────────────────────────────────────────────┐
│ UPSTREAM REPOS (9 — Tier A/B/C per docs/spikes/upstream-availability.md) │
│ │
│ anthropics/claude-plugins-official anthropics/skills │
│ anthropics/knowledge-work-plugins openai/codex │
│ openai/codex-plugin-cc openai/plugins │
│ openai/skills openai/swarm │
│ openai/openai-agents-python │
└──────────────────────────────────┬──────────────────────────────────────────┘
│ git ls-remote + sparse-clone (via GitHub CDN)
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ WORKER: universal-skills-indexer cron = "0 */6 * * *" │
│ indexer.marketplace.blazesportsintel.com │
│ │
│ For each upstream: │
│ ls-remote → delta check → sparse-clone → walk → frontmatter parse → │
│ translate → canonical JSON → sha256 → UPSERT D1 → R2 write │
│ │
│ Bindings: DB (D1), CONTENT (R2), INDEXER_STATE (KV), GITHUB_TOKEN │
└──────────────────────────────────┬──────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ STORAGE (see references/06-d1-schema-design.md + 07-r2-storage-patterns.md) │
│ │
│ D1 universal-skills R2 universal-skills-content KV (3 namespaces) │
│ skills skills/{id}/{ver}/skill.md CACHE │
│ skill_versions skills/{id}/{ver}/refs.tgz RATE_LIMIT │
│ skill_references skills/{id}/{ver}/assets.tgz INDEXER_STATE │
│ sources skills/{id}/{ver}/canonical.json │
│ skills_fts (FTS5) │
└────────────────────────┬─────────────────────────────────────┬──────────────┘
↓ ↓
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ WORKER: api │ │ WORKER: bridge │
│ api.marketplace.blaze...com │ │ marketplace.blaze...com │
│ │ │ registry.marketplace.blaze..│
│ POST /mcp (JSON-RPC 2.0) │ │ GET /.claude-plugin/ │
│ tools/list → 3 tools │ │ marketplace.json │
│ tools/call → search/get/ │ │ GET /.agents/plugins/ │
│ install │ │ marketplace.json │
│ GET /health │ │ GET /health │
│ │ │ │
│ 60 rpm/IP (KV) │ │ read-only D1 │
└──────────────┬──────────────┘ └──────────────┬──────────────┘
↑ ↑
│ │
┌───────────────┴─────────────────┐ ┌─────────────────┴─────────────────┐
│ CLAUDE CODE │ │ OPENAI CODEX │
│ ~/.claude/mcp.json: │ │ ~/.codex/config.toml: │
│ {"universal-skills": │ │ [mcp_servers.universal-skills] │
│ {"type":"http","url":"api.."} │ │ command = "npx" │
│ OR {"command":"npx",...}} │ │ args = ["-y","@bsi/u-s-mcp"] │
└─────────────────────────────────┘ └───────────────────────────────────┘See `../../../ARCHITECTURE.md` for the full-resolution diagram plus Mermaid source.
Key architectural decisions
1. MCP server on Cloudflare Workers
Per canonical guidance in ~/.claude/plugins/marketplaces/claude-plugins-official/plugins/mcp-server-dev/skills/build-mcp-server/SKILL.md:
- Remote streamable-HTTP is the default deployment for any server wrapping a cloud API
- Cloudflare Workers is the fastest deploy path ("zero to live URL in two commands")
- stdio fallback via the npm package keeps offline/dev experience fast
We ship both: Workers remote + npx stdio, same 3-tool surface.
2. Trivial plugin wrapper + real npm package (Context7 pattern)
.claude-plugin/plugin.json: 5 fields (name, description, author). That's it..codex-plugin/plugin.json: richer (required by Codex schema), still no code..mcp.json: just declaresnpx -y @blazesportsintel/universal-skills-mcp.- Everything real lives in
packages/mcp-server/.
Deviating from this (putting logic in the wrappers) means you got context7's lesson wrong. Re-read `10-context7-architectural-analysis.md`.
3. Canonical intermediate format
Translator operates ClaudePlugin ↔ CanonicalSkill ↔ CodexPlugin. Never directly Claude ↔ Codex — the canonical middle buys us:
- Symmetry (one schema to reason about)
- Version-ability (canonical version N can serve N-1 clients)
- Lossy-field tracking (lossy fields persist in
translation_log+ecosystem_extensionsregardless of direction)
Schema in packages/schema/src/canonical.ts (zod) and schema/canonical-skill.schema.json (JSON Schema).
4. Clone-and-walk indexer, not Code Search
GitHub Code Search is rate-limited (30 rpm authenticated). Sparse-cloning via the contents CDN is unmetered. Spike S3 documents the trade-off and math.
5. No signing in v0.1
provenance.signature_method = null. Trust model = "upstream repo URL visible in every result." v0.2 will add cosign/Sigstore keyless. Documented as non-goal; don't pretend otherwise.
Data flow: one complete resolve-skill round-trip
1. User in Claude Code: "find me a skill for PDF processing" 2. Claude Code → mcp__universal-skills__resolve_skill(query="PDF processing") 3. Client sends JSON-RPC over HTTPS to api.marketplace.blazesportsintel.com/mcp (or stdio via npx to local server) 4. Worker checks KV CACHE key resolve:{sha1(query+filters)} → miss 5. Worker queries D1 skills_fts with BM25 ranking on (name, description, tags) filtered by quality_score > 0 6. Worker joins skills_fts results with skills + skill_versions for version + source info 7. Worker builds response array: [{id, name, description, quality_score, source_ecosystem, source_url, compatibility, install_commands: {claude, codex}, content_hash, meta:{source, fetched_at}}, ...] 8. Worker writes result to KV CACHE with 10min TTL 9. Client receives 7 results, displays top match: anthropics/skills:pdf (score 82, available in both ecosystems)
This sequence is what success looks like. Anything that diverges is a bug.
Deployment targets
Enumerated in ARCHITECTURE.md. Briefly:
| Resource | Name |
|---|---|
| Worker (api) | universal-skills-api @ api.marketplace.blazesportsintel.com |
| Worker (indexer) | universal-skills-indexer @ indexer.marketplace.blazesportsintel.com |
| Worker (bridge) | universal-skills-bridge @ marketplace.blazesportsintel.com + registry.marketplace.blazesportsintel.com |
| D1 | universal-skills |
| R2 | universal-skills-content |
| KV | CACHE, RATE_LIMIT, INDEXER_STATE |
| npm | @blazesportsintel/universal-skills-mcp |
DNS setup in `docs/spikes/dns-setup.md`.
What you should read next
Recommended order if you're implementing this skill (not just consuming it):
1. This file (done) 2. `10-context7-architectural-analysis.md` — the pattern we're copying 3. `11-manifest-translator-algorithm.md` — the hardest component 4. `04-mcp-tool-design.md` — how the 3 tools are shaped 5. `02-claude-plugin-format.md` + `03-codex-plugin-format.md` — what we're translating between 6. Infrastructure trilogy: `05-cloudflare-workers-playbook.md`, `06-d1-schema-design.md`, `08-github-indexer-design.md` 7. Quality + verification: `09-quality-scoring-rubric.md`, `12-verification-playbook.md`
Non-goals (v0.1)
- Web dashboard
- Skill signing / Sigstore provenance
- Private/auth-gated skills
- Third-party marketplace federation
- 100%-lossless translation (some fields are explicitly lossy; see `11-manifest-translator-algorithm.md`)
Success criteria (excerpt; full matrix in 12-verification-playbook.md)
1. curl https://registry.marketplace.blazesportsintel.com/.claude-plugin/marketplace.json | jq '.plugins | length' ≥ 10 2. curl https://registry.marketplace.blazesportsintel.com/.agents/plugins/marketplace.json | jq '.plugins | length' ≥ 10 3. curl https://api.marketplace.blazesportsintel.com/health returns {"status":"ok",...} 4. npx @blazesportsintel/universal-skills-mcp prints "Server running on stdio" 5. In a fresh Claude Code session with the server registered: resolve-skill("pdf") returns ≥3 results spanning both ecosystems 6. Same query from a fresh Codex session returns parallel results 7. Translator round-trip claude→codex→claude preserves every semantic field via codex_ecosystem.json sidecar (lossy fields logged, not silently dropped)
01 — agentskills.io Spec Walkthrough
The shared skill format between Claude Code and OpenAI Codex. Also adopted (per the source prompt) by JetBrains Junie, Google AI Edge, and others. Apache 2.0 open standard.
Provenance caveat:agentskills.ioas a canonical hosted spec URL is not yet verified in Phase 0 (see `docs/spikes/agentskills-provenance.md`). This reference is derived from observed behavior of 35+ Claude-ecosystem and 16 Codex-ecosystem skills plus Anthropic's ownplugin-dev+skill-creatorskill authoring skills. Ifagentskills.ioresolves and publishes an authoritative version, we prefer it; lacking that, this observational doc is the working spec.
What a skill IS
A skill is a progressive-disclosure capability module that an LLM-backed agent loads on demand when a trigger matches. Anatomy:
<skill-name>/
├── SKILL.md # 1 file, ≤~100 lines, YAML frontmatter + markdown body
├── references/ # N files, optional, loaded when main SKILL.md routes to them
│ ├── 00-overview.md
│ └── ...
├── scripts/ # N files, optional, executable or source scripts
│ ├── validate.sh
│ └── ...
└── assets/ # N files/dirs, optional, templates/fixtures/binaries
├── templates/
├── real-examples/
└── fixtures/The router pattern (adopted by BSI skills, skill-creator, and most Anthropic first-party skills):
SKILL.mdis a dispatcher (≤100 lines)- Workflows extracted to
references/ - Templates + real examples in
assets/ - Validators + scaffolders in
scripts/
Frontmatter schema (union of observed keys)
---
name: my-skill # REQUIRED — kebab-case slug
description: | # REQUIRED — trigger copy, includes keywords/phrases
Use when X is needed. Triggers: "X", "Y", "Z".
version: 0.1.0 # optional — semver
allowed-tools: [Read, Edit, Bash] # optional (Claude-only) — runtime tool gating
disable-model-invocation: true # optional (Claude-only) — true = slash-command only
user-invocable: true # optional (Claude-only) — true = in slash menu
color: "#BF5700" # optional — UI accent (Claude)
model: claude-sonnet-4-5 # optional — model override (agent files only)
tools: [Read, Grep] # optional (agent.md only, not SKILL.md)
---The description field is load-bearing
It's what the host LLM reads to decide whether to auto-invoke the skill. Write it like a trigger copy ad:
- Say what scenarios trigger it ("Use when the user asks to X")
- Include keywords the user might say ("Triggers on 'X', 'Y', 'Z'")
- Keep to ≤3 sentences — context is expensive
Example (from our own SKILL.md):
Use when building, designing, extending, or consuming the ClaudOpenAI unofficial cross-ecosystem skills marketplace — a Context7-pattern MCP server that bridges Claude Code (.claude-plugin) and OpenAI Codex (.codex-plugin) skill catalogs. Triggers on "universal skills", "skills marketplace", "cross-ecosystem skill", ...
Progressive disclosure — the core design pattern
The LLM loads SKILL.md first (router). The router dispatches to specific references/<topic>.md files only when the user's intent matches. This keeps token cost low and context focused.
Anti-pattern: a single 400-line SKILL.md that dumps everything. Pattern: a 60-line SKILL.md with a routing table pointing to 12 smaller references.
The BSI skill discipline (from BSI-repo/skill-improvements/) enforces ≤100 lines in SKILL.md. Violations flagged by validate.sh.
Body conventions
Markdown body follows: 1. One-line mission statement below frontmatter 2. Routing table or phase dispatcher 3. Cross-links to references/<n>.md for deep content 4. Link to scripts/validate.sh / other tooling 5. Explicit list of hard rules (security, data protection, anti-fabrication, etc.)
references/ conventions
Numbered prefixes (00-, 01-, ..., 11-, 12-) make ordering explicit. Topic per file. Each reference file is self-contained — the LLM can load one without the others.
Cross-linking format: relative paths like [...](08-github-indexer-design.md). Tools verify no broken links.
scripts/ conventions
validate.sh— structural self-check. Required. Exit code 0 = skill valid.scaffold.sh— generate new instances from templates. Optional but common.*.ts/*.py— per-purpose scripts. Executables marked chmod +x in the skill package.
Environment variable ${CLAUDE_PLUGIN_ROOT} (Claude) or ${CODEX_PLUGIN_ROOT} (Codex — unverified name) resolves at runtime to the installed plugin root.
assets/ conventions
Common subdirs:
templates/—{{mustache}}or empty placeholders for scaffoldingreal-examples/— verbatim copies of canonical real-world artifacts, annotated with source pathsfixtures/— test inputs, both valid and intentionally-malformedschemas/— JSON Schema or zod schema filesdiagrams/— exported PNG/SVG (Mermaid sources indocs/sequence-diagrams/)
Cross-ecosystem compatibility
When the same skill is consumed by both Claude Code and Codex:
- Claude-only frontmatter keys (
allowed-tools,disable-model-invocation,user-invocable) are lossy on Codex target. Translator preserves them as HTML-comment shims in the SKILL.md body + ecosystem sidecar JSON. - Codex-only structure (
agents/openai.yamlat skill scope, nested skill agents) is lossy on Claude target. Preserved similarly. - Observable everywhere:
name,description, the body itself, and the three progressive-disclosure dirs.
Versioning
Semver in version field. Backwards-compatibility conventions are not formally specified — in practice Claude and Codex both ignore the version key at load time. Marketplaces use it for listing.
Validation
The validate.sh shell script (per BSI skill discipline) checks:
1. SKILL.md exists and is ≤100 lines 2. Frontmatter is valid YAML 3. Required keys (name, description) present 4. name matches directory name 5. All references/, scripts/, assets/ paths referenced from SKILL.md actually exist 6. No broken intra-skill relative links 7. scripts/ files are executable where shell scripts
Invalid skills fail pre-commit and fail skill-creator packaging.
Packaging
Skills are packaged as .skill files: ZIP (deflate) archives of the entire skill dir tree. The skill-creator plugin's scripts/package_skill.py implements the canonical packager:
# From skill-creator (Anthropic)
~/.claude/plugins/cache/claude-plugins-official/skill-creator/unknown/skills/skill-creator/scripts/package_skill.py <skill-dir> <output.skill>Our scripts/package-skill.sh wraps this.
Installation
Skills install to:
~/.claude/skills/<skill-name>/(Claude user scope).claude/skills/<skill-name>/(Claude project scope — repo-local)~/.codex/skills/<skill-name>/(Codex user scope).codex/skills/<skill-name>/(Codex project scope)
Or embedded inside a plugin at <plugin>/skills/<skill>/.
Cross-references
- Anthropic's
plugin-dev/skills/skill-development/SKILL.md— authoring guide, first-party - Anthropic's
skill-creator/skills/skill-creator/SKILL.md— templates + scaffolding - Observed Codex examples at
~/.codex/plugins/cache/openai-curated/<plugin>/skills/<skill>/SKILL.md - BSI skill examples at
BSI-repo/skill-improvements/*/SKILL.md
What this ClaudOpenAI skill contributes
A router SKILL.md pointing to 12 deep-dive references. This doc is #01 of those 12. Read the others:
- `00-architecture-overview.md` — the whole system
- `02-claude-plugin-format.md` — Claude-side detail
- `03-codex-plugin-format.md` — Codex-side detail
- ... through `12-verification-playbook.md`
02 — Claude Plugin Format
Reverse-engineered from 35+ real plugins under ~/.claude/plugins/marketplaces/claude-plugins-official/plugins/ and external_plugins/. The authoritative reference is Anthropic's own `plugin-dev/skills/plugin-structure` — this doc summarizes the observed shape for the translator's purposes.
Core insight: Claude plugins are convention-based
Unlike Codex, which packs everything into plugin.json, Claude plugins rely on directory conventions. The manifest carries only identity metadata:
{
"name": "context7",
"description": "...",
"author": { "name": "Upstash" }
}That's the whole .claude-plugin/plugin.json for context7 (7 lines, verbatim from ~/.claude/plugins/marketplaces/claude-plugins-official/external_plugins/context7/.claude-plugin/plugin.json).
Everything else is discovered by walking directories.
The directory conventions
A full Claude plugin looks like:
<plugin-name>/
├── .claude-plugin/
│ ├── plugin.json # minimal manifest (name, description, author)
│ └── codex_ecosystem.json # (optional; ClaudOpenAI sidecar for preserving Codex-only fields)
├── .mcp.json # MCP server declaration (FLAT shape)
├── hooks/
│ └── hooks.json # event → matcher → command bindings
├── skills/
│ └── <skill-name>/
│ ├── SKILL.md # YAML frontmatter + body
│ ├── references/ # progressive disclosure deep content
│ ├── scripts/ # executable or source scripts
│ └── assets/ # templates, fixtures, binaries
├── agents/
│ └── <agent-name>.md # subagent definition (frontmatter: name, description, tools, model, color)
├── commands/
│ └── <command-name>.md # slash-command definition (frontmatter: description, argument-hint, allowed-tools)
└── LICENSE # plain-text license fileNothing in plugin.json points to these — Claude's plugin loader walks them.
.claude-plugin/plugin.json schema (observed)
Required
name— string, kebab-case slug matching the directory namedescription— string, one-sentence summary
Common
author—{name, email?, url?}
Observed in some plugins (extension fields)
None beyond those three. Anthropic's plugin-dev skill may document additional optional fields but they aren't observed in the 35+ plugins sampled.
Therefore, the minimal viable Claude `plugin.json`:
{
"name": "my-plugin",
"description": "What it does in one sentence."
}Plus .mcp.json (if MCP integration) and whatever convention dirs.
.mcp.json schema — FLAT shape
{
"<server-name-1>": {
"type": "http",
"url": "https://...:mcp"
},
"<server-name-2>": {
"command": "npx",
"args": ["-y", "some-npm-package"]
}
}Two transports observed:
- Remote HTTP:
{ "type": "http", "url": "..." } - Local stdio:
{ "command": "...", "args": [...], "env": {...}? }
Each server can have "env" for environment-variable plumbing.
hooks/hooks.json schema
{
"description": "Optional plugin-level description",
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/pre-bash.sh", "timeout": 5000 }
]
}
],
"PostToolUse": [...],
"Stop": [...],
"SessionStart": [...],
"UserPromptSubmit": [...]
}
}Events observed: PreToolUse, PostToolUse, Stop, SessionStart, UserPromptSubmit. Tool matchers are strings (exact tool name) or regex.
${CLAUDE_PLUGIN_ROOT} is the installed-plugin root at runtime.
skills/<skill>/SKILL.md frontmatter keys (union, observed)
| Key | Type | Required | Notes |
|---|---|---|---|
name | string | yes | kebab-case; must match <skill> dir |
description | string | yes | one-sentence trigger explainer |
version | string | no | semver |
allowed-tools | string (CSV) OR array | no | narrows runtime tool permissions |
disable-model-invocation | boolean | no | true = skill only invokable via slash command, not auto-detected by LLM |
user-invocable | boolean | no | true = appears in slash-menu |
color | string | no | hex color for UI accent |
model | string | no | override default model for this skill |
tools | array<string> | no | (on agent.md frontmatter, not SKILL.md typically) |
agents/<agent>.md frontmatter
---
name: my-agent
description: When to invoke this agent
tools: [Read, Edit, Bash]
model: claude-sonnet-4-5
color: "#BF5700"
---
# My Agent
Body defines agent instructions.commands/<command>.md frontmatter
---
description: What the command does
argument-hint: <file-path>
allowed-tools: [Read, Edit]
---
Command body / prompt template.Marketplace catalog: .claude-plugin/marketplace.json
Ships at the marketplace-repo root (e.g. anthropics/claude-plugins-official/.claude-plugin/marketplace.json). Lists plugins the marketplace hosts.
Observed shape (from anthropics/claude-plugins-official):
{
"name": "claude-plugins-official",
"description": "Official Anthropic Claude Code plugins",
"owner": { "name": "Anthropic", "url": "https://anthropic.com" },
"plugins": [
{
"name": "mcp-server-dev",
"source": "./plugins/mcp-server-dev",
"description": "Build MCP servers"
},
{
"name": "context7",
"source": {
"source": "git-subdir",
"url": "https://github.com/upstash/context7",
"subdir": ".",
"ref": "main"
}
}
]
}Each plugin entry has:
name— plugin slugdescription— summarysource— either a local path (string) OR an object withsource: "git-subdir"+url+subdir+ref
Real examples in our assets/real-examples/
context7-plugin.json— the 7-line minimal baseline (we emulate this for.claude-plugin/plugin.json)mcp-server-dev-plugin.json— Anthropic's own MCP server development plugin (shows the real baseline from first-party)hookify-plugin.json+hookify-hooks.json— shows the hooks convention with real matchers
Annotations on each file indicate the absolute source path in the local filesystem, so the reader can verify.
What Claude's plugin loader actually reads
At runtime, Claude Code: 1. Reads plugin.json for identity 2. Walks skills/ — each subdir with a SKILL.md becomes a registered skill; trigger keywords come from frontmatter description 3. Walks agents/ — each .md file becomes a subagent; frontmatter description determines auto-invocation 4. Walks commands/ — each .md becomes a slash command 5. Reads hooks/hooks.json — registers event handlers 6. Reads .mcp.json — registers MCP servers
No plugin.json field drives any of this. Purely convention.
Translator implications
Claude→Codex must synthesize the Codex manifest's richer structure from walked dirs. Codex→Claude must strip to minimal plugin.json and stash the rest in sidecar (codex_ecosystem.json).
Implementation: packages/mcp-server/src/lib/translator.ts functions toCanonical(claude_dir) walks all conventions; fromCanonical(c, "claude") writes only the 3-field plugin.json plus all the walked dirs plus the sidecar.
See `11-manifest-translator-algorithm.md` for the full algorithm.
Gotchas
- Nested skills directories not observed — Claude expects
skills/<skill>/SKILL.md, neverskills/<group>/<skill>/SKILL.md. Flatten on translation. - `.md` vs `.markdown` — Claude reads
.mdonly (observed). Codex same. - UTF-8 BOM — several real plugins ship with BOM prefixes. Parser must strip BOM before YAML parse.
- Frontmatter YAML vs `frontmatter` package — use
gray-matter(handles---delimiters, block arrays, etc.). Do not hand-parse. - `${CLAUDE_PLUGIN_ROOT}` env substitution happens at hook execution time. Our translator leaves the string literal in place — it's a runtime concern.
Source references
~/.claude/plugins/marketplaces/claude-plugins-official/plugins/plugin-dev/skills/plugin-structure/SKILL.md~/.claude/plugins/marketplaces/claude-plugins-official/plugins/plugin-dev/skills/skill-development/SKILL.md~/.claude/plugins/marketplaces/claude-plugins-official/plugins/mcp-server-dev/.claude-plugin/plugin.json~/.claude/plugins/marketplaces/claude-plugins-official/external_plugins/context7/.claude-plugin/plugin.json~/.claude/plugins/marketplaces/claude-plugins-official/plugins/hookify/hooks/hooks.json
03 — Codex Plugin Format
Derived from 16 installed openai-curated plugins at ~/.codex/plugins/cache/openai-curated/*/fb0a18376bcd9f2604047fbe7459ec5aed70c64b/.codex-plugin/plugin.json. Full evidence + field frequencies in `docs/spikes/codex-schema-drift.md`. JSON Schema at `schema/codex-plugin.schema.json`.
Core insight: Codex plugins pack everything into plugin.json
Unlike Claude's minimalist convention-based model, Codex's plugin.json is a rich declarative document:
- Top-level metadata: name, version, description, author, homepage, repository, license, keywords
- Convention pointers:
skills,mcpServers,hooks,apps(paths to files/dirs) - A full
interface{}block for the Codex marketplace UI (displayName, category, capabilities, icons, colors, etc.)
The reason: Codex's plugin loader is declarative. It reads the manifest to know what the plugin contains.
Required top-level fields (100% frequency)
| Field | Type | Example |
|---|---|---|
name | string (kebab-case) | "canva", "build-web-apps" |
version | string (semver) | "0.1.0", "1.0.0", "2.0.7" |
description | string | one-sentence summary |
author | object | {name?, email?, url?} — shape varies |
homepage | string (URI) | "https://openai.com/" |
repository | string (URI) | "https://github.com/openai/plugins" |
license | string | "MIT", "Apache-2.0", "Proprietary", "LicenseRef-Figma-Developer-Terms" |
keywords | array<string> | 0-17 entries |
skills | string (path) | always "./skills/" |
interface | object | see "interface block" below |
Optional top-level fields (observed frequency)
| Field | Type | Frequency | Example |
|---|---|---|---|
apps | string (path to .app.json) | 56% (9/16) | "./.app.json" |
mcpServers | string (path to .mcp.json) | 31% (5/16) | "./.mcp.json" |
hooks | string (path to hooks.json) | 6% (1/16, figma only) | "./hooks.json" |
All optional-path fields always point to a file/dir at ./... relative to the plugin root.
The interface{} block
Codex-only catalog metadata. Used by Codex's marketplace UI to render plugin tiles, category browsing, brand theming.
100% required sub-fields
| Field | Type | Purpose |
|---|---|---|
displayName | string | Title-case name shown in UI |
shortDescription | string | ≤60 chars, tile tagline |
longDescription | string | Paragraph for detail page |
category | string | Enum observed: Coding, Productivity, Communication, Research, Design |
capabilities | array<string> | Enum observed: Interactive, Write, Read; can be empty |
websiteURL | string (URI) | Vendor homepage |
privacyPolicyURL | string | Required even if empty (some plugins use "") |
termsOfServiceURL | string | Same |
defaultPrompt | string \ | array<string> |
composerIcon | string | Path to small icon (./assets/...) |
logo | string | Path to logo |
screenshots | array | 0-N entries; shape unverified (observed empty in most) |
88% required sub-fields
| Field | Type | Present in |
|---|---|---|
developerName | string | 14/16 (absent in canva, stripe) |
brandColor | string (#RRGGBB) | 14/16 (absent in canva, stripe) |
.mcp.json schema — WRAPPED shape
Referenced from plugin.json.mcpServers (path). File contents:
{
"mcpServers": {
"<server-name-1>": {
"type": "http",
"url": "https://..."
},
"<server-name-2>": {
"command": "npx",
"args": ["-y", "some-package"]
}
}
}Note the outer mcpServers wrapper key. This is different from Claude's flat shape.
Translator handles the wrap/unwrap explicitly — see `11-manifest-translator-algorithm.md`.
.app.json schema
Referenced from plugin.json.apps (path). Connector/app registry:
{
"apps": {
"canva": { "id": "connector_c0ffee..." },
"github": { "id": "connector_abc123..." }
}
}Values are Codex connector identifiers. These represent OAuth-backed integrations into host services. No Claude equivalent exists — on Codex→Claude translation, stash in sidecar and emit docs/codex-apps.notes.md.
hooks.json schema
Root-level file (not under hooks/ like Claude's). Observed shape matches Claude's:
{
"description": "optional",
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./scripts/post.sh" }
]
}
]
}
}Event support (confirmed in corpus): PostToolUse (figma plugin). Event support (not confirmed): PreToolUse, Stop, SessionStart, UserPromptSubmit. See Spike S4.
skills/<skill>/ structure
Identical to Claude's: SKILL.md + references/ + scripts/ + assets/. Key differences:
- Codex SKILL.md frontmatter uses only
nameanddescriptionin observed samples (noallowed-tools,disable-model-invocation,user-invocable) - Some Codex skills have a nested
skills/<skill>/agents/openai.yaml— skill-level interface descriptor with:display_name,short_description,icon_small,icon_large,brand_color,default_prompt - Some Codex skills have a nested
skills/<skill>/agents/*.md— skill-scoped agent definitions
Pure Claude plugins don't have nested skill agents or skill-level YAML interfaces. Translator must flatten on Codex→Claude (or stash + emit HTML-comment shim in SKILL.md body).
agents/ directory
Codex plugins ship an agents/openai.yaml file at plugin level that mirrors interface{} but in YAML. Observed in most plugins. Plus standard *.md agent definitions (same shape as Claude).
Translator:
- Claude→Codex: synthesize
openai.yamlfrominterface{}when generating - Codex→Claude: discard
openai.yaml(data is already in canonicalinterfacefield)
Marketplace catalog: marketplace.json
Located at <repo>/.agents/plugins/marketplace.json. Format observed TBD in Phase 3 when we generate one via the bridge Worker; specification derived from observed plugin-list patterns.
Proposed shape (consistent with Claude's):
{
"name": "openai-curated",
"description": "OpenAI-curated plugins",
"owner": { "name": "OpenAI", "url": "https://openai.com" },
"plugins": [
{
"name": "canva",
"source": "./canva/fb0a18376bcd9f2604047fbe7459ec5aed70c64b",
"version": "1.0.0"
},
...
]
}Bridge Worker emits this dynamically at GET /.agents/plugins/marketplace.json from D1 queries — see `05-cloudflare-workers-playbook.md`.
Codex config registration
Users register plugins in ~/.codex/config.toml:
[plugins."<plugin-name>@<marketplace-name>"]
enabled = trueObserved real config (from Austin's machine):
[plugins."github@openai-curated"]
enabled = true
[plugins."cloudflare@openai-curated"]
enabled = trueThe <plugin-name>@<marketplace-name> form is namespace-qualified.
For MCP servers registered outside plugins:
[mcp_servers.<server-name>]
command = "npx"
args = ["-y", "@blazesportsintel/universal-skills-mcp"]This is expected syntax (not yet confirmed) — to be verified in Phase 2 P2-9.
Real examples in assets/real-examples/
openai-canva-plugin.json— verbatim from~/.codex/plugins/cache/openai-curated/canva/.../plugin.json. Shows minimal author ({url}only), empty keywords,appspointer, nomcpServers, nodeveloperName/brandColor.openai-cloudflare-plugin.json— showsmcpServerspointer, full author object,commands/directory convention.openai-github-plugin.json— showsappswith connector ID.openai-figma-plugin.json— the only plugin withhooksfield, exercisesPostToolUse.
Each annotated with absolute source path.
Gotchas
- `license` is a free-form string, not strictly SPDX. Observed:
MIT,Apache-2.0,Proprietary,LicenseRef-Figma-Developer-Terms. Translator must not coerce to enum. - `repository` may be malformed.
life-science-researchusesgithub.com/openai/openai/tree/master/plugins/life-science-research(doubleopenai) — preserve verbatim, don't "fix." - Nested skill agents are Codex-only. Flattening on Claude target loses the nesting hierarchy → logged as lossy; reversal restores from sidecar.
- `defaultPrompt` is union-typed. Canonical form is always
array<string>; single-string inputs become[value]. - Hidden dot-prefixed dirs (`.agents/`, `.codex-plugin/`) vs non-dot (`agents/`, `commands/`) — not the same.
.codex-plugin/holds the manifest;agents/holds content. Parser must respect this.
Source references
~/.codex/plugins/cache/openai-curated/<plugin>/fb0a18376bcd9f2604047fbe7459ec5aed70c64b/.codex-plugin/plugin.json× 16 plugins~/.codex/config.toml(live real config)- `docs/spikes/codex-schema-drift.md` — field frequency table + translator implications
- `schema/codex-plugin.schema.json` — machine-readable schema
04 — MCP Tool Design
How the three tools (resolve-skill, get-skill-content, install-skill) are shaped, what inputs they accept, what outputs they return, and how they fit the Pattern B (search + execute) strategy from Anthropic's canonical `build-mcp-server` skill.
Why Pattern B and not one-tool-per-action
Per build-mcp-server Phase 3, Pattern B fits when the action space is large. Our catalog will grow from ~50 to potentially thousands of skills as upstream repos evolve. Exposing one tool per skill would flood Claude's context window. Instead:
resolve-skill= search (returns IDs + lightweight metadata)get-skill-content= fetch one specific skill's content (progressive disclosure)install-skill= action on a resolved skill (write or emit command)
The server holds the full catalog internally. Client searches, picks, fetches, executes. Context stays lean.
Tool surface summary
| Tool | Purpose | When Claude/Codex invokes it |
|---|---|---|
resolve-skill | Find skills matching natural-language query + filters | User asks "find a skill for X"; assistant is choosing between candidates |
get-skill-content | Fetch parsed SKILL.md (metadata / body / references / scripts / assets) | After resolve, before install, when assistant needs to reason about a specific skill |
install-skill | Emit install command OR write skill to disk | User confirms install |
Tool 1: resolve-skill
Purpose
Given a natural-language query plus optional filters, return ranked matching skills from the catalog. Uses D1 FTS5 BM25 ranking on Phase 3; in-memory substring + GitHub Code Search for Phase 2.
Input schema (zod)
import { z } from "zod";
export const ResolveSkillInput = z.object({
query: z.string().min(1).max(500)
.describe("Natural-language query. Example: 'PDF processing', 'React component scaffolding', 'Stripe webhook validation'"),
ecosystem: z.enum(["claude", "codex", "universal", "any"]).default("any")
.describe("Filter by source ecosystem. 'universal' = works in both without translation. 'any' = no filter."),
category: z.string().optional()
.describe("Filter by Codex-style category (e.g. 'Coding', 'Productivity', 'Communication', 'Research', 'Design'). Matches against canonical.category."),
min_quality: z.number().int().min(0).max(100).default(30)
.describe("Minimum quality score 0-100. Default 30 excludes obvious stubs."),
source_repo: z.string().optional()
.describe("Filter by upstream source repo, e.g. 'anthropics/skills' or 'openai/plugins'"),
limit: z.number().int().min(1).max(50).default(10)
.describe("Max results to return"),
});Descriptions matter: Claude reads these into the tool-call decision. Be precise.
Output shape
export const ResolveSkillOutput = z.object({
results: z.array(z.object({
id: z.string(), // e.g. "anthropics-skills/pdf"
name: z.string(),
description: z.string(),
quality_score: z.number().int().min(0).max(100),
source_ecosystem: z.enum(["claude", "codex", "universal"]),
source_url: z.string().url(), // link to upstream repo @ commit
compatibility: z.object({
claude: z.boolean(),
codex: z.boolean(),
}),
install_commands: z.object({
claude: z.string(), // e.g. "/plugin install pdf@anthropics-skills"
codex: z.string(), // e.g. "$skill-installer install openai-curated/canva"
}),
content_hash: z.string(), // sha256 of canonical JSON
})),
meta: z.object({
source: z.literal("universal-skills-marketplace"),
fetched_at: z.string().datetime(),
timezone: z.literal("America/Chicago"),
registry_version: z.string(), // e.g. "0.1.0"
cache_hit: z.boolean(),
query_time_ms: z.number(),
}),
});Error cases (typed)
| Error | Code | When |
|---|---|---|
ValidationError | 400 | Zod input validation fails |
RegistryUnavailableError | 503 | D1 query times out, indexer in bad state |
GitHubRateLimitError | 429 | (Phase 2 only) GitHub API quota exhausted; returns retry_after_seconds |
NoResultsError | 200 | Empty results array; includes suggested alternate queries |
Empty results are NOT errors — they're a successful query with an empty result set. Claude should handle that state gracefully (suggest broader terms, or ecosystem=any).
Implementation hook — Phase 2 (GitHub-backed)
export async function resolveSkill(input: z.infer<typeof ResolveSkillInput>) {
const validated = ResolveSkillInput.parse(input);
const cacheKey = sha1(JSON.stringify(validated));
const cached = await cache.get(cacheKey);
if (cached) return { ...cached, meta: { ...cached.meta, cache_hit: true }};
// GitHub Search: filename:SKILL.md + query
const ghResults = await githubClient.searchCode({
q: `filename:SKILL.md "${validated.query}"`,
per_page: Math.min(validated.limit * 2, 50),
});
// Score, rank, filter, trim
const results = await Promise.all(
ghResults.items.map(async (item) => {
const content = await githubClient.getContents(item.url);
const canonical = await translator.toCanonical(content, detectEcosystem(item.path));
const score = scorer.scoreSkill(canonical);
return canonicalToResolveResult(canonical, score);
})
);
const filtered = results
.filter(r => r.quality_score >= validated.min_quality)
.slice(0, validated.limit);
await cache.set(cacheKey, { results: filtered, meta: { ... }}, 600);
return { results: filtered, meta: { source: "...", fetched_at: new Date().toISOString(), cache_hit: false, ... }};
}Implementation hook — Phase 3 (D1-backed)
export async function resolveSkill(env: Env, input: z.infer<typeof ResolveSkillInput>) {
const validated = ResolveSkillInput.parse(input);
const sql = `
SELECT s.id, s.name, s.description, s.quality_score, s.source_ecosystem, s.source_url,
s.compat_claude, s.compat_codex, s.content_hash, bm25(skills_fts) as rank
FROM skills s
JOIN skills_fts fts ON s.rowid = fts.rowid
WHERE skills_fts MATCH ?
AND s.quality_score >= ?
AND s.tombstoned = 0
${validated.ecosystem !== "any" ? "AND s.source_ecosystem = ?" : ""}
${validated.category ? "AND s.tags LIKE ?" : ""}
ORDER BY rank
LIMIT ?
`;
const bindings = [validated.query, validated.min_quality, ...conditionalBindings, validated.limit];
const rows = await env.DB.prepare(sql).bind(...bindings).all();
return {
results: rows.results.map(rowToResolveResult),
meta: { source: "universal-skills-marketplace", fetched_at: new Date().toISOString(), ... },
};
}Tool 2: get-skill-content
Purpose
Fetch a specific skill's content with progressive disclosure. Metadata is cheap; references can be heavy. Client asks for what it needs.
Input schema
export const GetSkillContentInput = z.object({
id: z.string().describe("Skill ID as returned by resolve-skill. Format: '{source}/{name}' e.g. 'anthropics-skills/pdf'"),
include: z.array(z.enum(["metadata", "body", "references", "scripts", "assets", "canonical_json"]))
.default(["metadata", "body"])
.describe("Which parts to return. 'metadata' is always cheap; 'references' can be heavy. Request only what you need."),
version: z.string().optional().describe("Specific version. Defaults to latest."),
});Output shape
export const GetSkillContentOutput = z.object({
id: z.string(),
version: z.string(),
metadata: z.object({
name: z.string(),
description: z.string(),
frontmatter: z.record(z.unknown()), // raw parsed YAML frontmatter
trigger_keywords: z.array(z.string()),
source_url: z.string().url(),
quality_score: z.number(),
}).optional(),
body: z.string().optional(), // the SKILL.md body (without frontmatter)
references: z.array(z.object({
path: z.string(), // relative: "references/00-architecture-overview.md"
content: z.string(),
sha256: z.string(),
size_bytes: z.number(),
})).optional(),
scripts: z.array(z.object({
path: z.string(),
content: z.string(),
mode: z.string(), // e.g. "755" for executables
})).optional(),
assets: z.array(z.object({
path: z.string(),
mime: z.string(),
url: z.string().url(), // R2 signed URL for binary downloads
size_bytes: z.number(),
})).optional(),
canonical_json: z.record(z.unknown()).optional(),
meta: z.object({
source: z.literal("universal-skills-marketplace"),
fetched_at: z.string().datetime(),
timezone: z.literal("America/Chicago"),
}),
});Progressive disclosure examples
Cheap:
{"id": "anthropics-skills/pdf", "include": ["metadata"]}
→ returns name + description + frontmatter only (~500 bytes)Standard:
{"id": "anthropics-skills/pdf"} // include defaults to ["metadata","body"]
→ adds SKILL.md body (typically 5-20KB)Deep-dive:
{"id": "anthropics-skills/pdf", "include": ["metadata","body","references","scripts"]}
→ adds all reference files (can be 100KB+)Binary assets:
{"id": "anthropics-skills/pdf", "include": ["assets"]}
→ returns signed R2 URLs (not inline bytes). Client fetches what it wants.Error cases
| Error | Code | When |
|---|---|---|
SkillNotFoundError | 404 | No skill with this id |
VersionNotFoundError | 404 | Skill exists but that version doesn't |
UpstreamFetchError | 502 | R2 fetch failed or GitHub fetch failed |
ContentIntegrityError | 500 | Returned content's sha256 doesn't match D1 content_hash — something's corrupted |
Tool 3: install-skill
Purpose
Either emit the exact install command for the target ecosystem OR write the skill directly to disk.
Input schema
export const InstallSkillInput = z.object({
id: z.string(),
target: z.enum(["claude", "codex", "auto-detect"]).default("auto-detect")
.describe("Which ecosystem's install layout to produce. 'auto-detect' reads $CLAUDE_CONFIG_DIR or $CODEX_HOME presence."),
mode: z.enum(["command-only", "write-to-disk"]).default("command-only")
.describe("'command-only' returns a shell command for the user to run. 'write-to-disk' writes files directly to the ecosystem skills dir."),
scope: z.enum(["user", "project"]).default("user")
.describe("'user' = ~/.claude/skills/ or ~/.codex/skills/. 'project' = .claude/skills/ or .codex/skills/ in $CWD."),
});Output shape
export const InstallSkillOutput = z.discriminatedUnion("mode", [
z.object({
mode: z.literal("command-only"),
ecosystem: z.enum(["claude", "codex"]),
command: z.string(), // e.g. "/plugin install pdf@anthropics-skills"
target_dir: z.string(), // where the command will install to
notes: z.array(z.string()), // e.g. ["Requires Claude Code 2.1.76+"]
}),
z.object({
mode: z.literal("write-to-disk"),
ecosystem: z.enum(["claude", "codex"]),
target_dir: z.string(),
written: z.array(z.object({
path: z.string(),
sha256: z.string(),
size_bytes: z.number(),
})),
skipped: z.array(z.object({
path: z.string(),
reason: z.string(), // e.g. "already exists with different sha256"
})),
}),
]);Auto-detect logic
function autoDetectTarget(): "claude" | "codex" | null {
const claudeExists = fs.existsSync(path.join(os.homedir(), ".claude"));
const codexExists = fs.existsSync(path.join(os.homedir(), ".codex"));
if (claudeExists && !codexExists) return "claude";
if (codexExists && !claudeExists) return "codex";
if (claudeExists && codexExists) return null; // ambiguous — ask user
return null;
}If ambiguous, return AmbiguousTargetError (code 400) asking the user to specify explicitly.
Safety
write-to-diskwill NEVER overwrite files with different sha256. Either skips (preserving user's version) or errors if a force flag is added in a future version.write-to-diskonly writes to~/.claude/skills/or~/.codex/skills/or./.claude/skills/or./.codex/skills/— nowhere else, ever. Path traversal is blocked.- All install paths are normalized with
path.resolve+ a suffix check before any write.
Error cases
| Error | Code |
|---|---|
AmbiguousTargetError | 400 — both ecosystems present, target was auto-detect |
SkillNotFoundError | 404 |
WriteProtectedError | 403 — target dir not writable |
PathEscapeAttempt | 400 — sanitization caught an attempt to write outside allowed dirs |
Tool metadata for MCP registration
server.registerTool({
name: "resolve-skill",
description: "Search the ClaudOpenAI universal skills marketplace for skills matching a natural-language query. Returns ranked results with quality scores and install commands for both Claude Code and Codex. Use when the user asks to 'find a skill for X' or 'search for skills'.",
inputSchema: zodToJsonSchema(ResolveSkillInput),
handler: resolveSkillHandler,
});
server.registerTool({
name: "get-skill-content",
description: "Fetch the full content of a specific skill by ID (as returned by resolve-skill). Supports progressive disclosure: fetch just metadata, or pull references/scripts/assets as needed. Use when you need to inspect a skill's SKILL.md body or reference files before installing.",
inputSchema: zodToJsonSchema(GetSkillContentInput),
handler: getSkillContentHandler,
});
server.registerTool({
name: "install-skill",
description: "Install a resolved skill either by emitting the exact CLI command (default, safe) or by writing files directly to the appropriate ecosystem skills directory (mode='write-to-disk'). Auto-detects target ecosystem from ~/.claude or ~/.codex presence.",
inputSchema: zodToJsonSchema(InstallSkillInput),
handler: installSkillHandler,
});Response formatting best practices
- Tool descriptions land in Claude's context. Keep them one-to-three sentences, concrete.
describe()on every zod field. Claude uses these.- Return ISO 8601 timestamps (
Zsuffix). Let the client parse. - Never return
undefinedornullfor required fields. Return the typed error. metablock on EVERY response. Consistency matters to the client.
Testing
Unit tests per tool in packages/mcp-server/tests/unit/tools/*.test.ts. Integration tests gated behind CI_REAL_GITHUB=1 flag (they consume rate limit). See `12-verification-playbook.md` for the full matrix.
See also
build-mcp-serverskillreferences/tool-design.md— deep dive on description writing- `11-manifest-translator-algorithm.md` — what the tools serve
build-mcp-serverPhase 3 (Pattern B vs Pattern A trade-offs)
05 — Cloudflare Workers Playbook
Deployment recipe for the three Workers in workers/. Cross-links Anthropic's canonical `building-mcp-server-on-cloudflare` skill and Austin's own reference implementation at BSI-repo/workers/college-baseball-mcp/.
Why Workers
Per build-mcp-server SKILL.md Phase 2 recommendation: "fastest deploy path (Workers-native scaffold) ... zero to live URL in two commands."
Our workloads fit:
- api — HTTP MCP serving stateless tool calls, low latency < 200ms budget
- indexer — scheduled cron with D1/R2 writes
- bridge — read-only catalog emission with D1 queries
All three are stateless request-handlers or scheduled jobs. No long-running processes. Perfect Workers fit.
Repository layout
workers/
├── universal-skills-api/
│ ├── src/
│ │ ├── index.ts # fetch() handler, JSON-RPC 2.0 routing
│ │ ├── routes/
│ │ │ ├── mcp.ts # POST /mcp
│ │ │ ├── resolve.ts # inner resolve-skill implementation
│ │ │ ├── content.ts # inner get-skill-content
│ │ │ ├── install.ts # inner install-skill
│ │ │ └── health.ts # GET /health
│ │ └── lib/
│ │ ├── d1.ts # D1 query helpers
│ │ ├── r2.ts # presigned URL generation
│ │ └── rate-limit.ts # KV-backed rate limiter
│ ├── wrangler.toml
│ ├── package.json
│ ├── tsconfig.json
│ └── tests/
├── universal-skills-indexer/
│ ├── src/
│ │ ├── index.ts # scheduled() handler
│ │ └── lib/
│ │ ├── github-client.ts
│ │ ├── sources.ts # 9 upstream repo definitions
│ │ ├── clone-walk.ts # sparse-clone + filesystem walk
│ │ ├── normalize.ts # → canonical via @blazesportsintel/universal-skills-schema
│ │ └── scorer.ts # shared with npm package
│ ├── wrangler.toml
│ └── ...
└── universal-skills-bridge/
├── src/
│ ├── index.ts
│ ├── routes/
│ │ ├── claude-marketplace.ts # GET /.claude-plugin/marketplace.json
│ │ ├── codex-marketplace.ts # GET /.agents/plugins/marketplace.json
│ │ ├── well-known.ts # GET /.well-known/universal-skills.json
│ │ └── health.ts
│ └── lib/
│ ├── d1.ts
│ ├── render-claude.ts
│ └── render-codex.ts
├── wrangler.toml
└── ...Each worker has its own wrangler.toml + package.json + tsconfig.json. Npm workspaces resolve shared deps (@blazesportsintel/universal-skills-schema).
wrangler.toml per Worker
api
name = "universal-skills-api"
main = "src/index.ts"
compatibility_date = "2026-04-01"
compatibility_flags = ["nodejs_compat"]
[[d1_databases]]
binding = "DB"
database_name = "universal-skills"
database_id = "<from wrangler d1 create>"
[[r2_buckets]]
binding = "CONTENT"
bucket_name = "universal-skills-content"
[[kv_namespaces]]
binding = "CACHE"
id = "<from wrangler kv:namespace create>"
[[kv_namespaces]]
binding = "RATE_LIMIT"
id = "<from wrangler kv:namespace create>"
[observability]
enabled = true
[[routes]]
pattern = "api.marketplace.blazesportsintel.com/*"
zone_name = "blazesportsintel.com"
custom_domain = true
[vars]
REGISTRY_VERSION = "0.1.0"indexer
name = "universal-skills-indexer"
main = "src/index.ts"
compatibility_date = "2026-04-01"
compatibility_flags = ["nodejs_compat"]
[triggers]
crons = ["0 */6 * * *"] # every 6 hours
[[d1_databases]]
binding = "DB"
database_name = "universal-skills"
database_id = "<same as api>"
[[r2_buckets]]
binding = "CONTENT"
bucket_name = "universal-skills-content"
[[kv_namespaces]]
binding = "INDEXER_STATE"
id = "<from wrangler kv:namespace create>"
[observability]
enabled = true
# No routes — internal only, cron-drivenSecret: GITHUB_TOKEN via wrangler secret put GITHUB_TOKEN --config workers/universal-skills-indexer/wrangler.toml.
bridge
name = "universal-skills-bridge"
main = "src/index.ts"
compatibility_date = "2026-04-01"
[[d1_databases]]
binding = "DB"
database_name = "universal-skills"
database_id = "<same>"
[observability]
enabled = true
[[routes]]
pattern = "marketplace.blazesportsintel.com/*"
zone_name = "blazesportsintel.com"
custom_domain = true
[[routes]]
pattern = "registry.marketplace.blazesportsintel.com/*"
zone_name = "blazesportsintel.com"
custom_domain = trueThe 5-command provision sequence
# 1. D1
wrangler d1 create universal-skills
wrangler d1 execute universal-skills --file=schema/d1-schema.sql
# 2. R2
wrangler r2 bucket create universal-skills-content
# 3. KV × 3
wrangler kv:namespace create CACHE
wrangler kv:namespace create RATE_LIMIT
wrangler kv:namespace create INDEXER_STATE
# 4. Secrets
wrangler secret put GITHUB_TOKEN --config workers/universal-skills-indexer/wrangler.toml
# 5. Deploy all three
npm run deploy:allHandler pattern — api worker
// workers/universal-skills-api/src/index.ts
import { handleMcp } from "./routes/mcp";
import { handleHealth } from "./routes/health";
import { rateLimit } from "./lib/rate-limit";
export interface Env {
DB: D1Database;
CONTENT: R2Bucket;
CACHE: KVNamespace;
RATE_LIMIT: KVNamespace;
REGISTRY_VERSION: string;
}
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(req.url);
try {
// Health check bypasses rate limit
if (url.pathname === "/health") {
return handleHealth(env);
}
// Rate limit
const rl = await rateLimit(req, env.RATE_LIMIT, { rpm: 60 });
if (!rl.ok) {
return new Response(JSON.stringify({ error: "rate_limited", retry_after: rl.retryAfter }), {
status: 429,
headers: { "Retry-After": String(rl.retryAfter), "content-type": "application/json" }
});
}
// MCP endpoint
if (url.pathname === "/mcp" && req.method === "POST") {
return handleMcp(req, env, ctx);
}
return new Response(JSON.stringify({ error: "not_found" }), { status: 404 });
} catch (err) {
console.error(err);
return new Response(JSON.stringify({
error: "internal_server_error",
message: err instanceof Error ? err.message : "unknown"
}), { status: 500, headers: { "content-type": "application/json" }});
}
}
};JSON-RPC 2.0 MCP handler shape
// workers/universal-skills-api/src/routes/mcp.ts
export async function handleMcp(req: Request, env: Env, ctx: ExecutionContext) {
const body = await req.json();
// Validate JSON-RPC 2.0 envelope
if (body.jsonrpc !== "2.0" || !body.method) {
return rpcError(-32600, "Invalid Request", body.id);
}
switch (body.method) {
case "tools/list":
return rpcResult({
tools: [RESOLVE_SKILL_TOOL_DEF, GET_SKILL_CONTENT_TOOL_DEF, INSTALL_SKILL_TOOL_DEF]
}, body.id);
case "tools/call":
const { name, arguments: args } = body.params || {};
switch (name) {
case "resolve-skill":
return rpcResult({ content: [{ type: "text", text: JSON.stringify(await handleResolveSkill(args, env)) }] }, body.id);
case "get-skill-content":
return rpcResult({ content: [{ type: "text", text: JSON.stringify(await handleGetSkillContent(args, env)) }] }, body.id);
case "install-skill":
return rpcResult({ content: [{ type: "text", text: JSON.stringify(await handleInstallSkill(args, env)) }] }, body.id);
default:
return rpcError(-32601, `Unknown tool: ${name}`, body.id);
}
default:
return rpcError(-32601, `Unknown method: ${body.method}`, body.id);
}
}
function rpcResult(result: unknown, id: unknown) {
return new Response(JSON.stringify({ jsonrpc: "2.0", id, result }), {
headers: { "content-type": "application/json" }
});
}
function rpcError(code: number, message: string, id: unknown) {
return new Response(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message }}), {
headers: { "content-type": "application/json" }
});
}This mirrors the pattern in BSI-repo/workers/college-baseball-mcp/src/worker.ts. Fork it when writing ours.
Streamable HTTP transport
Per MCP spec 2025-06-18, streamable-HTTP allows servers to send SSE events as tool results. For v0.1 we ship non-streaming responses (single JSON object in Response body). Upgrade path: add content-type: text/event-stream branch when tools/call yields chunks.
Scheduled (cron) handler pattern — indexer worker
// workers/universal-skills-indexer/src/index.ts
export interface Env {
DB: D1Database;
CONTENT: R2Bucket;
INDEXER_STATE: KVNamespace;
GITHUB_TOKEN: string;
}
export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
ctx.waitUntil(runIndexCycle(env));
},
// Also support manual run via fetch (for dev)
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(req.url);
if (url.pathname === "/health") return new Response(JSON.stringify({ status: "ok" }));
if (url.pathname === "/run" && req.method === "POST") {
ctx.waitUntil(runIndexCycle(env));
return new Response(JSON.stringify({ started: true }));
}
return new Response("not found", { status: 404 });
}
};
async function runIndexCycle(env: Env): Promise<void> {
const sources = await listSources(env);
for (const src of sources) {
try {
const headSha = await getRepoHead(src, env.GITHUB_TOKEN);
const lastSyncSha = await env.INDEXER_STATE.get(`sha:${src.name}`);
if (headSha === lastSyncSha) {
await env.INDEXER_STATE.put(`last_checked:${src.name}`, new Date().toISOString());
continue;
}
const files = await sparseClone(src, headSha, env.GITHUB_TOKEN);
for (const file of files) {
const canonical = await normalize(file);
const hash = sha256(JSON.stringify(canonical));
await upsertSkill(env.DB, canonical, hash);
if (file.content) {
await env.CONTENT.put(`skills/${canonical.id}/${canonical.version}/skill.md`, file.content);
}
}
await env.INDEXER_STATE.put(`sha:${src.name}`, headSha);
} catch (err) {
console.error(`Indexer failed for ${src.name}:`, err);
// Continue with next source
}
}
}See `08-github-indexer-design.md` for the full sparse-clone strategy + rate-limit math.
Local dev with wrangler dev
# API worker
wrangler dev --config workers/universal-skills-api/wrangler.toml --local
# Indexer — trigger cron manually
wrangler dev --config workers/universal-skills-indexer/wrangler.toml --test-scheduled --local
# Bridge
wrangler dev --config workers/universal-skills-bridge/wrangler.toml --local--local uses miniflare (simulated Workers runtime). D1/R2/KV bindings use local SQLite/filesystem/in-memory — no cloud API calls during dev.
Tests with miniflare
// workers/universal-skills-api/tests/health.test.ts
import { describe, it, expect } from "vitest";
import worker from "../src/index";
import { createD1, createR2, createKV } from "miniflare";
describe("health endpoint", () => {
it("returns 200 with status ok", async () => {
const env = { DB: createD1(":memory:"), CONTENT: createR2(), CACHE: createKV(), RATE_LIMIT: createKV(), REGISTRY_VERSION: "test" };
const req = new Request("https://api/health");
const res = await worker.fetch(req, env as any, {} as any);
expect(res.status).toBe(200);
expect(await res.json()).toMatchObject({ status: "ok" });
});
});Observability
[observability] enabled = true in every wrangler.toml turns on Workers logs + analytics. Tail a running worker:
wrangler tail universal-skills-api
wrangler tail universal-skills-indexer # see cron runsCloudflare dashboard → Workers → Analytics for request counts, error rates, P50/P99 latencies.
Deployment ceremony
npm run deploy:api # deploy api
npm run deploy:bridge # deploy bridge
npm run deploy:indexer # deploy indexer (cron picks up)
npm run deploy:all # all three in sequenceBound to git tag via .github/workflows/deploy-workers.yml (Phase 4).
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
wrangler deploy hangs > 5min | iCloud FileProvider backpressure | See `docs/spikes/icloud-build-strategy.md`; move build to /var/tmp/ |
D1_ERROR: Binding DB not found | wrangler.toml missing [[d1_databases]] | Re-run wrangler d1 list to get real ID; paste into toml |
429 Too Many Requests from GitHub | Indexer running without GITHUB_TOKEN secret | wrangler secret put GITHUB_TOKEN ... |
| MCP Inspector handshake fails | Response missing jsonrpc: "2.0" envelope | Check rpcResult/rpcError helpers; ensure content-type: application/json |
| Custom domain shows 1016 DNS error | CNAME not yet propagated or worker not bound | Wait 1-3 minutes; check wrangler dev logs for route match |
See also
- `building-mcp-server-on-cloudflare` — Anthropic's canonical Cloudflare-specific MCP guide
BSI-repo/workers/college-baseball-mcp/src/worker.ts— working reference implementation- `06-d1-schema-design.md` — D1 schema for
DBbinding - `07-r2-storage-patterns.md` — R2 key structure for
CONTENTbinding - `08-github-indexer-design.md` — indexer's full algorithm
07 — R2 Storage Patterns
The universal-skills-content R2 bucket holds the actual skill content bytes. D1 holds metadata. R2 holds bodies, reference files, and binary assets.
Bucket: universal-skills-content
Single bucket. Versioned by key path.
Key structure
skills/{id}/{version}/skill.md # Parsed SKILL.md (body only, no frontmatter separator)
skills/{id}/{version}/skill.full.md # Original SKILL.md including frontmatter
skills/{id}/{version}/canonical.json # CanonicalSkill JSON (full record)
skills/{id}/{version}/references.tgz # All references/ files tarballed
skills/{id}/{version}/scripts.tgz # All scripts/ tarballed, mode bits preserved
skills/{id}/{version}/assets/<path> # Individual asset files (binary, served directly)
skills/{id}/{version}/manifest.json # The original plugin.json or SKILL.md envelope{id}is the canonical skill ID:{source-namespace}/{name}. Slashes in the path preserve the hierarchy.{version}is semver fromskills.versionor synthetic (v+<commit-sha[0:7]>for Claude plugins without versions).- All content addressed — we never mutate an existing
{id}/{version}/*key. New versions get new paths.
Example
skills/anthropics-skills/pdf/0.3.1/skill.md
skills/anthropics-skills/pdf/0.3.1/skill.full.md
skills/anthropics-skills/pdf/0.3.1/canonical.json
skills/anthropics-skills/pdf/0.3.1/references.tgz
skills/anthropics-skills/pdf/0.3.1/assets/sample.pdf
skills/anthropics-skills/pdf/0.3.2/... # next version, separate tree
skills/openai-plugins/canva/1.0.0/skill.md
...Metadata headers
Every object uploaded with:
content-type—text/markdownfor.md,application/jsonfor.json,application/gzipfor.tgz, mime-detected for individual assetscache-control: public, max-age=86400, immutable— safe because content-addressed; never changesetag: sha256:<hash>— user-defined ETag set to the content's sha256 (lets clients verify)x-claudopenai-source: {source-repo}@{commit}— custom header for auditabilityx-claudopenai-indexed-at: {ISO8601}— custom header
Presigned URL generation
For get-skill-content with include=["assets"], the API Worker returns presigned R2 URLs instead of inline bytes:
import { AwsClient } from "aws4fetch";
async function presignR2Url(env: Env, key: string, ttl_seconds = 3600): Promise<string> {
// Use R2's S3-compatible API with temporary creds
const aws = new AwsClient({
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
service: "s3",
});
const url = new URL(`https://${env.R2_BUCKET_NAME}.${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${key}`);
url.searchParams.set("X-Amz-Expires", String(ttl_seconds));
const signed = await aws.sign(url.toString(), { aws: { signQuery: true }});
return signed.url;
}1-hour TTL default. Clients fetch directly from R2 (saves Worker CPU + bandwidth).
Caching via CDN
R2 serves via Cloudflare's CDN automatically. With cache-control: public, max-age=86400, immutable:
- First request from a given PoP: R2 origin
- Subsequent requests for 24h: CDN cache (no R2 cost)
immutable= browser never revalidates (perfect for content-addressed keys)
Upload pattern (indexer)
async function uploadSkillContent(env: Env, canonical: CanonicalSkill) {
const { id, version } = canonical;
const prefix = `skills/${id}/${version}`;
// 1. Canonical JSON
await env.CONTENT.put(`${prefix}/canonical.json`, JSON.stringify(canonical, null, 2), {
httpMetadata: {
contentType: "application/json",
cacheControl: "public, max-age=86400, immutable",
},
customMetadata: {
"source": `${canonical.origin.sourcePath}@${canonical.origin.sourceSha}`,
"indexedAt": new Date().toISOString(),
},
});
// 2. Each skill's SKILL.md (body)
for (const skill of canonical.skills) {
const skillPrefix = `${prefix}/skills/${skill.name}`;
await env.CONTENT.put(`${skillPrefix}/SKILL.md`, skill.body, { httpMetadata: { contentType: "text/markdown", ... }});
// 3. References tarball (bundle — avoid hundreds of small R2 ops)
if (skill.references.length > 0) {
const tarball = await createTarGz(skill.references);
await env.CONTENT.put(`${skillPrefix}/references.tgz`, tarball, { httpMetadata: { contentType: "application/gzip" }});
}
// 4. Individual assets (served individually — R2 handles binary well)
for (const asset of skill.assets) {
await env.CONTENT.put(`${skillPrefix}/assets/${asset.path}`, asset.bytes, {
httpMetadata: { contentType: asset.mime, cacheControl: "public, max-age=86400, immutable" },
});
}
}
}Tarball vs individual-file decision
- Tarball references & scripts: typically text, small, loaded together. One HTTP round-trip vs N. Win.
- Individual assets: often binary, may be large, loaded individually. Keep per-file so clients fetch only what they need.
Garbage collection
When a skill is tombstoned (skills.tombstoned = 1), its R2 objects are NOT immediately deleted. Kept for 30 days for rollback. Scheduled cleanup job (indexer worker, weekly cron):
async function gcTombstoned(env: Env) {
const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
const stale = await env.DB.prepare(`
SELECT id FROM skills WHERE tombstoned = 1 AND last_verified < ?
`).bind(cutoff).all<{ id: string }>();
for (const { id } of stale.results) {
// List objects under skills/{id}/
let cursor: string | undefined;
do {
const list = await env.CONTENT.list({ prefix: `skills/${id}/`, cursor });
for (const obj of list.objects) {
await env.CONTENT.delete(obj.key);
}
cursor = list.truncated ? list.cursor : undefined;
} while (cursor);
// Finally remove the skill row entirely
await env.DB.prepare(`DELETE FROM skills WHERE id = ?`).bind(id).run();
}
}Storage cost estimate
At 10,000 skills, each ~100KB of canonical+SKILL.md+refs+assets:
- 10,000 × 100KB = 1 GB
- R2 free tier: 10 GB storage, 1M Class-A (write) ops / month, 10M Class-B (read) ops / month
- Expected spend: $0/month for v0.1
Security
- R2 bucket is not public — clients always go through Workers (api + bridge)
- Presigned URLs have short TTL (1 hour)
- No user-uploaded content in R2 — only indexer writes. Keys come from trusted inputs (upstream repo paths), but still normalized + sanitized.
Keys containing / — naming safety
{id} has form {owner}/{name} (e.g. anthropics-skills/pdf). R2 treats / as part of the key name; it does NOT imply hierarchy at the storage layer. But the Cloudflare dashboard UI renders / as folder separators for browsing convenience.
When constructing keys:
- Escape
..(path traversal) - Reject keys with control chars (
\x00-\x1f) - Length limit: 1024 chars (R2 max)
Normalize via:
function canonicalizeKey(parts: string[]): string {
return parts
.map(p => p.replace(/\.\.\/|\.\.\\/g, "")) // strip path-traversal
.map(p => p.replace(/[\x00-\x1f]/g, "")) // strip control chars
.map(p => p.replace(/^\/+|\/+$/g, "")) // strip leading/trailing slashes
.join("/");
}Verification checks
After each upload, the indexer optionally verifies by reading the object's ETag and comparing against the expected sha256:
const uploaded = await env.CONTENT.head(key);
if (uploaded?.httpEtag !== `"sha256:${expectedHash}"`) {
logger.error(`R2 upload verification failed for ${key}`);
// Retry or alert
}See also
- `06-d1-schema-design.md` — D1 metadata that points to these R2 keys
- `04-mcp-tool-design.md` — how
get-skill-contentuses R2 - `08-github-indexer-design.md` — how the indexer populates R2
Related skills
FAQ
Is this affiliated with Anthropic or OpenAI?
No. The docs state it is an unofficial, independent community project and must never imply endorsement from either company.
How does translation avoid data loss?
The hard rules require translator loudness: no silent field drops; every lossy translation logs and shims.