MP-201b · Module 1
Resource Templates
3 min read
Resource templates solve the enumeration problem. A database with ten million rows cannot list every row as a static resource, but it can expose a template like db://customers/{customer_id} that addresses any row by primary key. Templates declare the shape of addressable resources without materializing them. The server advertises templates via resources/list alongside static resources, and the client fills in parameters at read time.
The distinction between list and read is fundamental. resources/list returns the catalog — static URIs and templates that describe what is available. resources/read takes a concrete URI (with all template parameters filled in) and returns the actual data. This two-phase pattern lets clients build navigation UIs, autocomplete suggestions, and exploration tools without fetching any actual data until the user (or AI model) selects a specific resource.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ name: "crm-server", version: "1.0.0" });
// Static resource — always available
server.resource(
"company-list",
"crm://companies",
async (uri) => ({
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(await db.query("SELECT id, name FROM companies")),
}],
})
);
// Template resource — parameterized by company ID
server.resource(
"company-detail",
new ResourceTemplate("crm://companies/{companyId}", { list: undefined }),
async (uri, { companyId }) => {
const company = await db.query(
"SELECT * FROM companies WHERE id = $1", [companyId]
);
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(company),
}],
};
}
);
// Template with list callback — enumerable
server.resource(
"company-contacts",
new ResourceTemplate("crm://companies/{companyId}/contacts", {
list: async () => {
const companies = await db.query("SELECT id FROM companies");
return companies.map((c: { id: string }) => ({
uri: `crm://companies/${c.id}/contacts`,
}));
},
}),
async (uri, { companyId }) => {
const contacts = await db.query(
"SELECT * FROM contacts WHERE company_id = $1", [companyId]
);
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(contacts),
}],
};
}
);
- Identify your primary keys For each data entity (customers, orders, tickets), determine the natural key. That key becomes the template parameter: entity://{entity_id}.
- Decide on enumerability If the dataset is small enough to list (under 1000 entries), provide a list callback. For large datasets, omit it and rely on the client providing the key from context.
- Layer your templates Use hierarchical templates for related data: crm://companies/{id}, crm://companies/{id}/contacts, crm://companies/{id}/deals. This mirrors the data model and makes navigation intuitive.