Index Ai Academy
Phase 9 · Model Context Protocol · Building servers

Resources and prompts

Readable data, URI templates and reusable prompt templates.

About 40 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

Tools get the attention, but MCP has two other primitives. Resources give a model data to read: documents, records, files. Prompts give users ready-made, well-tested requests. Knowing when to use each makes a server pleasant to use rather than a pile of tools. A help center fits as resources. A "review this campaign" workflow fits as a prompt. Picking well also keeps the model's context lean, because not everything has to be a tool.

What you'll be able to do

  • Explain who controls each MCP primitive: tools, resources and prompts
  • Serve resources by URI, including URI templates for whole families of resources
  • Serve prompt templates with arguments that hosts show to users
  • Choose the right primitive for a capability you want to expose

What it covers

  1. Who's in control
  2. Matching a URI against a template
  3. Filling a prompt template safely
  4. Who decides what
  5. Reading a resource

Who's in control

Picture a restaurant kitchen. The chef reaches for knives and pans on their own, as the dish needs them. The restaurant decides which ingredients are on the counter. And the customer picks from the set menu. MCP's three primitives differ the same way, in who decides when they're used:

primitivecontrolled bytypical host behavior
toolsthe modelthe model decides to call one during a conversation
resourcesthe applicationthe host (or the user) chooses what to read and add to the context
promptsthe usershown as a menu or slash command; the user picks one

A help center article is a resource. The app might show the list, let the user attach one, or search them and add the best match. "Write a weekly report" is a prompt. The user triggers it, fills in a couple of arguments, and gets a carefully written request that took the server's authors an afternoon to tune.

Resources

Each resource has a URI, an address for one item, like a library call number: file:///home/ada/notes.md, https://..., or a scheme the server invents, like docs://help/doc_shipping.

  • resources/list returns entries with uri, name, optional title, description and mimeType, paged with cursors like tools.
  • resources/read with a uri returns contents: a list of items, each with uri, mimeType, and either text or blob (base64 bytes, for images and other binary files).
  • A missing resource is error -32602. Revisions up to 2025-11-25 used -32002, so clients should accept both from older servers.
  • List and read results are cacheable, with ttlMs and cacheScope.

URI templates

Nobody prints a list of every apartment in a city. "Apartment {number}, 12 Main Street" covers them all. Listing every invoice or every customer record isn't practical either. A resource template describes a whole family with an RFC 6570 URI template, such as docs://help/{id} or crm://customers/{customer_id}/orders. resources/templates/list returns them. A client fills in the variables to build a URI, then reads it.

Try it now

The practice server's help articles are resources, with one template, docs://help/{id}. Its one prompt fills a campaign's data into a review request.

Quick check
Your server gives access to 40,000 customer records. How should it expose them as resources?
  1. List all 40,000 in resources/list
  2. A resource template like crm://customers/{customer_id}, plus maybe a search tool to find ids
  3. Put every record in one huge resource
  4. Customer data can't be a resource
Show the answer

A resource template like crm://customers/{customer_id}, plus maybe a search tool to find ids

Templates describe a family without listing it. A small search tool helps the model or user find the right id.

Matching a URI against a template

When a read arrives for docs://help/doc_shipping, the server must find which template it fits and pull out id = "doc_shipping". Think of a clerk checking an address against a form with one blank. For simple templates, where each {name} stands for one path segment, a regular expression does it:

import re

def match_template(template, uri):
    parts = re.split(r"(\{\w+\})", template)
    pattern = "".join(f"(?P<{p[1:-1]}>[^/]+)" if p.startswith("{") else re.escape(p) for p in parts)
    m = re.fullmatch(pattern, uri)
    return m.groupdict() if m else None

print(match_template("docs://help/{id}", "docs://help/doc_shipping"))
print(match_template("crm://customers/{cid}/orders/{oid}", "crm://customers/c42/orders/o7"))
print(match_template("docs://help/{id}", "docs://help/a/b"))
It runs right here in your browser, with nothing to install.

Each {name} becomes a named group that matches one or more characters other than /. Every other character is escaped, so dots and slashes in the template mean themselves. RFC 6570 defines richer forms too, such as {+path} that may include slashes, but this simple level covers most servers.

Prompts

A prompt is a form letter with blanks, written by someone who knows what works. prompts/list returns prompt definitions: name, title, description and arguments (each with a name, description and required flag). prompts/get with a name and arguments returns messages, ready to send to a model:

