Yesterday we drew the line between tools and resources. Today we went deep on resources — what they are, how they’re addressed, how a client finds them, and the one design trap nobody mentions.
1. The Test That Decides
Does calling it change anything?
| Rule | Library example | |
|---|---|---|
| Resource | Read-only. Call it a thousand times and the system is unchanged | Viewing the book catalogue |
| Tool | Executes an operation. It may change state | Reserving a book — the librarian’s records now differ |
A resource that modifies something is a bug, not a design choice. If your “resource” writes a record that matters, it’s a tool wearing the wrong label.
Tools can also carry behaviour hints — read-only, destructive — so a client can treat a delete differently from a lookup. Resources need no such hint, because read-only is the whole contract.
2. Every Resource Has a URI
A resource is identified by a URI, and that URI is its entire public interface. Get the addressing right and everything else follows.
library://policies/return
library://bookinfo/{isbn}
cafe://menu/breakfast
The scheme is yours to choose. library://, cafe://, file://, https:// — pick something that names your domain and stay consistent. The URI is what a client stores, logs and refers to later, so treat it with the care you’d give a database key or a REST path.
Design guidance that pays off:
- Hierarchical, not flat.
cafe://menu/breakfastbeatscafe://breakfastmenu. - Stable. A URI that changes breaks every client that recorded it.
- Predictable. Someone reading one of your URIs should be able to guess the next.
3. The Two Kinds of Resource
Static URIs — one address, one thing
library://policies/return
For content that doesn’t vary by parameter: a returns policy, a leave policy, today’s specials, an org chart. Fixed address, fixed meaning.
Resource templates — one pattern, unlimited items
library://bookinfo/{isbn}
The {isbn} is a parameter. You write the template once and every ISBN in the catalogue is addressable through it — thousands of resources from a single definition.
Templates are what make resources scale. Any time your data has an identifier — a book ISBN, an order number, an employee ID, a menu item code — that’s a template, not a hundred static entries.
@mcp.resource("library://bookinfo/{isbn}")
def book_info(isbn: str) -> str:
"""Catalogue details for a book, by ISBN."""
...
4. How a Client Actually Uses Them
Worth knowing the shape of the conversation, because it explains the design rules above:
| Step | What happens |
|---|---|
| List | The client asks what static resources exist |
| List templates | Separately, it asks what templates exist |
| Read | It requests a specific URI and gets the content back |
Templates are listed separately from static resources. A client that only lists one won’t see the other — another reason to be deliberate about which you’re building.
What a read returns is the URI, a MIME type, and the content itself — text for anything textual (plain text, JSON, Markdown), or binary for images and other blobs. Setting an honest MIME type matters: it’s how the client decides whether to render, parse or attach your content.
5. Keeping Clients Current: Subscriptions
This is a protocol feature, not a third kind of resource — the session grouped it with the two above, and it’s worth separating.
- A client can subscribe to a resource URI.
- When the content changes, the server sends an update notification, and the client re-reads it.
- A server can also announce that its list of resources has changed, when resources appear or disappear.
A path like /latest is just a naming convention you might adopt. It isn’t what makes updates work — the subscription is.
6. The Trap: Resources Are Application-Controlled
This is the part that catches people out, and it’s the most important thing in this post.
Tools are model-controlled — the model decides to call them. Resources are application-controlled: the host decides what to do with them. In some clients that means the user has to attach a resource explicitly before the model ever sees it. Other clients may ignore resources entirely.
The consequence for your design:
If a piece of information is essential to answering a question, don’t rely on the model finding it through a resource. Expose it as a tool as well.
Resources are excellent for content the user chooses to bring into context — a policy document, a reference table, a file. For data the agent must be able to fetch on its own initiative, a read-only tool is the reliable path.
That’s not a contradiction of section 1. It’s the practical layer on top: the read/write distinction tells you what something is; the control model tells you how it will actually get used.
7. What Each One Needs
| Required | |
|---|---|
| Resource | A unique URI (and a sensible MIME type) |
| Tool | A name and a defined set of arguments |
Forget the URI and the resource never registers — silently, with no error to point at.
8. Today’s Build: Charminar Cafe
A dummy MCP server for a Hyderabad cafe serving chai, bakery items and biryani.
Classify before you code:
| Capability | Type | Why |
|---|---|---|
| Today’s menu | Resource, static — cafe://menu/today | Pure information |
| One item’s details | Resource, template — cafe://item/{item_id} | Identifier in the address |
| Opening hours | Resource, static | Doesn’t change per call |
| Place an order | Tool | Changes state |
| Cancel an order | Tool | Changes state |
| Check an order’s status | Judgement call — read-only, but the agent needs it unprompted, so make it a tool | |
That last row is section 6 in action.
9. Pydantic: Validating What Comes In
The model fills in your tool’s arguments, and models are not careful typists.
The problem: a quantity field receives "4A".
What Pydantic does: in FastMCP, your type hints and Pydantic models generate the input schema. Bad input is rejected before your function body runs, and the validation error goes back to the model, which can read it and retry correctly.
- Declare a field as
intand it must arrive as a number, not"4"in quotes. - Dictionaries work, but a Pydantic class states the contract explicitly — which matters the moment a second person touches the code.
The same applies to template parameters: an ISBN or an order ID arriving in a URI deserves validation too.
10. A Useful Trick with the Git Server
Hand the MCP a commit URL and ask what changed. You get the change set explained without reading the diff yourself.
✅ Action Items
- Build the Charminar Cafe server, and classify every capability as resource or tool before writing it. Be ready to defend each choice.
- Use a resource template for anything with an identifier. If you’ve written three near-identical static resources, you wanted a template.
- Design your URI scheme first, on paper. Hierarchical, stable, predictable.
- Test what your client actually does with resources — whether they’re picked up automatically or need attaching. Then decide which of them also need a tool.
- Add Pydantic validation to one tool and feed it bad input deliberately.
New to Python? Around 120 recorded sessions exist. Two videos a day for ten days gets you to the current pace — the first ten are deliberately simple and carry most of the foundation.
⏭️ Next up: finishing the cafe server, and securing it.
