Menu

AI

Lesson 16: Add Resources

Add static resources and resource templates to an MCP server in Python and TypeScript, return text or JSON, and report a missing resource the right way.

By: Irina ShvayaOctober 28, 2026

Lesson 16: Add Resources

This lesson is part of Learn MCP, our free course on the Model Context Protocol, within The MCP Guide.

In this lesson you will add resources to the bookshop server. You will expose one fixed resource (the full title list) and one resource template (a record for any book), in Python and in TypeScript. You will also learn how to report a resource that does not exist.

A resource is read-only data that the host application loads into the model's context. The difference from a tool is who decides. The Python SDK's first-steps guide puts it simply: tools are controlled by the model, resources by the application, and prompts by the user.

The code adapts the Python Resources guide and the TypeScript Resources guide. It ran on 28 October 2026 with mcp 2.3.0 and @modelcontextprotocol/server 2.4.0.

Key Takeaways

  • A resource is addressed by a URI, such as catalog://titles, not by a function name.
  • A URI with a placeholder, such as books://{title}, is a resource template. It is listed separately from fixed resources.
  • Your read function runs only when a client reads that URI, not when it lists resources.
  • A missing resource is a protocol error (-32602 with the URI in data), not an isError result.
  • Use a resource for context the app should load; use a tool for actions the model should take.

Step 1: Decide what should be a resource

Before writing code, sort your data. The resources spec describes resources as context such as files, database schemas or application data, each identified by a URI.

Good resources for the bookshop:

  • The full list of titles (fixed, small, changes rarely).
  • One record per book (many of them, addressed by title).

A poor resource: "place an order." That changes something, so it is a tool. The SDK guide's rule of thumb is that a resource is like an HTTP GET and a tool is like a POST.

Step 2: Add resources in Python

Add this to your server.py from lesson 14, or start a new file:

from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ResourceNotFoundError

mcp = MCPServer("Bookshop")

CATALOG = {"Dune": "Frank Herbert", "Neuromancer": "William Gibson"}


@mcp.resource("catalog://titles")
def titles() -> str:
    """Every title in the catalog, one per line."""
    return "\n".join(sorted(CATALOG))


@mcp.resource("books://{title}", mime_type="application/json")
def book(title: str) -> dict:
    """The catalog record for one book."""
    if title not in CATALOG:
        raise ResourceNotFoundError(f"No book titled {title!r} in the catalog.")
    return {"title": title, "author": CATALOG[title]}

How the SDK reads it, per the Resources guide:

  • The URI in the decorator is the address. The function name becomes the resource name, and the docstring becomes its description.
  • A str return is sent as text. A dict is serialized to JSON text. bytes become a base64 blob.
  • mime_type defaults to text/plain. The SDK does not guess it from your return value, so label JSON yourself.
  • {title} in the URI must match a parameter called title. The guide says a mismatch fails at import time, before any client connects.

Step 3: Read them from a test client

import anyio
from mcp import Client, MCPError

from server import mcp


async def main() -> None:
    async with Client(mcp) as client:
        print([r.uri for r in (await client.list_resources()).resources])
        print([t.uri_template for t in (await client.list_resource_templates()).resource_templates])
        print((await client.read_resource("books://Dune")).contents[0].text)
        try:
            await client.read_resource("books://Nothing")
        except MCPError as e:
            print("MCPError", e.error.code, e.error.data)


anyio.run(main)

The output on 28 October 2026:

['catalog://titles']
['books://{title}']
{
  "title": "Dune",
  "author": "Frank Herbert"
}
MCPError -32602 {'uri': 'books://Nothing'}

Two things to notice. The template does not appear in the resource list; it has its own list. And the missing book raised an exception on the client instead of returning a result. The Python error-handling guide explains why: "A resource read either returns contents or fails." There is no isError half-result for resources.

Step 4: Add the same resources in TypeScript

In TypeScript you call registerResource with a name, a URI or a ResourceTemplate, metadata, and a read callback:

import { McpServer, ResourceNotFoundError, ResourceTemplate } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';

const CATALOG: Record<string, string> = { Dune: 'Frank Herbert', Neuromancer: 'William Gibson' };

