Resource templates
A resource template is a resource with a placeholder in its URI, so one function serves every value that fills it.
Last updated: 29 Sep, 2026 · MCP 2.2
The last lesson's resource had one fixed URI. No one writes a resource function per order. A placeholder turns the resource into a template that serves every order from one function.
- written in MCP Inspector with mcp dev
View the code here
from typing import Any
import httpx
from mcp.server import MCPServer
# Initialize the MCP server
mcp = MCPServer("weather")
# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
async def make_nws_request(url: str) -> dict[str, Any] | None:
"""Make a request to the NWS API with proper error handling."""
headers = {
"User-Agent": USER_AGENT,
"Accept": "application/geo+json"
}
async with httpx.AsyncClient() as client:
try:
response = await client.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None
def format_alert(feature: dict) -> str:
"""Format an alert feature into a readable string."""
props = feature["properties"]
return f"""
Event: {props.get('event', 'Unknown')}
Area: {props.get('areaDesc', 'Unknown')}
Severity: {props.get('severity', 'Unknown')}
Description: {props.get('description', 'No description available')}
Instructions: {props.get('instruction', 'No specific instructions provided')}
"""
@mcp.tool()
async def get_alerts(state: str) -> str:
"""Get weather alerts for a US state.
Args:
state: Two-letter US state code (e.g. CA, NY)
"""
url = f"{NWS_API_BASE}/alerts/active/area/{state}"
data = await make_nws_request(url)
if not data or "features" not in data:
return "Unable to fetch alerts or no alerts found."
if not data["features"]:
return "No active alerts for this state."
alerts = [format_alert(feature) for feature in data["features"]]
return "\n---\n".join(alerts)
@mcp.resource("echo://{message}")
def echo_resource(message: str) -> str:
"""Echo a message as a resource"""
return f"Resource echo: {message}"
from typing import Annotated, Literal
from pydantic import BaseModel, Field
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ResourceNotFoundError, ToolError
from mcp.types import ToolAnnotations
mcp = MCPServer("Shop support")
ORDERS = {
"A17": {"item": "blue mug", "status": "shipped", "total": 12.50},
"B42": {"item": "desk lamp", "status": "processing", "total": 48.00},
}
ARTICLES = {
"Where is my order?": "orders",
"Changing an order": "orders",
"How refunds work": "refunds",
"Refunds for damaged items": "refunds",
"Resetting your password": "account",
}
class Order(BaseModel):
id: str
item: str
status: Literal["processing", "shipped", "delivered"]
total: float
@mcp.tool(annotations=ToolAnnotations(read_only_hint=True))
def lookup_order(order_id: str) -> Order:
"""Look up an order by its id."""
if order_id not in ORDERS:
raise ToolError(f"No order with id {order_id!r}. Order ids look like A17.")
return Order(id=order_id, **ORDERS[order_id])
@mcp.tool()
def search_help(
query: Annotated[str, Field(description="Words to look for in the help articles.")],
topic: Literal["orders", "refunds", "account"] | None = None,
limit: Annotated[int, Field(ge=1, le=5)] = 3,
) -> str:
"""Search the help centre and return matching article titles."""
found = [title for title, t in ARTICLES.items() if query.lower() in title.lower() and topic in (None, t)]
return "; ".join(found[:limit]) or "No articles found."
@mcp.tool(
title="Refund an order",
annotations=ToolAnnotations(read_only_hint=False, destructive_hint=False, idempotent_hint=False),
)
def refund_order(order_id: str, reason: str) -> str:
"""Refund the full total of an order to the customer's card."""
if order_id not in ORDERS:
raise ToolError(f"No order with id {order_id!r}.")
return f"Refunded {ORDERS[order_id]['total']:.2f} for order {order_id}: {reason}."
@mcp.resource("policy://refunds", mime_type="text/markdown")
def refund_policy() -> str:
"""The shop's refund policy."""
return "# Refunds\n\nFull refund within 30 days of delivery. Damaged items: refund or replacement."
@mcp.resource("orders://{order_id}", mime_type="application/json")
def order_record(order_id: str) -> Order:
"""The full record for one order."""
if order_id not in ORDERS:
raise ResourceNotFoundError(f"No order with id {order_id!r}.")
return Order(id=order_id, **ORDERS[order_id])
The MCP crash course adds a resource to its weather server, copied from the Python SDK's documentation. Resources are how a server exposes data, like a GET endpoint in a REST API, while tools perform computation and side effects. The example is an echo resource with the URI template echo://{message}. In the Inspector it is listed under resource templates, and reading it with the message "Krish" returns the echo.
nws_weather.py from MCP Inspector with mcp dev already ends with that resource. Listed and read in code:
import asyncio
from mcp import Client
from nws_weather import mcp
async def main():
async with Client(mcp) as client:
templates = await client.list_resource_templates()
print([t.uri_template for t in templates.resource_templates])
result = await client.read_resource("echo://Krish")
print(result.contents[0].text)
asyncio.run(main())['echo://{message}']
Resource echo: Krish{message} in the URI filled the message parameter with "Krish". The shop uses the same mechanism for orders, one record per id.
A URI template
@mcp.resource("orders://{order_id}", mime_type="application/json")
def order_record(order_id: str) -> Order: # {order_id} must match the parameter
...The order_record template
{order_id} in the URI matches the order_id parameter. Reading orders://A17 calls the function with order_id="A17". The imports gain ResourceNotFoundError, the resource version of ToolError, on the same line: from mcp.server.mcpserver.exceptions import ResourceNotFoundError, ToolError.
@mcp.resource("orders://{order_id}", mime_type="application/json")
def order_record(order_id: str) -> Order:
"""The full record for one order."""
if order_id not in ORDERS:
raise ResourceNotFoundError(f"No order with id {order_id!r}.")
return Order(id=order_id, **ORDERS[order_id])Listing a template and reading one URI
A template is listed apart from plain resources, as a pattern, because there is nothing to read until the placeholder is filled.
import asyncio
from mcp import Client
from shop import mcp
async def main():
async with Client(mcp) as client:
templates = await client.list_resource_templates()
print([t.uri_template for t in templates.resource_templates])
print([r.uri for r in (await client.list_resources()).resources])
result = await client.read_resource("orders://A17")
print(result.contents[0].text)
asyncio.run(main())['orders://{order_id}']
['policy://refunds']
{
"id": "A17",
"item": "blue mug",
"status": "shipped",
"total": 12.5
}What the two lists show
- The template appears under
list_resource_templatesas the patternorders://{order_id}. - The plain resource from the last lesson still appears under
list_resources. - Reading a filled URI returned the
Order, turned into JSON text.
An order that does not exist
A resource read has no error result: it returns contents or the request fails.
import asyncio
from mcp import Client, MCPError
from shop import mcp
async def main():
async with Client(mcp) as client:
try:
await client.read_resource("orders://Z9")
except MCPError as error:
print(error.error.code, error.error.message)
print(error.error.data)
asyncio.run(main())-32602 No order with id 'Z9'.
{'uri': 'orders://Z9'}ResourceNotFoundError becomes the protocol error code the specification gives a missing resource, with the URI in data so the client knows which read failed. This differs from a tool, where a failure is a result the model reads.
A name mismatch caught early
The placeholder and the parameter must share a name. A mismatch can only be a bug, so the decorator refuses when the file is imported, before any client connects.
from mcp.server import MCPServer
mcp = MCPServer("Shop support")
@mcp.resource("orders://{order_id}")
def order_record(order: str) -> str:
return orderTraceback (most recent call last):
File "main.py", line 5, in <module>
@mcp.resource("orders://{order_id}")
ValueError: Mismatch between URI parameters {'order_id'} and function parameters {'order'}A plain resource vs a template
| Plain resource | Template | |
|---|---|---|
| URI | Fixed, like policy://refunds | Has a placeholder |
| Listed under | list_resources | list_resource_templates |
| Serves | One value | Every value that fills it |
| Missing value | The URI is absent | Raise ResourceNotFoundError |
When to use a template
- One record per id, like an order, a customer or a document.
- Any read where the URL carries a key you would otherwise pass as an argument.
Related
- Previous: Resources
- Next: Prompts
- Reference: MCP server concepts
- Add
customers://{customer_id}/orders, returning a list of order ids. - Read
orders://B42. - Remove
mime_typefrom the template and comparecontents[0].mime_type.
Little by little, you're building something great.