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.
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 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:
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 URI template. Clients discover it with resources/templates/list, fill in the parameters, and read the expanded URI:
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:
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:
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 splitapp://xyunambiguously. It is rejected:resources/templates/listand 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
resourcesare 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:
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:
#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:
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:
defineResource({
name: "profile",
uri: "app:///me",
cache: { ttlMs: 1_000, cacheScope: "private" },
handler: async (uri) => ({ contents: [{ uri: uri.toString(), text: "..." }] }),
});#Change Notifications
Telling clients a resource changed is era-specific:
- Modern — push through a
subscriptions/listenstream withsubscription.resourceUpdated(uri). - Legacy — track
resources/subscribe/resources/unsubscribeand pushnotifications/resources/updatedover the GET/SSE stream.