
# Resources

> Readable data, addressed by URI.

Resources are data a client can read directly — files, config, records — identified by URI rather than invoked as a function. Clients list them with `resources/list` and fetch them with `resources/read`.

```ts
import { defineResource } from "h3-mcp";

const configResource = defineResource({
  name: "config",
  uri: "app:///config.json",
  title: "App config",
  description: "Application configuration",
  mimeType: "application/json",
  handler: async (uri, event) => ({
    contents: [
      {
        uri: uri.toString(),
        mimeType: "application/json",
        text: JSON.stringify({ theme: "dark", lang: "en" }),
      },
    ],
  }),
});
```

The handler receives the requested URI as a parsed [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) and returns one or more `contents` entries. A single read may return several — a directory listing, or a document plus its attachments.

## Binary Content

Use `blob` with base64 data instead of `text`, and advertise `size` in bytes when you know it so clients can decide before fetching:

```ts
const logoResource = defineResource({
  name: "logo",
  uri: "app:///logo.png",
  description: "Application logo",
  mimeType: "image/png",
  size: 2048,
  handler: async (uri) => ({
    contents: [{ uri: uri.toString(), mimeType: "image/png", blob: "iVBOR..." }],
  }),
});
```

## Resource Templates

A template describes a _family_ of resources with a [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570) URI template. Clients discover it with `resources/templates/list`, fill in the parameters, and read the expanded URI:

```ts
import { defineResourceTemplate } from "h3-mcp";

const userTemplate = defineResourceTemplate({
  name: "user-profile",
  uriTemplate: "app://users/{userId}",
  title: "User Profile",
  description: "Retrieve a user profile by ID",
  mimeType: "application/json",
  // A template handler receives the expansion variables, already decoded.
  handler: async (uri, variables, event) => {
    const user = await db.getUser(variables.userId);
    return {
      contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify(user) }],
    };
  },
});
```

Register templates alongside static resources:

```ts
defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  resources: [configResource],
  resourceTemplates: [userTemplate],
});
```

On `resources/read`, static resources are matched first; if none match, Level 1 URI templates are tried in declaration order. The `resources` capability is advertised when either list is non-empty.

That same template is in the playground — fill in `userId` and read the URI it expands to:

::playground{item="templates/user-profile" label="templates/user-profile"}
::

> [!IMPORTANT]
> The URI in a template read is client-supplied, and so is every value in `variables`. They are percent-decoded, which means a `{path}` variable can come back containing `/` or `..` even though the template matched a single segment. Treat them exactly like tool arguments — validate them, and never interpolate them into a filesystem path, SQL query, or outbound URL unchecked.

### How a Template Matches

Each `{variable}` matches one or more characters inside a single path segment — it never crosses a `/` — and the leftmost variable takes as much as it can while still leaving the rest of the template a match. A few rules follow from that:

- **Variables need a delimiter between them.** `app://{a}{b}` has nothing to divide the two values, so no implementation can split `app://xy` unambiguously. It is rejected: `resources/templates/list` and any read that consults the template answer an error rather than advertising a pattern that cannot be honored. Put a literal between them (`app://{a}-{b}`) or use separate segments (`app://{a}/{b}`).
- **A repeated name is a backreference.** In `app://{id}/log/{id}` both occurrences must expand to the same value, exactly as expanding the template would have produced. A URI whose halves disagree does not match.
- **Very long URIs do not match a template.** A URI beyond 4096 characters is treated as no match — matching it would be work a client can ask for at will. Static `resources` are unaffected: they are compared by equality.

`variables` is a null-prototype object, so a name that collides with something on `Object.prototype` (`toString`, `constructor`, `__proto__`) arrives as an ordinary value and nothing inherited is reachable through it.

### Enumerating a Template

`resources/templates/list` advertises the _pattern_. A client that cannot guess `{userId}` has nothing to read, so give the template a `list` callback and its current members show up in `resources/list` as well:

```ts
const userTemplate = defineResourceTemplate({
  name: "user-profile",
  uriTemplate: "app://users/{userId}",
  mimeType: "application/json",
  list: async (event) => {
    const users = await db.listUsers();
    return users.map((user) => ({
      name: user.id,
      uri: `app://users/${user.id}`,
      title: `${user.name}'s profile`,
      mimeType: "application/json",
    }));
  },
  handler: async (uri, variables) => ({/* ... */}),
});
```

Enumerated entries are descriptors only — no `handler` of their own. They are read through the template's handler and inherit its `cache` hints, so a member listed here needs no duplicate entry in `resources`. That routing is by pattern, so every URI you list must match the template's `uriTemplate` — list `app://elsewhere/1` under `app://users/{userId}` and clients see a resource they cannot read.

`resources/list` returns the static `resources` first, then each template's members in declaration order. The callback runs on every `resources/list` request, including paginated follow-ups, so keep it cheap and its order stable — the list cursor is positional. Omit `list` entirely for a family that cannot be enumerated (an arbitrary filesystem path, say); the template is then discoverable through `resources/templates/list` alone.

## Autocompletion

Add a `complete` callback to a template to autocomplete its parameters:

:read-more{to="/guide/completions" title="Completions"}

## Annotations and Metadata

A resource can carry `annotations` (`audience`, `priority`, `lastModified`) and a `_meta` object for namespaced extension data. Both are emitted by `resources/list`:

```ts
defineResource({
  name: "changelog",
  uri: "app:///CHANGELOG.md",
  annotations: { audience: ["user"], priority: 0.8 },
  _meta: { "com.example/section": "release-notes" },
  handler: async (uri) => ({ contents: [{ uri: uri.toString(), text: "..." }] }),
});
```

## Caching

On the modern era, `resources/read` results carry `ttlMs` and `cacheScope`. The default is "immediately stale, never shared"; opt into caching per definition:

```ts
defineResource({
  name: "profile",
  uri: "app:///me",
  cache: { ttlMs: 1_000, cacheScope: "private" },
  handler: async (uri) => ({ contents: [{ uri: uri.toString(), text: "..." }] }),
});
```

:read-more{to="/protocols/modern/caching" title="Caching"}

## Change Notifications

Telling clients a resource changed is era-specific:

- **Modern** — push through a [`subscriptions/listen`](/protocols/modern/subscriptions) stream with `subscription.resourceUpdated(uri)`.
- **Legacy** — track `resources/subscribe` / `resources/unsubscribe` and push `notifications/resources/updated` over the [GET/SSE stream](/protocols/legacy/sse).
