-
Notifications
You must be signed in to change notification settings - Fork 1
Prompts
A skill is read by the model when it decides to. A prompt is picked by a person, who fills in a few arguments before the model sees anything. Different primitive, different audience — Claude Code lists them as slash commands — and the same plugins, libraries and scopes as everything else.
Point this server at a Claude Code command, a VS Code Copilot prompt file or one of its own prompt files and the same MCP prompt comes out. No schema is invented here: the target is the MCP prompt itself — a name, a description and arguments that are strings.
---
description: Debug a workload's recent logs in Loki.
arguments:
- name: app
description: The workload to look at.
required: true
- name: since
default: 1h
---
Investigate the logs of **{{ app }}** over the last {{ since }}.The richest of the three, and the only one that can mark an argument required or give it a default:
| Key | Means |
|---|---|
name |
the placeholder this fills |
description |
shown to whoever fills it in |
required |
true makes it mandatory; a required argument cannot also have a default |
default |
used when the field is left blank |
{{ name }}, not {name}: prompt bodies here are full of LogQL, PromQL and
JSON, all of which use single braces. An undeclared placeholder is an error
in this dialect and in no other — this is the one format where the author had a
way to declare it, so {{ nmae }} is a typo, and rendered blank it produces a
prompt that reads perfectly well and asks the model for the wrong thing.
---
description: Open an incident for a workload.
argument-hint: "[app] [severity]"
arguments: [app, severity]
---
Open an incident for $app at severity $severity. Everything else: $ARGUMENTSThe dialect the marketplaces ship, so it is the one that has to be exactly
right. arguments is a list of names (or one whitespace-separated string)
and says nothing more about them, so every argument is optional with no
description: those clients tolerate an unfilled placeholder, and inventing a
description from a name would publish a sentence the author never wrote.
Note the quotes on the hint. Frontmatter is YAML, and a bare
[app] [severity] is two flow sequences in a row, which no YAML parser accepts
— a file written that way is skipped with the parser's complaint as its reason,
so quote a hint that is not a single word or a proper YAML list.
The body substitutes $name, $ARGUMENTS, $ARGUMENTS[N] and $N — $0 is
the first declared name, or the first word of the free text when nothing is
declared. $5.00 is a price, not a placeholder. An undeclared $dir is prose
and is served as written.
---
description: Review a pull request against our conventions.
---
Review ${input:pr:Which PR?} and check it against ${input:standard}.Copilot declares nothing in frontmatter: the arguments are the body's inputs, in first-appearance order, and the placeholder text VS Code would show in its input box is the only description an author writes, so it becomes the argument's. Both spellings of one input take its value.
Strongest signal first, because a duck check on the body alone would misread prose:
-
What the config says.
dialect: mcp-kb | claude | copiloton a plugin,autoby default. An explicit answer always wins. -
How the file was found and what it is called. A file under
commands/or.claude/commands/is Claude's, and so is one a manifest or a marketplace entry declared undercommands, wherever in the tree it sits — penpot declares its underprompts/. A*.prompt.mdis Copilot's either way. Your ownprompts:glob says which files to serve and nothing about what dialect they are;dialect:is for that. -
Which frontmatter keys it carries.
applyToorglobsmeans the file is instructions, not a prompt at all, and it is skipped — publishing one as a command would offer the model a rule to run.agentortoolsis Copilot's;argument-hint, orargumentswritten as a list of names, is Claude's;argumentswritten as objects is this server's. -
Which placeholders the body uses, as the last resort:
{{ name }},${input:…},$ARGUMENTS/$N.
A file none of that decides is served in this server's own dialect, which adds
nothing to a description and a body. A file that cannot be parsed at all is
skipped with its reason under its plugin's skipped in
/health — one bad frontmatter block must not take a catalogue's
skills down with it.
Every dialect carries keys that have no meaning over MCP: model, tools,
agent, allowed-tools, disallowed-tools, disable-model-invocation,
user-invocable, when_to_use. They are dropped, and named once per file at
DEBUG rather than vanishing silently, so an author who set one can find out it
was ignored.
Claude and Copilot both have one thing this server's own dialect does not: the
text a person types after the command. It is published as an optional argument
named arguments, described by argument-hint when the file has one.
- A Claude command declaring no names gets it, and so does one whose body writes
$ARGUMENTSor$N. A file that declares the nameargumentsitself keeps its own. - A Copilot file gets it only when it declares an
argument-hintand its body has no${input:…}of its own, since its inputs are already its arguments. - A body that places nothing gets the free text appended rather than dropped —
as
ARGUMENTS: …for Claude, which is what Claude Code does, and unlabelled for Copilot, which has no$ARGUMENTSto name it after.
Only this server's own required: true makes an argument required. Every other
dialect's are optional, because those clients all tolerate an unfilled
placeholder. A field left blank arrives as an empty string from most pickers and
counts as not given: a default applies, and a required argument is missing.
prompts/get without a required argument is invalid params (-32602), naming the
argument.
On an MCP 2026-07-28 connection whose client declares elicitation, it is a question instead: the server returns an input-required result asking for that argument, and renders on the round that answers it. Nothing else changes, and every older connection keeps the error.
Two of Claude's placeholders are not client-side at all — they name this server's material — so they are resolved on every render, in every dialect:
| In a body | Becomes |
|---|---|
${CLAUDE_PLUGIN_ROOT} |
skill://<library>, the plugin's address base |
${CLAUDE_SKILL_DIR} |
skill://<library>/<folder>/<name>, in a SKILL.md
|
So a prompt citing ${CLAUDE_PLUGIN_ROOT}/shared/tokens.md renders as an
address its reader can actually read. ${CLAUDE_SKILL_DIR} has no answer in a
prompt — a command is not a skill and has no directory of its own — so there it
stays as written, like everything below.
These name things only the client has, and they are served exactly as written:
${selection} · ${file} · ${workspaceFolder} · #file: · #tool: ·
@path references · ${CLAUDE_PROJECT_DIR} · ${CLAUDE_SESSION_ID} ·
${input:…} beyond a declared argument
No arguments are published for them, no metadata, no machinery. A model reading
${selection} knows what it means, which is the whole reason this is safe to
leave alone — and it is what Claude Code itself does with a skill it did not
fetch locally: the placeholders, the @ references and the ! lines all reach
the model as literal text.
Nothing fetched is ever executed. A ! shell line and a !{…} block are
text, forever. A prompt server that ran shell out of a repository it fetched
would be a supply-chain hole.
A plugin's prompts: globs, or its manifest's commands, or the conventions —
prompts/**/*.md, .github/prompts/*.prompt.md, commands/*.md. See
Plugins.
Prompts usually live with the installation rather than with an upstream skill
repository, which is why a marketplace entry that declares its own commands is
worth trusting and a plugin of your own usually says prompts: []: a repository
of skills also holds prompts/ for its own development, and those are not yours
to serve.
The exposed name is <library>_<file stem>: prompts/debug.md in a plugin
the grafana library serves is grafana_debug. investigate.prompt.md is
investigate — the .prompt marks the file, it is not part of the name.
Prompt names are one flat namespace per MCP server, and every client
namespaces by the server rather than by anything inside it: Claude Code shows
/mcp__kb__grafana_debug, VS Code /kb.grafana_debug. So the plugin is not in
the name — the client already says which server a prompt came from — and the
library is, because two libraries shipping a debug.md would otherwise collide
and one of them would simply not be there.
A prompt belongs to a library, and carries its plugin's category and tags.
?library=grafana selects every prompt of that library, whichever plugin it
came from; ?categories= and ?tags= narrow to the plugins that declare them.
See Scoping.
A prompt outside a client's scope is not merely unlisted: prompts/get does not
know it either.
Claude Code lists prompts as slash commands, which then ask for the arguments. Nothing else is needed — a client that uses MCP prompts natively wants the bare MCP URL.
n8n and anything else without prompts gets the tool mirror instead:
list_prompts returns each prompt's name, description and arguments, and
get_prompt returns the rendered, role-tagged messages. Add ?prompts=off to
the URL to reveal them; Tools has both signatures.
The mirror is FastMCP's own and routes through the same prompts/list and
prompts/get this server implements, so a scope applies to the tools exactly as
it applies to the prompts.
A prompt from a cache: live source has its body re-read from disk each time
it is rendered, so an edit in Nextcloud lands on the next use. Its arguments
do not: those are what the harvest recorded, so declaring a new one still needs a
refresh. A file that has become unreadable falls back to the body last
harvested — the same rule the rest of live mode follows.
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