Skip to content

Resources

A resource is data a server can hand to the host: a file, a database schema, a log tail, a record. It is identified by a URI and read on demand. Unlike a tool, nothing happens when you read it — which is exactly why the interaction model is application-driven: the host decides how to surface resources. Common patterns are a tree or list picker, a search box, an @-mention in the chat input, or automatic inclusion by heuristics.

sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: resources/list
  S-->>C: resources[] (ttlMs, cacheScope)
  C->>S: resources/templates/list
  S-->>C: resourceTemplates[]
  C->>S: resources/read uri=file:///project/src/main.rs
  S-->>C: contents[] text or blob
Discover, then read
{
"uri": "file:///project/README.md",
"name": "README.md",
"title": "Project Documentation",
"description": "Top-level project overview",
"mimeType": "text/markdown",
"size": 4096,
"icons": [ { "src": "https://example.com/md.png", "mimeType": "image/png", "sizes": ["48x48"] } ],
"annotations": { "audience": ["user"], "priority": 0.8, "lastModified": "2026-09-01T15:00:58Z" }
}
FieldNotes
uriUnique identifier; must be a valid RFC 3986 URI
nameShort name
titleHuman-readable display name
descriptionWhat it contains and why it matters
mimeTypeHelps the host decide how to render and whether the model can use it
sizeBytes, optional — lets hosts budget context before reading
annotationsHints for selection and display (below)

Resources, templates, and content blocks share one annotation shape:

  • audience — ["user"], ["assistant"], or both. A raw stack trace might be assistant-only; a rendered summary might be user-only.
  • priority — 0.0 to 1.0. 1 means effectively required; 0 means entirely optional. Hosts use it to decide what to include when context is tight.
  • lastModified — ISO 8601 timestamp, for sorting by recency and cache decisions.
{ "jsonrpc": "2.0", "id": 1, "method": "resources/list", "params": { "cursor": "optional" } }
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"resources": [ { "uri": "file:///project/src/main.rs", "name": "main.rs", "mimeType": "text/x-rust" } ],
"nextCursor": "next-page-cursor",
"ttlMs": 300000,
"cacheScope": "public"
}
}

Reading returns a contents array. Each entry is either text or blob (base64):

{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "complete",
"contents": [
{ "uri": "file:///project/src/main.rs", "mimeType": "text/x-rust", "text": "fn main() {}" }
],
"ttlMs": 60000,
"cacheScope": "private"
}
}
{ "uri": "file:///logo.png", "mimeType": "image/png", "blob": "iVBORw0KGgo…" }

A single read MAY return several contents — reading a directory resource might return every file in it. resources/read results also carry ttlMs and cacheScope; a per-user document is private, a public doc can be public.

And, like tools/call and prompts/get, resources/read MAY return an InputRequiredResult when the server needs user input first (for example, to elicit which environment’s config to read). See MRTR.

When a server has a family of resources — every file in a project, every customer record — it should not list them all. It exposes a template with an RFC 6570 URI template and lets the client (or user) fill in the variables:

{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resultType": "complete",
"resourceTemplates": [
{
"uriTemplate": "repo://{repo}/readme",
"name": "Repository README",
"title": "README of one repository",
"mimeType": "text/markdown"
}
],
"ttlMs": 300000,
"cacheScope": "public"
}
}

Variables can be auto-completed through completion/complete, so a picker can offer valid repo values as the user types. That is the same completion API prompts use — see Prompts.

import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
const server = new McpServer({ name: 'repos', version: '1.0.0' });
// A fixed resource
server.registerResource(
'guidelines',
'docs://guidelines',
{ title: 'Contribution guidelines', mimeType: 'text/markdown' },
async (uri) => ({
contents: [{ uri: uri.href, mimeType: 'text/markdown', text: '# How we work\n…' }],
}),
);
// A template with completion for the {repo} variable
server.registerResource(
'readme',
new ResourceTemplate('repo://{repo}/readme', {
list: undefined,
complete: {
repo: async (value) => {
const repos = await listRepos();
return repos.filter((r) => r.startsWith(value));
},
},
}),
{ description: 'The README of one repository' },
async (uri, { repo }) => ({
contents: [{ uri: uri.href, mimeType: 'text/markdown', text: await readReadme(Array.isArray(repo) ? repo[0] : repo) }],
}),
);

list: undefined means the template does not enumerate concrete resources in resources/list; provide a list callback if you want some of them to appear there too.

The scheme is a contract with the client about how to get the bytes:

SchemeRule
https://Use it only when the client can fetch the resource directly from the web on its own, without the MCP server. If your server downloads something and serves it, use another scheme — even if the origin is on the internet
file://Resources that behave like a filesystem. They need not map to a real disk; a database can present tables as files. Non-regular entries may use an XDG MIME type such as inode/directory
git://Git version-control integration
custom (repo://, docs://, ticket://)Allowed and encouraged; must be RFC 3986-valid

The https:// rule surprises people. A server that proxies https://api.example.com/users/42 through authenticated calls should expose it as users://42 or similar, because a client that sees https:// may try to GET it directly — and fail, or worse, hit it unauthenticated.

  • Resource not found → JSON-RPC error -32602 (Invalid params). Include the URI in data.
  • Internal failures → -32603.
  • Legacy servers (2025-11-25 and earlier) used -32002 for not-found. Clients SHOULD still accept it; new servers MUST NOT emit it.
  • A server MUST NOT answer a missing resource with an empty contents array. Empty is ambiguous — “exists but empty” versus “does not exist” — so always error.
{ "jsonrpc": "2.0", "id": 5, "error": { "code": -32602, "message": "Resource not found", "data": { "uri": "file:///nonexistent.txt" } } }

Servers MUST validate every URI they receive and MUST sanitize paths when serving file:// resources — file:///project/../../etc/passwd is the classic attack. Access controls SHOULD be applied to sensitive resources and checked before every read, binary data MUST be correctly base64-encoded, and, as everywhere in MCP, the list you return MAY depend on the caller’s authorization but not on connection history.

When may a server use the https:// scheme for a resource URI?
A client asks to read a URI that does not exist. What must the server return?
What does a resource template provide that a plain resource does not?
Which annotation tells a host how important a resource is when context is limited?