Skip to content

Prompts

Kelly Ferrone edited this page Sep 18, 2026 · 5 revisions

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.

Three dialects

This server's own

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

Claude Code commands

---
description: Open an incident for a workload.
argument-hint: "[app] [severity]"
arguments: [app, severity]
---
Open an incident for $app at severity $severity. Everything else: $ARGUMENTS

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

Copilot .prompt.md

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

Which dialect a file is

Strongest signal first, because a duck check on the body alone would misread prose:

  1. What the config says. dialect: mcp-kb | claude | copilot on a plugin, auto by default. An explicit answer always wins.
  2. 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 under commands, wherever in the tree it sits — penpot declares its under prompts/. A *.prompt.md is Copilot's either way. Your own prompts: glob says which files to serve and nothing about what dialect they are; dialect: is for that.
  3. Which frontmatter keys it carries. applyTo or globs means 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. agent or tools is Copilot's; argument-hint, or arguments written as a list of names, is Claude's; arguments written as objects is this server's.
  4. 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.

What is dropped

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.

The free-text arguments argument

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 $ARGUMENTS or $N. A file that declares the name arguments itself keeps its own.
  • A Copilot file gets it only when it declares an argument-hint and 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 $ARGUMENTS to 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.

A missing required argument

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.

Placeholders this server resolves

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.

Placeholders no server can fill

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.

Where prompts come from

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.

Naming

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.

Scope

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.

In a client

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 live prompt

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.


Home · Plugins · Scoping · Tools

Clone this wiki locally