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),
      }],
    };
  }
);
  1. Identify your primary keys For each data entity (customers, orders, tickets), determine the natural key. That key becomes the template parameter: entity://{entity_id}.
  2. 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.
  3. 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.