In the Model Context Protocol, a resource is a piece of context a server makes available to the host application, addressed by a URI and read on demand. That sounds like a tool that returns data, and the confusion is the most common mistake in MCP server design. The difference is not what the call does — it is who decides to make it. Tools are model-invoked. Resources are application-controlled: the host, or the human driving it, decides which resources enter the context window and when. Everything else about the primitive — URI addressing, the split between listing and reading, the content shape, the metadata — falls out of that one commitment. This piece walks the model end to end, including the problem nobody warns you about: a resource can be far larger than the window it is destined for, and the protocol does not say whose job it is to shrink it.

Resources are application-controlled context

MCP servers expose three context primitives, separated by control rather than capability. Tools are model-controlled. Prompts are user-controlled — a human picks a template from a menu. Resources are application-controlled: the host decides what to fetch and inject.

‘Application-controlled’ does not name one algorithm. It means the host chooses a policy, and hosts choose differently. An IDE-style client shows resources in a picker and attaches only what the user selects. A chat client might auto-attach whatever is relevant to the current file. A bolder host might expose the list to the model and let it request reads — legal, but that is the host’s choice to delegate, not the server’s.

The consequence for server authors: never assume a resource will be read. You are publishing an addressable catalogue, not issuing instructions. If something must happen for your server to work, it is not a resource.

Advertisement

Resource or tool? The test that actually decides

The wrong test is ‘does it read or write?’ Plenty of read-only operations belong as tools. The real test is agency.

Make it a resource when the thing is an addressable, re-readable piece of state a human could point at and say ‘use this’: a file, a database schema, a config blob, the current contents of a ticket. Reading it is idempotent, free of side effects, and meaningful without arguments beyond an address.

Make it a tool when the operation is a verb the model should decide to perform: run a query it composed, search with a phrase it chose, create a branch, send a message. Tools take structured arguments, may have side effects, and carry descriptions written to persuade a model to invoke them.

The blurry middle is parameterized reads — ‘fetch the log for run N.’ URI templates cover much of that ground. But if the parameter space is open-ended, such as a free-text search, that is a tool wearing a resource’s coat.

URI addressing: schemes as namespaces

Every resource is identified by a URI, and MCP is deliberately relaxed about what that URI looks like. Servers use familiar schemes — file:///path/to/README.md, https:// — and are free to define custom ones such as db://sales/schema/orders or ticket://PROJ-1421. The scheme is a namespace telling the reader, and the host UI, which world the identifier belongs to.

Two properties matter more than syntax. First, the URI is opaque to the client: the host is not meant to parse it and infer structure, only pass it back on a read. Servers that encode meaning in path segments should treat that as internal convention, not contract. Second, the URI should be stable. Hosts cache resource lists and conversation histories reference URIs long after they were first read, so a server that reshuffles identifiers between restarts produces dangling references.

URI templates: describing a space, not a list

Enumeration breaks down fast: a filesystem server cannot list every path in a large repository, and a database server cannot enumerate every row. URI templates let a server describe a parameterized space of resources instead of individual members — file:///{path}, or db://{database}/schema/{table}. The syntax follows RFC 6570, where {var} marks an expansion point.

Templates are advertised through their own listing method rather than being mixed into the concrete resource list, which keeps the two ideas cleanly separated: concrete resources exist and can be shown in a picker; templates are a grammar the host or user instantiates. A well-designed server offers both — a short concrete list of the few resources worth surfacing by default (README, schema overview, changelog) plus templates for the long tail. Give every template a human-readable name and description; a bare pattern is unusable in a UI.

resources/list returns descriptors, not content

resources/list answers ‘what is available?’ and returns descriptors: URI, name, and optional metadata. It does not return content. That split is the load-bearing decision of the whole primitive. Listing is cheap, so it can be called eagerly on connect and rendered in a picker without pulling a byte of payload.

Because a catalogue can be arbitrarily large, listing is paginated — a page plus an opaque cursor the client passes back for the next one. The cursor mechanics belong to the dedicated pagination article; the point here is that a client must loop rather than assume one call gives it everything.

Server authors should treat listing as a curation problem. Ten well-named resources beat ten thousand raw paths, because a list nobody can navigate is the same as no list at all.

resources/read and the contents array

