Skills Registry
Supports SSO through OIDC with Authelia as an identity provider, using IdP groups to assign admin/member roles.
Supports SSO through OIDC with Authentik as an identity provider, using IdP groups to assign admin/member roles.
Imports skills from public GitHub repositories via the client CLI and installs them for agents.
Supports SSO through OIDC with Keycloak, including a groups client scope/Group Membership mapper for role mapping.
Provides OpenID Connect SSO integration with OIDC-compliant identity providers, mapping IdP groups to admin/member access.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Skills Registrysearch my skills for anything about code review"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Skills Registry
A self-hosted skills registry for Claude Code. It stores skills with full version history and serves
them to the agent over MCP (search_skills, get_skill, list_folders), so a team keeps its
skills in one place. Skills are grouped into folders:
search_skills takes an optional folders argument. Search is hybrid: full-text plus semantic,
using the built-in multilingual-e5-small model. Everything runs inside your own perimeter, with no
external services or API keys: the model ships in the image.
Besides MCP, there is a web UI for editing skills, tokens and users, a REST API, and a client CLI that imports skills from public GitHub repositories and installs them for any agent.
Components: the app container (Node 22, port 3000, no TLS) and the db container
(Postgres 17 + pgvector, not exposed to the outside).
Running
You need Docker and Docker Compose.
cp .env.example .env
# fill in .env:
# POSTGRES_PASSWORD — openssl rand -base64 32
# ADMIN_PASSWORD — uncomment and set the first administrator's password (at least 12 characters)
# PUBLIC_URL — the address users reach the server at
docker compose up -dCheck: curl http://localhost:3000/healthz answers ok. On first start the server applies
migrations and, if there are no users yet, creates the administrator ADMIN_LOGIN / ADMIN_PASSWORD.
/healthz checks only the database. The embedding model is not part of it: while the model is
loading or unavailable, search works by words only, without semantics, and the server stays healthy.
The model is ready once the log shows the line embedder ready.
All settings, with explanations, are in .env.example. Optional lines there are
commented out; an empty value (KEY=) counts as unset. SESSION_SECRET is reserved and currently
unused (sessions are random identifiers; the database stores their hash). If the settings contain
errors, the server prints the list of invalid variables, one per line, and exits with code 1.
Related MCP server: Claude Skills MCP Connector
Web UI
Open PUBLIC_URL (for example, http://localhost:3000) and sign in as the administrator from
ADMIN_LOGIN/ADMIN_PASSWORD. The UI is in English and makes no external requests: fonts,
icons and styles are bundled.
Skills — a list with search and a status filter, a skill page with its files, archiving.
New skill — skills are created only in the editor (the "New skill" button in the list): slug,
SKILL.mdwith frontmatter (name,description); additional files can be added or uploaded. Existing skills are edited there too; every save is a new version.Tokens — your own tokens for agents. A new token and the ready-made
claude mcp add …command are shown once and can be copied with a button. Copying to the clipboard works only over HTTPS or onlocalhost; over plain HTTP the text is selected and has to be copied manually (Ctrl+C). Revoking asks for confirmation.Users (administrators only) — add (password at least 12 characters, role), reset a password, disable or enable. A disabled user cannot sign in, their sessions are ended and their tokens are revoked. You cannot disable yourself. With SSO, people are added and their passwords managed in the identity provider — see Single sign-on.
If sign-in does not stick (after signing in you are back on the sign-in page), the server requires
HTTPS for the session cookie: open the service over https:// or set COOKIE_SECURE=false on an
internal network.
Single sign-on (OIDC)
SSO is optional. Without OIDC settings the server works as described above: users sign in to the
web UI with a login and password. When OIDC_ISSUER, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET are
all set, the web UI signs in only through the identity provider (IdP). Setting some but not all
three is a configuration error: the server reports it and exits.
What changes in SSO mode:
The sign-in page shows a single "Sign in with SSO" button; the password form is gone (
POST /api/auth/loginanswers403 sso_only).API tokens (
sk_…) on/mcpand/apikeep working for every user, including password users created before SSO was turned on. Their agents and the client CLI need no changes.People are managed in the IdP: the "Users" page has no "New user" and no "Reset password" (
POST /api/usersand password resets answer409 sso_managed). Disabling and enabling users, tokens and sign-out work as before; disabling still ends sessions and revokes tokens.Access and role come from the person's IdP groups: a member of
OIDC_ADMIN_GROUPis anadmin, a member ofOIDC_LOGIN_GROUPis amember, anyone else is refused. The role is re-synced on every SSO sign-in. A known person who is refused because they left both groups also loses their web sessions at that moment; their API tokens are not revoked (tokens are independent of SSO), so off-boarding is complete only after disabling the user on the "Users" page.The first SSO sign-in of an unknown person creates a user. Its login is taken from
preferred_username, else the local part of the email (only if the IdP marks itemail_verified), else the IdP subject (sub): NFKC-normalised, lower-cased and reduced toa-z,0-9,.,_,-(other characters become-),userif nothing is left, then-2,-3… on collision. The login is not changed on later sign-ins.An IdP account is identified by the issuer and its subject (
OIDC_ISSUER+sub). After changingOIDC_ISSUER, sign-ins never land on users linked to the old issuer: everyone gets a new user until the operator links the old ones again (link-sso, below).Starting a sign-in is limited to 30 per IP address per 10 minutes (
/login?loginError=rate_limited).ADMIN_LOGIN/ADMIN_PASSWORDare ignored (with a warning in the log). The first administrator is whoever signs in as a member ofOIDC_ADMIN_GROUP.
Provider setup
Any OIDC provider works if it can put the person's groups (or roles) into one claim of the ID token or the UserInfo response: a list of names, or an object whose keys are the names. If the ID token has no such claim, the server asks UserInfo.
Create two groups: the login group (
skills-users) and the admin group (skills-admins). Membership in either lets a person in; the admin group also makes them an admin.Create a confidential OIDC client (Authorization Code with a client secret):
redirect URI:
<PUBLIC_URL>/api/auth/sso/callback, e.g.https://skills.example.com/api/auth/sso/callback;PKCE allowed (the server always sends S256);
the groups claim is emitted (see the table below).
Put the issuer, client ID and secret into
.envand restart:docker compose up -d.
Provider | Settings |
Pocket ID, Authentik, Authelia, Kanidm | defaults |
Keycloak | defaults; add a client scope |
Zitadel |
|
These are starting points, not tested recipes: check the claims your provider actually issues (for example on its token preview page).
Variable | Default | Meaning |
| unset | issuer URL; discovery at |
| unset | client ID of the confidential client |
| unset | client secret |
|
| members of this group may sign in (as |
|
| members of this group may sign in and are admins |
|
| scopes of the sign-in request, space- or comma-separated; must include |
|
| claim with the person's groups (ID token, else UserInfo): a list of names, or an object whose keys are the names |
|
| allow a plain |
OIDC_ISSUER must be exactly the issuer value from the discovery document (mind the trailing
slash): the server checks it on every sign-in. Check it with
curl -s https://id.example.com/.well-known/openid-configuration.
Linking an existing account
The server never matches IdP accounts to existing users by name or email. A password user who signs in through SSO for the first time gets a new, separate user. To keep one account (its skill authorship and tokens), the operator links the IdP subject to the existing user:
docker compose exec app node dist/cli.js link-sso alice <sub>The link is stored for the configured OIDC_ISSUER (so link-sso needs the OIDC settings).
<sub> is the person's subject identifier at the IdP. It is shown on the user's page in most
providers (Pocket ID, Authentik, Keycloak: the user ID). If the person has already signed in through
SSO, the subject is stored on the separate user that sign-in created:
docker compose exec db psql -U skills -d skills -c \
"select login, oidc_iss, oidc_sub, email, last_login_at from identity.users where oidc_sub is not null"(The sign-in itself is in the audit log: platform.audit_log, action auth.login,
meta.method = 'sso', actor_id is that user.)
A subject can be linked to only one user, so release it from the separate user first, link it, then disable the separate user on the "Users" page:
docker compose exec app node dist/cli.js unlink-sso alice-2
docker compose exec app node dist/cli.js link-sso alice <sub>From then on SSO sign-ins with that subject land on alice, and her role follows her IdP groups.
link-sso with the same pair again is a no-op; unlink-sso <login> removes the link (the next SSO
sign-in with that subject creates a new user again).
Troubleshooting
A failed SSO sign-in returns to /login?loginError=<code>, and the page explains it:
Code | Meaning |
| the server cannot reach |
| the person is in neither |
| the user is disabled in the registry; enable them on the "Users" page |
| the sign-in attempt expired (10 minutes) or was started in another browser; start again from the sign-in page |
| too many sign-in attempts from this IP address (more than 30 in 10 minutes); wait and try again |
| anything else; the reason is in the server log |
A rejected code exchange (wrong client secret, issuer mismatch, clock skew…) is shown as
invalid_state; the server log has a warning oidc code exchange rejected with the OAuth error
code (oauthError, e.g. invalid_client) and the library's error code. Failed attempts are also
recorded in the audit log as auth.login_failed with meta.reason.
Connecting Claude Code
Create a token (printed once) and the connect command:
docker compose exec app node dist/cli.js create-token admin --name laptopThe first line is the sk_… token, the second is the ready-made command:
claude mcp add --transport http skills http://localhost:3000/mcp --header "Authorization: Bearer sk_…"Run it on the machine where Claude Code runs. Tokens can also be created and revoked
on the "Tokens" page of the web UI or via the REST API (/api/tokens).
Creating skills
Skills are created in the web UI: "New skill" opens the editor, and saving sends
POST /api/skills. There is no bulk import of folders or zip archives; skills from public GitHub
are imported by the client CLI.
New and changed skills are indexed for search in the background within seconds.
Search
The search_skills result combines full-text and semantic search. A semantic
candidate is included only if its cosine similarity to the query is at least
SEARCH_MIN_SIMILARITY (0.80 by default). So an off-topic query returns an empty result. score
in the range 0..1 is the fraction of the best possible score: 1 means first place both by words and
by meaning. Measurements and the formula are in ADR 0005.
Client CLI
skills-registry is a CLI for the user's machine (Node ≥ 22): it imports skills from public GitHub,
updates them from upstream and installs them for any agent (Claude Code, Codex, Cursor and others).
It talks to the server only over REST with an API token. It is not published to npm — build it from
the repository:
pnpm install && pnpm build:cli
cd packages/cli && npm link # puts the skills-registry command on PATH
# or without link: node packages/cli/dist/cli.js <command>Sign in (the token comes from the "Tokens" page or create-token; it is read from stdin and not echoed):
skills-registry login https://skills.example.comThe config is ~/.config/skills-registry/config.json (mode 0600). The variables
SKILLS_REGISTRY_URL and SKILLS_REGISTRY_TOKEN take precedence over the config (handy in CI).
# import: a folder in the registry is created from the skill name; --folder <slug> puts it in the given folder
# and creates that folder if it does not exist yet (an archived one is an error)
skills-registry import anthropics/skills --skill pdf --dry-run
skills-registry import https://github.com/anthropics/skills/tree/main/skills/pdf
# a new version from upstream if there is a new commit there; it overwrites registry edits with a warning,
# while upstream stays on the same commit, update downloads nothing and the edits are kept
skills-registry update pdf --dry-run
skills-registry update pdf
# install: a copy in ./.agents/skills/<slug>, links for agents; -g — into the home directory
skills-registry install pdf -a claude-code
skills-registry uninstall pdf -a claude-codeGITHUB_TOKEN (optional) raises the GitHub API rate limit. The repository is downloaded
as a whole archive; limits: SKILLS_REGISTRY_MAX_TARBALL_BYTES (compressed archive, 200 MiB by default)
and SKILLS_REGISTRY_MAX_UNPACKED_BYTES (unpacked, 1 GiB by default). skills-registry without
arguments prints help.
Other CLI commands
# a user (the password is read from stdin; in interactive mode it is typed without echo)
read -rs -p 'Password: ' PW && printf '%s' "$PW" | docker compose exec -T app node dist/cli.js create-user bob --role member; unset PW
# rebuild the whole search index
docker compose exec app node dist/cli.js reindexIf the embedding model stays unavailable for a long time, an indexing event is given up after 10 attempts,
and the error outbox: handler failed, giving up is logged. Word search keeps working meanwhile.
As soon as the model is ready again, the server re-indexes documents without an embedding on its own
once a minute, 20 at a time. To restore everything at once, run reindex.
node dist/cli.js without arguments prints help.
Backups
All data (skills, versions, users, tokens, index) lives in Postgres, in the pgdata volume.
# backup
docker compose exec -T db pg_dump -U skills -d skills -Fc > skills-$(date +%F).dump
# restore into the running database
docker compose exec -T db pg_restore -U skills -d skills --clean --if-exists < skills-2026-10-28.dump
docker compose restart appExternal database
Set DATABASE_URL in .env with the password inside the string. docker-compose.yml still requires
POSTGRES_PASSWORD: the variable is mandatory for the db service, and without it docker compose
will not start. Set any value. The db service will start as well. If you don't need it,
start only the application: docker compose up -d --no-deps app.
Behind a reverse proxy (HTTPS)
The server does not terminate TLS itself. Put nginx, Traefik or Caddy in front of it and set in .env:
Variable | Value |
| the external address, e.g. |
|
|
| the number of trusted proxies in front of the server ( |
| additional host names for |
|
|
Without a proxy, keep TRUST_PROXY=false: otherwise a client can spoof its IP with the header.
Manual check with Claude Code
docker compose up -d, create at least one code review skill in the web UI (see "Creating skills").docker compose exec app node dist/cli.js create-token admin --name checkand run the printedclaude mcp add …command.claude mcp list— theskillsserver is connected.In a new Claude Code session, ask: "which skills do I have for code review?". Expected: the agent calls
search_skills, thenget_skillfor the skill it found, and summarizes its content.
Development
Requirements: Node 22+, pnpm 11, Docker (for integration tests on Testcontainers).
pnpm install
pnpm verify # lint, format, types, architecture, knip, unit and integration tests
pnpm build # server bundle: apps/server/dist/{main,cli}.js
bash scripts/smoke.sh # build the image and check the whole box via docker compose
pnpm test:e2e # Playwright (Chromium) against the web UI of the built imageFor pnpm test:e2e, install the browser once: pnpm exec playwright install chromium.
Code structure and rules are in CLAUDE.md, the specification is in docs/superpowers/specs/,
decisions are in docs/adr/.
Contributing
Contributions are welcome — see CONTRIBUTING.md.
Security
To report a vulnerability, see SECURITY.md.
License
Third-party components shipped in the image and the web bundle are listed in THIRD_PARTY_NOTICES.md.
This server cannot be deployed
Maintenance
Related MCP Connectors
Search verified Claude Code plugins and skills; fetch portable SKILL.md sources. Read-only.
Governed AI agent skills — one library, distributed to devs and exposed to remote agents over MCP.
Search, browse & read 340+ Claude Code marketing & SEO skills from any MCP client.
Search and inspect source-pinned agent skills, plugins and tools through a free public MCP.
Related MCP Servers
- AlicenseBqualityDmaintenanceEnables AI-driven semantic code search via natural language queries, integrating with MCP clients like Claude Desktop to retrieve relevant code context from any codebase.1MIT
- FlicenseNot gradedqualityCmaintenanceRemote MCP server for Claude that exposes a library of SKILL.md packages, enabling listing, searching, and retrieving skill instructions and supporting files for use in conversations.-
- AlicenseNot gradedqualityAmaintenanceEnables MCP-compatible harnesses to load and invoke Claude Code style skills from SKILL.md folders, including listing and reading skill files with user-approval gating for model-invocation restrictions.343 npmGPL 2.0
- AlicenseNot gradedqualityCmaintenanceEnables MCP clients to discover, search, and load agent skills from many sources, including other agents' skill directories, Claude Code plugin marketplaces, and remote git repositories, with no copying or syncing required.06MIT