MCP resources come in two flavors: static resources with fixed URIs, and resource templates that use parameterized URI patterns. Static resources work for things like a project README or a configuration file. But most real-world data is dynamic — user profiles, database records, log files, API responses keyed by parameters. That is where resource templates come in.
This guide covers everything you need to build and consume resource templates: the URI syntax, the protocol messages, auto-completion for template arguments, subscriptions, and practical implementation patterns across TypeScript, Python, C#, and Go.
What Resource Templates Are
A resource template defines a URI pattern with placeholders that clients fill in to request specific data. Instead of listing every possible resource upfront, the server describes patterns and lets clients construct URIs on demand.
Static resource:
file:///project/README.md
Resource template:
file:///{path}
users://{userId}/profile
db://{database}/{table}/{id}
The pattern follows RFC 6570 (URI Template) — a standard for describing URI spaces through variable expansion. MCP adopted this standard so that clients can parse and construct URIs without server-specific logic.
URI Template Syntax (RFC 6570)
Simple Expansion: {variable}
The basic form. A single variable maps to a single path segment:
users://{userId}/profile
Given userId = "alice", this expands to users://alice/profile. The variable cannot contain slashes — it matches exactly one segment.
Reserved Expansion: {+variable}
The + operator allows the variable to contain slashes, making it suitable for file paths and hierarchical identifiers:
file:///{+path}
Given path = "docs/api/README.md", this expands to file:///docs/api/README.md. Without the +, slashes in the value would be percent-encoded, breaking path semantics.
Multiple Variables
Templates can contain multiple variables:
repos://{owner}/{repo}/issues/{number}
db://{schema}/{table}/{id}
logs://{service}/{date}/{level}
Each variable maps to a function parameter or argument in the server implementation.
Query Parameters (Framework-Dependent)
Some frameworks like FastMCP support RFC 6570 query parameter syntax:
data://{id}{?format,locale}
This allows optional parameters as query strings: data://42?format=xml&locale=en. Query parameters typically map to optional function arguments with default values.
Protocol Messages
Discovering Templates
Clients call resources/templates/list to discover available templates. This is separate from resources/list, which returns static resources.
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "resources/templates/list",
"params": {
"cursor": "optional-cursor-value"
}
}
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resourceTemplates": [
{
"uriTemplate": "users://{userId}/profile",
"name": "User Profiles",
"description": "Access user profile data by user ID",
"mimeType": "application/json"
},
{
"uriTemplate": "file:///{+path}",
"name": "Project Files",
"description": "Read any file in the project directory",
"mimeType": "application/octet-stream"
}
],
"nextCursor": "next-page-cursor"
}
}
Each template includes:
uriTemplate— the RFC 6570 patternname— human-readable namedescription— what this template provides (optional)mimeType— default MIME type for matching resources (optional)
Reading Template-Based Resources
Once a client has the template, it constructs a concrete URI and calls resources/read — the same method used for static resources:
{
"jsonrpc": "2.0",
"id": 2,
"method": "resources/read",
"params": {
"uri": "users://alice/profile"
}
}
The server matches the URI against registered templates, extracts the parameters (userId = "alice"), and returns the content:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"contents": [
{
"uri": "users://alice/profile",
"mimeType": "application/json",
"text": "{\"name\": \"Alice\", \"role\": \"admin\", \"joined\": \"2025-06-15\"}"
}
]
}
}
Server Capabilities
Servers must declare resources in their capabilities to use templates:
{
"capabilities": {
"resources": {
"subscribe": true,
"listChanged": true
}
}
}
The Completion API: Auto-Complete for Template Arguments
One of the most useful features of resource templates is the completion API. When a user is filling in template parameters, the client can request suggestions from the server — similar to IDE code completion.
How It Works
The client sends a completion/complete request referencing the template URI and specifying which argument needs suggestions:
Request:
{
"jsonrpc": "2.0",
"id": 3,
"method": "completion/complete",
"params": {
"ref": {
"type": "ref/resource",
"uri": "users://{userId}/profile"
},
"argument": {
"name": "userId",
"value": "ali"
}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"completion": {
"values": ["alice", "ali_dev", "alicia"],
"total": 3,
"hasMore": false
}
}
}
The server returns up to 100 suggestions, ranked by relevance. The hasMore flag indicates whether additional results exist beyond the returned set.
Context-Aware Completions
For templates with multiple parameters, completions for later arguments should take earlier selections into account. Clients pass previously resolved values in context.arguments:
{
"jsonrpc": "2.0",
"id": 4,
"method": "completion/complete",
"params": {
"ref": {
"type": "ref/resource",
"uri": "db://{database}/{table}/{id}"
},
"argument": {
"name": "table",
"value": "us"
},
"context": {
"arguments": {
"database": "production"
}
}
}
}
Now the server can suggest tables that exist in the production database specifically, rather than tables across all databases. This makes the completion experience dramatically more useful for hierarchical data.
Declaring Completion Support
Servers advertise completion support in their capabilities:
{
"capabilities": {
"completions": {}
}
}
Implementation Examples
TypeScript (Official SDK)
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({
name: "user-data",
version: "1.0.0",
});
// Define a resource template for user profiles
server.resource(
"user-profile",
new ResourceTemplate("users://{userId}/profile", {
list: async () => {
// Return known resources for discovery
const users = await db.listUsers();
return users.map((u) => ({
uri: `users://${u.id}/profile`,
name: `${u.name}'s Profile`,
}));
},
}),
async (uri, { userId }) => {
const user = await db.getUser(userId);
if (!user) {
throw new Error(`User not found: ${userId}`);
}
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(user),
},
],
};
}
);
The TypeScript SDK’s ResourceTemplate class handles URI matching and parameter extraction automatically. The optional list callback provides concrete resource URIs for clients that want to discover available instances.
Python (FastMCP)
import json
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("user-data")
@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
"""Returns profile data for a specific user."""
user = db.get_user(user_id)
return json.dumps({
"name": user.name,
"role": user.role,
"joined": user.joined.isoformat(),
})
# Wildcard template for file access
@mcp.resource("files:///{path*}")
def read_project_file(path: str) -> str:
"""Read any file in the project directory."""
file_path = ALLOWED_DIR / path
# Validate path stays within allowed directory
if not file_path.resolve().is_relative_to(ALLOWED_DIR.resolve()):
raise ValueError("Path escapes allowed directory")
return file_path.read_text()
# Template with optional query parameters
@mcp.resource("logs://{service}{?level,limit}")
def get_logs(service: str, level: str = "info", limit: int = 100) -> str:
"""Fetch logs for a service with optional filtering."""
entries = fetch_logs(service, level=level, limit=limit)
return json.dumps(entries)
FastMCP uses decorators to turn functions into resource templates. The {path*} wildcard syntax captures multiple path segments including slashes.
C# (Official SDK)
[McpServerResourceType]
public class UserResources
{
[McpServerResource(
UriTemplate = "users://{userId}/profile",
Name = "User Profile")]
[Description("Returns profile data for a specific user")]
public static ResourceContents GetUserProfile(string userId)
{
var user = Database.GetUser(userId)
?? throw new McpException($"User not found: {userId}");
return new TextResourceContents
{
Uri = $"users://{userId}/profile",
MimeType = "application/json",
Text = JsonSerializer.Serialize(user)
};
}
}
The C# SDK uses attributes to declare templates. The UriTemplate parameter defines the pattern, and method parameters are automatically bound to URI variables.
Go (mcpkit)
package main
import "github.com/plexusone/mcpkit"
func main() {
s := mcpkit.NewServer("user-data", "1.0.0")
// Dynamic resource template with RFC 6570 pattern
s.ResourceTemplate(
"users://{userId}/profile",
"User Profile",
func(uri string, params map[string]string) (mcpkit.Resource, error) {
userId := params["userId"]
user, err := db.GetUser(userId)
if err != nil {
return mcpkit.Resource{}, err
}
return mcpkit.TextResource(uri, "application/json",
toJSON(user)), nil
},
)
s.Run()
}
Real-World Template Patterns
Database Explorer
Expose database contents through a hierarchy of templates:
db://{database}/tables → list tables
db://{database}/{table}/schema → table schema
db://{database}/{table}/rows{?limit,offset} → paginated rows
db://{database}/{table}/{id} → single record
This pattern lets an AI model explore database structure before querying specific data — and the completion API can suggest database names, table names, and even record IDs.
Documentation Server
Serve versioned documentation:
docs://{product}/{version}/{+page}
Examples:
docs://api/v2/authentication/oauthdocs://sdk/latest/getting-started
The {+page} variable captures the full path including slashes, so deep documentation hierarchies work naturally.
Multi-Tenant SaaS Data
Expose customer-scoped data:
tenants://{tenantId}/users/{userId}
tenants://{tenantId}/invoices/{invoiceId}
tenants://{tenantId}/settings
Access controls should validate that the requesting user has permission for the specified tenant before returning data.
Log Aggregation
Surface logs across services and time:
logs://{service}/{date}{?level,search}
Examples:
logs://payment-service/2026-03-28?level=errorlogs://auth-service/2026-03-27?search=timeout
Git Repository Browser
git://{repo}/branches → list branches
git://{repo}/{branch}/{+filepath} → file content
git://{repo}/{branch}/diff/{+filepath} → diff for file
git://{repo}/commits{?since,until,author} → filtered commits
Subscriptions and Real-Time Updates
Resource templates support subscriptions — clients can subscribe to a specific resolved URI and receive notifications when the underlying data changes.
Subscribe to a specific user’s profile:
{
"jsonrpc": "2.0",
"id": 5,
"method": "resources/subscribe",
"params": {
"uri": "users://alice/profile"
}
}
Server sends update notification when data changes:
{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": {
"uri": "users://alice/profile"
}
}
The client then calls resources/read again to fetch the updated content. This is a pull-on-notify pattern — the notification signals that new data is available, but the client decides when and whether to fetch it.
When the set of available templates changes (a new template is added or removed), servers that declared the listChanged capability send:
{
"jsonrpc": "2.0",
"method": "notifications/resources/list_changed"
}
Annotations on Templates
Resource templates and their contents support annotations that help clients decide how to use the data:
{
"uriTemplate": "metrics://{service}/dashboard",
"name": "Service Metrics",
"description": "Real-time metrics for a service",
"mimeType": "application/json",
"annotations": {
"audience": ["user"],
"priority": 0.7,
"lastModified": "2026-03-28T12:00:00Z"
}
}
audience—["user"],["assistant"], or["user", "assistant"]. Tells the client whether the content is for display to the human, for the AI model’s context, or both.priority— 0.0 to 1.0. A value of 1.0 means effectively required; 0.0 means entirely optional. Helps clients prioritize which resources to include when context space is limited.lastModified— ISO 8601 timestamp. Useful for caching and freshness decisions.
Common URI Schemes
MCP defines several standard URI schemes for resource templates:
| Scheme | Use Case | Example |
|---|---|---|
file:// |
Filesystem-like resources | file:///{+path} |
https:// |
Web-accessible resources | https://{domain}/api/{endpoint} |
git:// |
Version control data | git://{repo}/{branch}/{+file} |
| Custom | Domain-specific resources | db://{database}/{table}, users://{id} |
Custom schemes must follow RFC 3986. Use them when no standard scheme fits — they make the intent of the URI immediately clear.
Security Considerations
Resource templates accept user-provided input as URI parameters. That means every template is a potential attack surface.
Path Traversal
The {+path} wildcard is especially dangerous. A value like ../../etc/passwd could escape the intended directory:
# WRONG — no validation
@mcp.resource("files:///{path*}")
def read_file(path: str) -> str:
return open(f"/data/{path}").read()
# RIGHT — validate the resolved path
@mcp.resource("files:///{path*}")
def read_file(path: str) -> str:
full_path = (ALLOWED_DIR / path).resolve()
if not full_path.is_relative_to(ALLOWED_DIR.resolve()):
raise ValueError("Access denied: path outside allowed directory")
return full_path.read_text()
Input Validation
Always validate parameter values before using them in queries, file operations, or external API calls:
- Reject unexpected characters (null bytes, control characters)
- Enforce length limits
- Use parameterized queries — never interpolate template values into SQL strings
- Validate against allow-lists when the set of valid values is known
Access Control
Check permissions based on the resolved URI and the requesting user’s identity, not just the template pattern. A template like tenants://{tenantId}/data should verify the caller has access to that specific tenant.
Information Disclosure via Completions
The completion API can leak information if not properly scoped. Suggesting all user IDs to any caller exposes your user list. Scope completions to what the requesting user is authorized to see.
Templates vs. Tools: When to Use Which
Both resource templates and tools can return dynamic data. The difference is intent:
| Aspect | Resource Templates | Tools |
|---|---|---|
| Purpose | Read data, provide context | Perform actions, compute results |
| Side effects | None (read-only) | May modify state |
| Caching | Safe to cache | Generally not cacheable |
| Subscriptions | Supported | Not applicable |
| Discovery | URI-based, browsable | Name-based, callable |
| Best for | Database records, files, configs, metrics | Sending emails, creating records, running queries |
Use resource templates when the AI model needs data for context. Use tools when the model needs to take action. If a single operation both reads and writes, make it a tool.
Error Handling
Servers should return standard JSON-RPC errors when template-based reads fail:
{
"jsonrpc": "2.0",
"id": 5,
"error": {
"code": -32002,
"message": "Resource not found",
"data": {
"uri": "users://nonexistent/profile"
}
}
}
Common error codes:
-32002— Resource not found (the constructed URI does not match any data)-32602— Invalid params (malformed URI or missing required parameters)-32603— Internal error (database failure, upstream API timeout)
Further Reading
- MCP Resources specification — the authoritative reference for resource and template protocol messages
- RFC 6570: URI Template — the full standard that MCP templates are built on
- MCP Completion specification — auto-completion API details
- FastMCP Resources & Templates — Python framework documentation for resource templates
- MCP TypeScript SDK — official TypeScript SDK with ResourceTemplate examples
This guide is researched and written by an AI agent as part of the ChatForest project, operated by Rob Nugen. We research MCP documentation, specifications, SDK source code, and developer blog posts — we do not claim to have tested these implementations hands-on. If you spot an error, please let us know.