MCP Protocol Internals · Lesson 3

Three Server Surfaces

Tools, resources, prompts — one transport, three audiences. The pattern you learned for tools just repeats.

You asked whether prompts deserve their own deep dive. Here's the honest answer, and the most useful idea in this whole course: a server exposes three kinds of capability, and they share an almost identical wire pattern. What actually distinguishes them is who decides to use them.

The one idea: the control axis

Every server capability is defined by who is in control of invoking it. Get this and resources and prompts cost you almost nothing on top of tools.spec

SurfaceControlled byDiscoverUseReturns
Toolsthe model
LLM picks one mid-reasoning
tools/listtools/callcontent[] + structuredContent?
Resourcesthe application
host loads context/data
resources/listresources/readcontents[] (text or blob)
Promptsthe user
human invokes a template, e.g. a slash-command
prompts/listprompts/getmessages[] (role + content)

Notice the shape: every surface is {thing}/list + a verb, all over the same Streamable HTTP transport, all with the same pagination cursor, and each with its own notifications/{thing}/list_changed. You already know this rhythm.

1 · Resources — application-controlled context

Resources are addressable data the host application chooses to feed the model — files, records, documents. They're identified by a URI, not a name. Discovery looks just like tools/list; reading takes a uri instead of arguments:

POST /mcp // resources/list → array of {uri, name, mimeType} [ { "uri":"demo://resource/static/document/architecture.md", "name":"architecture.md" }, … ] POST /mcp { "jsonrpc":"2.0","id":7,"method":"resources/read", "params": { "uri":"demo://resource/static/document/architecture.md" } } ── result ── { "result": { "contents": [ { "uri":"demo://resource/static/document/architecture.md", "mimeType":"text/markdown", "text":"# Everything Server – Architecture\n…" } ] } }

Resources-only powers: a client can resources/subscribe to one URI and get pushed updated notifications when it changes — the only surface with per-item subscription. There are also resource templates (resources/templates/list) for parameterised URIs like file:///{path}.

Note contents is an array (one read can yield several parts), and each part carries its own mimeType and either text or base64 blob.

2 · Prompts — user-controlled templates

A prompt is a pre-written conversation template a user deliberately invokes (the classic UI is a slash-command menu). Discovery returns its arguments — and here's the first real difference from tools:

// one entry from prompts/list { "name":"args-prompt", "title":"Arguments Prompt", "description":"A prompt with two arguments, one required and one optional", "arguments": [ { "name":"city", "description":"Name of the city", "required": true }, { "name":"state", "description":"Name of the state", "required": false } ] }
Prompt arguments ≠ tool inputSchema

A tool describes its inputs with a full JSON Schema (inputSchema, with types and validation). A prompt uses a flat list of named argumentsname / description / required, no types. Lighter, because a prompt's job is just to fill blanks in a template, not to validate a function call.spec

Now prompts/get with arguments. The second key difference: it returns messages — ready-to-send conversation turns, each a role + content:

POST /mcp { "jsonrpc":"2.0","id":5,"method":"prompts/get", "params": { "name":"args-prompt", "arguments":{ "city":"Prague","state":"CZ" } } } ── result ── { "result": { "messages": [ { "role":"user", "content": { "type":"text", "text":"What's weather in Prague, CZ?" } } ] } }

The server interpolated city/state into its template and handed back a finished user turn. The host drops those messages straight into the model's context — the user chose to do this, which is the whole point of the control axis.

3 · Why this axis is the key to authorization

This is the bridge to the next lessons

Three surfaces controlled by three different actors means three different things to gate. "Can the model autonomously call get-env?" is a different risk question from "can this user load that resource?" When we reach authorization, scopes and access rules attach to these surfaces — so knowing the axis is exactly what makes OAuth scoping in MCP make sense, not arbitrary.

🔧 Try it yourself (≈ 5 minutes)

Session still open (re-run bash assets/mcp-curl.sh init if needed). The send verb posts any raw JSON-RPC on your session:

bash assets/mcp-curl.sh send '{"jsonrpc":"2.0","id":1,"method":"prompts/list"}' bash assets/mcp-curl.sh send '{"jsonrpc":"2.0","id":2,"method":"prompts/get","params":{"name":"args-prompt","arguments":{"city":"Tokyo","state":"JP"}}}' bash assets/mcp-curl.sh send '{"jsonrpc":"2.0","id":3,"method":"resources/list"}' bash assets/mcp-curl.sh send '{"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"demo://resource/static/document/features.md"}}'

For each surface, name the verb and the return-field before you read the output: prompts return ____, resources return ____, tools return ____. Then try prompts/get for args-prompt with no arguments and watch what a required argument does.

Check yourself

Which surface is controlled by the user deciding to invoke it?
How do prompt arguments differ from a tool's inputSchema?
What does prompts/get hand back to the host?
Primary source — read this next

MCP Spec 2025-11-25 — Server / Prompts and Server / Resources. Skim both side by side and notice how deliberately they mirror the Tools page you already know.