# Resource URIs

Course: [Model Context Protocol](https://www.devtutorials.ai/learn/mcp) · Section: Execution and Context
Source: https://www.devtutorials.ai/learn/mcp/execution-context/resource-uris

> 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:

1. The parts of a URI, and common schemes.
2. How URI templates use path and query placeholders.
3. How text, base64 bytes and MIME types work together.
4. The resource methods: list, read and templates/list.
5. How to keep resources fresh: `ttlMs`, `cacheScope` and 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

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 as `orders`.
- The **authority** is the part after `//`. It often names a machine, but custom schemes may use it for an identifier. In `orders://A-1002`, for example, `A-1002` is the authority.
- The **path** narrows the address further, as `/returns` in a web URI.
- The **query** adds options as `name=value` pairs after `?`.

```js runnable title="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:

```text
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.

```js runnable title="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:

```text
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 <code>{"{name}"}</code> 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.

```js runnable title="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:

```text
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.

```js runnable title="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:

```text
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. `0` means "don't keep it".
- **`cacheScope`**: who the cached copy is for. The lab's server sends `"private"`: a copy for this client only.

```js runnable title="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:

```text
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 <code>{"files://{path}"}</code> into a disk path, a value like <code>../../secrets</code> could escape the folder. Servers must check every URI they read against what is allowed.

## Common mistakes

- **Unstable URIs.** If `orders://latest` changes 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 **`text`** or base64 **`blob`**; a server may include a **MIME type** to describe them.
- Methods: `resources/list`, `resources/templates/list`, `resources/read`. Missing resource: **`-32602`**.
- Cache with **`ttlMs`** and **`cacheScope`**. Listen for **`list_changed`** and **`updated`** notices.
- 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.
