Skills MCP
by kubed-io
README.md
# Skills MCP
Serves [Agent Skills](https://code.claude.com/docs/en/skills) over MCP, so any client can
discover and read them — including clients that only speak tools.
Skills are declared as pinned dependencies in `skills.toml` and fetched at image build
time. Nothing is fetched at runtime.
```
docker run -p 8000:8000 kubed/skills-mcp:latest
```
Point a client at `http://localhost:8000/mcp` and it gets **four tools — never one per
skill**. Skills are data behind `read_skill`, not entries in the tool list.
| tool | returns | cost |
| --- | --- | --- |
| `list_packs()` | every pack and its groups, with counts | ~75 tokens |
| `list_skills(pack)` | `name: description` for that pack or group | ~1–2k tokens |
| `read_skill(skill, file)` | one skill's instructions, manifest, or a file | one skill |
| `read_pack_file(pack, file)` | a file the pack ships outside any skill | one file |
Each layer is cheap enough to call speculatively and narrow enough that the next one
stays small:
```
list_packs() → grafana (50), n8n (14), penpot (12), and grafana's 7 groups
list_skills(pack="grafana-lgtm") → 6 skills, ~945 tokens
read_skill(skill="loki") → the instructions to follow
read_skill(skill="loki", file="_manifest") → what else it ships
```
## Pack-level files
The Agent Skills spec keeps a skill self-contained: references are "relative paths from
the skill root". Some kits ignore that. Penpot's twelve skills point at `shared/*` from
190 places, so served on their own they are a maze of dead links.
A source can declare those directories in `skills.toml`, and they are served by
`read_pack_file` — never as skills:
```toml
extras = ["shared", "workflows"]
```
Nothing about a skill changes. `read_skill` still serves each skill's own directory
completely, and the two spaces do not overlap: a skill's files are unreachable through
`read_pack_file`, and pack files are absent from any skill's `_manifest`.
## Filtering to one pack
`pack` accepts either a source (`n8n`, `grafana`, `penpot`) or one of its groups
(`grafana-core`, `grafana-lgtm`). That is the soft filter, chosen per call.
For a **hard** scope there are two levers, and both are ceilings the model
cannot widen past.
**Per client — the `X-Skill-Pack` header.** Set it once in the client's
connection config and that client sees one pack, whatever it asks for. This is
how one deployment serves several single-pack agents:
```
X-Skill-Pack: penpot
```
In n8n that is a Header Auth credential on the MCP Client Tool node — a plumbed
constant on the node, not something the model fills in.
**Per deployment — `SKILL_PACKS`.** Scopes the whole instance; the rest of the
catalogue is not loaded at all:
```
SKILL_PACKS=n8n
```
They compose: the header narrows within whatever `SKILL_PACKS` already allows.
## Why three tools and not resources
MCP has three primitives — tools, resources and prompts. Skills map naturally onto
resources, and `SkillsDirectoryProvider` still publishes them that way for clients that
speak the resource half of the protocol. But many clients only implement tools — n8n's
MCP Client Tool is one — and to those a resource-only server looks empty.
FastMCP ships a generic `ResourcesAsTools` bridge for exactly that, but it is too
expensive here: it lists three entries per skill (`SKILL.md`, `_manifest`, and a file
template), each repeating the skill's full description. For 64 skills that is 192
entries and **~16k tokens on every listing call** — the opposite of what skills are for.
The three tools above are hand-rolled to give the same access for a fraction of it.
## Skills as dependencies
`skills.toml` is the source of truth:
```toml
[[source]]
name = "n8n"
repo = "https://github.com/n8n-io/skills.git"
ref = "180b8415e3b73f78828cfa01e908e67f89f2a139"
path = "skills"
```
`ref` is a commit, so an image is reproducible. `path` is the subdirectory holding the
skill folders — not the repo root. `skills/` is gitignored; upstream markdown is never
vendored into this repo, so a skill bump reviews as a one-line ref change.
Currently served: **64 skills** from [n8n-io/skills](https://github.com/n8n-io/skills)
and [grafana/skills](https://github.com/grafana/skills).
Fetch them locally:
```
python scripts/fetch_skills.py # fetch at the pinned refs
python scripts/fetch_skills.py --update # repin everything to upstream HEAD
```
The **Update Skills** workflow runs that weekly and opens a PR.
## Adding a source
Add a `[[source]]` block, then one `COPY` line in the Dockerfile's `skills` stage.
A source may nest its skills at any depth — the server discovers roots by walking for
`SKILL.md`, because `SkillsDirectoryProvider` itself does not recurse.
## Configuration
| variable | default | meaning |
| --- | --- | --- |
| `SKILLS_DIR` | `/skills` | directory to scan |
| `SKILL_PACKS` | *(all)* | comma-separated packs to serve; hard scope |
| `TRANSPORT` | `http` | `http` or `stdio` |
| `HOST` | `0.0.0.0` | bind address |
| `PORT` | `8000` | port |
`GET /health` reports status, the packs served, and the skill count.
## Deploying
```
kubectl apply -k .
```
Runs in the `flow` namespace as `skills-mcp:8000`. There is no authentication: every
skill served is public markdown, the server has no write path and holds no credentials.
## Development
```
pip install -e .[test]
pytest
```
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues