MCP resources: context the application decides to use

MCP resources: context the application decides to use

Resources have been the forgotten primitive of the Model Context Protocol (MCP), because hosts were slow to show them. What surprised me is how much now runs on them: two extensions serve their files through resources/read, so a server with no documents to share may still need resources.

This is part 10 of my series on what an MCP server does under the 2026-07-28 specification, after tool errors and tool design in part 9. The facts are as I read them in October 2026.

In brief

  1. Resources are application-driven context. Each is data with a Uniform Resource Identifier (URI), such as a file, a schema or a documentation page, and the host decides how and when to include it.
  2. A server answers three methods: resources/list, resources/templates/list and resources/read, and can send updates through subscriptions/listen.
  3. A resource that doesn’t exist MUST get error -32602, ideally with the URI in data, never an empty contents array.
  4. Servers MUST validate every URI, and MUST sanitize file:// paths against directory traversal.
  5. My advice: implement resources even though hosts were slow to show them. They pay off once you adopt MCP Apps or Skills, and a resource_link keeps a big document out of the model’s context until someone asks for it.

Context the host decides to use

Resources are data a server shares as context: files, database schemas, documentation, configuration, application records. The model picks tools; the host decides how and when to include resources.

The specification’s Resources page doesn’t mandate a user interface (UI), but three patterns are common:

  • a picker, as a tree or a list, where the user selects resources;
  • search and filtering over the resources available;
  • automatic inclusion, based on heuristics or on the model’s own selection.

As of October 2026, four hosts show them like this:

HostHow resources appear
Claude (web, Desktop)Attach from the connector menu in the chat
Claude Code@server:protocol://path mentions, fuzzy-searchable alongside files
VS Code (GitHub Copilot)Add Context, then MCP Resources, or MCP: Browse Resources
Gemini CLI@server://resource/path references

ChatGPT works differently. As of 8 October 2026, OpenAI documents two uses of resources. It loads ui:// templates, served as text/html;profile=mcp-app, to render an app’s interface. It also imports Skills files as a snapshot when a plugin is submitted. It doesn’t document general-purpose resources you can attach as context.

Listing, reading and templates

Your server declares resources in its capabilities, with two optional flags:

{ "capabilities": { "resources": { "listChanged": true, "subscribe": true } } }
  • listChanged: the server sends notifications/resources/list_changed when the set changes.
  • subscribe: the server supports update notifications for single resources, requested through subscriptions/listen, covered in a later part.

Either flag, both or neither may be declared. As with tools and prompts, the listed set MUST NOT vary per connection, but MAY vary with the authorization on the request.

A client lists resources, reads one by its URI, and can ask to hear when it changes:

Note (Client, Server): Discovery; Client → Server: resources/list; Server → Client: resources + nextCursor + ttlMs + cacheScope; Client → Server: resources/templates/list; Server → Client: resourceTemplates (RFC 6570 URI templates); Note (Client, Server): Access; Client → Server: resources/read (uri); Server → Client: contents (text or blob) + ttlMs + cacheScope; Note (Client, Server): Watch for changes; Client → Server: subscriptions/listen (resourceSubscriptions [uri]); Server → Client: notifications/subscriptions/acknowledged; Server → Client: notifications/resources/updated (uri); Client → Server: resources/read (uri); Server → Client: updated contentsServerClientDiscoveryAccessWatch for changesresources/listresources + nextCursor + ttlMs + cacheScoperesources/templates/listresourceTemplates (RFC 6570 URI templates)resources/read (uri)contents (text or blob) + ttlMs + cacheScopesubscriptions/listen (resourceSubscriptions [uri])notifications/subscriptions/acknowledgednotifications/resources/updated (uri)resources/read (uri)updated contents

Listing and reading

An entry in a resource list has a uri and a name, and may carry title, description, icons, mimeType, size (in bytes) and annotations. A complete read returns one or more content items, each either text or a base64 blob, with the caching hints ttlMs and cacheScope that part 6 covers:

{
  "resultType": "complete",
  "contents": [
    { "uri": "file:///project/src/main.rs", "mimeType": "text/x-rust", "text": "fn main() { ... }" }
  ],
  "ttlMs": 60000,
  "cacheScope": "private"
}

A server MAY return several content items for one read: reading a directory resource might return the files in it.

Templates

Resource templates expose parameterized resources with RFC 6570 URI templates, such as file:///{path} or db://customers/{customerId}. A client can autocomplete their arguments as the user types; the next part covers completion.

Asking for input

resources/read is one of the three methods allowed to return resultType: "input_required", part of Multi Round-Trip Requests, which a later part explains. A server can, for example, ask which environment’s configuration to read before returning it. That interim result carries no caching hints.

Annotations

Resources, templates and content blocks can carry optional annotations:

AnnotationValuesUse
audience["user"], ["assistant"], or bothLets a host decide whether to show the content, give it to the model, or both
priority0.0 to 1.01 means effectively required context, 0 means entirely optional
lastModifiedAn ISO 8601 timestampSort by recency, show freshness

URIs, errors and safety

Pick your scheme by what the resource is and who fetches it:

