<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Adding a Prompt

Adding a Prompt

A prompt is the third primitive, and the user-controlled one: a message template a person selects by name, typically as a slash command, rather than something the model reaches for or the host attaches automatically.

The three arguments to registerPrompt

server.registerPrompt(
  "prompt-name",                                  // 1. name
  {                                               // 2. config
    title: "Display Name",
    description: "What invoking this prompt will do",
    argsSchema: z.object({
      argumentName: z.string().describe("What this argument is for")
    })
  },
  ({ argumentName }) => ({                        // 3. callback
    messages: [
      {
        role: "user" as const,
        content: {
          type: "text" as const,
          text: `Instructions for the model, using ${argumentName}.`
        }
      }
    ]
  })
);

The name is what a person types, which is why it surfaces as a slash command in most hosts. Unlike a tool's name, this one is read by a human choosing from a menu.

The config carries a display title, a description, and argsSchema. That last one is a Zod object schema, same as a tool's inputSchema, doing the same three jobs: it generates what prompts/list advertises, validates arguments, and types the callback.

The callback returns { messages }. Each message names a role, "user" or "assistant", and a content block. The host hands these to the model in the order you return them, with the caller's arguments already filled in.

Notice what a prompt does not do. It returns text, not results. It cannot read your data, call your tools, or fetch your resources. All it does is tell the model what to do, and the model decides whether to act on that, which is why the next section matters more than it looks.

✏️ Exercise: a standup prompt

Add a prompt to src/stdio-server.ts that puts the task list to use:

  • Named daily-standup, titled Daily Standup, with a description saying it turns the task list into a short update.
  • One required argument, name, a string, describing who the update is for.
  • Returning a single user message whose text asks the model to read the tasks resource and write a short standup update for that name.

The catch worth getting right: your callback should not touch the tasks array or read demo://tasks itself. Write instructions that tell the model to go read it. Fetching the list yourself and pasting it into the message text would also produce a standup update, but it would be a different design, and the exercise below shows why.

Verify it

npx @modelcontextprotocol/inspector --cli npx tsx src/stdio-server.ts \
  --method prompts/get --prompt-name daily-standup --prompt-args name=Alex
{
  "messages": [
    {
      "role": "user",
      "content": {
        "type": "text",
        "text": "Read the tasks resource and write a short daily standup update for Alex based on what's there."
      }
    }
  ]
}

You get message text back with Alex substituted in, and no task list, because nothing has read one yet. That is the correct result: a prompt hands over instructions, and the model is what acts on them.

Now drop the argument and call it again. A prompt with bad arguments fails differently than a tool does. A tool comes back as a normal result with isError: true, something the model reads and can react to. A prompt is rejected as a JSON-RPC error before your callback ever runs:

{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"Invalid arguments for prompt daily-standup: name: Invalid input: expected string, received undefined"}}

A prompt is invoked by a person through the host's UI, not by the model mid-conversation, so there is no model on the other end to read a recovery message. The failure goes to whatever is driving the host instead. Seeing that error is confirmation your argsSchema marked name as required.

Solution
server.registerPrompt(
  "daily-standup",
  {
    title: "Daily Standup",
    description: "Turn the current task list into a short standup update",
    argsSchema: z.object({
      name: z.string().describe("Who the standup update is for")
    })
  },
  ({ name }) => ({
    messages: [
      {
        role: "user" as const,
        content: {
          type: "text" as const,
          text: `Read the tasks resource and write a short daily standup update for ${name} based on what's there.`
        }
      }
    ]
  })
);

Try it with your assistant

Inspector is deliberately precise: exact arguments in, exact response out. That's not how anyone will use your server. Restart demo-server the way you did in lesson 3, so your assistant runs the file as it stands, with the prompt included this time.

The restart reset your server's memory, so add a couple of tasks first, the way you did in lesson 3, so the prompt has something to read.

Then invoke the prompt. In Copilot that's /demo-server.daily-standup. In Claude Code it's /mcp__demo-server__daily-standup Alex, with arguments space-separated after the command. Watch the model read the task list on its own before writing the update, it decides to fetch demo://tasks, the same resource you attached by hand in lesson 4, without you telling it to.

Your server now exposes all three primitives.

Check your understanding

Question 1 of 2

Why do prompts fail differently than tools?

A tool with invalid arguments returns a normal result with `isError: true`. A prompt with invalid arguments is rejected as a JSON-RPC error before your callback runs. Why the difference?