Tools are actions the model takes. Resources are the data it reads. They are the two halves of what a server offers, and they are controlled differently: the model decides to call a tool, but the application decides which resources to put in front of the model. This post reads the four resource operations on the wire, list, read, templates, and subscriptions, against a server built to exercise each.
This is part of MCP on the Wire, a series that takes the Model Context Protocol apart message by message, in Go. It comes out of building and running MCP servers in production, including the open-source
txn2/mcp-data-platform, an Apache-2.0 platform in Go that connects AI assistants to Trino, DataHub, and S3 through one MCP endpoint, enriching every result with semantic context (ownership, lineage, PII, data quality) behind OAuth 2.1 auth, personas, and an audit trail. Everything here is read straight off the wire against spec revision2025-11-25, with the official Go SDK atv1.6.1.
§Context, Not Actions
The tools posts described tools as model-controlled: the model reads the catalog and decides, on its own, which tool to call. Resources are the opposite. The spec calls them application-driven, meaning the host decides how a resource reaches the model: a picker the user clicks, a search box, a rule that attaches a file automatically. A resource is reading material the application chooses to include, not a verb the model fires. That difference runs through every operation below, and it is the cleanest way to keep the two primitives straight.
tools/callresources/readtools/listresources/list + templates/listtools/list_changedlist_changed + per-resource updated§Listing and Reading
Discovery is resources/list, paginated like tools/list. The server above exposes one static resource, and listing it returns its metadata, captured from the wire:
{ "resources": [
{ "uri": "config://app", "name": "app-config",
"description": "the application configuration", "mimeType": "application/json" }
] }
A resource object carries a uri (its unique identifier), a name, an optional title and description, a mimeType, and an optional size in bytes. The uri is the handle; everything else is for the application to display and for the model to understand what the data is. To get the data, the client sends resources/read with that uri:
{ "contents": [
{ "uri": "config://app", "mimeType": "application/json", "text": "{\"theme\":\"dark\",\"retries\":3}" }
] }
The result is a contents array, because one URI can resolve to more than one piece of content. Each entry is either textual or binary. Text data lives in a text field, as above; binary data lives in a base64 blob field instead. The mimeType tells the client which it is dealing with and how to render it.
The uri scheme is meaningful. The spec defines https:// for resources the client can fetch directly off the web, file:// for anything filesystem-like (which need not be a real file), and git:// for version control, and a server is free to define a custom scheme like the config:// above, as long as it follows RFC 3986. The scheme is a hint about where the data lives and who fetches it.
§Templates: A Family From One URI
A server rarely wants to list every resource it could ever serve. A log server does not enumerate every date. Instead it publishes a template: one RFC 6570 URI template standing in for a whole family. resources/templates/list returns them:
{ "resourceTemplates": [
{ "uriTemplate": "file:///logs/{date}", "name": "daily-log",
"description": "a day's log file", "mimeType": "text/plain" }
] }
The {date} is a slot the client fills. It reads a specific member of the family by expanding the template into a concrete URI and calling resources/read:
// resources/read { "uri": "file:///logs/2026-05-17" }
{ "contents": [
{ "uri": "file:///logs/2026-05-17", "mimeType": "text/plain", "text": "log entries for 2026-05-17" }
] }
One template, unbounded resources, none of them enumerated. The template’s variables can be auto-completed through the completion API, which is its own post later in the series. In Go, a static resource is AddResource and a template is AddResourceTemplate; the handler reads req.Params.URI and, for a template, parses the filled-in values out of it.
§When a Resource Is Not There
Reading a URI the server does not recognize is a protocol error, not a tool-style execution error, because there is no result to return. The code is -32002:
{ "id": 6, "error": { "code": -32002, "message": "Resource not found",
"data": { "uri": "config://nope" } } }
The data object names the offending URI. This is the counterpart to the tool distinction from the last post: a tool that runs and fails returns isError content the model can act on, but a resource that does not exist has nothing to read, so it is a flat protocol error.
§Subscriptions: Tell Me When It Changes
Resources change. A config file is edited, a log grows. A client that has attached a resource as context wants to know when its copy is stale, and the protocol has two separate notifications for two separate events, gated by two independent capability flags the server declares at initialization:
"resources": { "listChanged": true, "subscribe": true }
listChanged covers the set of resources changing, a new one appearing or one going away, and fires notifications/resources/list_changed. subscribe covers a single resource’s contents changing. A client subscribes to one URI, and the server pushes notifications/resources/updated for it whenever it changes. Subscribing returns an empty acknowledgement, and then the updates arrive on their own, captured here from a server pushing three of them:
-> resources/subscribe { "uri": "config://app" }
<- { "id": 7, "result": {} }
<- notifications/resources/updated { "uri": "config://app" }
<- notifications/resources/updated { "uri": "config://app" }
<- notifications/resources/updated { "uri": "config://app" }
The notification carries only the uri, not the new contents. It is a nudge, not a delivery: the client decides whether to call resources/read again to fetch the fresh data. That keeps the server from pushing large payloads to a client that may not care anymore. In Go, setting both a SubscribeHandler and an UnsubscribeHandler in the server options turns the subscribe capability on; the SDK requires the pair and panics if you set only one. server.ResourceUpdated(ctx, params) is the call that pushes the notification to every client subscribed to that URI.
txn2/mcp-data-platform exposes resources two ways. RFC 6570 templates, schema://{catalog}.{schema_name}/{table}, glossary://{term}, and availability://{catalog}.{schema_name}/{table}, stand in for table schemas, glossary terms, and data availability, so an agent reads any table’s shape by URI, and managed resources hold human-curated material scoped global, per-persona, or per-user. Both are context the application attaches, not tools the model fires.
§What’s Next
Tools are actions, resources are context, and the third server primitive is neither. The next post, Prompts: Server-Authored Conversation Starters, covers prompts/list and prompts/get: reusable, parameterized message templates the server defines and the user invokes, the one primitive of the three that is driven by an explicit human choice rather than by the model or the application’s heuristics.
The production data platform behind this series is txn2/mcp-data-platform, available hosted as Plexara.