Skip to content
Kelly Ferrone edited this page Sep 21, 2026 · 11 revisions

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>.

The grammar

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.

Identity is the URI, not the name

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.

Read down, not across

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.

What makes a skill

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.

Directories serve nothing

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.

_manifest — what else a skill ships

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.

_files.md — what a library ships outside its skills

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 citation that misses is tried against the plugin root

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.

${CLAUDE_PLUGIN_ROOT} and ${CLAUDE_SKILL_DIR} in a body

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.

What is never a library-level file

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.

Media types

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.


Home · Plugins · Scoping · Tools

Clone this wiki locally