Skip to main content
Glama
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