Resources & Prompts
Lesson 06 · MCP Servers · ~10 min read
The three primitives, in scope
Lesson 01 sketched the three things an MCP server can expose; this lesson is where two of them finally get hands-on. The reason the protocol bothers separating them is that the LLM and the user consume each one differently, and confusing the categories produces servers that technically work but feel wrong in a real client. Tools are actions — the LLM decides when to invoke them and the call mutates state, calls an external API, or computes something fresh. Resources are data — read-only blobs the client lists and reads on the user's (or the LLM's) behalf, much like files in a filesystem. Prompts are templates — pre-authored message scaffolds the user picks from a menu, often via a slash command, to seed a conversation.
| Primitive | Direction | When consumed |
|---|---|---|
| Tool | LLM → server (call) | Mid-conversation, when the model decides |
| Resource | Client → server (read) | On demand by client/user; attached to context |
| Prompt | User → client → server (fetch) | At the start of a turn, as a slash-command template |
Resources in depth
A resource has three things that matter: a URI, a MIME type, and
contents. The URI uses standard scheme://path form with an optional
query string. The scheme is yours to choose — built-in servers reach for file:///
for filesystem paths and sqlite:/// for database rows, and your own server is welcome
to invent a scheme that names its domain (the PKB you are building uses pkb://). MIME
types tell the client how to render and how the LLM should treat the bytes: text/plain,
text/markdown, application/json, or image/png are the common
ones.
Resources come in two flavours. Static resources are enumerable up front — the server
knows their URIs at start-up and returns them all from resources/list. Dynamic
resources are templated: the URI carries a parameter (pkb://notes/{title}) and the
client substitutes a value before calling resources/read. The MCP spec also defines
a notifications/resources/list_changed message the server can push when its catalogue
changes — so a long-running server that ingests new notes can tell the client to re-fetch the list
without restarting the connection.
In FastMCP, the surface is one decorator: @mcp.resource(uri_template). The function
body returns the bytes (or a string, which becomes text/plain automatically); the
decorator handles registration with resources/list and dispatch from
resources/read. Parameters in the URI template become arguments of the decorated
function, exactly the way Flask or FastAPI path parameters do. Look at the reference
mcp-server-everything server in the official repo for canonical examples covering
text, image, and binary resources end to end.
Prompts in depth
Prompts answer a question tools can't: "what's the canonical way to start a conversation
about this server's domain?" They are user-facing templates, not LLM-callable actions. In Claude
Desktop they appear as slash commands; in Cursor they show up in the command palette. When the
user picks one, the client fetches it with prompts/get, substitutes any parameters
the prompt declares, and inserts the resulting messages into the conversation as if the user had
typed them.
A FastMCP prompt is, again, one decorator: @mcp.prompt(). The simplest shape returns
a single string — that becomes a single-message prompt with the string as user text. The fuller
shape returns a list of Message objects, each with a role
("user" or "assistant") and content. Returning a list lets you stage a
whole opening, including an assistant-side primer ("I'll help you triage your notes; what should
we look at first?"). Parameters declared on the function appear as required fields in the prompt's
schema; the client collects them in a form before invoking the prompt. The everything
reference server ships both flavours — a one-liner simple_prompt and a parameterised
complex_prompt — and is the fastest way to see the shapes side by side.
Choosing the right primitive
Once you know all three exist the temptation is to reach for whichever is most familiar — usually tools, because they were introduced first and feel like "doing things". Resist that. The decision-tree is short:
- Tool — anything that does an action: search, compute, write, call an external API, mutate state, or return a value that depends on parameters and/or the current moment in time.
- Resource — anything read-only the client should be able to discover and fetch independently of the LLM's reasoning. If a human user, browsing the server's catalogue, would want to attach this blob to their context window, it is a resource.
- Prompt — anything that's a reusable user-invoked template. If the answer to "who decides when this fires?" is the user picking from a menu, not the LLM mid-turn, it is a prompt.
Two worked examples for the ambiguous cases that come up every time someone designs their first
server. Should get_weather(city) be a tool or a resource? A tool, because the
city is a dynamic input picked by the LLM and the result varies over time — listing every
weather://{city} in resources/list would be either impossibly large or
immediately stale. Should the contents of ~/.bashrc be exposed? A resource:
the bytes are read-only, the URI is naturally file:///Users/.../.bashrc, and the
client should be able to enumerate the user's dotfiles without the LLM having to "call" anything.
URI design
URIs are part of the public contract of your server; you live with the ones you ship. A few
conventions repay themselves over and over. Use plural collection nouns:
pkb://notes/{title} reads as "the notes collection, the one named {title}", whereas
pkb://note/{title} reads as a single thing that needs naming twice. Nest paths
for nested concepts: pkb://notes/{title}/attachments/{name} beats
pkb://attachment?note={title}&name={name} on every measure — it sorts, it
documents itself, and it composes. Avoid query strings unless you actually need
filtering: a query string says "this URI represents a search", and a search is almost
always a tool, not a resource. The bare pkb://{title} form is the worst of all
worlds — it gives the LLM no clue what kind of thing it is fetching.
Common pitfalls
- Using a tool when a resource fits better. If the data is read-only and the
client could meaningfully list it, exposing it as a tool forces the LLM to call the
tool to discover anything, rather than seeing the entries in
resources/listfor free. - Returning HTML or binary from a prompt. Prompts are user-readable text that end up in the message stream verbatim. If you need to embed structured content, expose it as a resource and reference the URI from the prompt.
- Hard-coding URIs the client can't discover. A tool that mints a random
pkb://notes/abc123and returns the string is fine as a confirmation, but if there's no matching entry inresources/listthe LLM has no way to fetch that note later. Always make sure dynamic resources are reachable via the templated URI.
The full PKB shape after this lesson
Pulling the two lessons together, your server.py now exposes five primitives. From
Lesson 04, the echo tool — your sanity-check round-tripper. From Lesson 05, the
search tool with a Pydantic input model. New in this lesson: one static resource
(pkb://about, a one-paragraph description of the server itself), one dynamic resource
(pkb://notes/{title}, the body of a named note), and one prompt
(welcome, a single-message opener that introduces the PKB and suggests two
starting actions). That is the seed shape every later lesson grows from: tools for what the LLM
does, resources for what the user/LLM reads, and prompts for how a conversation
begins.
References
- MCP specification — resources — normative spec for
resources/list,resources/read, URI templates, and thelist_changednotification. - MCP specification — prompts — normative spec for
prompts/list,prompts/get, message structure, and prompt arguments. - FastMCP docs — resources — decorator reference and worked examples for
@mcp.resource. - FastMCP docs — prompts — decorator reference and worked examples for
@mcp.prompt. - mcp-server-everything source — canonical reference implementations of every primitive, including text/image/binary resources and single/multi-message prompts.