Skip to main content
Glama
ashwnn
by ashwnn

Skills Registry

A personal, read-only MCP skill registry: one GitHub repository, one Cloudflare Worker, four stable MCP tools. Every approved push builds and deploys a validated snapshot; every client keeps the same endpoint URL.

Architecture

Deployment-time publishing, not runtime GitHub fetching:

flowchart TD
    G["GitHub repository"] --> V["CI validation and snapshot build"]
    V --> D["Deploy Worker at stable URL"]
    C["AI clients"] --> A["MCP authentication"]
    A --> W["Worker: four registry tools"]
    D --> W
    W --> S["Bundled index and skill files"]
    V --> F["Validation failure: retain existing deployment"]

The Worker embeds a generated catalog (src/generated/catalog.json) built from one exact Git commit. It answers MCP requests with no GitHub API calls, no database, and no session state — see src/worker.ts.

Related MCP server: skillet

Repository layout

Path

Purpose

skills/<id>/SKILL.md

Metadata (YAML frontmatter) + primary instructions

skills/<id>/references/, templates/

Supporting Markdown/text, explicitly allowlisted per skill

skills/<id>/scripts/

Supporting shell/Python/JS/TS source, same allowlist rules, never executed by the registry

registry.config.json

Schema, allowed file types, size limits, search/pagination defaults

src/

Worker, GitHub token check, MCP dispatch, tools, search, registry loader

scripts/

validate-skills.mjs, build-snapshot.mjs

tests/

Contract, search, retrieval, revision, security, auth tests

docs/client-setup.md

Per-client connection steps and the shared bootstrap instruction

.github/workflows/

validate.yml (PRs + push), deploy.yml (push to main)

src/generated/ is a build output (gitignored), regenerated by CI from the exact checked-out commit. It is never a second source of truth.

Authoring a skill

  1. Create skills/<id>/SKILL.md with frontmatter:

    ---
    schema_version: 1
    id: your-skill-id
    name: Human Readable Name
    description: One sentence, used in search and listings.
    version: 1.0.0
    tags: [development]
    keywords: [relevant, terms]
    requires: []        # capabilities the client needs; does not grant them
    files: []           # explicit retrieval allowlist, relative to the skill dir
    required_zdr: false # true = only served to callers cleared for Zero Data Retention (see "Data handling")
    ---
    
    # When to use
    ...
    
    # Instructions
    ...
  2. id must be a stable lowercase slug matching the directory name (^[a-z0-9]+(-[a-z0-9]+)*$) — treat renaming it as a breaking change.

  3. List every supporting file under files; anything on disk that isn't listed fails validation, and anything listed that doesn't exist also fails.

  4. Bump version (semver) whenever the body or a supporting file changes — CI computes a content hash against the base branch and fails if the version didn't move.

  5. Run npm run validate locally before pushing.

  6. If a skill's instructions genuinely need a local script, put it under scripts/ (.sh, .py, .js, .ts, .rb — see registry.config.json allowed_script_extensions) and declare it in files like any other supporting file. It is served as plain text via skills_get_file with executable: true; the registry never runs it. State plainly in the skill's own instructions that the client must read the script before running it under its own local execution permissions.

  7. If a skill's content must never leave a Zero-Data-Retention-cleared provider, set required_zdr: true. See "Data handling" below — this is an access-control decision, not a content-type decision, so it applies regardless of file type.

Local development

npm install
npm run validate   # metadata, duplicate IDs, file allowlist, size, version bumps
npm run build      # generates src/generated/catalog.json from the working tree
npm run typecheck
npm test           # unit tests against fixture catalogs, independent of build output
npm run dev        # wrangler dev, serves the just-built snapshot locally

MCP interface

Four tools, all read-only/non-destructive, all returning api_version, registry_sha, and published_at:

Tool

Purpose

skills_list

Paginated metadata, optional tags/limit/cursor/revision

skills_search

Ranked keyword/tag search with match reasons and excerpts

skills_get

Metadata, primary instructions, supporting-file manifest for one skill

skills_get_file

