Also at Deasil Works · txn2 · Plexara
Profiles GitHub · X · LinkedIn
Theme Light · Auto · Dark
Professional notes by Craig Johnston
long-form, short-form, working drafts · since 2008
VOL. XIX · MMXXVI
106 NOTES IN PRINT
FOLIO XCII 2026-05-17 · 6 MIN · SHORT-FORM

Resources: list, read, templates, and subscriptions

The primitive for data the model reads, the counterpart to tools, with URI templates for whole families and live notifications when something changes

Diagram · folio xcii
sequenceDiagram
  autonumber
  participant C as Client
  participant S as Server
  C->>S: resources/list  (and resources/templates/list)
  S->>C: resources and templates
  C->>S: resources/read { uri }
  S->>C: contents (text or blob)
  C->>S: resources/subscribe { uri }
  S--)C: notifications/resources/updated { uri }
  C->>S: resources/read { uri }
  S->>C: fresh contents

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 revision 2025-11-25, with the official Go SDK at v1.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
Resources
Controlled by
the model, which decides to call
the application, which decides to attach
It is
an action with side effects
data the model reads as context
Use with
tools/call
resources/read
Discover with
tools/list
resources/list + templates/list
On change
tools/list_changed
list_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.

← back to all notes