function createServer(): McpServer {
    const server = new McpServer({ name: 'bookshop', version: '1.0.0' });

    server.registerResource(
        'titles',
        'catalog://titles',
        { title: 'Catalog titles', description: 'Every title in the catalog, one per line', mimeType: 'text/plain' },
        async uri => ({ contents: [{ uri: uri.href, text: Object.keys(CATALOG).sort().join('\n') }] })
    );

    server.registerResource(
        'book',
        new ResourceTemplate('books://{title}', { list: undefined }),
        { title: 'Book record', description: 'The catalog record for one book', mimeType: 'application/json' },
        async (uri, { title }) => {
            const key = decodeURIComponent(String(title));
            const author = CATALOG[key];
            if (!author) throw new ResourceNotFoundError(uri.href);
            return {
                contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ title: key, author }) }]
            };
        }
    );

    return server;
}

void serveStdio(createServer);
console.error('bookshop resources server running on stdio');

Points from the TypeScript guide:

  • The callback returns { contents: [...] }. Each item echoes the uri it answers for and carries text or a base64 blob.
  • list is a required key on ResourceTemplate. Pass undefined when the set is unbounded, or a callback that returns concrete URIs when you can enumerate them.
  • The matched placeholder values arrive as the callback's second argument.
  • ResourceNotFoundError takes the missing URI and sets the error code the spec requires.

The decodeURIComponent call is our addition. A title with spaces, such as "The Left Hand of Darkness", arrives URL-encoded.

Step 5: Check it with the Inspector

npx @modelcontextprotocol/inspector --cli npx tsx src/resources.ts \
  --protocol-era modern --method resources/templates/list --format json

npx @modelcontextprotocol/inspector --cli npx tsx src/resources.ts \
  --protocol-era modern --method resources/read --uri books://Dune --format json

The read returned the JSON record. Reading books://Nothing failed with "Resource not found: books://Nothing" and exit code 1, not the tool-error code 5. That difference is a quick way to tell protocol errors from tool errors in a test script.

Step 6: Know when the host actually reads them

Exposing a resource does not mean the model sees it. The host decides. Some hosts let the user attach resources to a conversation; others read them automatically. Check your target host's docs.

If the model must be able to fetch data on its own, also offer a tool. A common pattern is a read-only tool that returns the same data, marked with readOnlyHint (lesson 23 covers annotations).

Try it

  1. Add a resource template authors://{name}/books that lists every title by one author.
  2. Make the Python version return a list[str] and check that it arrives as JSON text.
  3. Read an author with no books. Decide whether that is "not found" or an empty list, and implement your choice. One rule to respect: the spec says servers must not return an empty contents array for a resource that does not exist.
  4. Run resources/templates/list in both protocol eras and confirm both templates appear.

Recap

  • Resources are URI-addressed, read-only data for the application to load.
  • A placeholder in the URI makes a template, listed under resources/templates/list.
  • Python: @mcp.resource(uri). TypeScript: registerResource(name, uriOrTemplate, metadata, callback).
  • Label JSON with a MIME type, and raise ResourceNotFoundError for a missing record.

Next step

Lesson 17 adds the third primitive: prompts. For the protocol details behind this lesson, read MCP resources explained, resource templates and resource subscriptions.

Course navigation

Previous lesson: Lesson 15: Build Your First Tool in TypeScript.

Next lesson: Lesson 17: Add Prompts.

Back to the Learn MCP course outline.

Frequently asked questions

What URI scheme should I use?

Use one that fits your data. The spec lists common schemes such as https://, file:// and git://, and allows custom ones that follow RFC 3986. It says to use https:// only when the client can fetch the resource from the web by itself. Custom schemes like books:// suit data with no natural URL.

Can a resource return an image or a PDF?

Yes. Return bytes in Python, or a base64 blob in TypeScript, and set the correct MIME type. The client receives a blob content item instead of text.

Are resources cached?

The 2026-07-28 resources spec examples include caching hints on results, ttlMs and cacheScope. The TypeScript server added them to its resources/read result in the modern era on 28 October 2026. What a client does with those hints is up to the client.

How do clients learn a resource changed?

Through change notifications. In the 2026-07-28 spec, clients open a subscriptions/listen stream and the server notifies them. The Python SDK's prompts guide shows the same pattern for prompt list changes.

Should every resource also be a tool?

No. Duplicate only data the model needs to fetch by itself during a task. Too many overlapping tools make tool selection worse.

You Might Also like to Read

Get a FREE Website Audit

Enter your website and get a 20–30 page SEO, GEO, AEO & CRO audit within 24 hours. Completely free.

Don't have a site yet? Get in touch →

Ready to speak with a marketing expert?
Give us a call! (916) 866-7893

Websites Launched
1407
Websites Launched
Marketing Experts
29
Marketing Experts
Hours of Dedicated Work
204533
Hours of Dedicated Work
Irina S.Benjamin G.Jeanette M.David G.