SchemeGuidance
https://Use only when the client can fetch the resource from the web itself, without going through the server
file://Filesystem-like resources, which don’t have to map to a real filesystem. Directories may use inode/directory
git://Git version-control integration
Custom (db://, crm:// and so on)Allowed if RFC 3986-compliant. Prefer a custom scheme over https:// whenever the server does the fetching
ui://Reserved for MCP Apps UI templates
skill://The conventional scheme for Skills over MCP

On errors, the specification is precise:

  • A resource that doesn’t exist MUST get -32602 (Invalid Params), ideally with the URI in data. Clients SHOULD also accept the older -32002, for backward compatibility.
  • A server MUST NOT return an empty contents array for a resource that doesn’t exist, because an empty array is ambiguous.
  • Internal failures SHOULD return -32603.

On safety, your server MUST validate all resource URIs, MUST encode binary data properly and, if it serves file:// resources, MUST sanitize file paths against directory traversal. Access controls and permission checks SHOULD be applied to sensitive resources.

In Python

With the official Python software development kit (SDK), mcp 2.3.0, a resource is a function under @mcp.resource. A URI with variables makes it a template, whose variables must match the function’s parameters; a URI without them makes a static resource:

from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ResourceNotFoundError

mcp = MCPServer("acme-docs")

PAGES = {
    ("api", "auth"): "# Authentication\nSend a bearer token with every request.",
    ("api", "errors"): "# Errors\nEvery error has a code and a message.",
}


@mcp.resource("docs://index", mime_type="text/markdown")
def index() -> str:
    """The table of contents."""
    return "\n".join(f"- docs://{area}/{slug}" for area, slug in PAGES)


@mcp.resource(
    "docs://{area}/{slug}", title="Documentation page", mime_type="text/markdown"
)
def page(area: str, slug: str) -> str:
    """One documentation page, by area and slug."""
    if (area, slug) not in PAGES:
        raise ResourceNotFoundError(f"No page {area}/{slug}")
    return PAGES[(area, slug)]

The in-memory client lists both kinds, reads a page, and asks for one that doesn’t exist:

import anyio
from mcp import Client, MCPError


async def main():
    async with Client(mcp) as client:
        listed = await client.list_resources()
        templates = await client.list_resource_templates()
        print([r.uri for r in listed.resources])
        print([t.uri_template for t in templates.resource_templates])
        item = (await client.read_resource("docs://api/auth")).contents[0]
        print(item.mime_type, repr(item.text))
        try:
            await client.read_resource("docs://api/billing")
        except MCPError as err:
            print(err.code, err.message, err.data)


anyio.run(main)

It prints (the server also logs the failed read to stderr):

['docs://index']
['docs://{area}/{slug}']
text/markdown '# Authentication\nSend a bearer token with every request.'
-32602 No page api/billing {'uri': 'docs://api/billing'}

What the SDK does here:

  • The static resource is listed by resources/list, the template by resources/templates/list, each with its docstring as its description.
  • ResourceNotFoundError reaches the client as -32602 with the URI in data, as the spec asks. Its parent ResourceError gives -32603 with your message, and an MCPError you raise keeps its own code. Any other exception gives -32603 with a generic message naming only the URI.
  • Template values are checked first: .., absolute paths and null bytes are rejected by default, so reading docs://api/.. got -32602 (“Unknown resource”) without reaching page.

More than context

Resources play more roles than the word context suggests:

Resources primitive resources/list, resources/read → Static context docs, schemas, config; Resources primitive resources/list, resources/read → Templated context db://customers/{id}; Resources primitive resources/list, resources/read → Live context status, metrics + subscriptions; Resources primitive resources/list, resources/read → Tool output links resource_link in tool results; Resources primitive resources/list, resources/read → UI templates ui://, MCP Apps; Resources primitive resources/list, resources/read → Skill files skill://, Skills extensionResources primitiveresources/list, resources/readStatic contextdocs, schemas, configTemplated contextdb://customers/{id}Live contextstatus, metrics + subscriptionsTool output linksresource_link in tool resultsUI templatesui://, MCP AppsSkill filesskill://, Skills extension

Two extensions use them as plumbing: MCP Apps serves its HTML through resources/read, and Skills over MCP serves SKILL.md and its supporting files the same way. Later parts cover both.

My advice: use resources for anything big that the model shouldn’t receive by default. Return a short summary and a resource_link from your tool (see part 8), and let the host or the user decide whether to pull in the full document. That keeps context lean while the full content stays one click away.

Method and caveats

  • Built from my guide’s chapter on resources, written against the 2026-07-28 specification. Host behaviour is as of October 2026 (ChatGPT’s, 8 October 2026).
  • The Python sample ran against mcp 2.3.0 on 9 October 2026 and printed the output above. The other SDK notes come from the package’s source and from probes of a traversal URI and the other errors.
  • Where OpenAI’s documentation and the specification’s Caching page are more precise than my notes (ChatGPT also reads app UI templates; only a complete result carries caching hints), this article follows them.
Series What an MCP server actually does in 2026 Part 10 of 35
  1. What an MCP server does in 2026: much more than a list of tools
  2. MCP goes stateless: what changed in the 2026-07-28 specification
  3. server/discover: the one method every MCP server must implement
  4. Server instructions: the paragraph every model reads first
  5. Stateless MCP requests: _meta, resultType and explicit handles
  6. Caching hints and pagination in MCP
  7. MCP transports in 2026: stdio and Streamable HTTP
  8. Designing an MCP tool: the definition and the result
  9. MCP tool errors, and tools that models use well
  10. MCP resources: context the application decides to use
  11. MCP prompts: slash commands your server ships
  12. Tool, resource or prompt? Choosing the right MCP primitive

I’m Amir Pournasserian. I build AI and platform systems for a living, maintain FluentCMS and YeSvelte, and write here about what I find along the way.