Model Context ProtocolMCP Python SDK 2.2 · LangChain 1.4 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
27 small wins to finish your pathNext lesson →

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.

A resource template in the Inspector · from the MCP Agentic AI Crash Course With Python · 30:57 to 34:29
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports these files. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep them in the same folder.
View the code here
nws_weather.py
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}"
shop.py
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:

ExampleFrom the video, run on MCP 2.2
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())

{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

python
@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.

python
@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.

Example
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())

What the two lists show

  • The template appears under list_resource_templates as the pattern orders://{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.

Example
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())

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.

Example
from mcp.server import MCPServer

mcp = MCPServer("Shop support")

@mcp.resource("orders://{order_id}")
def order_record(order: str) -> str:
    return order

A plain resource vs a template

Plain resourceTemplate
URIFixed, like policy://refundsHas a placeholder
Listed underlist_resourceslist_resource_templates
ServesOne valueEvery value that fills it
Missing valueThe URI is absentRaise 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.
Watch out. The placeholder name and the parameter name must match exactly, or the decorator raises at import time. That is a feature: the bug surfaces before a client ever connects, not on the first read.
Try it yourself
  • Add customers://{customer_id}/orders, returning a list of order ids.
  • Read orders://B42.
  • Remove mime_type from the template and compare contents[0].mime_type.
PreviousResources

Little by little, you're building something great.