Chapter 258
MCP Resources and Prompts: Addressable Context for Stateless Servers
MCP Resources and Prompts: Addressable Context for Stateless Servers
Tools perform operations. Resources expose addressable content. Prompts package user-selected message templates. A good MCP server keeps those contracts separate and predictable.
Type: Build Languages: Python Prerequisites: Phase 13, Lesson 07 (Building an MCP Server), Phase 13, Lesson 09 (MCP Transports) Time: ~60 minutes
Learning Objectives
- Choose among tools, resources, and prompts from the consumer's intent.
- Advertise the resource and prompt surface through mandatory
server/discover. - Build deterministic
resources/listandprompts/listresults. - Apply
ttlMsandcacheScopewithout leaking user-specific data. - Return JSON-RPC error
-32602for an invalid or unknown resource URI. - Open a
subscriptions/listenPOST-response stream and correlate every event by subscription ID. - Treat resource content and prompt templates as untrusted server output.
Start With the Consumer
The easiest way to misuse MCP is to begin with implementation code. A database query becomes a tool because functions are familiar. A reusable workflow becomes a resource because it is stored in a file. A prompt becomes hidden policy because the host can inject it.
Begin with who chooses and what they expect.
| Primitive | Primary intent | Selection owner | Typical result |
|---|---|---|---|
| Tool | Perform an operation | Model or application | Structured action result |
| Resource | Read content at a URI | Host, application, or user | Text or binary content |
| Prompt | Start a reusable message workflow | User through host UI | One or more prompt messages |
A note at notes://note-1 is a resource because it is addressable content. delete_note is a tool because it changes state. review_note is a prompt because a user chooses a prepared review workflow.
Do not expose one operation as all three merely to look complete. Each extra surface needs discovery, authorization, caching, error handling, tests, and documentation.
The 2026-07-28 Stateless Envelope
This lesson targets MCP protocol revision 2026-07-28. There is no initialization handshake or protocol session in this profile. Every request carries its protocol version and client capabilities in reserved _meta keys.
{
"jsonrpc": "2.0",
"id": 1,
"method": "resources/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "course-client",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}A server must implement server/discover. Its result advertises supported
versions, resource and prompt capabilities, implementation identity, and
cache hints. A client may call another method directly, but discovery gives it
one stable snapshot before it builds a UI.
{
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": {
"resources": {"listChanged": true, "subscribe": true},
"prompts": {"listChanged": true}
},
"ttlMs": 3600000,
"cacheScope": "public"
}A normal result declares "resultType": "complete". The response _meta identifies the serving implementation with io.modelcontextprotocol/serverInfo. This information is useful for diagnostics. It is not an authentication identity. A request carrying an unsupported revision returns -32022 with both the requested revision and the server's supported revisions.
The stateless contract changes your design instincts. A list cannot depend on a prior call on one connection. Authorization may change the visible set because credentials are request input, but connection history must not.
Resources Are Stable URI Contracts
A resource is content identified by a URI. Design the URI before the handler.
Good URI properties:
- Stable enough to bookmark or pass between requests.
- Namespaced to the server's domain.
- Independent from a process ID or connection.
- Validated before storage access.
- Authorized on every read.
notes://note-1 is better than note-1 because its namespace is explicit. A file server may use file:// URIs, but it must still check configured directory boundaries after resolving symlinks and relative segments.
resources/list returns the resources currently visible to the caller. Sort by a stable key such as URI. Deterministic order prevents noisy cache misses, changing snapshots, and host UIs that jump between refreshes.
{
"resultType": "complete",
"resources": [
{
"uri": "notes://note-1",
"name": "Architecture decision",
"description": "Why the service uses a stateless boundary",
"mimeType": "text/markdown"
}
],
"ttlMs": 300000,
"cacheScope": "public",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "notes-server",
"version": "2.0.0"
}
}
}resources/read returns one or more content items. An unknown URI is not a successful empty read. The current Resources specification assigns invalid or unknown resource URIs to JSON-RPC invalid parameters, code -32602.
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32602,
"message": "Unknown or invalid resource URI",
"data": {
"uri": "notes://missing"
}
}
}That distinction lets a client separate absence from a valid empty document. It also prevents accidental fallback to a broader lookup.
Resource templates
A resource template describes a family of parameterized URIs. Use one when listing every concrete item would be expensive or unbounded. For example, notes://projects/{project}/decisions/{decision} tells a client how to form a valid address without returning every decision.
A template does not weaken validation. Parse variables, apply authorization, enforce length and character limits, and construct storage queries with typed parameters. Never concatenate an arbitrary URI tail into a filesystem path or database statement.
Content is not trusted instruction
Resource text may contain prompt injection, secrets, misleading commands, or malformed markup. The host should preserve provenance and treat resource content as data. The server should limit content size, return an accurate MIME type, redact fields the caller cannot access, and avoid returning unrelated records.
Prompts Are User-Controlled Templates
MCP prompts are designed for explicit user selection. A host may render them as slash commands, menu items, or workflow buttons. The protocol does not require one UI.
prompts/list should be deterministic for the same request authorization. Each prompt needs a stable name, a useful description, and argument declarations that let the host collect input before prompts/get.
{
"resultType": "complete",
"prompts": [
{
"name": "review_note",
"title": "Review a note",
"description": "Review one note for a named concern",
"arguments": [
{
"name": "uri",
"description": "The note resource URI",
"required": true
}
]
}
],
"ttlMs": 600000,
"cacheScope": "public"
}prompts/get resolves arguments into messages. It does not replace the host's system instructions. The host decides how returned messages enter model context and keeps its own trusted policy at higher priority.
Validate prompt arguments at the server boundary. A prompt URI should pass the same authorization check as a direct resource read. Do not make a prompt a side channel around resource access.
Cache Hints Are Part of Correctness
ttlMs tells a client how long a result may be reused. cacheScope describes who may share that cached value.
| Scope | Meaning | Typical use |
|---|---|---|
public | May be reused across users when authorization permits | Public prompt catalog |
private | Bound to the requesting user or credential context | User-owned note content |
Choose a TTL from the data's change rate and the damage of staleness. Five minutes may suit a public prompt catalog. A private note read may use one minute.
MCP defines only public and private as cacheScope values. For a secret-bearing or rapidly changing result, return cacheScope: "private" with ttlMs: 0, then apply any stricter no-store rule in the host cache policy. no-store itself is not an MCP cacheScope value.
Cache hints never replace authorization. A cache key must include every request dimension that changes visibility, including tenant, user, scope, locale, and pagination cursor. If a shared cache cannot express those dimensions safely, use private with a zero TTL and a host-level no-store policy.
Subscriptions Use a Client-Opened Response Stream
The modern subscription pattern replaces the former resources/subscribe RPC and the old HTTP GET event endpoint.
The client sends subscriptions/listen as a normal JSON-RPC request. Over Streamable HTTP this is a POST whose response remains open as an SSE stream. The notifications object is an allowlist. A server must not deliver notification types that were not requested.
{
"jsonrpc": "2.0",
"id": 17,
"method": "subscriptions/listen",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {
"name": "course-client",
"version": "1.0.0"
}
},
"notifications": {
"resourcesListChanged": true,
"promptsListChanged": true,
"resourceSubscriptions": [
"notes://note-1"
]
}
}
}The request ID is the subscription ID. Before any requested event, the server sends notifications/subscriptions/acknowledged. Its filter contains only the subset the server accepted.
{
"jsonrpc": "2.0",
"method": "notifications/subscriptions/acknowledged",
"params": {
"_meta": {
"io.modelcontextprotocol/subscriptionId": 17
},
"notifications": {
"resourcesListChanged": true,
"resourceSubscriptions": [
"notes://note-1"
]
}
}
}Every later event on that stream carries the same metadata.
{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": {
"_meta": {
"io.modelcontextprotocol/subscriptionId": 17
},
"uri": "notes://note-1"
}
}The notification says the resource changed. The client reads it again through resources/read, subject to current authorization. It does not assume the event contains the new document.
Several subscriptions can share one stdio channel. The subscription ID lets the client demultiplex them. Over HTTP, closing the response stream cancels the subscription. A server that ends the stream gracefully returns a final resultType: "complete" response correlated to the original request.
Do not use a subscription stream as a protocol session. A later read is still a complete request that can reach any healthy server instance.
t3-primitive-sortInteractive Lab
Use the figure to classify five capabilities from a project tracker: issue details, create issue, sprint review template, project policy, and close issue. Then decide which lists can be cached publicly, which reads must remain private, and which resources deserve update notifications.
For every classification, name the chooser. If the model performs an action, use a tool. If a host reads URI-addressed content, use a resource. If the user starts a prepared message workflow, use a prompt.
Practice Lab
Run the simulator from the repository root:
cd phases/13-tools-and-protocols/10-mcp-resources-and-prompts/code
python3 main.py
python3 -m unittest discover tests -vInspect the transcript in this order:
- Confirm
server/discoveradvertises the current revision and both capabilities. - Confirm both list results are sorted and use
resultType: "complete". - Confirm the list and read results carry intentional cache hints.
- Change the read URI to
notes://missingand observe-32602. - Confirm the subscription acknowledgment precedes the resource event.
- Confirm the event and graceful close both carry subscription ID
5.
The Python model does not open a real HTTP connection. It represents the messages an SDK must place on the request-scoped response stream. Use an official SDK for framing and transport in production.
Shipped Artifact
outputs/skill-primitive-splitter.md is a reusable design review for MCP primitive selection. It now checks deterministic discovery, cache scope, invalid URI behavior, and modern subscription filters.
The lesson also ships assets/primitive-split.svg, a static version of the primitive and subscription boundary for offline study.
Verify It
cd phases/13-tools-and-protocols/10-mcp-resources-and-prompts/code
python3 main.py
python3 -m unittest discover tests -vExpected result: the main program prints a JSON transcript and the test command reports at least twelve passing tests.
Capstone Connection
Use this contract when your capstone server exposes addressable knowledge beside actions. Include one deterministic catalog snapshot, one authorized resource read, one prompt resolution, one invalid URI case, and one subscription transcript.
Your evidence should show that no list depends on connection history and that a subscription event never grants access to the underlying resource.
Exercises
- Add a
notes://projects/{project}/notes/{id}resource template and validate both variables. - Add pagination to
resources/listwhile preserving deterministic order. - Change one resource to
cacheScope: "private"withttlMs: 0, add a host-level no-store policy, and explain the threat that justifies both controls. - Add a prompt-list change subscription and prove no event is sent when the filter omits
promptsListChanged. - Create two simultaneous subscriptions and prove each event carries the correct request ID.
- Add an authorization subject to the read handler and prove a cache entry cannot cross subjects.
Key Terms
- Resource: URI-addressed content exposed by an MCP server.
- Prompt: A user-controlled message template exposed by an MCP server.
- Deterministic list: A discovery result with stable membership and ordering for the same request inputs.
ttlMs: Cache freshness duration in milliseconds.cacheScope: The sharing boundary for a cached result.subscriptions/listen: A long-lived request whose response stream delivers explicitly filtered notifications.- Subscription ID: The original listen request ID, repeated in notification metadata.
- Invalid parameters: JSON-RPC error
-32602, used for an invalid or unknown resource URI. - Unsupported protocol version: JSON-RPC error
-32022, includingsupportedandrequestedrevisions. server/discover: Mandatory server method that returns supported revisions, capabilities, identity, and optional cache hints.
