TAI BUI
Back to curriculum
BuildPython

MCP Resources and Prompts — Context Exposure Beyond Tools

Tools get 90 percent of the attention in MCP, but the other two server primitives solve different problems. Resources expose data for reading. Prompts expose reusable templates as slash-commands. Plenty of servers should use resources instead of wrapping reads in tools, and prompts instead of hard-coding workflows in client prompts. This lesson gives you the decision rule and walks the `resources/*` and `prompts/*` messages.

TIME: ~45 minutesPREREQUISITES: Phase 13 · 07 (MCP server)

Learning Objectives

  • Decide between exposing a capability as a tool, a resource, or a prompt for a given domain.
  • Implement `resources/list`, `resources/read`, `resources/subscribe` and handle `notifications/resources/updated`.
  • Implement `prompts/list` and `prompts/get` with argument templates.
  • Recognize when the host surfaces prompts as slash-commands vs auto-injected context.

The Problem

A naive MCP server for a notes app exposes everything as tools: notes_read, notes_list, notes_search. Every data access becomes a model-driven tool call, and that has a few costs:

  • The model has to decide whether to call notes_read for each query that might benefit from context.
  • Read-only content cannot be subscribed to or streamed to the host's side panel.
  • Client UIs (Claude Desktop's resource attachment panel, Cursor's "Include file" picker) cannot surface the data.

The better split: data becomes a resource, mutating or computed actions become tools, and reusable multi-step workflows become prompts. Each primitive has its own UX affordance and its own access pattern.

Warm-up0/3
1. What message format do MCP clients and servers use on the wire, regardless of transport?Warm-up
An MCP client box and an MCP server box exchange messages in both directions. A dashed box below asks what format every message has on the wire, whichever transport carries it.
Schematic. The dashed box is the unknown.
2. During initialization, how does an MCP server tell the client which features it supports?Warm-up
A client and a server with dashed lifelines. Step 1: the client opens the connection and starts setup. Step 2, drawn dashed: the server tells the client which features it supports, with the way it does so left as a question.
Schematic of the start of a connection. Step 2 is the unknown.
3. In a basic MCP server, which primitive does the model itself decide to invoke mid-conversation?Warm-up
A user message asking what they noted about transports, then the model, mid-answer, reaching out to a notes server. A dashed box asks which kind of call the model itself decides to make.
Schematic. The dashed box is the unknown.
Check: The Problem0/1
1. Why is wrapping every read in a tool like notes_read the wrong design for a notes server?Explain
A panel listing the three tools of a naive notes server, notes_read, notes_list and notes_search, connected to a host box with a model and chat UI. A dashed box asks why wrapping every read in a tool like notes_read is the wrong design.
Tool names are from the lesson's Problem section. The dashed box is the unknown.

The Concept

Tools vs resources vs prompts: the decision rule

CapabilityPrimitive
User wants to search, filter, or transform datatool
User wants the host to include this data as contextresource
User wants a templated workflow they can re-runprompt

The guideline: if the model would benefit from calling it on any related query, make it a tool. If the user would benefit from attaching it to a conversation, make it a resource. If a whole multi-step workflow is the unit the user wants to reuse, make it a prompt.

Resources

resources/list returns {resources: [{uri, name, mimeType, description?}]}. resources/read takes {uri} and returns {contents: [{uri, mimeType, text | blob}]}.

URIs can be anything addressable:

  • file:///Users/alice/notes/mcp.md
  • postgres://my-db/query/SELECT ...
  • notes://note-14 (custom scheme)
  • memory://session-2026-04-22/recent (server-specific)

contents[] supports both text and binary. Binary uses blob as a base64-encoded string plus a mimeType.

Resource subscriptions

The server declares {resources: {subscribe: true}} in its capabilities. The client calls resources/subscribe {uri}, and the server sends notifications/resources/updated {uri} when that resource changes. The client then re-reads it.

A typical case is a notes server whose resources are files on disk. A file watcher triggers the update notifications, and Claude Desktop re-pulls the file into context when it is edited outside the host.

Resource templates (2025-11-25 addition)

resourceTemplates expose a parameterized URI pattern such as notes://{id}, with id as a completion target, so the client can autocomplete ids in the resource picker.

Prompts

prompts/list returns {prompts: [{name, description, arguments?}]}. prompts/get takes {name, arguments} and returns {description, messages: [{role, content}]}.

A prompt is a template that fills out to a list of messages the host feeds its model. A code_review prompt, for example, might take a file_path argument and return three messages: a system message, a user message with the file body, and an assistant kickoff with a reasoning template.

Hosts and prompts

Claude Desktop, VS Code, and Cursor expose prompts as slash-commands in the chat UI. The user types /code_review and picks arguments from a form. The server's prompt is the contract between the user's shortcut and the full prompt sent to the model.

Not every client supports prompts yet, so check capability negotiation. If a server declares the prompt capability and the client has no prompt support, the user just won't see the slash commands.

The "list changed" notification

Resources and prompts both emit a list-changed notification (notifications/resources/list_changed and notifications/prompts/list_changed) when the set mutates. A notes server that just imported 20 new notes emits notifications/resources/list_changed, and the client calls resources/list again to pick up the additions.

Content type conventions

For text, use mimeType: "text/plain", text/markdown, or application/json. For binary, use image/png or application/pdf plus the blob field. For MCP Apps (Lesson 14), use text/html;profile=mcp-app in a ui:// URI.

Dynamic resources

A resource URI does not have to correspond to a static file. notes://recent can return the latest five notes on every read, and db://query/users/active can execute a parameterized query. The server is free to compute content dynamically.

The rule: if the client can cache by URI, keep the URI stable. If the computation is one-shot, put a timestamp or nonce in the URI so the client cache doesn't go stale.

Subscriptions vs polling

Clients that support subscriptions get server push via notifications/resources/updated. Older clients, or hosts without subscription support, poll by re-reading. Both are spec-compliant, and the server's capability declaration tells the client which one it can use.

Subscriptions cost per-session state on the server, since it has to track who is subscribed to what. Keep the subscribed set bounded and let disconnected clients time out.

Prompts vs system prompts

Prompts in MCP are not system prompts. The host's system prompt (its own operating instructions) and MCP prompts (server-supplied templates the user invokes) live side by side. A well-behaved client layers them and does not let a server prompt override its own system prompt.

Check: The Concept0/25
1. Which method does a client call to get a resource's actual content, as opposed to the list of what exists?Define
A panel listing three entries a client already has, each with a notes:// address, a name and a mime type. An arrow leads from the first entry to a dashed box asking which method fetches the content of notes://note-1.
The three catalog rows are real output of the lesson's main.py. The dashed box is the unknown.
2. How does a server tell a subscribed client that a resource changed?Define
A client that has subscribed to notes://note-1 on the left and a server on the right whose note-1 body was just edited. A dashed arrow from server to client carries a message whose kind is left as a question.
Schematic. The dashed arrow is the unknown.
3. What shape does a prompts/get response have?Define
A panel showing the request: method prompts/get with name review_note and arguments note_id note-1 and style concise. An arrow leads to a dashed box asking what shape the server's response has.
The request is the one in main.py's demo step 6. The dashed box is the unknown.
4. What do resourceTemplates let a server do?Define
On the left, the five notes held by the notes server in main.py with their titles. On the right, a code excerpt of a server declaration with a resourceTemplates list whose entry is a question mark. A dashed box asks what resourceTemplates let a server do.
NOTES titles are real. The question mark is the unknown.
5. What is an MCP resource?Define
A code panel showing one real entry from a notes server's list: uri notes://note-1, name MCP primitives, mimeType text/markdown and description 'tools, resources, prompts'. A dashed box asks what this kind of entry is.
Real entry from the lesson's main.py. The dashed box is the unknown.
6. What is a resource URI in MCP?Define
A code panel showing one real entry from a notes server's list with name Sampling loop, mimeType text/markdown and description 'server asks client for LLM', but with the value of its uri field replaced by a question mark. A dashed box asks what a resource URI is.
Real entry from main.py with the uri value hidden. The question mark is the unknown.
7. What does a client do by calling resources/subscribe with a uri?Define
A client sends resources/subscribe with uri notes://note-1 to a server. A panel shows the server's subscription set as empty before the call and a question mark after it. A dashed box asks what the client has done by making this call.
Before-state is the empty set from main.py. The question mark is the unknown.
8. What is an MCP prompt?Define
A code panel showing one real entry from a notes server: name review_note and description 'Produce a critique of a note with concrete improvements.' A dashed box asks what this kind of entry is.
Real entry from the lesson's main.py, arguments omitted. The dashed box is the unknown.
9. What are prompt arguments?Define
A code panel listing the two arguments of the review_note entry, note_id described as 'Id of the note to review' and style described as 'concise' or 'thorough'. An arrow leads to a dashed box asking what these arguments are and who supplies them.
Names and descriptions are real, from main.py. The dashed box is the unknown.
10. What is a content block in an MCP message?Define
A code panel showing the content field of a real prompt result: an object with type text and a text string beginning 'You are reviewing a note (concise mode). Title: MCP primitives.' A dashed box asks what this content object is.
Real value from main.py, with newlines shown escaped and lines wrapped. The dashed box is the unknown.
11. What does slash-command UX mean for MCP prompts?Define
A panel listing the two prompt names a notes server offers, review_note and summarize_tag, an arrow to a host chat window, and a dashed box asking how the host presents these prompts to the user.
Prompt names come from main.py; host names come from the lesson. The dashed box is the unknown.
12. A notes server exposes data reads, mutations and a reusable review workflow. Which mapping follows the decision rule in the Concept section?Explain
Three rows from a notes server that needs data reads, mutations and a reusable review workflow, each with an arrow to a dashed box holding a question mark for its primitive.
Row labels come from the stem. The question marks are the unknowns.
13. A dynamic resource's content is computed once per request and never reused. How should its URI be designed?Explain
Three requests, each leading to the server computing content and producing a result used once. A dashed box asks how the resource's URI should be designed for these requests.
Schematic of the stem's situation. The dashed box is the unknown.
14. How should a well-behaved client treat a server-supplied MCP prompt relative to its own system prompt?Explain
Two boxes feeding the model: the host's own system prompt (its operating instructions) and an MCP prompt from a server (a template the user invokes). A dashed box asks how the two relate.
Schematic. The dashed box is the unknown.
15. What must a server declare in its capabilities to support resource subscriptions?Explain
A client that wants to subscribe to notes://note-1 and a server that serves notes resources, with an arrow between them labelled setup. A dashed box asks what the server must declare at setup to support resource subscriptions.
Schematic. The dashed box is the unknown.
16. What is the server-side cost of supporting subscriptions?Explain
A notes server box connected to three client sessions, Session 1, Session 2 and Session 3. A dashed box asks what the server-side cost of supporting subscriptions is.
Schematic. The dashed box is the unknown.
17. Why does a prompt like code_review take arguments instead of being fixed text?Explain
A panel describing the lesson's code_review prompt: it takes a file_path argument and returns three messages, a system message, a user message with the file body and an assistant kickoff. A dashed box asks why it takes arguments instead of fixed text.
Description from the lesson's Prompts section. The dashed box is the unknown.
18. A server declares the prompts capability but the host has no prompt support. What happens?Explain
A server box that declares the prompts feature and offers review_note and summarize_tag, an arrow to a host box that has no prompt support, and a dashed box asking what happens.
Prompt names come from main.py. The dashed box is the unknown.
19. How does notifications/resources/list_changed differ from notifications/resources/updated?Explain
Two boxes, notifications/resources/list_changed and notifications/resources/updated, each with a dashed box below asking what it means.
Message names from the lesson. The dashed boxes are the unknowns.
20. A host does not support subscriptions. How does it keep resource content current?Explain
Two panels. On the server, notes://note-1 has body 'tools, resources, prompts' and later 'UPDATED body content'. On a host that does not support subscriptions, the copy in context still reads 'tools, resources, prompts'. A dashed box asks how the host keeps the content current.
Body strings are from main.py's demo. The dashed box is the unknown.
21. A server imports 20 notes and emits updated for notes://recent, but the resource picker still shows the old list. Which notification was missed?Analyze
A server box that imported 20 new notes and sent an updated message for notes://recent, an arrow to the client's resource picker, which still lists note-1 to note-5 and Recent notes. A dashed box asks which notification the client missed.
Schematic from the stem; picker rows typed. The dashed box is the unknown.
22. A server prompt's text tells the model to ignore the host's own rules, and the host complies. What went wrong?Analyze
A host's system prompt and a server's prompt text that says to ignore the host's own rules, both leading to a box saying the host complies. A dashed box asks what went wrong in how the host handled the server's prompt.
Schematic of the stem. The dashed box is the unknown.
23. Your team wants users to type /release_notes, choose a version, and get the same multi-step instructions each time. Which primitive fits?Design
A panel showing what users want: typing /release_notes, choosing a version, and getting the same multi-step instructions each time. An arrow leads to a dashed box asking which MCP primitive fits.
Schematic of the stem. The dashed box is the unknown.
24. A server holds 40 log files. Users want to find lines by keyword, and sometimes attach a whole file to a chat. Which design fits?Design
A server holding 40 log files with two user needs below it, finding lines by keyword and sometimes attaching a whole file to a chat, each pointing to a dashed box asking which primitive fits.
Schematic of the stem. The dashed boxes are the unknowns.
25. Your notes server will run many long-lived sessions, each subscribing to many URIs. Which approach fits the lesson's guidance?Design
A notes server box connected to sessions 1, 2, an ellipsis and N, each labelled as holding many URIs. A dashed box asks which approach fits the lesson's guidance for this server.
Schematic of the stem. The dashed box is the unknown.