Bounded, paginated text from one allowlisted supporting file

revision means the registry's Git SHA, not a skill's semantic version. An omitted revision targets the active snapshot; a stale one returns REVISION_UNAVAILABLE naming the active SHA so a client restarts discovery instead of mixing revisions. Application errors (SKILL_NOT_FOUND, FILE_NOT_FOUND, INVALID_PATH, REVISION_UNAVAILABLE, INVALID_CURSOR) come back as a normal tool result with isError: true, never disguised as an empty search result. Malformed arguments are JSON-RPC -32602 errors.

Search is deterministic keyword/tag matching (src/search.ts): exact id/name match ranks highest, then tag, then keyword, then description substring, tie-broken by id. No embeddings, no invented relevance.

Every skill's metadata carries required_zdr (see "Data handling"), and every file manifest entry carries executable (true for anything under scripts/). A caller not cleared for required_zdr skills doesn't see them at all — skills_list/skills_search omit them, skills_get/ skills_get_file return SKILL_NOT_FOUND rather than a permission error, so their existence isn't leaked to callers who can't read them.

Authentication

Decision: direct GitHub personal-access-token validation, no OAuth App, no redirect flow. A client sends a GitHub PAT as its bearer token; the Worker asks GitHub who it belongs to (one call to api.github.com/user, see src/github.ts) and checks the returned login against ALLOWED_GITHUB_LOGINS. See src/worker.ts's resolveCaller.

This replaced an earlier full OAuth-provider design (GitHub OAuth App + /authorize + /github/callback + @cloudflare/workers-oauth-provider issuing its own per-client tokens). For a single-operator personal registry where every client can be configured with a static bearer/API-key header (Claude Code, Codex, OpenCode all support this), that was more machinery than the problem needed: no OAuth App to register, no callback URL, no KV namespace for grant storage, no DCR/CIMD/PKCE surface to maintain. The tradeoff is Claude Web / ChatGPT Web connectors, which are generally built around an interactive "Authorize" redirect button rather than a pasted token field — verify each one actually has a usable auth field before relying on it (see "Open questions still unresolved").

Setup required before this works (not done yet — Cloudflare account secrets were just added, GitHub PAT/allowlist still need setting):

  1. Create a GitHub personal access token (github.com → Settings → Developer settings → Personal access tokens). No scopes are required — this only reads the token owner's public identity via /user.

  2. wrangler secret put ALLOWED_GITHUB_LOGINS — your GitHub username(s), comma-separated. Optionally ZDR_GITHUB_LOGINS (see "Data handling").

  3. Configure each client with Authorization: Bearer <your PAT> — see docs/client-setup.md.