{"resultType": "complete", "description": "Review of Brand Awareness - Video",
 "messages": [{"role": "user", "content": {"type": "text", "text": "Here is one campaign's data: ... In three sentences, say how it's performing and suggest one change."}}]}

Messages can also embed resources or images. A missing required argument is error -32602. Hosts typically show prompts as slash commands: type /review_campaign cmp_103, and the host calls prompts/get and sends the messages.

Quick check
You want users of your MCP server to run a carefully tuned "quarterly report" request whenever they choose. Which primitive fits?
  1. A prompt, with arguments such as the quarter
  2. A notification
  3. A tool the model might call
  4. A resource
Show the answer

A prompt, with arguments such as the quarter

Prompts are user-controlled templates. The user picks it; the server supplies the tuned wording.

Worked example

Filling a prompt template safely

Prompts are templates with named slots. string.Template fills $name slots and won't evaluate anything else, so user text can't break the template.

from string import Template

PROMPT = Template("Write a $tone product description for $product, under $words words.")
ARGS = [{"name": "product", "required": True}, {"name": "tone", "required": False}, {"name": "words", "required": False}]
DEFAULTS = {"tone": "friendly", "words": "60"}

def get_prompt(arguments):
    missing = [a["name"] for a in ARGS if a["required"] and a["name"] not in arguments]
    if missing:
        raise ValueError(f"missing required arguments: {', '.join(missing)}")
    text = PROMPT.substitute({**DEFAULTS, **arguments})
    return {"resultType": "complete", "messages": [{"role": "user", "content": {"type": "text", "text": text}}]}

print(get_prompt({"product": "the River tent"})["messages"][0]["content"]["text"])
print(get_prompt({"product": "the {secret} mug", "tone": "playful"})["messages"][0]["content"]["text"])
try:
    get_prompt({})
except ValueError as err:
    print("rejected:", err)
In the lesson you can change this and run it.

Optional arguments fall back to defaults, and a missing required one is refused. The second call puts braces in the user's text, and they come through as plain characters. str.format would treat {secret} as another slot and fail. Remember that a prompt's output goes to a model, so text a user supplies is still untrusted input inside it.

Quick check
A server matches resource URIs against the template docs://help/{id}. What does this print?
import re

template = "docs://help/{id}"
pattern = "^" + re.escape(template).replace(r"\{id\}", "(?P<id>[^/]+)") + "$"
m = re.match(pattern, "docs://help/billing")
print(m.group("id"))
  1. docs://help/billing
  2. help
  3. {id}
  4. billing
Show the answer

billing

The template's {id} becomes a named group that captures one path segment, so m.group("id") gives billing, and the server knows which article to read.

Who decides what

MCP has three primitives, and they differ mainly in who's in charge:

primitivewho chooses itan example
toolthe modellook up an order during a conversation
resourcethe app or the userattach a help article as context
promptthe usera "draft a reply" command they pick from a menu

A common mistake is making everything a tool. If your data is something a person wants to attach, like a document, a resource fits better. The app can show a picker, and the model doesn't have to ask for it, or spend a turn finding it.

Prompts never run by themselves. A user chooses one, usually from a slash menu, and the host fills it in. They're templates, not automation.

In plain words: the model picks tools, the app or user picks resources, and the user picks prompts.

Don't list what you can't list

A server with 200,000 help articles shouldn't return all of them from resources/list. A client would have to fetch and hold them all, and most of them would never be used. A URI template, such as docs://help/{id}, describes the whole collection in one line. List only the handful of articles people use most, and let the template cover the rest.

Filling templates safely

str.format looks like the natural way to fill a prompt template. It isn't safe with user text. A form letter works because the blanks hold data. Trouble starts when what someone writes in a blank gets read as part of the form's instructions.

class Config:
    secret = "sk-123"

template = "Reply about: {topic}"
print(template.format(topic="my order"))

# A value is safe, but a template written with user text is not:
user_text = "{config.secret}"
print(("Reply about: " + user_text).format(config=Config))

If user text ever becomes part of the format string, the braces in it get read as instructions, and it can reach into the objects you pass in. Keep templates fixed, validate each argument, and use plain replacement for the values. Remember too that whatever the prompt contains is what the model will read, so a filled-in prompt is untrusted content. Treat it that way.

Reading a resource

A resource is read-only by design. Looking at a library book shouldn't change it. If reading something changes data, it should be a tool.

Practice what you just read

  • 3 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

Which primitive is controlled by the model?

  1. Tools
  2. Resources
  3. Prompts
  4. All three equally

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson