Resources and prompts
Readable data, URI templates and reusable prompt templates.
Create a free account Try part of it here
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
- Who's in control
- Matching a URI against a template
- Filling a prompt template safely
- Who decides what
- 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:
| primitive | controlled by | typical host behavior |
|---|---|---|
| tools | the model | the model decides to call one during a conversation |
| resources | the application | the host (or the user) chooses what to read and add to the context |
| prompts | the user | shown 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/listreturns entries withuri,name, optionaltitle,descriptionandmimeType, paged with cursors like tools.resources/readwith aurireturnscontents: a list of items, each withuri,mimeType, and eithertextorblob(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
ttlMsandcacheScope.
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.
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.
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"))
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.
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.
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)
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.
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"))
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:
| primitive | who chooses it | an example |
|---|---|---|
| tool | the model | look up an order during a conversation |
| resource | the app or the user | attach a help article as context |
| prompt | the user | a "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
Try a quiz question
Which primitive is controlled by the model?