Skip to main content
MCP (Model Context Protocol) · lesson 4 of 13
MCP (Model Context Protocol) beginner Lesson 4 of 13

MCP Resources and URI Templates

Expose data as MCP resources: static URIs, {name} templates, MIME types and base64 blobs, plus what a client gets for a missing URI, all with real output.

By SkillByExample · Published

mcp model-context-protocol resources python

Data the Client Reads

Tools are actions a model decides to take. Resources are data a client reads by URI: a config file, a doc page, a database row. A host typically lets the user or the application choose which resources to put in front of the model, instead of leaving it to the model. These examples use the in-memory client from the first lesson.

Static Resources

A decorated function serves one fixed URI. Its return value becomes the content.

# static.py - fixed resources: list them, then read one
import asyncio

from mcp import Client
from mcp.server import MCPServer

mcp = MCPServer("docs")


@mcp.resource("config://app", mime_type="application/json")
def app_config() -> dict:
    """Runtime settings for the app."""
    return {"debug": False, "region": "eu-west-1"}


@mcp.resource("docs://changelog", mime_type="text/markdown")
def changelog() -> str:
    return "## 1.2.0\n- Faster search"


async def main():
    async with Client(mcp) as client:
        for r in (await client.list_resources()).resources:
            print(r.uri, r.name, r.mime_type, r.description)
        result = await client.read_resource("config://app")
        print(result.contents[0].text)


asyncio.run(main())
$ python static.py
config://app app_config application/json Runtime settings for the app.
docs://changelog changelog text/markdown 
{
  "debug": false,
  "region": "eu-west-1"
}

The listing gives each resource’s URI, a name taken from the function, the MIME type you declared and a description from the docstring. changelog has no docstring, so its description is empty. The URI scheme (config://, docs://) is yours to choose. MCP only requires a valid URI, so pick names that tell a host what the data is.

app_config returned a dict, and the SDK serialised it to JSON text. A resource is always delivered as text or as a binary blob, so the client sees a string and parses it itself.

Templates Serve a Family of URIs

Put a {placeholder} in the URI and the handler receives it as an argument.

# template.py - one handler serves a whole family of URIs
import asyncio

from mcp import Client, MCPError
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ResourceNotFoundError

mcp = MCPServer("docs")
PAGES = {"setup": "Run pip install mcp.", "faq": "Q: Is it stdio? A: By default."}


@mcp.resource("docs://pages/{name}", mime_type="text/markdown")
def page(name: str) -> str:
    """One documentation page by name."""
    if name not in PAGES:
        raise ResourceNotFoundError(f"no page named {name!r}")
    return PAGES[name]


async def main():
    async with Client(mcp) as client:
        print("static:", (await client.list_resources()).resources)
        print("templates:", [t.uri_template for t in (await client.list_resource_templates()).resource_templates])
        print((await client.read_resource("docs://pages/setup")).contents[0].text)
        try:
            await client.read_resource("docs://pages/billing")
        except MCPError as e:
            print("MCPError", e.error.code, e.error.message)


asyncio.run(main())
$ python template.py 2>/dev/null
static: []
templates: ['docs://pages/{name}']
Run pip install mcp.
MCPError -32602 no page named 'billing'

The template isn’t in list_resources(). A server can’t list every value {name} might take, so templates have their own listing, and the client fills in the placeholder. Reading docs://pages/setup matched the pattern and called page(name="setup").

The missing page behaves differently from a failing tool. It isn’t a result with an error flag; read_resource raised MCPError with code -32602 and your message. ResourceNotFoundError is the “this doesn’t exist” error. The server also logged the failure to stderr, which the 2>/dev/null hid. The prompts lesson shows how a client can ask which values {name} accepts.

Binary Content

Return bytes and the content becomes a base64 blob instead of text.

# binary.py - bytes travel as base64 blobs
import asyncio
import base64

from mcp import Client
from mcp.server import MCPServer

mcp = MCPServer("assets")


@mcp.resource("assets://logo.png", mime_type="image/png")
def logo() -> bytes:
    return b"\x89PNG\r\n\x1a\n"  # the 8-byte PNG signature, standing in for an image


async def main():
    async with Client(mcp) as client:
        item = (await client.read_resource("assets://logo.png")).contents[0]
        print(type(item).__name__, item.mime_type)
        print("blob:", item.blob)
        print("bytes:", base64.b64decode(item.blob))


asyncio.run(main())
$ python binary.py
BlobResourceContents image/png
blob: iVBORw0KGgo=
bytes: b'\x89PNG\r\n\x1a\n'

JSON can’t carry raw bytes, so the SDK base64-encoded them into a blob field and the client got BlobResourceContents instead of TextResourceContents. Decoding gives back the exact eight bytes. Base64 makes data about a third larger on the wire, so serve big files by reference (a URL, or a URI the host fetches when needed) rather than inlining them in every read.

Practice

  1. Add a docstring to changelog in static.py. What changes in the listing?
  2. Make a template with two placeholders, docs://{lang}/pages/{name}, backed by a dict keyed by (lang, name). Read docs://de/pages/setup.
  3. In your two-placeholder handler, look up a missing key without checking first, so it raises KeyError. What does the client see, and what’s missing from the message?
  4. Change app_config to return the string '{"debug": false}' instead of a dict. Does the client’s output change?
Solution 1

The docstring becomes the description, so the line ends in your text instead of a trailing space. A host may show that description to a user choosing what to attach, so write it for a person.

Solution 2
PAGES = {("en", "setup"): "Run pip install mcp.", ("de", "setup"): "pip install mcp ausführen."}


@mcp.resource("docs://{lang}/pages/{name}", mime_type="text/markdown")
def page(lang: str, name: str) -> str:
    """One page in one language."""
    return PAGES[(lang, name)]

Reading docs://de/pages/setup prints pip install mcp ausführen.. Each placeholder becomes a keyword argument with the same name.

Solution 3

Reading docs://fr/pages/setup raises MCPError -32603 Error creating resource from template docs://fr/pages/setup. -32603 is “internal error”: the SDK treated the KeyError as a crash, so the message names only the URI and the KeyError text stays in the server log. To report a missing page as -32602 with your own message, raise ResourceNotFoundError, as template.py does.

Solution 4

The output is the same text minus the indentation: {"debug": false}. A str return is sent as-is, while a dict is serialised for you. Either way the client receives text and parses it.

Frequently Asked Questions

What is the difference between an MCP resource and a tool?
A resource is data identified by a URI that the client reads, like a file or a config document. A tool is an action the model chooses to call with arguments. Hosts usually let the user or the application pick which resources to attach to a conversation, while tools are offered to the model to call on its own.
What is an MCP resource template?
A resource template is a URI pattern with placeholders, such as docs://pages/{name}, that one handler serves for many URIs. Templates appear in resources/templates/list rather than resources/list, because the server can't enumerate every value a placeholder might take. Reading docs://pages/setup calls the handler with name='setup'.
How does MCP send binary data like images?
Binary resource contents are sent as a blob field holding base64 text, together with a mimeType such as image/png. With the Python SDK, a resource function that returns bytes produces a blob automatically, and the client decodes it with base64.b64decode.
What error does an MCP client get when a resource doesn't exist?
In the Python SDK, raising ResourceNotFoundError from a handler gives the client a JSON-RPC error with code -32602 and your message, and an unknown URI gets the same code. Unlike a tool error, it arrives as an exception (MCPError), not as a result with isError set.