Introduction

Static resources work when you know every URI at registration time. But what if you want to expose user profiles, database rows, or log files where the URI depends on a parameter? Resource templates let you define URI patterns with placeholders that clients fill in at read time.

The ResourceTemplate class creates parameterized URIs, and a list callback tells clients which specific instances are currently available.

Key Concepts

  • ResourceTemplate is a class from the SDK that defines a URI pattern with {param} placeholders
  • List callback returns the currently available resource instances that match the template
  • Template parameters are automatically parsed from the URI and passed to the handler
  • completable() wraps a Zod schema to provide autocompletion suggestions for template parameters
  • Templates and static resources both use server.registerResource() — the difference is the second argument

Real World Context

Resource templates shine when your data is indexed by some identifier:

  • User profiles: user://{userId}/profile
  • Database tables: db://{tableName}/schema
  • Log files: logs://{date}/entries
  • API endpoints: api://{service}/{endpoint}

The LLM can discover available instances through the list callback and then read any specific one by filling in the parameter.

Deep Dive

Here is how to register a resource template that exposes user profiles.

typescript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';

const server = new McpServer({ name: 'my-server', version: '1.0.0' });

server.registerResource(
  'user-profile',
  new ResourceTemplate('user://{userId}/profile', {
    list: async () => ({
      resources: [
        { uri: 'user://123/profile', name: 'Alice' },
        { uri: 'user://456/profile', name: 'Bob' }
      ]
    })
  }),
  { title: 'User Profile', description: 'User profile data', mimeType: 'application/json' },
  async (uri, { userId }): Promise<ReadResourceResult> => ({
    contents: [{ uri: uri.href, text: JSON.stringify({ userId, name: 'Example User' }) }]
  })
);

The ResourceTemplate constructor takes two arguments: the URI pattern string and a configuration object with a list method. The list method is called when clients request resources/list and returns all currently available instances of this template.

The handler's second argument is an object containing the parsed template parameters. In this case, { userId } is extracted from the URI pattern user://{userId}/profile.

For autocompletion support, wrap your template parameters with completable(). This provides suggestions to clients as users type parameter values.

typescript
import { McpServer, ResourceTemplate, completable } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';

const users = ['alice', 'bob', 'charlie'];

server.registerResource(
  'user-data',
  new ResourceTemplate('user://{userId}/data', {
    list: async () => ({
      resources: users.map(u => ({ uri: `user://${u}/data`, name: u }))
    }),
    complete: {
      userId: completable(
        z.string(),
        (value) => users.filter(u => u.startsWith(value))
      )
    }
  }),
  { title: 'User Data', description: 'User data by ID', mimeType: 'application/json' },
  async (uri, { userId }): Promise<ReadResourceResult> => ({
    contents: [{ uri: uri.href, text: JSON.stringify({ userId, joined: '2024-01-15' }) }]
  })
);

The completable() function takes a Zod schema and a callback that returns matching suggestions. The callback receives the current input value, letting you filter results as the user types.

The list callback can be dynamic — fetching from a database, scanning a directory, or calling an external API.

Multiple parameters are supported in a single template.

typescript
new ResourceTemplate('db://{database}/{table}/schema', {
  list: async () => ({
    resources: [
      { uri: 'db://main/users/schema', name: 'Users table schema' },
      { uri: 'db://main/orders/schema', name: 'Orders table schema' }
    ]
  })
})

The handler receives all parsed parameters: async (uri, { database, table }) => ....

Common Pitfalls

  • Forgetting the list callback: without it, clients cannot discover available instances and must guess URIs
  • Returning stale list results: if your data changes, the list callback should return current data, not cached results from startup
  • Not destructuring template parameters: the second argument to the handler is an object keyed by parameter name, not positional arguments
  • Confusing templates with static resources: static resources use a plain URI string; templates use a ResourceTemplate instance

Best Practices

  • Always implement the list callback so clients can discover available resources
  • Keep the list callback efficient — it may be called frequently as clients refresh
  • Use meaningful parameter names (userId, tableName) that describe the data dimension
  • Validate parameter values in your handler before using them in database queries or file paths
  • Use completable() when the set of valid parameter values is known or queryable

Summary

Resource templates define parameterized URI patterns using ResourceTemplate. The list callback exposes available instances, and the handler receives parsed parameters from the URI. This pattern lets you expose collections of data — users, files, tables — through a single registration, while clients discover and read individual items by filling in the parameters.

Code Examples

typescript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';

server.registerResource(
  'user-profile',
  new ResourceTemplate('user://{userId}/profile', {
    list: async () => ({
      resources: [
        { uri: 'user://123/profile', name: 'Alice' },
        { uri: 'user://456/profile', name: 'Bob' }
      ]
    })
  }),
  { title: 'User Profile', description: 'User profile data', mimeType: 'application/json' },
  async (uri, { userId }): Promise<ReadResourceResult> => ({
    contents: [{ uri: uri.href, text: JSON.stringify({ userId, name: 'Example User' }) }]
  })
);
typescript
server.registerResource(
  'table-schema',
  new ResourceTemplate('db://{database}/{table}/schema', {
    list: async () => ({
      resources: [
        { uri: 'db://main/users/schema', name: 'Users table' },
        { uri: 'db://main/orders/schema', name: 'Orders table' }
      ]
    })
  }),
  { title: 'Table Schema', description: 'Database table schema', mimeType: 'application/json' },
  async (uri, { database, table }): Promise<ReadResourceResult> => ({
    contents: [{ uri: uri.href, text: JSON.stringify({ database, table, columns: [] }) }]
  })
);
✓ Completed