skills-mcp
Reads agent skills from GitHub repositories, which the server treats as one of its catalog sources. A GitHub source performs a sparse, shallow, blobless clone of a repository limited to configured paths (default skills), optionally pinned to a branch/tag (ref) or a full commit id (sha) so the fetched skills stay reproducible; unpinned sources follow the default branch. The cloned skills are synced into a local cache and merged into the searchable catalog that the find and load tools operate on. GitHub sources are treated as untrusted: paths are resolved with realpath and must stay inside each skill directory, and skill files are read but never executed.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
skills-mcp
skills-mcp is a Model Context Protocol (MCP) server that keeps a catalog of agent skills out of the model's context. When the agent starts a task, it asks find which skill fits, then calls load on that one skill.
Any MCP-capable harness can use it. The author runs it with OpenCode V2, and the OpenCode-specific scripts (budget, coverage, cutover) are in this repository too. The server is TypeScript for Bun. It reads skills from GitHub repositories, local directories and command-line tools, and it never runs a skill's files.
Contents
Terms follow GLOSSARY.md. The work plan is in docs/plan.md.
The problem and the headline numbers
A harness that loads skills natively places every skill's description in the model's context on every step. That is the native skill list. It costs context whether or not the task needs a skill, and in a long list the skill that matters gets forgotten.
skills-mcp moves that list out of the context. The server adds a small fixed cost to each step, and the agent receives one suggested skill on demand.
Claim | Value | Source | Caveat |
Fixed cost of this server | 299 tokens |
| An estimate: characters divided by 4, for the four tools and the server instructions as OpenCode's Code Mode prints them. The renderer is ported from anomalyco/opencode at commit |
Native skill list cost | Not measured in this repository |
| The plan estimates about 170 tokens for two builtin skills kept in the native list. A native list grows with every skill it holds. |
Classifier accuracy | 2 of 40 covered requests (5.0%) end with a suggestion other than the right skill, or none |
| One labelled set of 60 requests against one catalog of 179 skills. The thresholds were fitted on the same 60 requests. See Evaluation. |
Related MCP server: dsh-skills-anywhere
How it works
flowchart LR
A[Agent] -->|find with task| C[Classifier]
C -->|no decision model| L[Lexical ranker]
C -->|decision model| G{Gate}
G -->|closed| N[needsSkill false]
G -->|open| S[Shortlist of three]
S -->|best fit meets threshold| O[One suggestion]
S -->|below threshold| Z[No suggestion]
C -->|model fails| L
A -->|load and read| K[Catalog]
K --> R[Sources: GitHub, local, command]Catalog and sources
A source is a configured place that skills come from. There are three kinds:
GitHub source (
kind: "github"): a sparse, shallow, blobless clone of a repository, limited topaths(defaultskills). Pin it withref(a branch or tag) orsha(a full commit id). An unpinned source follows the default branch.Local source (
kind: "local"): a directory on your machine. It is scanned in place.Command source (
kind: "cli"in the config): a program that prints its skills as JSON. The server runs the configuredlistandgetcommands from an argument array. No shell is involved.
Sync brings a source's skills onto disk, in ~/.cache/skills-mcp unless SKILLS_MCP_CACHE says otherwise. Startup and every rebuild read the cache only, so a start never touches the network. Run sources sync to refresh. If an online sync fails and a cached copy exists, the cached copy is used and a warning is recorded.
The catalog merges all sources into one skill per skill id, which is the name of the skill's directory. Sources have an order. When two sources provide the same id, the earlier one keeps it and the later one is reported as shadowed. A skill directory holds a SKILL.md whose frontmatter has a name and a description. Copies under upstream/ or plugins/ (vendored copies) fold into the original.
Trusted and untrusted sources
Every skill, resource and source is data. The server reads skill files and never runs them.
A local source is trusted: its top-level symlinks may point outside it.
A GitHub source and a command source are untrusted: every path is resolved with
realpathand must stay inside its skill directory.
Who may add or remove sources is covered in Tools and Security and privacy.
Finding a skill
find takes a task, the description of what the agent is about to do. It returns at most one suggestion and a list of candidates.
Without decision.model, find ranks candidates with the lexical ranker (BM25 over the id, name, description and body excerpt) and never suggests a skill.
With decision.model, the classifier runs two rounds against a decision model:
Call 1, one question per shard. The catalog is split into shards of at most
shardSizeskills (default 120). Each shard asks the model to choose the skill that fits the task, and the shards run in parallel. Shard 0 also asks the gate questions: three yes-or-no questions about whether the task needs a specific procedure (averaged, one of them inverted) and one about whether the task names a specific tool, format, template or method.Gate. The gate opens when the average of the procedure questions reaches
thresholds.action, or the named score reachesthresholds.named. A closed gate returnsneedsSkill: false.Shortlist and rerank (call 2). The top three candidates are read in full: each one's description and the first 700 characters of its body. The model chooses one, and answers a fits check (yes or no) for each of the three.
Suggestion. The chosen skill is suggested only when its fits probability reaches
thresholds.fits.
Candidates come from call 1: the top three of each shard, merged and cut to the requested limit (default 5, at most 10).
Situation |
|
|
|
|
No |
|
|
| absent |
Gate closed |
|
|
| absent |
Gate open, no skill fits |
|
|
| absent |
Gate open, a skill fits |
|
| one skill id | absent |
Model missing, failed, timed out or answered invalidly |
|
|
| why |
On fallback, find still returns lexical candidates, so the agent is never left with nothing. A missing key, an unknown model id, a timeout, rejected credentials, a refusal or a malformed answer all fall back, and reason says which.
Code Mode and the fixed cost
The text the server adds to every model step is kept small:
Every tool declares an
outputSchema, so Code Mode shows a typed return, and the schemas are shallow.The first line of each tool description is at most 120 characters and is self-sufficient.
The server instructions are one line:
Call find before non-trivial work; load the suggested skill.Input schemas carry no length limits, because OpenCode would print each one. The handlers enforce the limits instead.
bun run budget renders the tools and instructions the way OpenCode prints them, and fails above 320 tokens.
MCP skills extension
For harnesses without Code Mode, the server also serves the MCP Skills extension (io.modelcontextprotocol/skills). It answers:
skills/list: every catalog skill, with its name and description, pointing at itsSKILL.md;skills/get: theSKILL.mdentries;resources/readonskill://<id>/<path>:SKILL.mdand the listed resources.
Resources are dynamic. When connected, the server sends a resource-list change after each catalog rebuild.
Evaluation
docs/eval.md measured the classifier on one labelled set, against the author's catalog of 179 skills from six sources. The set has 60 requests: 40 covered requests, each with exactly one correct skill (40 different skills in all), and 20 uncovered requests, which are everyday tasks, conceptual questions and near-miss tool requests that no served skill covers.
Ranker | Wrong, covered (of 40) | Needless, uncovered (of 20) | Right skill in top 3, covered | p50 / p95 ms |
Decision model, cookbook defaults (0.30 / 0.50 / 0.30) | 4 (10.0%) | 1 (5.0%) | 39 (97.5%) | 392 / 611 |
Decision model, shipped values (0.25 / 0.50 / 0.35) | 2 (5.0%) | 0 (0.0%) | 39 (97.5%) | 392 / 611 |
Lexical ranker (no gate) | top-1 wrong: 15 (37.5%) | uncovered with a candidate: 17 (85.0%) | 28 (70.0%) | 3 / 4 |
"Wrong" counts a covered request whose suggestion is not its skill, and no suggestion counts as wrong. "Needless" counts an uncovered request that received a suggestion. The lexical ranker cannot abstain, so its row shows what a top-1 answer would cost without a gate.
Read these numbers with their limits:
Small sample. Sixty requests, forty of them covered. One request moves a covered rate by 2.5 points.
Thresholds tuned on the test set. The shipped values were chosen on the same 60 requests:
actionmoved on two requests andfitson one. A held-out set is the real test.Requests written with the skill descriptions in view. Real requests are vaguer, so the real wrong rate will likely be higher.
One latency sample from one live pass, with no rate-limit or overload runs.
Hand-checked gold labels. A contested label counts as wrong even when the suggestion is reasonable.
Cost not recorded. Filling the cache took 180 live calls; no provider usage figures were collected.
To re-run: bun run eval -- --ranker bm25 needs no key. A model id fills the answer cache with live calls, and --sweep re-scores cached answers at every threshold setting with no network. The request set is eval/requests.jsonl.
Tools
The server exposes four tools. Through OpenCode they appear under tools.skills.* inside Code Mode.
Tool | Read-only | Input | Output |
| yes |
|
|
| yes |
|
|
| yes |
|
|
| no |
|
|
loadreturns the body (the instructions after the frontmatter), the resources the body can point to, andbase, the absolute path of the skill directory.readserves onlySKILL.mdand the listed resources; any other path ismissing. Files over 256 KiB aretoo-large, and binary files arebinary.Errors come back as MCP error results with a one-line, sanitized message.
What agents may do with sources
sources is the only tool that changes state. It is not marked read-only.
Agents may | Agents may not |
| add or remove a local or command source |
| set |
| change what a command source runs. |
| write or run skill files |
To add or remove local and command sources, use the admin CLI. The refusal message names it.
Install and run
Requirements
Bun. The project is developed and tested on Bun 1.4.3. It uses Bun APIs (
Bun.spawn,Bun.YAML).git on your
PATH, for GitHub sources.A decision-model key, optional. Without one,
findstill works and ranks lexically. See Decision models.The
opencodeCLI, only forbun run coverageandbun run cutover verify.
Install
git clone https://github.com/heliowap/skills-mcp.git
cd skills-mcp
bun install
bun run verify:all # typecheck, tests and the fixed-cost budgetRun
bun start # runs src/index.tsThe server speaks MCP over stdio. A harness starts it, so you normally do not run it by hand. Run in a terminal, it simply waits for input. stdout carries MCP messages only; diagnostics go to stderr.
Connect a harness
Replace <repo> with the absolute path of your checkout.
OpenCode V2. Add the server under mcp.servers in your OpenCode config (for example ~/.config/opencode/opencode.jsonc):
{
"mcp": {
"servers": {
"skills": {
"type": "local",
"command": ["bun", "<repo>/src/index.ts"]
}
}
}
}Code Mode then exposes tools.skills.find, tools.skills.load, tools.skills.read and tools.skills.sources. The server's instructions tell the agent to call find first.
Any other MCP harness. Register a stdio server whose command is bun and whose argument is the entry point. Key names differ between clients. This is the common shape:
{
"mcpServers": {
"skills": {
"command": "bun",
"args": ["<repo>/src/index.ts"],
"env": { "SKILLS_MCP_CONFIG": "/path/to/config.json" }
}
}
}The env block is optional. Check your harness's documentation for its exact file and key names. Reconnect the harness after changing its config.
Configuration
Where it lives
What | Default | Override |
Config file |
|
|
Cache of synced sources |
|
|
A missing config file means no sources and the defaults. sources add creates the file when it is missing. Tests point both variables at temporary directories.
Sample
This config has one source of each kind, plus the decision model and its thresholds:
{
"sources": [
{ "kind": "github", "id": "example-skills", "repo": "owner/skills-repo", "ref": "main", "paths": ["skills"] },
{ "kind": "local", "id": "my-skills", "path": "~/my-skills" },
{
"kind": "cli",
"id": "example-cli",
"list": ["example-cli", "skills", "list"],
"get": ["example-cli", "skills", "get"],
"namesPath": "data.skills",
"contentPath": "data.content"
}
],
"decision": {
"model": "typesafe-ai:jev-1.14.0",
"thresholds": {
"typesafe-ai:jev-1.14.0": { "action": 0.25, "named": 0.5, "fits": 0.35 }
}
},
"shardSize": 120,
"timeoutMs": 3000
}Field | Default | Meaning |
|
| Sources in priority order. An earlier source wins a skill-id collision. |
| none | An id from |
| the providers.json entry, else 0.30 / 0.50 / 0.30 |
|
| 120 | Skills per decision shard. Values above 255 are clamped to 255. |
| 3000 | Time budget for each decision round (call 1, then call 2), retries included. |
Source fields by kind:
Kind | Fields | Notes |
|
|
|
|
| A leading |
|
|
|
Source ids and skill ids use lowercase letters, digits and single hyphens. Source ids are at most 64 characters.
The decision model, thresholds, shardSize and timeoutMs are read when the server starts. Restart it after changing them.
Decision models
decision.model must name an entry in src/decision/providers.json:
Model id | Provider | Key variables (first one set wins) | Thresholds in providers.json |
| TypeSafe AI |
| 0.25 / 0.50 / 0.35 |
| AI SDK gateway |
| none, so the config defaults apply |
Threshold precedence, per model: a decision.thresholds entry in the config wins. Within that entry, a missing field takes the config default (0.30, 0.50 or 0.30), not the providers.json value. With no config entry, providers.json applies, and then the defaults.
Keys are read from the environment by name when the server starts. They never go in the config file.
With no usable key the server still starts. find falls back to the lexical ranker, ranker is bm25-fallback, and reason names the variable to set:
no API key for "typesafe-ai:jev-1.14.0": set TYPESAFE_AI_API_KEY or TYPESAFE_API_KEYAn id missing from providers.json gives unknown decision model "<id>"; add it to providers.json.
To add a model, add an entry to providers.json with provider (typesafe-ai or gateway), modelId, keyEnv (variable names in priority order), and optionally thresholds and baseURL.
Admin CLI
Run these from the checkout with Bun. The CLI reads and writes the same config file and cache as the server. It prints JSON on stdout and failures on stderr.
bun src/cli.ts find --json "fill in a PDF form"
bun src/cli.ts sources list
bun src/cli.ts sources add '<json source spec>'
bun src/cli.ts sources sync [id]
bun src/cli.ts sources remove <id>find --json "<task>" prints the result shape of the find tool. The --json flag is required, and the task is the rest of the command line. With no decision.model, no model is called:
{
"needsSkill": null,
"suggestion": null,
"ranker": "bm25",
"candidates": [
{ "id": "pdf-forms", "description": "Fill in PDF forms and extract their fields.", "score": 3.03 }
]
}sources list prints the configured sources as stored, in priority order.
sources add '<json>' validates a full source spec (all three kinds) and appends it. It stores the spec and does not sync, so follow it with sources sync <id>:
bun src/cli.ts sources add '{"kind":"local","id":"my-skills","path":"~/my-skills"}'
bun src/cli.ts sources sync my-skillssources sync [id] syncs one source, or every source when no id is given. It prints one outcome per source, and exits 1 if any sync failed.
sources remove <id> removes the source from the config, and deletes the cache of a GitHub or command source. It prints { "removed": "<id>" }.
Exit codes. 0: done. 1: failure, with the reason on stderr; for example an invalid spec, an unknown source id, or a failed sync. 2: usage; for example no command, an unknown command, or find without --json.
A running server does not see CLI changes until it rebuilds its catalog, which happens at startup or after one of its own sources calls. Reconnect the harness after a CLI change.
OpenCode cutover
The cutover moves an OpenCode V2 setup from its native skill list and its CLI-backed MCP servers to this server. It is reversible.
bun run cutover plan # print the diff that apply would make; writes nothing
bun run cutover apply # back up the config, then make the edits
bun run cutover verify # read OpenCode's state and check it; writes nothing
bun run cutover revert # restore the newest backup, byte for byteOptions: --config <file> (or SKILLS_MCP_OPENCODE_CONFIG; default ~/.config/opencode/opencode.jsonc) and --repo <dir> (or SKILLS_MCP_REPO; default this checkout). verify also takes --dir and --opencode.
apply changes only these paths, and checks that before it writes:
adds
mcp.servers.skills, a local server that runsbun <repo>/src/index.ts;disables the two CLI-backed MCP servers that the skills server replaces;
removes the legacy V1-shaped entry that sits directly under
mcp. OpenCode still loads that entry as a live server, so removing it turns off the MCP server it defines. docs/cutover.md lists this as a known risk;appends the cutover plugin,
opencode-plugin/skills-cutover, toplugins.
The plugin keeps OpenCode's builtin skills in the native list, sets autoinvoke: false on every other skill, and disables an MCP server that a third-party plugin registers. autoinvoke: false only hides a skill from the native list. docs/cutover.md records an isolated check that hidden skills stay loadable by id, and this server's load serves every catalog skill anyway.
Safety properties:
applyrefuses rather than guesses: a comment inside a removed entry, an existingmcp.servers.skillswith other settings, or an edit outside the listed paths. Each refusal names the path.Comments and layout survive. Edits are spliced into the original text.
Each apply writes a new backup named
<config>.bak-<timestamp>-before-skills-mcp, and never overwrites one.revertkeeps the bytes it replaces in a...-before-revertfile, so edits made afterapplyare not lost.verifyexpects the three builtin skills to be advertised,mcp skillsto be connected, and the disabled servers to be absent or disabled. It reads up to three times, three seconds apart, because a freshly reloaded server can answer with an empty list for a moment.
Run plan and read the diff before apply, since apply edits your global OpenCode config. Then apply, wait for OpenCode to reload, run verify, and revert if needed. The config paths the cutover writes are absolute. If you move the checkout, run revert, then apply again.
Development
Command | Purpose |
| Run the stdio server |
| Every test, offline. |
|
|
| Fixed-cost estimate; fails above 320 tokens. |
| Compares OpenCode's registered skills with the catalog. Exits 1 if one is missing or unreadable. Needs the |
| Scores the classifier on |
| The OpenCode cutover |
| Typecheck, tests and budget |
Project layout:
src/
index.ts stdio entry point
cli.ts admin CLI
app.ts composition root: config, sources, catalog, classifier
config.ts config schema, load and save
paths.ts config and cache locations
catalog.ts merge, dedupe, safe file reads
types.ts shared contracts
skill/ frontmatter, validation, directory scan
sources/ GitHub, local and command sources; registry; process and git helpers
decision/ decision-model adapter (the only file that imports the AI SDK), providers.json, retry
rank/ lexical ranker, classifier, sharding
mcp/ server, tools, skills extension, in-process JSON-RPC driver
scripts/ budget, coverage, eval, cutover
opencode-plugin/ skills-cutover, the OpenCode plugin the cutover registers
test/ offline tests and fixtures
eval/ labelled requests; the answer cache is git-ignored
docs/ plan, evaluation results, cutover runbook, coding standardsRead these before changing code:
AGENTS.md: English for every file, and the glossary terms.
docs/coding-standards.md: module boundaries, the rules for untrusted content, the MCP surface, and TypeScript style.
GLOSSARY.md: the domain terms.
Tests stay offline. They use stub decision models, local bare git repositories and fake CLI scripts. The config and cache point at temporary directories through SKILLS_MCP_CONFIG and SKILLS_MCP_CACHE, so tests never read your configuration. Live tests run only when their credentials are set: test/decision/live.test.ts needs a TypeSafe key, and the command-source live test needs SKILLS_MCP_LIVE_CLI.
Security and privacy
Untrusted content
Every skill, resource and source is data. The server reads and returns text. It never executes a file from a skill.
Every path is resolved with
realpathand must stay inside its skill directory.readchecks containment before it serves a file. The one exception is a local source's top-level symlinks, which are trusted.Names and descriptions shown to the agent are validated and sanitized: control characters and XML-like tags are removed, and length is capped.
load'sbodyandread'stextare returned verbatim. They are untrusted skill content, and the harness should treat them as third-party text.Size limits: 512 files and 16 MiB per skill; 256 KiB per served resource; a scan stops at depth 6 or 20,000 entries.
Who may add sources
Local and command sources come only from the config file or the admin CLI.
Agents may add and remove GitHub sources through the
sourcestool.A command source runs a program, so only you add one.
Processes and environment
Commands start from an argument array, with a timeout and an output cap. No shell is involved.
Command sources get an allowlisted environment:
PATH,HOME,LANG,LC_ALL,TMPDIR,USERandSHELL. API keys are not passed to them.Git runs with
GIT_TERMINAL_PROMPT=0and without the repository override variables. Git otherwise inherits the server's environment, so API keys are visible to git. Git does not use them.
Keys and logs
Keys are read by name from the environment at startup. They do not appear in the config file, logs, errors or tool output. Errors name the variable, never its value.
Diagnostics go to stderr. Tool errors reach the agent as one sanitized line, and the same sanitized line is written to stderr.
What leaves your machine
findwith no decision model sends nothing.findwith a decision model sends the task text, the skill ids and descriptions (each cut to a character budget), and, for the three shortlisted skills, the first 700 characters of their bodies.typesafe-aientries send this to the TypeSafe AI API (https://api.typesafe.ai/v1);gatewayentries send it through the AI SDK gateway. Resources, full bodies, paths, the config file and keys are not sent.sources syncon a GitHub source contacts GitHub. A command source runs its configured program, which can do whatever that program does.loadreturnsbase, the absolute path of the skill directory on your machine, into the agent's context. Keep that in mind before you share a transcript.
Limitations and known issues
No model, no suggestion. Without
decision.model,findreturns candidates and a null suggestion. The lexical ranker cannot abstain.Latency and cost with a model. A
findmakes up to two rounds of model calls, each bounded bytimeoutMs. The eval measured p50 392 ms and p95 611 ms on one pass. Provider cost is per call and was not measured.Thresholds are provisional. They were fitted on 60 requests from one catalog, so expect different rates on yours. Run the eval on your own labelled requests before you rely on them.
Restart for configuration changes. The decision model, thresholds,
shardSizeandtimeoutMsare read at startup. Changes made with the CLI reach a running server at its next rebuild. Reconnect the harness after editing the config.Local source edits need a rebuild. A local source is scanned in place, but only when the catalog is rebuilt. A new or edited skill appears after
sources syncor a restart.disable-model-invocationis ignored. A skill whose frontmatter setsdisable-model-invocationis still served byfindandload. The server reads onlynameanddescriptionfrom frontmatter. This was checked in the code and by a run against a temporary source.Agent-added GitHub sources are limited. The tool stores the default branch and the
skillspath. Pins and other paths need the config file or the CLI.No cross-process lock. The server serializes its own source changes, but the CLI and a running server each write the config file without a lock. Do not run both at the same moment.
Git sees your environment. See Security and privacy.
Stdio only. The server has no HTTP transport.
The fixed-cost figure is an estimate. It depends on OpenCode's renderer as ported at
e23d54ca. Re-runbun run budgetafter OpenCode changes how it renders tools.
License
skills-mcp is released under the MIT License. Copyright (c) 2026 Helio Pinheiro.
This server cannot be deployed
Maintenance
Related MCP Connectors
Gives your agent the right public skill for each task and loads it on demand.
Governed AI agent skills — one library, distributed to devs and exposed to remote agents over MCP.
Search and load AI agent skills. Free metadata; full content requires a Pro or Studio account.
Agent skills for Claude, Codex and Cursor: find, install and load skills from one connector.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to search, browse, install, and manage a large library of skills via only 7 MCP tools, with skills stored locally and loaded on demand to minimize context overhead.4MIT
- 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.1,058 npm6MIT
- AlicenseNot gradedqualityCmaintenanceEnables agents to search a local catalog, load skills, inspect MCP tool schemas, and invoke selected tools through a single unified MCP tool, reducing per-request overhead.1MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to keep skill catalogs outside the main LLM context and load only task-relevant SKILL.md instructions via a single MCP tool, reducing context overhead.4MIT