When the app hands Claude the data
Picture the user experience you want: someone typing into the chatbot can reference a specific document by typing @ and the document's name, the way @ mentions work in Slack. The UI needs two things from the MCP server to pull that off — a list of available document IDs for the autocomplete, and the actual content of whichever document the user picks.
Neither of those is a tool. You do not want Claude deciding to fetch the list of documents. Your application decides, because your application is drawing the autocomplete menu. That is the distinction resources capture.
Resources expose read-only data to clients — the MCP equivalent of GET endpoints on an HTTP server. They are the right primitive whenever application code needs to fetch data upfront, rather than asking the model to fetch it during a conversation.
How resources flow
A resource is identified by a URI. When your client wants data, it sends a ReadResourceRequest with the URI; the server matches the URI to a function, runs it, and returns a ReadResourceResult. Same request/response shape as tools — different semantics.
Two types of resources
There are exactly two kinds of resources you need to know about:
- Direct (static) resources — the URI is fixed. No parameters. Perfect for fetching a list or a fixed blob of data.
- Templated resources — the URI contains parameter placeholders like
{doc_id}. The SDK parses the URI, extracts the parameters, and passes them to your function as keyword arguments.
In the document system, you want both: a direct resource that returns the list of all document IDs, and a templated resource that returns the contents of a specific document.
Defining a direct resource
Back in mcp_server.py, add the direct resource for listing documents:
@mcp.resource(
"docs://documents",
mime_type="application/json"
)
def list_docs() -> list[str]:
return list(docs.keys())
Three things to notice. The URI docs://documents is static — no braces, no parameters. The mime_type tells the client how to interpret the returned data. And the function just returns a plain Python list — you do not need to call json.dumps. The SDK handles serialisation automatically.
Defining a templated resource
Now the templated resource for fetching one document's contents:
@mcp.resource(
"docs://documents/{doc_id}",
mime_type="text/plain"
)
def fetch_doc(doc_id: str) -> str:
if doc_id not in docs:
raise ValueError(f"Doc with id {doc_id} not found")
return docs[doc_id]
The {doc_id} segment in the URI becomes the doc_id keyword argument on the function. When a client requests docs://documents/plan.md, the SDK parses out plan.md, calls fetch_doc(doc_id="plan.md"), and ships the result back as plain text. Validation stays the same as on tools — if the document does not exist, raise and let the SDK translate.
MIME types are hints, not constraints
The mime_type parameter tells the client what kind of data to expect so it can deserialise intelligently. Common values:
application/json— structured data the client should parse as JSON.text/plain— raw text, no parsing needed.application/pdf— binary blob.
The SDK does not enforce the MIME type against your return value. Picking the right hint is your job, because the client side will branch on it when it decides whether to call json.loads or return the text untouched.
Testing resources in the Inspector
Resources show up in the Inspector alongside tools — but in two separate tabs:
- Resources — lists your direct/static resources. Click one to invoke it and see the result.
- Resource Templates — lists your templated resources. Select one and you are prompted for each URI parameter before it runs.
Spin up the inspector:
uv run mcp dev mcp_server.py
Open the Resources tab and click docs://documents. You should get back the list of six document IDs. Then switch to Resource Templates, pick docs://documents/{doc_id}, enter plan.md, and run it. You should see the plan document's content come back as plain text.
The inspector shows you the exact response structure your client will receive, including the MIME type and serialised payload — which matters for the next lesson, where you will write the client-side code that consumes these responses.
Key Takeaways
- 1Resources expose read-only data from an MCP server using URIs, playing a similar role to GET endpoints on an HTTP server.
- 2Direct resources use static URIs with no parameters; templated resources embed parameters that the SDK parses into function keyword arguments.
- 3The @mcp.resource decorator handles auto-serialisation, so your function can return plain Python values without manual JSON conversion.
- 4The mime_type parameter is a hint to clients about how to deserialise the response — picking the right one matters for the client-side parsing logic.