Mid · 6 chapters
Resources
Exposing readable context addressed by URI, resource templates, subscriptions and updates
Treating every read as a tool adds model latency and decision risk, while treating dynamic data as a static resource creates stale context and missed updates.
Read these first
What you will know
Expose a resource by URI and read it from a client
A client must resolve the URI, retrieve the expected MIME-typed contents, and handle missing or stale documents without treating the read as a tool invocation.
Compare a resource with a tool using a concrete data-access scenario
The distinction determines whether the application or the model controls invocation, which affects consent, authorization, latency, and auditability.
Use a resource template for a parameterised family of documents
URI-template expansion must produce stable, correctly encoded identifiers; sloppy path or query handling can expose the wrong tenant’s document.
Decide whether something should be pulled as a resource or fetched by a tool
The choice assigns ownership of timing, authorization, and freshness; routing every read through a model-visible tool adds latency and an unnecessary decision point.
Handle a subscription and an update
Long-lived sessions need correct change notifications and client refresh behavior, or they silently continue using stale context.
Chapters
- 1
Application-controlled resources and model-controlled tools
MCP sorts what a server offers by who is permitted to fire it, and read-only is a correlate of that split rather than the rule behind it. A sprint board a user picks and attaches enters the context deterministically, at a moment the host chose; a ticket search whose query can only be composed mid-reasoning has to be a tool. On a replay of 240 stored support turns a read-only tool fired on 71% of the turns where its data was relevant, against 100% for the same data attached by the host, and the objection that resources are useless because the model cannot call them turns out to be half right.
- 2
Advertising resources and resource templates
Concrete resources come back from resources/list, each entry carrying a uri, a name, a description and a mimeType, and those four fields are what a host renders into something a user can browse and attach. Parameterised families are advertised separately, through resources/templates/list, with a uriTemplate in place of a uri — so a server that publishes docs://team/{team}/policy/{name} on the concrete listing alone is invisible to every client that connects to it. Enumeration has a ceiling too: at 45 ms a page, 340 documents cost 180 ms of menu latency and 50,000 cost 22 seconds.
- 3
The resources/read response
A read carries one URI and comes back as a contents array, each entry echoing its uri, declaring a mimeType, and putting the payload in exactly one of two mutually exclusive fields — text for a document, base64 blob for a PNG, which inflates 1,834,208 bytes on disk to 2,445,612 on the wire. Missing and stale documents surface as JSON-RPC errors to the host rather than as failed tool calls the model has to interpret, and -32002, -32602 and -32603 want three different host behaviours.
- 4
Template expansion and read authorization
Expanding docs://team/{team}/policy/{name} with a value containing a slash or an ampersand lets it escape its own segment unless each variable is percent-encoded before substitution, which is what RFC 6570 specifies and what a hand-rolled replace() skips. That is a real bug and it is not the one where a customer sees another tenant's report, since there the trace shows a well-formed read the server answered happily. Expansion was never the authorization step, so the check — session principal against the identifier, about 2 ms against a 180 ms render — belongs in the read handler, on every read.
- 5
Resource subscriptions and staleness
A resource read has no half-life: a document fetched at the start of a long session stays exactly as true as it was at that moment. resources/subscribe plus a re-read on notifications/resources/updated is necessary and not sufficient, because the new value still has to get in front of the model and earlier turns that quoted the old one keep steering the transcript unless something supersedes them. Where a server declares no subscribe, poll on a stated interval — 60 s costs 60 reads per session-hour against 450 for every turn — and make the staleness window visible.
- 6
A fetch tool beside resources
When hosts never surface resources, a get_document(name) tool looks free and is not: two access paths mean two authorization sites, two freshness stories, a tool listing that grows 68% in every request, and a model that fetches a document already attached on 34% of turns. Sometimes it is still the right call, and the version that survives is one resolver behind two front doors, gated on clientInfo and defended with read reach and duplicate-delivery thresholds written down before the change ships.
External resources
- Resources — Model Context Protocol specification (2025-06-18)modelcontextprotocol.io
resources/list, resources/read, templates and subscribe semantics, normative.
- MCP Resources: The Overlooked Primitive (and Why That's a Problem)layered.dev
Argues when to expose data as a resource instead of another tool.
- Resources - MCP Python SDKpy.sdk.modelcontextprotocol.io
MCP Python SDK resources: exposure, URI templates, listings, MIME types, and runnable client examples.
- MCP Core Concepts - Resources, Tools, Prompts & Transports!BugBytes
Places resources against the other primitives with live examples.
- MCP Servers: Resources, Prompts & Sampling CapabilitiesFiberplane
Quick framing of server capabilities beyond tools.