Use It

code/main.py extends the notes server from Lesson 07 with:

  • Per-note resources (notes://note-1, etc.) with resources/subscribe support.
  • A review_note prompt that renders to a single user message, plus a summarize_tag prompt.
  • An update_note function that stands in for a file watcher and emits notifications/resources/updated for subscribed URIs.
  • A notes://recent dynamic resource that returns the last five notes on each read.

Run the demo to see the full flow. Everything is dispatched in-process, so there is no transport to set up.

Check: Use It0/10
1. What four things does main.py add to the Lesson 07 notes server?Define
A box for the Lesson 07 notes server with an arrow to a highlighted box saying code/main.py extends it with four things, below which four dashed boxes each ask what the addition is.
Schematic. The dashed boxes are the unknowns.
2. In the demo server, only notes://recent is subscribed and update_note('note-2', ...) runs. What notifications are emitted?Apply
A board listing the six resources of the demo server, five notes and notes://recent, with only notes://recent marked subscribed and note-2 marked as being edited. A panel below shows the call update_note on note-2 and asks which notifications were emitted.
Resource names are real, from main.py. The question mark is the unknown.
3. In main.py, what does resources/read return for uri notes://note-1, as mimeType and text?Apply
A panel with the stored note-1, title 'MCP primitives' and body 'tools, resources, prompts', an arrow to a panel showing the call to read notes://note-1 whose mimeType and text are question marks.
Stored values are real, from main.py. The question marks are the unknowns.
4. The NOTES dict in main.py holds five notes. What does resources/read return for notes://recent?Apply
A panel listing the five notes held in main.py, note-1 to note-5 with their titles, an arrow to a panel showing the call to read notes://recent and asking what the parsed JSON is.
Notes are real, from main.py. The question mark is the unknown.
5. Both notes://note-1 and notes://recent are subscribed, then update_note('note-1', 'x') runs. What is emitted?Apply
A board listing the six resources of the demo server, with notes://note-1 and notes://recent marked subscribed and note-1 marked as being edited. A panel below shows update_note on note-1 and asks what is emitted.
Resource names are real, from main.py. The question mark is the unknown.
6. A client calls prompts/get for review_note with only {'note_id': 'note-2'}. What description comes back?Apply
A panel with a prompts/get call for review_note with only the argument note_id set to note-2, an arrow to a dashed box showing the description 'Review note note-2' followed by a question mark in parentheses.
Call as in the stem. The question mark is the unknown.
7. What does dispatch('resources/read', {'uri': 'notes://note-9'}) do in main.py, given there is no note-9?Apply
A panel listing the five NOTES keys, note-1 to note-5, with a note that there is no note-9, an arrow to a panel showing the call to read notes://note-9 with the result as a question mark.
Keys are real, from main.py. The question mark is the unknown.
8. What does step 5 of the demo print for the summarize_tag prompt?Apply
A terminal panel showing the command python code/main.py and the heading '5) prompts/list' with two lines, /review_note and /summarize_tag, whose args lists are hidden behind question marks. A dashed box asks what the summarize_tag line ends with.
Line layout is real, from main.py. The question marks are the unknowns.
9. A client subscribed to notes://note1 (no hyphen). note-1 is then edited and no notification arrives. What explains it?Analyze
A client subscribed to notes://note1 without a hyphen and a server whose note-1, uri notes://note-1, was edited, with a box saying the client received no notifications. A dashed box asks what explains the missing notification.
The two uris are the ones in the stem. The dashed box is the unknown.
10. A user runs review_note with a mistyped note_id and gets a polished review of a note titled '?'. What is the design flaw?Analyze
A typed slash command /review_note with a mistyped note_id of not-1, an arrow to a box saying the user gets a polished review of a note titled '?', and a dashed box asking what the design flaw is in how the server handled it.
The typed id is illustrative. The dashed box is the unknown.

Ship It

This lesson produces outputs/skill-primitive-splitter.md. Given a proposed MCP server, the skill sorts each capability into tool, resource, or prompt and gives a rationale.

Check: Ship It0/1
1. What does the primitive-splitter skill do with a proposed MCP server?Define
An input box for a proposed MCP server and its list of capabilities, an arrow into a highlighted box named skill-primitive-splitter, and an arrow out to a dashed box asking what the skill outputs.
Schematic from the Ship It section. The dashed box is the unknown.
Author's Note

I'd run main.py and read the output next to the handlers. The demo walks all six messages in order, and each handler is a few lines of plain dict manipulation, so the protocol shape is easy to see without a transport in the way.

The part I'd look at twice is update_note. It emits notifications/resources/updated for notes://note-1 only if that URI is in the subscription set, and it emits a second one for notes://recent if that is subscribed too. Editing one note changes two resources, and the server has to know that. It is the same per-session bookkeeping cost the subscriptions section warns about, just small enough to see.

Two gaps are worth knowing about. review_note returns one user message, where a real review prompt would likely add system and assistant turns. And review_note on an unknown id renders a prompt about a note titled ? with the body (missing) rather than raising, which I'd tighten before shipping anything. The exercises cover adding resources/list_changed, which this server does not emit.

Exercises

  1. Run code/main.py. Observe the initial resource list, then trigger a note edit and verify the notifications/resources/updated event fires.

  2. Add a resources/list_changed emitter: when a new note is created, send the notification so clients re-discover.

  3. Design three prompts for a GitHub MCP server: summarize_pr, triage_issue, release_notes. Each with argument schemas. The prompt body should be runnable without further edits.

  4. Take an existing tool in the Lesson 07 server and classify whether it should remain a tool or be split into a resource plus tool pair. Justify in one sentence.

  5. Read the spec's server/resources and server/prompts sections. Identify the one field in resources/read that is rarely populated but spec-supported. Hint: look at _meta on resource content.

Key Terms

TermWhat people sayWhat it actually means
Resource"Exposed data"URI-addressable content the host can read
Resource URI"Pointer to data"Scheme-prefixed identifier (file://, notes://, etc.)
resources/subscribe"Watch for changes"Client-opt-in server-push updates for a specific URI
notifications/resources/updated"Resource changed"Signal to client that a subscribed resource has new content
Resource template"Parameterized URI"URI pattern with completion hints for the host picker
Prompt"Slash-command template"Named multi-message template with argument slots
Prompt arguments"Template inputs"Typed parameters the host collects before rendering
prompts/get"Render template"Server returns the filled-in message list
Content block"Typed chunk"{type: text | image | resource | ui_resource}
Slash-command UX"User shortcut"Host surfaces prompts as commands starting with /

Further Reading

Code

Ships With