-
Notifications
You must be signed in to change notification settings - Fork 1
Skills
Everything this server serves has one address, and reading one is the only
operation there is. A client with MCP resources reads them as resources; one
without reads them with read_resource. Same URIs, same answers — see
Tools.
The grammar follows the MCP Skills extension
(SEP-2640): skill://<skill-path>/<file-path>, where the final segment of
<skill-path> is the skill's name and everything before it is a
server-chosen prefix. This server's prefix is <library>/<folder>.
skill://<library>/<folder>/<name>/SKILL.md a skill's instructions
skill://<library>/<folder>/<name>/_manifest what else that skill ships
skill://<library>/<folder>/<name>/<file> one of those files
skill://<library>/_index.md the folders and skills in a library
skill://<library>/<folder>/_index.md the folders and skills in a folder
skill://<library>/_files.md files outside every skill
skill://<library>/<path> one of those
<folder> is the skill's directory below its plugin root — with
skills/, .github/skills/, .claude/skills/ or .agents/skills/ stripped
when the skill sits under one. It is empty when the skill sits directly in that
root, and several segments when it does not. It mirrors the disk layout exactly,
which is what keeps a sibling reference inside a SKILL.md —
../other/SKILL.md — resolving to the sibling's real address rather than to
something this server invented.
<library> is the library name, which the config declares; the plugins one
library serves share its namespace. A skill is always addressed library-first,
so two libraries shipping a testing/ skill are two different skills rather
than one silently winning, and a folder name is never enough on its own to pick
one out — see Scoping.
Anything a scope excludes resolves to nothing, exactly as an address that does not exist does. That conflation is deliberate: knowing a skill's exact URI must not be enough to confirm it is there.
A skill's name — the one in its frontmatter — is a label, not an identifier.
Two skills named refunds at different addresses are both legitimate, and
nothing here treats a bare name as reachable: skill://grafana/loki is not an
address, because a name alone does not say which folder or which library.
Reach a skill only by its full URI, whether that URI came from a listing, from an index, or from a sibling reference inside another skill's file. A URI this server names — in an index, in an error, in another skill's own text — resolves exactly as written; it is never a shorthand for something shorter.
The server resolves . and .. in a skill URI itself, so a relative reference
works whether or not the client normalises it:
skill://grafana/grafana-lgtm/loki/../tempo/SKILL.md is tempo's instructions.
A .. past the library's root is dropped, as it is in any URI — the library is
the first thing in the address, and no number of .. reaches another one.
The listing is indexes, not skills — one row per library, one per top-level
folder of a library, and one _files.md row for a library that ships material
outside its skills: the top of the tree the indexes themselves walk down. A dozen rows, whether the catalogue holds nine
skills or ninety.
list → the indexes, ~1.9 KB
read skill://grafana/grafana-lgtm/_index.md → the skills in that folder, as URIs
read skill://grafana/grafana-lgtm/loki/SKILL.md → the instructions to follow
An index lists only what is directly under it. The library's is one line per
top-level folder — its _index.md and how many skills are below it — then the
skills that sit in no folder, then _files.md when the library ships library
files. A folder's index does the same one level down, so a folder that holds
only folders still has one. Fifty skills in seven folders is seven lines, not
fifty descriptions:
# grafana — 50 skills
skill://grafana/grafana-app-sdk/_index.md: 4 skills
skill://grafana/grafana-cloud/_index.md: 19 skills
…
An index's lines are <uri>: <description> rather than <name>: …, so reading
one teaches the grammar for the next call. A name would have to be turned back
into an address anyway, and it is not even unique.
?skills=full puts every skill's SKILL.md in the listing as well, at its
real URI. It exists for clients that sync skills to disk by scanning a listing
for /SKILL.md; for anything else it is ~16k tokens per listing for rows the
model was going to narrow down anyway.
A directory holding a SKILL.md that a plugin's skills globs select — see
Plugins. Its frontmatter names it:
---
name: loki
description: Query and troubleshoot logs in Grafana Loki.
---
# Loki
…name is what appears in the URI's final segment, falling back to the
directory name when the frontmatter has none. It has to keep the
Agent Skills naming rule — 1–64
lowercase letters, digits and hyphens, no hyphen first, last or doubled — and
equal the name of the directory it sits in. A skill that breaks either is not
served: it would mint an address that does not resolve, or a
../<dir>/SKILL.md reference to it would not. Neither is two skills of one
plugin at the same address, as a tree that reaches one skill through both
skills/ and a symlinked .claude/skills/ has; the first found serves. Each
one left out is listed, with the reason, under that plugin's skipped in
/health.
description is the index row — it is the whole basis on which an agent
decides whether to read the skill, so it is worth the sentence.
A folder is the directory a skill sits in, below its plugin's skill root.
skills/grafana-lgtm/loki/SKILL.md is the loki skill in the grafana-lgtm
folder, addressed skill://grafana/grafana-lgtm/loki/SKILL.md; skills/loki/
is the loki skill in no folder at all, addressed
skill://grafana/loki/SKILL.md.
A skill may sit inside another skill's directory, as the extension allows.
Both are served, each at its own URI. The enclosing skill's directory is also
the nested skill's folder, so the index lists that folder and the folder's index
lists the nested skill. The nested
skill's files are also the enclosing skill's supporting files — its _manifest
lists them — and a URI under both names one file, so reaching it through either
skill reads the same bytes. Nothing about nesting is a conflict.
A library, a folder, or a skill's own root directory is not a file, and reading one is not found — exactly as on any other skill server built to the extension. The error names the file to read instead:
read skill://grafana → not found: it is a library,
read skill://grafana/_index.md
read skill://grafana/grafana-lgtm → not found: it is a folder,
read its _index.md
read skill://grafana/grafana-lgtm/loki → not found: it is a skill's
directory, read its SKILL.md
or its _manifest
The hint is answered from what the caller's scope can see, so it never confirms
a directory the caller could not otherwise list. When a client's host
implements resources/directory/read these become real directory listings;
until then they are addresses with nothing behind them, and the hint is the
only way a caller learns what to read next.
read skill://grafana/grafana-lgtm/loki/_manifest
{
"skill": "grafana/grafana-lgtm/loki",
"files": [
{"path": "SKILL.md", "size": 2310, "hash": "sha256:…"},
{"path": "references/logql.md", "size": 8142, "hash": "sha256:…"}
]
}Path, size and hash for every regular file in the skill directory, which is the shape the ecosystem already reads — a client syncing a skill to disk needs all three. Then read one by its path under the same skill URI:
read skill://grafana/grafana-lgtm/loki/references/logql.md
A skill citing a file is not a reason to fetch it. Reading is per-URI and nothing is prefetched; that is the whole point of the manifest being a separate read. Fetch a reference when you are about to use it.
The manifest applies the same rules the harvest does, so what it advertises is
what the skill actually ships, and a read refuses what it leaves out: nothing
whose path has a segment starting with a dot — a .env, a swap file, a .git/
— except the three directories agent tooling conventionally lives in,
.github/, .claude/ and .agents/, which are kept at any depth. A symlink
is never listed, and a read follows one only while it stays inside the skill.
Some libraries factor shared material up out of the individual skills: a
shared/ directory, a set of workflow documents, a schema several skills cite.
Those are not skills and do not belong inside one, so they are addressed at the
library level, keeping their path from the plugin root:
read skill://penpot/_files.md → the URIs
read skill://penpot/shared/api.md → one of them
What _files.md lists is opt-in: a plugin's files: globs name them, because
files has no conventions. A library whose plugins name none gets no
_files.md row — the listing offers one exactly when reading it would return
something.
A skill usually cites shared material the way its repository is laid out —
shared/api.md, not ../shared/api.md — and resolved against the skill's own
directory, as a relative reference is, that would name an address with nothing
at it.
So it is tried once more against the plugin root, which is the ceiling of
everything the plugin publishes and exactly what ${CLAUDE_PLUGIN_ROOT} reaches
in Claude Code:
read skill://penpot/penpot-router/shared/api.md → the plugin's shared/api.md
read skill://penpot/shared/api.md → the same file
Four rules keep that honest. A file the skill has always wins — the fallback only runs when the skill's own directory has nothing at that path. Nothing is rewritten: both addresses read the same bytes, and neither becomes the other. The ceiling is the plugin root, so a hidden file, and anything outside the root, is still not there. And no skill directory of the library is reachable this way — not the plugin's own, and not another plugin's on the same root: that path is the skill's address, answered under the skill's own scope. A library is often fed by two plugins over one repository (see Libraries), and without that rule a client scoped to the one shipping the shared files could read the other's skills at the library base.
What the fallback does not change is the listing. _files.md lists the
files: globs and nothing else, so a plugin that publishes no library files
still advertises none — the fallback is for a citation that would otherwise
dangle, not a way to publish a tree by accident.
A SKILL.md that writes either is served with them resolved into this address
space: ${CLAUDE_PLUGIN_ROOT} becomes skill://<library> and
${CLAUDE_SKILL_DIR} becomes that skill's own base,
skill://<library>/<folder>/<name>. They are the two placeholders this server
is the authority on; every other ${…} is the client's and is served as
written. See Prompts, where the same rule applies to a prompt body.
A file inside a skill directory is never a library-level file. Nor is one whose
address would lie inside a skill's — docs/** served beside a skill named
docs — or one named _index.md or _files.md, in any folder: the skill or
the index answers that URI, so the file could be listed and never read. Such a
file is not served, and its plugin lists it under skipped in
/health. When the skill belongs to another plugin of the
library, it is a conflict the later plugin loses instead — see
Libraries. Either way no URI means two things.
The "inside a skill directory" here is the library's skills, not the
plugin's. A plugin's files: globs are applied to its own root, and a helper
plugin declared over a repository it shares with a kit — files: ["**/*"],
skills: [] — therefore harvests the kit's skill files, since at that point
nothing has met both plugins yet. The library drops them from _files.md and
from the read alike, so the listing and the read agree and the skill's own
address stays the only one. These carry no skipped entry: they were never
that plugin's to skip.
A read returns text/markdown unless the path says otherwise — _manifest is
application/json, and anything with a known extension gets its own type.
Defaulting to markdown rather than application/octet-stream matters: a client
told "octets" may decline to show a file it could perfectly well read.
Configuration, Tools and Endpoints are generated by scripts/generate_wiki.py from the config models, a live server's tool list and the OpenAPI document — so they describe the server that shipped, not the one someone remembered. Prose for a generated page goes in wiki/notes/<page>.notes.md.
mcp-kb · MIT
Getting started
The config file
What clients get
Reference