Skip to main content

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 mimeType tells 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 TypeUse CaseExample
text/plainPlain text files, unformatted content.txt files, raw logs, configuration values
text/markdownMarkdown-formatted documentationREADME.md, guides, formatted notes
text/htmlHTML contentWeb pages, HTML reports
application/jsonStructured data (objects, arrays)Configuration files, API responses, data indexes
application/xmlXML/structured markupConfiguration files, data exports
application/pdfPDF documents (returned as base64)Reports, forms, archived documents
image/png, image/jpegImages (returned as base64)Screenshots, diagrams
text/csvComma-separated valuesData exports, spreadsheet data

Good MIME type usage:

  • docs://config.jsonapplication/json (because it's structured data)
  • docs://readme.mdtext/markdown (because it needs formatting preserved)
  • docs://system.logtext/plain (because it's raw text)

Wrong MIME types:

  • docs://config.jsontext/plain (loses structure information)
  • docs://readme.mdtext/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:

  1. Decorate function with @mcp.resource() - Registers it as MCP resource
  2. Provide URI pattern (no placeholders for direct resources)
  3. Provide name and description - These appear in resources/list
  4. Specify mime_type - How to interpret returned content
  5. 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:

  1. URI pattern includes {doc_id} placeholder
  2. When Claude Code requests docs://documents/quarterly-report-2024, FastMCP extracts doc_id = "quarterly-report-2024"
  3. Passes doc_id as parameter to your function
  4. Your function validates and returns content
  5. 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:

AspectResourcesTools
ControlApp-controlled (application decides what's exposed)Model-controlled (model decides when to invoke)
PurposeRead existing dataPerform actions or side effects
TimingPre-authorized accessOn-demand invocation
Side effectsNone (read-only)Possible (mutations, side effects)
Use caseDocument mention (@file), context injection, reference dataCreating issues, sending emails, updating records
Security modelApplication pre-approves exposureModel must request permission implicitly
Example"Fetch this document I mentioned""Create a GitHub issue from this problem"

When to use Resources:

  1. Pre-authorized data: Application has already decided this data should be available
  2. Read-only access: No side effects, just fetching information
  3. Document mentions: @document features, context injection
  4. Reference data: Configuration, schemas, lookup tables
  5. User-initiated context: Data the user explicitly referenced

When to use Tools:

  1. Autonomous actions: Model decides to create/modify/delete
  2. Side effects required: Changes state in external systems
  3. Conditional execution: "Should I do this?" requires model reasoning
  4. Permissions: Model must request, user (or system) approves
  5. 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:

  1. Claude Code requests resources/list → Sees docs://documents and pattern
  2. User types @quarterly-report-2024 in their message
  3. Claude Code matches against pattern, requests resources/read with that doc
  4. Application exposes document (pre-authorized)
  5. Claude Code analyzes document in context
  6. If Claude Code wants to reference this for future use, it calls create_document_reference tool
  7. 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.