Resource Templates are dynamic resources that can accept parameters in their URIs. Unlike static resources, they use URI patterns to match different paths and extract parameters. In mcp-nest, they're defined using the @ResourceTemplate() decorator.
import { Injectable, Scope } from '@nestjs/common';
import { ResourceTemplate } from '@rekog/mcp-nest';
@Injectable({ scope: Scope.REQUEST })
export class GreetingResource {
@ResourceTemplate({
name: 'user-language',
description: "Get a specific user's preferred language",
mimeType: 'application/json',
uriTemplate: 'mcp://users/{name}',
})
getUserLanguage({ uri, name }) {
const users = {
alice: 'en',
carlos: 'es',
marie: 'fr',
hans: 'de',
yuki: 'ja',
'min-jun': 'ko',
wei: 'zh',
sofia: 'it',
joão: 'pt',
};
const language = users[name.toLowerCase()] || 'en';
return {
contents: [
{
uri: uri,
mimeType: 'application/json',
text: JSON.stringify({ name, language }, null, 2),
},
],
};
}
}Resource templates use path-to-regexp style patterns:
uriTemplate: 'mcp://users/{userId}'
// Matches: mcp://users/123
// Extracts: { userId: '123' }uriTemplate: 'mcp://users/{userId}/posts/{postId}'
// Matches: mcp://users/123/posts/456
// Extracts: { userId: '123', postId: '456' }uriTemplate: 'mcp://files/{path*}'
// Matches: mcp://files/docs/readme.md
// Extracts: { path: 'docs/readme.md' }Resource template methods receive:
uri: The actual URI that was requested- Individual parameters extracted from the URI pattern
- Additional context if needed
@ResourceTemplate({
name: 'file-content',
description: 'Read file contents from the system',
mimeType: 'text/plain',
uriTemplate: 'mcp://files/{path*}',
})
getFileContent({ uri, path }) {
// Validate path for security
if (path.includes('..') || path.startsWith('/')) {
return {
contents: [{
uri,
mimeType: 'text/plain',
text: 'Error: Invalid file path',
}],
};
}
// In real implementation, read actual file
const content = `Content of file: ${path}`;
return {
contents: [{
uri,
mimeType: 'text/plain',
text: content,
}],
};
}@ResourceTemplate({
name: 'user-profile',
description: 'Get user profile information',
mimeType: 'application/json',
uriTemplate: 'mcp://profiles/{userId}',
})
async getUserProfile({ uri, userId }) {
// In real app, query database
const profile = await this.userService.findById(userId);
if (!profile) {
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify({ error: 'User not found' }),
}],
};
}
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify(profile),
}],
};
}@ResourceTemplate({
name: 'api-endpoint',
description: 'Proxy external API data',
mimeType: 'application/json',
uriTemplate: 'mcp://api/{service}/{endpoint}',
})
async getApiData({ uri, service, endpoint }) {
const allowedServices = ['github', 'weather', 'news'];
if (!allowedServices.includes(service)) {
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify({ error: 'Service not allowed' }),
}],
};
}
// Proxy to external API
const data = await this.httpService.get(`https://api.${service}.com/${endpoint}`);
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify(data),
}],
};
}Always handle invalid parameters gracefully:
@ResourceTemplate({
name: 'database-record',
description: 'Get database records by ID',
mimeType: 'application/json',
uriTemplate: 'mcp://db/{table}/{id}',
})
async getRecord({ uri, table, id }) {
try {
// Validate table name
const allowedTables = ['users', 'posts', 'comments'];
if (!allowedTables.includes(table)) {
throw new Error('Table not allowed');
}
// Validate ID format
if (!/^\d+$/.test(id)) {
throw new Error('Invalid ID format');
}
const record = await this.dbService.findOne(table, id);
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify(record || { error: 'Record not found' }),
}],
};
} catch (error) {
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify({ error: error.message }),
}],
};
}
}Run the playground server:
npx ts-node-dev --respawn playground/servers/server-stateful.tsnpx @modelcontextprotocol/inspector@0.16.2 --cli http://localhost:3030/mcp --transport http --method resources/templates/listExpected output:
{
"resourceTemplates": [
{
"name": "user-language",
"description": "Get a specific user's preferred language",
"mimeType": "application/json",
"uriTemplate": "mcp://users/{name}"
}
]
}Test with a known user:
npx @modelcontextprotocol/inspector@0.16.2 --cli http://localhost:3030/mcp --transport http --method resources/read --uri "mcp://users/carlos"Expected output:
{
"contents": [
{
"uri": "mcp://users/alice",
"mimeType": "application/json",
"text": "{\n \"name\": \"alice\",\n \"language\": \"en\"\n}"
}
]
}Test with another user:
npx @modelcontextprotocol/inspector@0.16.2 --cli http://localhost:3030/mcp --transport http --method resources/read --uri "mcp://users/carlos"Expected output:
{
"contents": [
{
"uri": "mcp://users/carlos",
"mimeType": "application/json",
"text": "{\n \"name\": \"carlos\",\n \"language\": \"es\"\n}"
}
]
}Test with unknown user (fallback behavior):
npx @modelcontextprotocol/inspector@0.16.2 --cli http://localhost:3030/mcp --transport http --method resources/read --uri "mcp://users/unknown"Expected output:
{
"contents": [
{
"uri": "mcp://users/unknown",
"mimeType": "application/json",
"text": "{\n \"name\": \"unknown\",\n \"language\": \"en\"\n}"
}
]
}For interactive testing, use the MCP Inspector UI:
npx @modelcontextprotocol/inspector@0.16.2Connect to http://localhost:3030/mcp and try accessing different URIs to test your templates.
See the complete example at: playground/resources/greeting.resource.ts