Optional service-token path. A static bearer token (SKILLS_READ_TOKENS) is checked first, before the GitHub lookup, so automation (the deploy workflow's smoke test, a script) doesn't need to hold your actual GitHub credential. It's equivalent in privilege to a GitHub-authenticated call, not a separate tier.

Cloudflare deployment credentials (CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID) live only in CI secrets — never in client configuration or tool responses. Your GitHub PAT lives only in your own client configs and is never sent anywhere but GitHub's API and this Worker.

Data handling: required_zdr

Some skills may describe workflows unsuitable for disclosure to every connected AI provider (per your answer to the open question below). Those skills set required_zdr: true in frontmatter. A caller — a GitHub identity or a static token — is cleared to see them only if it's in ZDR_GITHUB_LOGINS or SKILLS_ZDR_TOKENS respectively; both default to empty, so clearance is opt-in and fails closed. This is configured per client, exactly as asked: a client whose connected provider has no ZDR agreement simply never gets a login/token added to those lists, and the skill doesn't exist as far as it can tell (see "MCP interface" above for why that's SKILL_NOT_FOUND, not a permission error).

required_zdr is an access-control flag, not a content-type flag — it applies the same way to a Markdown instruction file or a scripts/ file.

Publishing

  1. Edit a skill, bump its version.

  2. Push/merge to main.

  3. validate.yml runs on PRs and pushes; deploy.yml runs only on push to main: validate → build snapshot from the exact commit → typecheck → test → wrangler deploy → smoke test.

  4. A failed step leaves the previous deployment live. Deploys are serialized (concurrency: production-deploy) so an older build can never overwrite a newer one.

  5. Rollback: re-run deploy.yml from a previous green commit (or wrangler rollback if using Cloudflare's built-in version history).

"Latest" means latest successfully deployed commit, not necessarily branch HEAD.

Security notes

  • Skill files are restricted to .md/.txt (or, under scripts/, .sh/.py/.js/.ts/.rb) under configured size limits; retrieval rejects path traversal, absolute paths, and anything outside a skill's declared files allowlist (src/tools.ts, tested in tests/tools.test.ts).

  • No skill script is ever executed by validation, the build, or retrieval — scripts/ files are served as inert text with an executable flag; a client chooses whether to run one, under its own local permission model.

  • requires in frontmatter documents what a skill's instructions assume a client has access to — it grants nothing by itself.

  • Retrieved content still enters the connected AI provider's context. Don't put secrets or incident-specific sensitive detail into a skill.

  • Registry instructions are not higher priority than system instructions, user authorization, or local access controls — the bootstrap text in docs/client-setup.md says this explicitly so it travels with every client's standing instructions.

Resolved decisions

  • Auth mechanism: direct GitHub PAT validation against api.github.com, no OAuth App, no redirect flow — see "Authentication" above. (An earlier pass built a full OAuth-provider design; it was replaced once a simpler option was pointed out — see git history if you want the OAuth version.)

  • Client surfaces: all of them — Claude Web, Claude Code, Codex CLI and hosted surfaces, OpenCode, and ChatGPT Web. Claude Code, Codex, and OpenCode all take a static bearer/API-key header in their MCP config, so a pasted GitHub PAT works directly. Claude Web and ChatGPT Web connectors are generally built around an interactive OAuth "Authorize" redirect rather than a pasted-token field — confirm each actually has a usable auth field for this before relying on it (see "Open questions still unresolved"). If one doesn't, the OAuth-provider design is the fallback.

  • Publish freshness: "publish after CI finishes" is fine as-is; no change made.

  • Local scripts/binaries: text scripts (.sh/.py/.js/.ts/.rb) are now supported under scripts/, served as plain text with an executable flag, never run by the registry — see "Authoring a skill" step 6. Compiled/binary assets remain out of scope. A binary can't be reviewed by a human or model before a client runs it, which defeats the trust model this whole registry depends on (retrieval never grants execution); it also adds integrity/signing and storage concerns this registry doesn't currently handle. If a real skill turns out to need a binary, the right shape is a separate, explicitly-confirmed release channel with signed artifacts — not skills_get_file — and should be designed against a concrete skill that needs it, not speculatively.

  • Long-running tasks spanning a deploy: staying with active-snapshot- only for now. The existing REVISION_UNAVAILABLE behavior already keeps a client from silently mixing revisions mid-task — it just has to restart discovery. No evidence yet that this is a real problem for any actual task; revisit with historical-revision serving only if one shows up.

  • ZDR-sensitive skills: yes, some may need this — see required_zdr under "Data handling" above.

Open questions still unresolved

  • Does the Claude Web and ChatGPT Web connector UI actually expose a field for a static bearer token / API key, or only an OAuth "Authorize" button? Not yet verified — no live deployment exists yet to test against.

Explicitly out of scope for now

Embeddings/full-text search, a database, an admin UI, remote execution, automatic native-skill installation, write tools, one-tool-per-skill registration, runtime GitHub sync, historical-revision retrieval, and compiled/binary skill assets.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Converts AI Skills (following Claude Skills format) into MCP server resources, enabling LLM applications to discover, access, and utilize self-contained skill directories through the Model Context Protocol. Provides tools to list available skills, retrieve skill details and content, and read supporting files with security protections.
    3
    28
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to discover, install, and manage SKILL.md skills from a Git-backed registry via MCP tools for search, install, and list operations.
    4 npm
    1
    MIT