Resource URIs
By the Syncfusion DevTutorials team · Last updated
Resource URI parts and schemes, path and query templates, text and base64 contents with MIME types, the resource methods, and freshness with ttlMs, cacheScope and update notices.
A street address has parts: a country, a city, a street and a house number. Each part narrows the search, and anyone who knows the format can find the house.
A resource's URI works the same way. Its parts tell the client what kind of thing it is, where it lives, and which exact piece of data to return. A well-designed URI scheme lets a host find, cache and check resources without special code.
The main idea: a URI is a resource's address, made of a scheme, a path and sometimes a query. Templates build URIs from values. The contents come back as text or base64 bytes, labeled with a MIME type.
A Real-World Example
Brightgrid's logging team wanted to share logs with the assistant. Their first idea was one resource: logs://all. It held a million lines. Reading it was slow and filled the model's context with noise.
They redesigned the URIs:
logs://{service}/{day}picks one service and one day:logs://api/2026-10-09.- An optional query narrows it further:
logs://api/2026-10-09?level=error.
Now the host can read the service and date it needs instead of loading every log line. A dated URI also makes it easier to choose a cache lifetime, though the server's data may still change.
In simple words: good URIs point at exactly the data you need, and say what kind of data it is.
What You Will Learn
In this lesson, you will learn:
- The parts of a URI, and common schemes.
- How URI templates use path and query placeholders.
- How text, base64 bytes and MIME types work together.
- The resource methods: list, read and templates/list.
- How to keep resources fresh:
ttlMs,cacheScopeand update notices.
In the MCP Lab Explorer, the URI box is editable. Try reading orders://A-1001, orders://A-1003 and orders://A-9999, and watch the Inspector.
URI Parts and Schemes
To read, create, or debug resource URIs, you need to understand how a URI is divided into pieces. Each part has a different job.
The Parts
The parts of a resource URI, and a template that builds such URIs from values.
Many URIs look like scheme://host/path?query, but not every URI has a //host part. A URI always starts with a scheme; it may also have an authority (often shown as a host), a path, a query, or a fragment. This lesson focuses on the scheme, authority/host, path and query:
- The scheme says what kind of address it is:
https,file, or a custom scheme such asorders. - The authority is the part after
//. It often names a machine, but custom schemes may use it for an identifier. Inorders://A-1002, for example,A-1002is the authority. - The path narrows the address further, as
/returnsin a web URI. - The query adds options as
name=valuepairs after?.
Split URIs into parts
// Every URI has the same parts. The browser's URL class can split them.
const uris = [
"https://docs.brightgrid.example/returns?lang=en",
"file:///home/ravi/project/README.md",
"orders://A-1002",
"logs://api/today?level=error",
];
for (const u of uris) {
const url = new URL(u);
const host = url.host || "(none)";
const query = url.search || "(none)";
console.log(
`${url.protocol.padEnd(7)} host=${host.padEnd(24)} path=${url.pathname.padEnd(26)} query=${query}`,
);
}
Expected output:
https: host=docs.brightgrid.example path=/returns query=?lang=en
file: host=(none) path=/home/ravi/project/README.md query=(none)
orders: host=A-1002 path= query=(none)
logs: host=api path=/today query=?level=error
Common Schemes
Different schemes identify different kinds of resources. The scheme tells a client how to interpret the rest of the URI.
| Scheme | Used for |
|---|---|
https:// | Web pages and files fetched by a server or client |
file:// | Files on a disk, usually for local servers |
git:// | Files in a Git repository |
Custom, like kb:// or orders:// | Anything else. The server defines what the scheme means |
A custom scheme should follow URI syntax: start with a letter, then use only letters, digits, +, - and .. Pick a name that is unlikely to clash with another server's.
URI Templates
Hard-coding every possible URI would be impractical. Templates provide a reusable pattern that can generate many resource URIs from different values.
Path Placeholders
A template like orders://{order_id} has one placeholder in the authority. When a template value is expanded, reserved characters such as spaces and slashes are percent-encoded. For example, a space becomes %20, so it does not accidentally become part of the URI's structure. Encoding is not an access-control check; servers still need to validate the resulting URI.
Query Placeholders
URI templates (RFC 6570) also have a form for queries: {?level,limit} adds ?level=...&limit=... for the values that are given, and adds no query when none are provided.
Expand path and query placeholders
// URI templates with a path part and an optional query part (RFC 6570 style).
function expand(template, values) {
return template
.replace(/\{\?([\w,]+)\}/g, (_, names) => {
const pairs = names
.split(",")
.filter((n) => values[n] !== undefined)
.map((n) => `${n}=${encodeURIComponent(values[n])}`);
return pairs.length ? "?" + pairs.join("&") : "";
})
.replace(/\{(\w+)\}/g, (_, n) => encodeURIComponent(values[n]));
}
const t = "logs://{service}/{day}{?level,limit}";
console.log(expand(t, { service: "api", day: "2026-10-09" }));
console.log(expand(t, { service: "api", day: "2026-10-09", level: "error" }));
console.log(expand(t, { service: "billing", day: "today", level: "warn", limit: 50 }));
console.log(expand("customers://{name}", { name: "Ana López" }));
Expected output:
logs://api/2026-10-09
logs://api/2026-10-09?level=error
logs://billing/today?level=warn&limit=50
customers://Ana%20L%C3%B3pez
SDK support for URI-template forms can differ. Simple placeholders are common; check your SDK's support before relying on query templates.
Content Formats
After a client identifies a resource URI, the next question is how the resource content is returned. MCP resources can contain text or binary data, so the protocol needs a way to represent both.
Text and Base64 Blobs
A resources/read result contains resource content in one of two forms:
text: the data as text, for text, JSON, Markdown, CSV, or code.blob: bytes encoded as base64, for images, PDFs, audio, and other binary files.
Base64 makes large byte sequences about one third bigger, so large binaries cost more to send.
How big is base64?
// Text or blob? Base64 makes bytes about one third bigger.
function sizes(bytes) {
const base64Length = Math.ceil(bytes / 3) * 4;
return `${bytes} bytes -> ${base64Length} base64 characters (+${Math.round((base64Length / bytes - 1) * 100)}%)`;
}
console.log("16x16 icon: ", sizes(105));
console.log("photo: ", sizes(300000));
console.log("text is sent as text, so a 2,000-character policy stays 2,000 characters.");
Expected output:
16x16 icon: 105 bytes -> 140 base64 characters (+33%)
photo: 300000 bytes -> 400000 base64 characters (+33%)
text is sent as text, so a 2,000-character policy stays 2,000 characters.
MIME Types
A MIME type is a label for the kind of data, such as text/plain, application/json, image/png, or application/pdf. The server may include a MIME type with resource content. The host uses it to help decide how to handle the content: add text to context, display an image, extract text from a PDF, or leave an unsupported file as a download. The MIME type describes the format; it does not prove that the content is safe or trustworthy.
Decide by MIME type
// The MIME type tells the host what to do with the contents.
function handle(item) {
const m = item.mimeType ?? "application/octet-stream";
if (m.startsWith("text/") || m === "application/json") {
return "put the text into the context";
}
if (m.startsWith("image/")) {
return "show it, or send it to a model that can see images";
}
if (m === "application/pdf") {
return "extract the text first, then use it";
}
return "don't send it to the model; offer it as a download";
}
const items = [
{ uri: "kb://shipping", mimeType: "text/plain" },
{ uri: "orders://A-1002", mimeType: "application/json" },
{ uri: "assets://logo.png", mimeType: "image/png" },
{ uri: "docs://contract.pdf", mimeType: "application/pdf" },
{ uri: "backup://db.dump" },
];
for (const item of items) {
console.log(item.uri.padEnd(22), handle(item));
}
Expected output:
kb://shipping put the text into the context
orders://A-1002 put the text into the context
assets://logo.png show it, or send it to a model that can see images
docs://contract.pdf extract the text first, then use it
backup://db.dump don't send it to the model; offer it as a download
The Resource Methods
Once resources and templates exist, clients need protocol methods to discover them and retrieve their contents.
list, read and templates/list
| Method | Returns |
|---|---|
resources/list | The concrete resources the server wants to show, possibly in pages with nextCursor |
resources/templates/list | The templates, each with a uriTemplate |
resources/read | The contents of one URI |
Together, these methods let a client discover available resources, learn the URI patterns it can generate, and retrieve resource contents.
For Streamable HTTP, resources/read puts its URI in the Mcp-Name header, just as tools/call puts its tool name there. A missing resource returns error -32602.
Keeping Resources Fresh
Resources can change over time. Clients often cache resource lists and content to avoid repeated requests, but they also need a way to know when cached data is stale.
ttlMs and cacheScope
List results in 2026-07-28 carry two caching hints:
ttlMs: how many milliseconds the client may keep this answer before asking again.0means "don't keep it".cacheScope: who the cached copy is for. The lab's server sends"private": a copy for this client only.
Cache a list with ttlMs
// List results carry ttlMs and cacheScope. A client keeps the list until it expires.
const cache = new Map();
function getList(method, now, fetchFromServer) {
const hit = cache.get(method);
if (hit && now < hit.expires) {
return `cache (expires in ${hit.expires - now} ms)`;
}
const result = fetchFromServer();
cache.set(method, { result, expires: now + result.ttlMs });
return `server (ttlMs ${result.ttlMs}, cacheScope ${result.cacheScope})`;
}
const server = () => ({
resources: [],
ttlMs: 60000,
cacheScope: "private",
resultType: "complete",
});
console.log("t=0 ", getList("resources/list", 0, server));
console.log("t=30000 ", getList("resources/list", 30000, server));
console.log("t=61000 ", getList("resources/list", 61000, server));
const noCache = () => ({ resources: [], ttlMs: 0, cacheScope: "private" });
console.log(
"ttlMs 0: ",
getList("other/list", 0, noCache),
"->",
getList("other/list", 1, noCache),
);
Expected output:
t=0 server (ttlMs 60000, cacheScope private)
t=30000 cache (expires in 30000 ms)
t=61000 server (ttlMs 60000, cacheScope private)
ttlMs 0: server (ttlMs 0, cacheScope private) -> server (ttlMs 0, cacheScope private)
Update Notices
Caching reduces traffic, but clients still need a way to learn when data changes before the cache expires.
A client can also hear about changes instead of polling:
notifications/resources/list_changed: the list of resources changed. Fetch the list again.notifications/resources/updated: the contents of one URI changed. Read that URI again.
Both arrive on a subscriptions/listen stream, which you met in Chapter 2.
A URI is user-controlled input too. If a server turns into a disk path, a value like ../../secrets could escape the folder. Servers must check every URI they read against what is allowed.
Common mistakes
- Unstable URIs. If
orders://latestchanges meaning every hour, caches and citations break. Prefer ids and dates. - Raw values in templates. A space or slash in a value can change the URI's structure. Encode template values, then validate access separately.
- Huge blobs. Base64 adds about one third to large payloads. Send links or smaller versions when you can.
- Ignoring
ttlMs. Caching forever shows stale data. Caching never wastes requests. - Trusting the path. Check every URI against the allowed folders or records before reading.
Exercises
Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.
What to Remember
- A URI starts with a scheme and may also have an authority, path, query, or fragment.
- Templates fill
{name}path placeholders and{?a,b}query placeholders with encoded values. - Contents use
textor base64blob; a server may include a MIME type to describe them. - Methods:
resources/list,resources/templates/list,resources/read. Missing resource:-32602. - Cache with
ttlMsandcacheScope. Listen forlist_changedandupdatednotices. - Check every URI before reading. Encoding helps preserve URI structure, but it does not authorize access.
Next: Resource Context Assembly, turning resources into clean, safe context for the model.