Resources
By the Syncfusion DevTutorials team · Last updated
The MCP resource model: URIs, names and MIME types, text and base64 blob contents, resource templates, common kinds of resources, and why reads must be read-only and deterministic.
Think about a library. You can walk in, take a book off the shelf, and read it. Reading doesn't change the book. The next person who reads it sees the same words. Each book also has an address: a shelf number that tells you exactly where it is.
MCP resources are the library's books. They are data an app can read, each at its own address, called a URI. Reading a resource never changes anything.
The main idea: a resource is read-only data with a URI. The app decides which resources to read and put into the model's context.
A Real-World Example
Brightgrid's support assistant often answers questions about returns and shipping.
- At first, the team made a tool called
get_policy. The model called it on almost every message, "just in case". That was slow and cost tokens. - They turned the policies into resources:
kb://returns-policyandkb://shipping. - Now the host decides: when the question mentions returns, it reads
kb://returns-policyand adds it to the context. The model doesn't have to ask.
Order records became resources too, through one template: orders://{order_id}.
In simple words: tools are for doing. Resources are for reading. The app chooses what to read.
What You Will Learn
In this lesson, you will learn:
- The resource model: URI, name, title, description and MIME type.
- How text and binary resources are returned.
- How resource templates give one URI pattern for many resources.
- Common kinds of resources: static files, records, logs and schemas.
- The rules resources must follow: read-only, no side effects, the same answer every time.
The MCP Lab Explorer now lists Resource templates too. Pick one, fill in the URI, and press Read. Images that a server returns are shown as pictures.
Now that you know why resources matter, let's look at how a server describes them. Before a host can read a resource, it needs to know what resources are available and what kind of data they contain.
The Resource Model
The host discovers available resources through resources/list. Along with each resource's URI, the server can provide additional information that helps the host and users understand what the resource contains.
What a Resource Has
resources/list returns a list of resources.
| Field | Meaning |
|---|---|
uri | Its address, like kb://shipping. Unique and stable |
name | A short name |
title | A friendly label for people (optional) |
description | What it holds, for the host and the model (optional) |
mimeType | What kind of data it is, like text/plain or image/png (optional) |
List resources by kind
// The resources/list result from the lab server (trimmed).
const resources = [
{
uri: "kb://returns-policy",
name: "returns-policy",
title: "Returns policy",
mimeType: "text/plain",
},
{
uri: "kb://shipping",
name: "shipping",
title: "Shipping times",
mimeType: "text/plain",
},
{
uri: "assets://logo.png",
name: "logo",
title: "Brightgrid logo",
mimeType: "image/png",
},
{ uri: "orders://A-1001", name: "Order A-1001", mimeType: "application/json" },
];
for (const r of resources) {
const kind = r.mimeType.startsWith("text/")
? "text"
: r.mimeType.startsWith("image/")
? "image"
: "data";
console.log(r.uri.padEnd(22), kind.padEnd(6), r.title ?? r.name);
}
Expected output:
kb://returns-policy text Returns policy
kb://shipping text Shipping times
assets://logo.png image Brightgrid logo
orders://A-1001 data Order A-1001
Application-Driven
Resources are application-driven. The host decides which ones to read and when:
- It may show a list and let the user attach one, like attaching a file to an email.
- It may pick some automatically, like the policy that matches the question.
- It may read one because a tool result pointed to it.
Knowing that a resource exists is only the first step. To use the data it contains, the host reads the resource using its URI.
Reading a Resource
resources/read takes a URI and returns contents, a list of items. Each item has its uri, its mimeType, and the data:
textfor text, like a policy or a JSON record.blobfor bytes, like an image or a PDF, written as base64. Base64 turns any bytes into plain letters, digits,+and/, so they can travel inside JSON.
Text and blob contents
// resources/read returns "contents". Text comes as text. Bytes come as base64 in "blob".
const contents = [
{
uri: "kb://shipping",
mimeType: "text/plain",
text: "Standard shipping takes 3 to 6 days.",
},
{
uri: "assets://logo.png",
mimeType: "image/png",
blob: "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMElEQVR42mPkndLxn4ECwMRAIRh4A1jQBT5ll+PVwDe1c7iFwagBWNIBejyPgDAAAO2SBnj02loiAAAAAElFTkSuQmCC",
},
];
for (const c of contents) {
if ("text" in c) {
console.log(c.uri, "-> text,", c.text.length, "characters");
} else {
const bytes = atob(c.blob); // base64 text -> raw bytes
console.log(
c.uri,
"-> blob,",
c.blob.length,
"base64 characters =",
bytes.length,
"bytes, starts with",
bytes.slice(1, 4),
);
}
}
Expected output:
kb://shipping -> text, 36 characters
assets://logo.png -> blob, 140 base64 characters = 105 bytes, starts with PNG
The lab reads the logo's base64 blob and shows it as a small picture.
Listing every resource works when there are only a few items. However, some data sets contain thousands or even millions of records. Resource templates solve this problem by describing many resources with a single URI pattern.
Resource Templates
One Pattern, Many Resources
One template, orders://{order_id}, stands for one resource per order. The client fills in the placeholder and reads the URI.
Some data has too many items to list one by one, like every order. A resource template describes them all with one pattern: orders://{order_id}. The part in braces is a placeholder. The client fills it in to make a real URI, like orders://A-1002.
resources/templates/listreturns the templates, each with auriTemplate.resources/readtakes the filled-in URI.- The server matches the URI to the template and reads the right record.
Fill and match a template
// A resource template: one pattern, many resources.
const template = "orders://{order_id}";
function expand(t, values) {
return t.replace(/\{(\w+)\}/g, (_, name) => encodeURIComponent(values[name]));
}
function match(t, uri) {
const names = [];
const pattern = t.replace(/\{(\w+)\}/g, (_, name) => {
names.push(name);
return "([^/]+)";
});
const m = uri.match(new RegExp("^" + pattern + "$"));
return m
? Object.fromEntries(names.map((n, i) => [n, decodeURIComponent(m[i + 1])]))
: null;
}
console.log(expand(template, { order_id: "A-1002" }));
console.log(JSON.stringify(match(template, "orders://A-1003")));
console.log(JSON.stringify(match(template, "tickets://T-501")));
Expected output:
orders://A-1002
{"order_id":"A-1003"}
null
Now try a missing order. In the Explorer, change the URI to orders://A-9999 and press Read. The server answers with error -32602, "Resource not found".
In the SDK, a template is a ResourceTemplate. It can also list the items it knows and suggest placeholder values while the user types, through completion/complete.
Templates follow a standard called URI templates (RFC 6570). The simple form is all most servers need. You will meet query parameters and other schemes in the Resource URIs lesson.
Resources can hold many kinds of information. Recognizing common types helps you choose a clear URI and MIME type for the data the host needs.
Kinds of Resources
Static Files
Files that do not change often: a README, a policy, or a style guide. Their URIs are often file:// paths or a custom scheme like kb://.
Records
One row or document from a database, often through a template: orders://{order_id}, customers://{id}.
Logs
Recent log lines from a system, like logs://api/today. They change over time, but each read still only reads. The server should say clearly what time range a read covers.
Schemas
The shape of a database or an API: table names, columns, and types. A model that writes queries needs this, and it is a perfect resource: read-only and rarely changed.
Kinds of resources and their MIME types
// Common kinds of resources, and the MIME type that tells the host how to show them.
const kinds = [
["Static file", "file:///project/README.md", "text/markdown"],
["Database schema", "db://shop/schema", "application/sql"],
["Log file", "logs://api/today", "text/plain"],
["Record via template", "orders://A-1002", "application/json"],
["Image", "assets://logo.png", "image/png"],
];
for (const [kind, uri, mime] of kinds) {
console.log(kind.padEnd(20), uri.padEnd(26), mime);
}
Expected output:
Static file file:///project/README.md text/markdown
Database schema db://shop/schema application/sql
Log file logs://api/today text/plain
Record via template orders://A-1002 application/json
Image assets://logo.png image/png
No matter what they contain, resources should behave predictably. Hosts may cache resources, read them multiple times, or load them automatically. To support these scenarios, resources must follow a few important rules.
The Rules for Resources
Read-Only and No Side Effects
Reading a resource must never change anything: no 'mark as read', no counters stored in the data, no emails sent. A host may read the same resource many times, read it ahead of time, or keep a copy. If reading had side effects, all of that would cause trouble.
Deterministic Reads
Deterministic means "the same input gives the same output." Two reads of the same URI, close together, should return the same data unless the data itself changed in between.
A good read and a bad read
// Reading a resource must not change anything. Two reads in a row give the same answer.
const store = { "kb://shipping": "Standard shipping takes 3 to 6 days." };
let reads = 0;
function goodRead(uri) {
reads++;
return store[uri];
}
let counter = 0;
function badRead(uri) {
counter++;
return store[uri] + " (viewed " + counter + " times)";
}
console.log(
"good:",
goodRead("kb://shipping") === goodRead("kb://shipping")
? "same both times"
: "different",
);
console.log(
"bad: ",
badRead("kb://shipping") === badRead("kb://shipping")
? "same both times"
: "different: the read changed something",
);
console.log("reads counted outside the data:", reads);
Expected output:
good: same both times
bad: different: the read changed something
reads counted outside the data: 2
Immutability Where You Can
Some resources never change once made, like an invoice for a past month or a specific version of a file. Put the version or date in the URI, like invoices://2026-09. Then a host can keep a copy forever, and a URI always means the same thing.
Resource text goes into the model's context, so it can carry prompt injection: text that tries to give the model new instructions. A support ticket or a web page saying "ignore your rules" is data, not a command. Hosts should label resource text as data and never let it change the rules.
Common mistakes
- Using a tool where a resource fits. If it only returns data and the app can choose it, make it a resource.
- Forgetting templates. Listing a million records is impossible. A template covers them all.
- Sending big binaries as text. Use
blobwith base64, and the right MIME type. - Side effects on read. A read that changes data breaks caching and surprises users.
- Trusting resource text. It is data. Label it, and never let it override the host's rules.
Exercises
Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.
What to Remember
- A resource is read-only data with a URI, a name, an optional title and description, and a MIME type.
- Resources are application-driven: the host chooses what to read.
resources/readreturnstextor base64blobcontents.- Templates like
orders://{order_id}cover many resources with one pattern. - Typical resources: static files, records, logs and schemas.
- Reads must be side-effect free and deterministic. Resource text is data, never instructions.
Next: Prompts, ready-made message templates a user can pick.