resources/read takes a URI and returns the payload. The detail that surprises people is that the response carries a contents array, not a single body — one read can legitimately yield several items.

That plural shape exists for good reasons. A URI may address a directory, and returning its members in one round trip beats forcing a client to walk it. A single document may have several representations — rendered text and raw source. Each entry carries its own URI and content, so the host can attribute every fragment correctly.

The implication for clients: do not write code that reads contents[0] and stops. That works against simple file servers and quietly drops data against anything richer. Servers owe the reverse discipline — return the members that genuinely belong to the URI, not a convenience dump of everything nearby.

Advertisement

Text, binary, and what MIME types are really for

Each content item is either text or binary. Text carries the string directly. Binary is base64-encoded into a blob field — the only sane option inside a JSON-RPC message, but a costly one: base64 inflates the payload by roughly a third, spent on a transport designed for control messages, not bulk data.

Both forms carry a MIME type, and its job is routing, not decoration. It is how a host decides whether a payload can be dropped into the prompt as text, handed to a vision-capable model as an image, offered as a download, or refused. A server that labels everything text/plain or application/octet-stream strips the host of what it needs to make that call, and the result is a garbled context window or a resource the client silently ignores.

Be specific: text/markdown, application/json, image/png. Specificity is what makes content usable.

Resource metadata and what the host does with it

Beyond the URI, a descriptor carries metadata whose purpose is to let the host decide well before reading anything.

FieldWhat the host does with it
NameThe label in a picker. Human-friendly, not a path fragment.
DescriptionWhat it contains, and when it is worth attaching.
MIME typeDecides how, and whether, the payload can be rendered or injected.
SizeAn estimate in bytes, so the host can budget context before fetching.

Size is the quietly critical one. Without it, a host must fetch a resource to discover it is a 40 MB log file — paying the transfer and the base64 overhead to learn it cannot use the result. With it, the host can grey out the entry, warn the user, or fetch a narrower alternative. Current revisions of the protocol have steadily enriched this descriptor surface, so treat metadata as part of the interface, not optional polish.

The size problem: resources bigger than the window

Here is the tension the protocol does not resolve for you. A resource addresses an arbitrary piece of state, and arbitrary state is routinely orders of magnitude larger than the context window it is headed for. Nothing in resources/read constrains a response to fit: a one-line request can ask for a 200 MB table dump, and a naive server will try to serialize it.

Responsibility splits across three layers. The server should never make an unbounded read possible: cap what a single read returns, and where content is naturally huge, expose narrower addresses instead — a template with a line range, a summary alongside the raw resource, a per-table schema rather than the whole database. The transport should enforce hard message limits so a pathological read fails fast instead of stalling a connection. The host owns the last mile: it knows the model, the window, and what else competes for space, so truncation and summarization are its call.

The failure mode to design against is the middle being skipped — a server that returns everything and a host that truncates blindly, cutting a config file in half and letting the model reason from the fragment.

Static and dynamic resources, and where subscription fits

Resources divide by how they come into being. Static ones mirror something that already exists — a file on disk, a stored document. The server is a thin adapter, and reads are cheap and repeatable. Dynamic ones are computed at read time: the health of a service, a schema assembled by introspection. Reads cost real work, and two reads a second apart may legitimately disagree. They need explicit thought about cost, because a host that re-reads every turn turns a ‘free’ read into a per-message database query. Say so in the description.

Change notification is the other half of freshness. A server advertises its resources capability with optional sub-flags indicating whether it supports subscribe (per-URI interest, with the server pushing updates when that resource changes) and whether it emits a list-changed notification when the catalogue itself shifts. The mechanics of subscribing and delivering updates are covered separately; what matters here is that a host reads those flags to know whether it must poll or can wait to be told.

A resource is an address, not an action. Tools are model-invoked; resources are application-controlled, so the host — and usually the human behind it — decides what enters the context window. Build the catalogue accordingly: stable, opaque URIs; a short curated list of concrete resources plus URI templates for the long tail; a paginated resources/list that returns descriptors only; and a resources/read whose contents array clients must not truncate to its first element. Label content with specific MIME types and publish a size — those two fields let a host budget before it fetches. Above all, share the size problem: cap reads and expose narrower addresses on the server, enforce message limits at the transport, and leave the final context-window trade-offs to the host.