Accessing resources
The server exposes resources. The inspector confirms they work. Now the client needs a matching read_resource method so your application can fetch resources and splice their content into prompts. The mechanics are almost identical to call_tool, with one small wrinkle: the response can carry different content types, and your client has to parse each one appropriately.
Adding read_resource to the client
Open mcp_client.py and add a new method alongside list_tools and call_tool:
import json
from typing import Any
from mcp import types
from pydantic import AnyUrl
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
# Binary or unknown content types — return raw for now
return resource
Walk through what this does:
self.session().read_resource(AnyUrl(uri))— sends aReadResourceRequestover the session.AnyUrlis Pydantic's permissive URL type; wrapping the string in it lets the SDK validate the URI without being overly strict about scheme.result.contents[0]— the response carries a list of content blocks. Most resources return a single content entry, so index zero is what you want.isinstance(resource, types.TextResourceContents)— the first content block's type determines parsing. For text-based resources (JSON, plain text, markdown),TextResourceContentsis what you'll see.resource.mimeType == "application/json"— check the MIME type to decide whether to parse. JSON getsjson.loads(); anything else stays as a raw string.- The fallback — binary resources or unknown content types come back as raw objects. You can extend this later for image/PDF resource types as needed.
Required imports
Two new imports are worth calling out explicitly:
json— for parsing JSON-typed responses.AnyUrlfrompydantic— the URI wrapper type the MCP SDK expects.
You'll also use types from the MCP SDK for TextResourceContents; that's the same types module you've been importing for Tool and CallToolResult.
Testing directly
Before wiring resources into the main chat loop, test the method in isolation:
async def main():
async with MCPClient(
command="uv", args=["run", "mcp_server.py"]
) as client:
doc_list = await client.read_resource("docs://documents")
print("Available docs:", doc_list)
content = await client.read_resource("docs://documents/deposition.md")
print("Deposition content:", content)
Run it. You should see:
- A Python list of document IDs for the first call (because the resource's MIME type is
application/jsonandjson.loadsparsed it into a list). - The plain string of the deposition document for the second call (because the MIME type was
text/plain).
If you get back raw strings when you expected lists, that's the symptom of a missing or wrong MIME type on the server side. If the call hangs, the URI probably doesn't match a registered resource.
The document mention feature
With read_resource in place, you can build the @document_name mention feature:
- User types
@— the UI callsclient.list_resources()or fetchesdocs://documentsto populate an autocomplete dropdown. (The course's simpler version hits the resource directly.) - User selects a document — the UI inserts
@deposition.mdinto the message. - User submits the message — before sending to Claude, your application:
a. Parses out any
@doc_idtokens. b. Callsclient.read_resource(f"docs://documents/{doc_id}")for each one. c. Injects the fetched content into the prompt (wrapped in XML tags, Module-5 style) before sending. - Claude receives the enriched prompt — with the document content already embedded — and responds without needing a tool call to fetch it.
The key phrase is "without needing a tool call to fetch it." This is why resources exist: when your application already knows which document the user wants to reference, there's no reason to ask Claude to make that decision. You fetch the data directly, compose the prompt, and pay one round trip instead of two.
Direct injection vs tool calls — when each wins
| Scenario | Use a resource | Use a tool call | |---|---|---| | User explicitly picked the document via UI | ✓ | | | User asks "what documents do I have?" | ✓ (list resource) | | | User asks "summarise the one about the condenser tower" | | ✓ (Claude has to search) | | App wants to pre-load context every request | ✓ | | | Claude needs to decide which document based on the conversation | | ✓ |
Resources win when the data requirement is explicit and static. Tools win when the data requirement depends on Claude's reasoning about the conversation. Most real applications use both, depending on the interaction.
Separation of concerns
The important structural point: the MCP client is a reusable layer, and read_resource is the same kind of reusable method as list_tools and call_tool. Your application code — main.py, the chat loop, the UI — uses these methods through the client without knowing or caring about URIs, session state, or MIME type parsing.
When you later want to add another resource to the server — say config://settings or users://{user_id}/profile — the client doesn't change. The server adds the resource, the application calls read_resource(uri), and the existing parsing logic handles the response. The client is the integration surface; everything MCP-specific lives there and nowhere else.
The next lesson introduces prompts — the third type of MCP endpoint, for exposing pre-authored prompt templates that clients can parameterise and pass to Claude.
Key Takeaways
- 1Add read_resource(uri) to the MCPClient wrapper to fetch resources by URI without exposing session details to the rest of the application.
- 2The SDK's session.read_resource expects an AnyUrl from pydantic, so wrap your URI string in AnyUrl(uri) when calling it.
- 3Response contents carry a MIME type; parse application/json with json.loads and return text/plain as-is, with a fallback for binary or unknown types.
- 4Use resources for the document mention feature: parse @doc_id tokens from user input, fetch the content via read_resource, and inject it directly into the prompt.
- 5Resources win when the data requirement is explicit and static; tools win when Claude's reasoning needs to decide which data to fetch.
- 6The MCP client is a reusable layer — adding new resources on the server does not require changes in the client, only in the application code that chooses to use them.