Also at Deasil Works · txn2 · Plexara
Profiles GitHub · X · LinkedIn
Theme Light · Auto · Dark
Professional notes by Craig Johnston
long-form, short-form, working drafts · since 2008
VOL. XIX · MMXXVI
106 NOTES IN PRINT
FOLIO XCIII 2026-05-19 · 6 MIN · SHORT-FORM

Prompts: Server-Authored Conversation Starters

The third server primitive and the one people misread: templates the user selects, rendered into messages that can embed a server's own resources

Diagram · folio xciii
flowchart LR
  U["user picks a prompt<br>(slash command)"] --> C["client"]
  C -->|"prompts/get { name, arguments }"| S["server"]
  S -->|"messages[]: text + embedded resource"| C
  C -->|"messages enter the conversation"| M["model"]

Prompts are the third thing a server can offer, after tools and resources, and the one most people read wrong. A tool is invoked by the model. A resource is attached by the application. A prompt is selected by the user. It is a template the human picks from a menu, fills in, and drops into the conversation. This post reads prompts/list and prompts/get on the wire, and renders a parameterized prompt that embeds one of the server’s own resources.

This is part of MCP on the Wire, a series that takes the Model Context Protocol apart message by message, in Go. It comes out of building and running MCP servers in production, including the open-source txn2/mcp-data-platform, an Apache-2.0 platform in Go that connects AI assistants to Trino, DataHub, and S3 through one MCP endpoint, enriching every result with semantic context (ownership, lineage, PII, data quality) behind OAuth 2.1 auth, personas, and an audit trail. Everything here is read straight off the wire against spec revision 2025-11-25, with the official Go SDK at v1.6.1.

§The Third Kind of Control

The resources post drew the line between tools and resources by who controls them. Prompts complete the set with a third controller. The spec calls prompts user-controlled: they are exposed so the user can explicitly select them, usually as slash commands or menu items. The model does not reach for a prompt the way it reaches for a tool, and the application does not slip one in the way it attaches a resource. A person chooses it. That is the whole distinction, and it is the figure below.

 
Tools
Resources
Prompts
Controlled by
the model
the application
the user
Surfaced as
a call the model decides to make
context the app includes
a slash command or menu pick
Discover
tools/list
resources/list
prompts/list
Fetch
tools/call
resources/read
prompts/get

§Listing Prompts

Discovery is prompts/list, paginated like the others. A server with one code_review prompt lists it like this, from the wire:

{ "prompts": [
  { "name": "code_review", "title": "Code Review",
    "description": "Ask for a focused review of a file.",
    "arguments": [
      { "name": "file",  "description": "path of the file to review", "required": true },
      { "name": "focus", "description": "what to look for (default: bugs)" }
    ] }
] }

A prompt object is name, optional title and description, and a list of arguments. Each argument has a name, a description, and a required flag, omitted here on focus because it is optional. The arguments are the template’s slots, and because a prompt is user-controlled, they are what the client’s interface asks the user to fill before invoking it. The description on each is the label the user reads.

§Getting a Prompt

Selecting the prompt and supplying its arguments is prompts/get. The client sends the name and an arguments map, and the server renders the template into messages:

// prompts/get { "name": "code_review", "arguments": { "file": "main.go", "focus": "concurrency" } }
{
  "description": "Code review for main.go",
  "messages": [
    { "role": "user", "content": {
      "type": "text", "text": "Review the following file for concurrency. Be specific." } },
    { "role": "user", "content": {
      "type": "resource", "resource": {
        "uri": "file://main.go", "mimeType": "text/x-go",
        "text": "package main\n\nfunc main() {}\n" } } }
  ]
}

The focus argument was substituted into the text, and the result is a messages array ready to drop into the conversation. This is the payoff of the user-controlled model: the user picked a prompt and gave it a file, and the server returned a fully formed opening turn. Each message has a role, either user or assistant, and a content block. The content can be any of the types from the tool results post: text, image, audio, or an embedded resource.

That second message is the one worth noticing. It is an embedded resource, the same shape a resource read returns, carrying the file’s contents inline. A prompt can pull the server’s own data straight into the conversation, so code_review does not just say “review the file,” it includes the file. This is where prompts and resources meet: a prompt is a template, and one of the things it can template in is a resource the server already manages.

§Required Is a Hint, Not a Gate

The required: true on the file argument looks like validation, and it is worth being precise about what it is not. Call the prompt with the required argument missing and the SDK does not stop the call. The handler runs with an empty file:

// prompts/get { "name": "code_review", "arguments": {} }
{ "description": "Code review for ",
  "messages": [ { "role": "user", "content": { "type": "text",
    "text": "Review the following file for bugs. Be specific." } }, ... ] }

This is a real difference from tools. A tool’s inputSchema is validated by the SDK before the handler runs, so a missing required field is rejected automatically. A prompt’s required flag is not. The spec says a server should validate its arguments and return -32602 for a missing required one, but it is the handler’s job, not the framework’s. The required flag’s first purpose is to tell the client’s interface to collect that value from the user before calling, which fits the user-controlled model: the human is expected to fill it in. Asking for a prompt that does not exist is a flat protocol error, the same -32602:

{ "id": 5, "error": { "code": -32602, "message": "unknown prompt \"no_such_prompt\"" } }

So validate required arguments in your handler. The SDK that catches a bad tool call for you will hand your prompt handler an empty string and let it run.

§The Go Server

The handler is a function that builds the messages. It reads the arguments off the request and returns a GetPromptResult:

s.AddPrompt(&mcp.Prompt{
	Name:      "code_review",
	Arguments: []*mcp.PromptArgument{
		{Name: "file", Description: "path of the file to review", Required: true},
		{Name: "focus", Description: "what to look for (default: bugs)"},
	},
}, func(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
	file := req.Params.Arguments["file"]
	focus := req.Params.Arguments["focus"]
	if focus == "" {
		focus = "bugs"
	}
	return &mcp.GetPromptResult{
		Description: "Code review for " + file,
		Messages: []*mcp.PromptMessage{
			{Role: "user", Content: &mcp.TextContent{Text: "Review the following file for " + focus + ". Be specific."}},
			{Role: "user", Content: &mcp.EmbeddedResource{Resource: &mcp.ResourceContents{
				URI: "file://" + file, MIMEType: "text/x-go", Text: readFile(file)}}},
		},
	}, nil
})

Two more details round it out. A prompt’s arguments can be auto-completed through the completion API, which is the next post, so the user typing a partial filename gets suggestions. And like tools and resources, a prompt list is not frozen: a server that declares the prompts capability with listChanged sends notifications/prompts/list_changed when its set of prompts changes.

txn2/mcp-data-platform serves MCP prompts the same way. Its platform-overview prompt is a server-authored starting point that orients an agent to what the platform covers and how to use it, listed through prompts/list and rendered by prompts/get alongside the curated prompts a team adds.

§What’s Next

Three times now the series has mentioned that an argument can be auto-completed, for resource templates, for prompt arguments, and it has deferred the detail every time. The next post pays the debt. Completion: Argument Autocomplete for Prompts and Resources reads completion/complete on the wire: how a client asks the server to suggest values for a half-typed argument, how the server scopes those suggestions to what the user has already filled in, and why it is a primitive of its own rather than a field on the others.


The production data platform behind this series is txn2/mcp-data-platform, available hosted as Plexara.

← back to all notes