Read resources by MIME type
The server exposes resources. Now the client needs to read them — and, importantly, make the right parsing decision based on the MIME type the server hands back. This is the client-side counterpart to the previous lesson. Once it is in place, the @document_name mention flow actually works end to end.
Why resources instead of tools here
Stop and notice what is happening. When the user types @plan.md, your application is deciding — right there, before Claude ever sees the message — that the plan document should be part of the context. Claude is not asking for it. The app is providing it. The document contents get injected directly into the prompt that goes to the model, no tool call required.
That is the key insight about resources. Resources let your application augment prompts with data upfront, eliminating the need for Claude to make a tool call to access the same information. Tools are model-controlled. Resources are app-controlled. Use the one whose control pattern matches your UX.
Adding the imports
Open mcp_client.py. Two new imports at the top:
import json
from pydantic import AnyUrl
You need json for parsing JSON-typed resources, and AnyUrl from Pydantic to wrap the URI string into the type the SDK expects.
Implementing read_resource
Add this method to your client class:
async def read_resource(self, uri: str) -> Any:
result = await self.session().read_resource(AnyUrl(uri))
resource = result.contents[0]
if isinstance(resource, types.TextResourceContents):
if resource.mimeType == "application/json":
return json.loads(resource.text)
return resource.text
Walk through it. You call session().read_resource with the URI wrapped in AnyUrl. The SDK sends a ReadResourceRequest, the server matches it to the right resource function, and you get back a result whose .contents is a list — typically with one entry, so you grab result.contents[0].
From there, the MIME type tells you how to parse:
- If the resource is text-typed (
TextResourceContents) and the MIME type isapplication/json, calljson.loads(resource.text)and return the parsed object. This is howdocs://documents— your document list — becomes a real Python list on the client side. - Otherwise, return
resource.textuntouched. This is howdocs://documents/{doc_id}— your plain-text document content — flows through without accidental parsing.
Two branches, one per MIME type you care about. Add more branches as you add more resource types.
What the user actually experiences
With read_resource in place, run the chatbot:
uv run main.py
Now the @ mention flow lights up:
- The user types
@in the chat input. - The CLI calls
read_resource("docs://documents")to get the list of document IDs and shows them in an autocomplete. - The user picks one with the arrow keys and space.
- The CLI calls
read_resource("docs://documents/{selected}")to get the content. - That content is injected directly into the prompt sent to Claude.
Claude never makes a tool call. It just receives a prompt that already contains the document content. The round-trip from @ to a response is faster, cheaper, and more predictable than it would be if Claude had to decide to fetch the document itself.
The pattern to hold onto
Resources are how you say: "my application knows, ahead of time, what data Claude should have — just put it in the prompt." Tools are how you say: "Claude will figure out at runtime what it needs — let it ask." Both are valid. The question is always who should be in control of the fetch, and resources are the MCP answer for app-controlled data.
Key Takeaways
- 1The client-side read_resource function fetches data from an MCP server and branches on the MIME type to decide how to parse the response.
- 2Resources enable prompt augmentation: your application can inject data directly into the prompt without making Claude issue a tool call.
- 3Parsing logic is driven by the resource's mimeType — JSON content is parsed via json.loads, while text content is returned as-is.
- 4Resources are app-controlled and tools are model-controlled — choose the primitive whose control pattern matches who should decide when to fetch the data.