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.

Advertisement

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.

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.