Resources: The App-Controlled Primitive
In the previous lesson, you learned about tools: the model-controlled primitive where Claude Code decides when and how to invoke them. Tools are perfect for actions—"Create an issue," "Send an email," "Delete a file."
But what about data access? When you want Claude Code to read documents, fetch configurations, or search through existing information without making decisions about when to expose that data—that's where resources come in.
Resources represent the inverse control model: The application decides when to expose data. The model reads it.
Think of the difference like this:
- Tools are like buttons your agent can press. Claude Code looks at available buttons and decides which one to push.
- Resources are like file cabinets your application unlocks. Claude Code can browse what's inside, but your application controls the key.
This distinction becomes critical when building Digital FTEs. You don't always want your agent making autonomous decisions about data access. Sometimes you want the agent to work with data you've already decided to provide.
The App-Controlled Paradigm
Consider a document mention feature in Claude Code: You type @quarterly_report.pdf in your prompt, and Claude Code should fetch that document for context.
With tools approach (wrong for this use case):
You type: @quarterly_report.pdf
Claude Code: "I could call the fetch_document tool with this name"
Claude Code: "But should I? Do I have permission? Is this secure?"
Result: Friction, uncertainty, potential security issues
With resources approach (correct):
You type: @quarterly_report.pdf
Application: "This document is already mentioned by the user. I'll expose it as a resource."
Claude Code: "Perfect! I have access to this resource. Reading..."
Result: Seamless, safe, intentional
The key insight: The application has already made the security decision. Resources are pre-authorized data the application explicitly exposes. The model doesn't need to decide—it just reads what's available.
Resource Discovery: resources/list
Before Claude Code can read a resource, it must know what resources exist. MCP handles this through the resources/list request.
The Sequence
Client (Claude Code) Server (Your MCP)
| |
|-----> resources/list ---->|
| |
|<--- Resource definitions -|
| |
The Request
The client sends a request with no parameters:
{
"jsonrpc": "2.0",
"id": 1,
"method": "resources/list",
"params": {}
}
Simple: "What resources do you have?"
The Response
Your MCP server responds with the resource inventory:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resources": [
{
"uri": "docs://documents",
"name": "All documents",
"description": "Directory of all available documents",
"mimeType": "application/json"
},
{
"uri": "docs://documents/quarterly-report-2024",
"name": "Q4 2024 Quarterly Report",
"description": "Financial and operational report for Q4 2024",
"mimeType": "text/plain"
},
{
"uri": "docs://documents/architecture-guide",
"name": "System Architecture Guide",
"description": "Technical documentation for system design",
"mimeType": "text/markdown"
}
]
}
}
Key observations:
- Each resource has a
uri(unique identifier following scheme://path pattern) - Each has a
name(human-readable label) - Each has a
description(what it contains, why the model might want it) - Each has a
mimeType(how to interpret the content—text, JSON, PDF, etc.) - Resources are static metadata—you're describing what's available, not fetching it yet
The URI pattern is critical. Notice:
docs://documents- A direct resource (list all documents)docs://documents/{doc_id}- A templated resource (dynamic, matches patterns)
Direct vs Templated Resources
MCP resources support two patterns:
Direct Resources
Direct resources have static URIs that represent exactly one piece of data:
{
"uri": "docs://documents",
"name": "Documents Index",
"description": "Complete list of available documents",
"mimeType": "application/json"
}
Example use: "Show me all available documents"
Claude Code knows this resource exists, can request it, and gets back the full list.
Templated Resources
Templated resources use URI patterns with placeholders that Claude Code fills in dynamically:
{
"uri": "docs://documents/{doc_id}",
"name": "Document by ID",
"description": "Read full contents of a specific document",
"mimeType": "text/plain"
}
Example use: Claude Code sees @quarterly-report-2024 in your message, recognizes the pattern, and requests docs://documents/quarterly-report-2024.
The flow:
You type: @quarterly-report-2024
Claude Code sees this matches pattern docs://documents/{doc_id}
Claude Code: "I have a templated resource for this!"
Claude Code requests: resources/read with uri = docs://documents/quarterly-report-2024
Server: Reads the document, returns content
Claude Code: Includes it in context automatically
This is why document mentions work seamlessly. The application discovered the template, Claude Code matched it to your mention, and the resource was fetched without explicit tool calls.
Resource Reading: resources/read
Once Claude Code identifies which resource to read, it sends resources/read:
Client (Claude Code) Server (Your MCP)
| |
|---- resources/read ---->|
| (uri) |
| |
|<----- Content ------------|
| |
The Request
The client requests a specific resource by URI:
{
"jsonrpc": "2.0",
"id": 2,
"method": "resources/read",
"params": {
"uri": "docs://documents/quarterly-report-2024"
}
}
The Response
Your server reads the resource and returns the content with appropriate MIME type:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"contents": [
{
"uri": "docs://documents/quarterly-report-2024",
"mimeType": "text/plain",
"text": "QUARTERLY REPORT Q4 2024\n\nRevenue: $2.3M\nCosts: $1.1M\nProfit: $1.2M\n\nKey Highlights:\n- Cloud revenue up 45%\n- Customer retention: 94%\n- Geographic expansion: 3 new markets"
}
]
}
}
Or for JSON resources:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"contents": [
{
"uri": "docs://documents",
"mimeType": "application/json",
"text": "[{\"id\": \"quarterly-report-2024\", \"title\": \"Q4 2024 Report\", \"type\": \"financial\", \"size\": 2048}, {\"id\": \"architecture-guide\", \"title\": \"System Architecture\", \"type\": \"technical\", \"size\": 5120}]"
}
]
}
}
Or if the resource doesn't exist:
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32600,
"message": "Resource not found",
"data": {
"reason": "No resource at docs://documents/unknown-doc"
}
}
}
Key observations:
- The URI in the response matches the request (confirm which resource was fetched)
- The
mimeTypetells Claude Code how to interpret content (text/plain, application/json, text/markdown, etc.) - For binary content (PDFs), return as base64-encoded text
- Errors use JSON-RPC error format with meaningful messages
MIME Type Handling
MIME types tell Claude Code how to interpret resource content. Choosing the right type is critical:
| MIME Type | Use Case | Example |
|---|---|---|
text/plain | Plain text files, unformatted content | .txt files, raw logs, configuration values |
text/markdown | Markdown-formatted documentation | README.md, guides, formatted notes |
text/html | HTML content | Web pages, HTML reports |
application/json | Structured data (objects, arrays) | Configuration files, API responses, data indexes |
application/xml | XML/structured markup | Configuration files, data exports |
application/pdf | PDF documents (returned as base64) | Reports, forms, archived documents |
image/png, image/jpeg | Images (returned as base64) | Screenshots, diagrams |
text/csv | Comma-separated values | Data exports, spreadsheet data |
Good MIME type usage:
docs://config.json→application/json(because it's structured data)docs://readme.md→text/markdown(because it needs formatting preserved)docs://system.log→text/plain(because it's raw text)
Wrong MIME types:
docs://config.json→text/plain(loses structure information)docs://readme.md→text/plain(loses markdown semantics)
Python Implementation with FastMCP
FastMCP makes resource implementation straightforward.
Direct Resource
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("DocumentServer")
@mcp.resource(
"docs://documents",
name="Documents Index",
description="Complete list of available documents",
mime_type="application/json"
)
def list_documents() -> str:
"""
Returns JSON array of all available documents with metadata
"""
documents = [
{
"id": "quarterly-report-2024",
"title": "Q4 2024 Quarterly Report",
"type": "financial",
"size": 2048
},
{
"id": "architecture-guide",
"title": "System Architecture Guide",
"type": "technical",
"size": 5120
}
]
import json
return json.dumps(documents)
How this works:
- Decorate function with
@mcp.resource()- Registers it as MCP resource - Provide URI pattern (no placeholders for direct resources)
- Provide
nameanddescription- These appear in resources/list - Specify
mime_type- How to interpret returned content - Return string content - FastMCP handles JSON serialization
Templated Resource
@mcp.resource(
"docs://documents/{doc_id}",
name="Document by ID",
description="Read full contents of a specific document",
mime_type="text/plain"
)
def fetch_document(doc_id: str) -> str:
"""
Returns full contents of document by ID
"""
# Validate doc_id format
if not doc_id or "/" in doc_id:
raise ValueError(f"Invalid doc_id: {doc_id}")
# Load document from database (pseudo-code)
documents = {
"quarterly-report-2024": "QUARTERLY REPORT Q4 2024\n\nRevenue: $2.3M\nCosts: $1.1M...",
"architecture-guide": "SYSTEM ARCHITECTURE GUIDE\n\nThis document describes...",
}
if doc_id not in documents:
raise ValueError(f"Document not found: {doc_id}")
return documents[doc_id]
How templating works:
- URI pattern includes
{doc_id}placeholder - When Claude Code requests
docs://documents/quarterly-report-2024, FastMCP extractsdoc_id = "quarterly-report-2024" - Passes
doc_idas parameter to your function - Your function validates and returns content
- FastMCP returns with appropriate MIME type
Multiple Templated Parameters
@mcp.resource(
"docs://documents/{doc_type}/{doc_id}",
name="Filtered Document Access",
description="Read documents filtered by type and ID",
mime_type="application/json"
)
def fetch_filtered_document(doc_type: str, doc_id: str) -> str:
"""
Returns document matching both type and ID filters
"""
# Validate parameters
valid_types = ["financial", "technical", "legal"]
if doc_type not in valid_types:
raise ValueError(f"Invalid doc_type: {doc_type}")
if not doc_id or "/" in doc_id:
raise ValueError(f"Invalid doc_id: {doc_id}")
# Load from database
doc = database.find_document(doc_type=doc_type, doc_id=doc_id)
if not doc:
raise ValueError(f"Document not found: {doc_type}/{doc_id}")
import json
return json.dumps(doc)
Output: MCP broadcasts this resource pattern:
{
"uri": "docs://documents/{doc_type}/{doc_id}",
"name": "Filtered Document Access",
"description": "Read documents filtered by type and ID",
"mimeType": "application/json"
}
Claude Code can now request: docs://documents/financial/quarterly-report-2024
Resources vs Tools: When to Use Each
This is the critical design decision. Both primitives provide data access, but they differ fundamentally:
| Aspect | Resources | Tools |
|---|---|---|
| Control | App-controlled (application decides what's exposed) | Model-controlled (model decides when to invoke) |
| Purpose | Read existing data | Perform actions or side effects |
| Timing | Pre-authorized access | On-demand invocation |
| Side effects | None (read-only) | Possible (mutations, side effects) |
| Use case | Document mention (@file), context injection, reference data | Creating issues, sending emails, updating records |
| Security model | Application pre-approves exposure | Model must request permission implicitly |
| Example | "Fetch this document I mentioned" | "Create a GitHub issue from this problem" |
When to use Resources:
- Pre-authorized data: Application has already decided this data should be available
- Read-only access: No side effects, just fetching information
- Document mentions: @document features, context injection
- Reference data: Configuration, schemas, lookup tables
- User-initiated context: Data the user explicitly referenced
When to use Tools:
- Autonomous actions: Model decides to create/modify/delete
- Side effects required: Changes state in external systems
- Conditional execution: "Should I do this?" requires model reasoning
- Permissions: Model must request, user (or system) approves
- Error recovery: Tool calls support structured error handling and retries
Example scenarios:
Scenario: "I want Claude Code to access my codebase"
- Resource: Expose project files as
code://files/{filename}- Claude Code reads files you mention - Tool: Provide
code_search(query)- Claude Code decides when to search
Which is better? Both! Resources for browsing, tools for actions.
Scenario: "I want Claude Code to modify my codebase"
- Resource: WRONG - You don't want pre-authorized write access
- Tool: RIGHT - Model requests to modify, system validates safely
Design Pattern: Document Mention Feature
Here's how to implement a robust document mention system combining resources and tools:
@mcp.resource(
"docs://documents",
name="Documents Index",
description="List of all available documents for mention",
mime_type="application/json"
)
def list_documents() -> str:
"""Expose document list so Claude Code knows what's available"""
documents = load_all_documents()
return json.dumps([
{
"id": doc.id,
"name": doc.name,
"type": doc.type
}
for doc in documents
])
@mcp.resource(
"docs://documents/{doc_id}",
name="Document Content",
description="Full content of a specific document",
mime_type="text/plain"
)
def fetch_document(doc_id: str) -> str:
"""Fetch requested document - application controls exposure"""
doc = database.find_document(doc_id)
if not doc or not doc.is_accessible():
raise ValueError(f"Document not accessible: {doc_id}")
return doc.content
@mcp.tool(
name="create_document_reference",
description="Create a reference to a document for future context"
)
def create_reference(doc_id: str, context: str) -> dict:
"""Tool for creating document references - model-controlled action"""
doc = database.find_document(doc_id)
if not doc:
raise ValueError(f"Document not found: {doc_id}")
reference = database.create_reference(
doc_id=doc_id,
context=context,
created_by="claude-code"
)
return {
"reference_id": reference.id,
"doc_id": doc_id,
"status": "created"
}
How it flows:
- Claude Code requests
resources/list→ Seesdocs://documentsand pattern - User types
@quarterly-report-2024in their message - Claude Code matches against pattern, requests
resources/readwith that doc - Application exposes document (pre-authorized)
- Claude Code analyzes document in context
- If Claude Code wants to reference this for future use, it calls
create_document_referencetool - Tool is model-controlled—application validates that this action is safe
Try With AI
Use Claude Code or your AI companion for these exercises.
Prompt 1: Design a Resource Schema
I want to create an MCP resource system that exposes a knowledge base
to Claude Code. The knowledge base has:
- Articles (organized by category and ID)
- Configuration documents
- Frequently asked questions
Design a resource schema for me:
1. What direct resources should I expose? (list all articles, FAQ index)
2. What templated resources should I expose? (access specific articles)
3. What URIs and MIME types would work best?
4. How would a user mention an article in their prompt?
Show me the resources/list JSON response your system would return.
What you're learning: Resource schema design requires understanding both how to expose data and how users (and Claude Code) will discover and access it.
Prompt 2: Resource vs Tool Decision
I'm building a project management MCP server. For each of these scenarios,
help me decide: Resource or Tool?
1. User wants Claude Code to read the current project plan
2. User wants Claude Code to create a new task
3. User wants Claude Code to check the status of in-progress tasks
4. User wants Claude Code to update task descriptions
5. User wants Claude Code to reference a specific document in discussions
6. User wants Claude Code to generate a report from task data
For each, explain:
- Why resource or tool is appropriate
- What the API would look like
- How the user would trigger it
What you're learning: Understanding the fundamental difference between app-controlled (resources) and model-controlled (tools) primitives helps you design secure, intuitive agent systems.
Prompt 3: Implement a FastMCP Resource
I want to build an MCP resource that exposes configuration data from YAML files.
The resource should:
- List all available .yaml files in a config directory (direct resource)
- Allow fetching specific config files by name (templated resource)
- Return proper MIME types (application/json or text/plain)
- Validate that only .yaml files are accessible (security)
- Handle missing files gracefully
Write Python code using FastMCP that implements both the direct
and templated resources. Include error handling and validation.
What you're learning: Implementation patterns for real resources that handle validation, security, and dynamic content—the skills you'll need to build production MCP servers.
Safety Note
Resources provide read-only access, but be careful: If a resource exposes sensitive data (API keys, passwords, private documents), you've given Claude Code access to that data. Always validate which resources are exposed and consider access controls based on the context or user. Resources are pre-authorized—make sure you intended to authorize them.