MCP defines three primitives — tools, resources, and prompts — but nearly every server in the wild only implements the first. Here's what resources actually do, why client support lagged, and the specific cases where skipping them leaves your agent acting on stale context.
Open the source of almost any Model Context Protocol server you can find today — the reference servers, the ones listed in Anthropic's and the community's directories, the ones bundled into Claude Desktop, Cursor, or VS Code extensions — and you'll find the same pattern. A tools/list handler. A tools/call handler. Maybe a system prompt injected through prompts/list if the author was feeling thorough. And almost never a working resources/list or resources/read.
That's strange, because resources were in the spec from day one, sitting right next to tools as one of MCP's three core primitives. They were designed to solve a real problem: giving a model access to context — files, database rows, logs, API responses — without forcing every piece of context through the same request/response shape as an action. Three years into the agentic-tooling boom, resources are still the primitive everyone skips. Understanding why — and the handful of cases where skipping them actually costs you — is a better use of an afternoon than reading another "MCP explained" post that only covers tools.
MCP defines exactly three things a server can expose to a client:
Tools won because tools map directly onto the mental model everyone already had from function calling: give the model a schema, let it decide to call something, get a result back, feed it into the next turn. Resources break that symmetry on purpose. A resource isn't something the model asks for mid-conversation the way it asks for a tool call — it's something the host attaches to context, typically before the model ever sees the turn, the same way a human would drag a file into a chat window or @-mention a document.
That distinction sounds small. It isn't. It's the reason resources ended up underused, and it's also the reason they're the right primitive for a specific, recurring class of problem that tools handle badly.
A resource is identified by a URI — anything from file:///home/user/report.pdf to a custom scheme like postgres://orders/12345 or github://repo/owner/name/issues/42. The server advertises what it has through resources/list, which returns a flat list of concrete resources, and optionally resources/templates/list, which returns URI templates (RFC 6570) for resources that are parameterized rather than enumerable — postgres://orders/{id} rather than every order row individually.
// resources/list response (abbreviated)
{
"resources": [
{
"uri": "file:///project/src/auth.ts",
"name": "auth.ts",
"mimeType": "text/x-typescript"
},
{
"uri": "logs://service/api-gateway/today",
"name": "API gateway logs (today)",
"mimeType": "text/plain"
}
]
}
When the host (or, in some implementations, the model via a thin tool wrapper) wants the content, it calls resources/read with the URI and gets back one or more content blocks — text or base64-encoded binary, tagged with a MIME type. Nothing about this requires the model to have "chosen" to read anything; the host can resolve a resource and splice its content into context before the model generates a single token.
The spec also defines two pieces of liveness that tools have no equivalent for:
resources/subscribe — a client tells the server it wants to be notified when a specific resource changes.notifications/resources/updated — the server pushes a notification when that resource's content changes, without being polled.That's the part most implementers never build, and it's the part that makes resources worth the extra interface.
Here's the shape of a resource subscription, end to end:
Try to express that with tools alone and you end up with one of two bad patterns: either the model has to proactively re-call a read_file tool on every turn just in case something changed (burning tokens and latency on a cache-miss-by-default strategy), or the host polls the server out-of-band and hopes it notices drift before the model acts on stale content. Neither is what you want for, say, a config file an agent is actively editing while a human edits it too, or a ticket whose status changes while an agent is mid-investigation.
Ask anyone who's shipped an MCP server why they didn't bother with resources and you'll get three honest answers, in roughly this order:
read_file(path) as a tool and get 90% of what resources/read gives you, minus the host-mediated attachment flow and live-update notifications. Most servers ship the 90% solution because the remaining 10% doesn't show up in a demo.None of these are indictments of the spec. They're a reasonably accurate description of what happens when a protocol ships three primitives but the ecosystem only builds muscle memory for one.
The cases where this matters aren't hypothetical:
src/auth.ts every time to check whether it changed. A subscribed resource with update notifications handles that for free; a tool-calling agent has to either burn a call to check or risk acting on a stale read from six turns ago.Where resources genuinely don't matter: anything the model needs to actively decide to fetch based on reasoning mid-turn — a search result, a computed value, an API call with parameters the model derives on the fly. That's what tools are for, and no amount of resource-primitive purity should talk you out of a tool call there.
Using the TypeScript SDK, a server exposing a handful of log files as resources — static entries plus a templated one for arbitrary dates — looks like this:
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ name: "log-reader", version: "1.0.0" });
server.resource(
"today-log",
"logs://service/api-gateway/today",
async (uri) => ({
contents: [{
uri: uri.href,
mimeType: "text/plain",
text: await readTodayLog(),
}],
})
);
server.resource(
"log-by-date",
new ResourceTemplate("logs://service/{service}/{date}", { list: undefined }),
async (uri, { service, date }) => ({
contents: [{
uri: uri.href,
mimeType: "text/plain",
text: await readLogFor(service, date),
}],
})
);
Subscriptions are a separate capability declaration and a notification call (server.sendResourceUpdated(uri)) fired whenever the backing data changes — a file watcher event, a webhook, a cron tick. Most SDKs make the plumbing straightforward; the work is almost entirely in deciding what counts as a resource in your domain, not in the wire format.
When you're deciding whether something in your server belongs behind tools/call or resources/read, the question that actually discriminates is: does the model need to decide, with arguments it computes, when and what to fetch — or does a human or the host just need to make a fixed piece of context available? If it's the former, it's a tool. If it's the latter — a file, a log stream, a document someone is editing, a dataset that occasionally updates — it's a resource, and if it updates on its own timeline, it should be a subscribed resource.
A server exposing 262 developer-utility tools, as Utilix's own MCP server does, is a legitimate all-tools design — every one of those is an action the model decides to invoke with computed arguments (validate this JSON, parse this connection string, compute this checksum), not a fixed piece of background context. That's the right call for that shape of server. It's also exactly the kind of server that will never need a resource, which is part of why tools-only implementations feel so natural by default — most servers really are mostly actions. The ones quietly getting this wrong are the ones wrapping a filesystem, a ticket tracker, or a document store in tool calls and wondering why their agent keeps acting on context that's three turns stale.
Resources aren't a legacy primitive waiting to be deprecated — they're the one piece of MCP's design built specifically for host-mediated, independently-updating context, and almost nothing else in the protocol covers that case as cleanly. If your server's job is giving a model something to act on, tools are correct and resources would just add ceremony. If your server's job is giving a model (or the human steering it) something to look at that changes without being asked, you're almost certainly under-serving your users by shoving it through a tool call instead — and client support for resources, however patchy it was a year ago, has gotten good enough that there's no longer a strong excuse not to check.