MCP Resources — Keeping Context Tidy w/ File URIs
This article is part of a 6-part series. To check out the other articles, see:
Primitives: Tools | Resources | Prompts | Sampling | Roots | Elicitation
Enabling a server to return large amounts of text is challenging due to LLM context size limitations 🚫
However, what if it could instantly reference any file—allowing the LLM to read from it securely—without dragging files into the chat? That’s the Resources primitive: a lightweight URI that points to data instead of moving it. It lets an AI "see" a 500 MB file via a local Python tool or access confidential financial reports without crashing the system, exceeding budget, or violating compliance.
Unlike most tool-calls that return data directly to the LLM, Resources return a URI for that resource, simplifying access. For example, if you need to return 100K keywords for SEO analysis—data too large for context—you can avoid degrading performance by referencing the CSV instead of loading it entirely. A Python MCP (or code-interpreter tool) can then read that reference, preserving efficiency and preventing LLM overload.
In Part I we’ll see why stuffing data into prompts fails at scale and how Resources fix it with a simple pointer-based model. Part II walks through the four core JSON-RPC methods—list
, read
, subscribe
, and templates
—that make discovery and access predictable. Part III shows working code from a basic file server to a HIPAA-compliant audit trail, and Part IV closes with the production checklist—caching, rate limiting, and security guards—so you can deploy with confidence.
Part I: Foundation - The "Why & What"
This section establishes the conceptual foundation. If a reader only reads this part, they will understand what Resources are, why they are critically important, and where they fit in the ecosystem.
1. Core Concept
1.1 One-Sentence Definition
The Resources
primitive provides AI agents with secure, read-only, token-efficient access to large-scale or proprietary data by referencing it with a pointer (a URI) instead of passing the data itself.
1.2 The Core Analogy
The best analogy for a Resource
is a secure library card for an AI.
- The World Without Resources: To give an AI information, you had to photocopy every page of every relevant book in the library and mail them in a massive, expensive crate. The AI would then try to read all of it at once. This approach is catastrophically expensive, impossibly slow, and the AI's "desk" (the context window) is too small to hold all the pages, so most of the information is lost.
- The World With Resources: You simply give the AI a library card. This card is a
Resource
—a tiny, secure pointer. It grants the AI permission to request specific books (or even specific pages) from the librarian (the Host application) as needed. The librarian fetches the information and provides a concise summary, ensuring the AI gets the context it needs without being overwhelmed.
This analogy highlights how Resources
solve the problems of cost, scale, and security by separating the reference to data from the data itself.
1.3 Architectural Position & Control Model
A Resource
exists on the Server (the capability provider) but is governed by the Host/Client (the AI application).

The Control Model is explicitly Application/Client-Controlled. This is the most critical design choice for this primitive. The AI model cannot autonomously decide to access a Resource
. It can only work with Resources
that the user or the Host application has explicitly made available in the current context. This prevents the AI from hallucinating a file path to sensitive data and gaining unauthorized access, making it the bedrock of MCP's security model.
The MCP specification defines Resources
as application-driven, meaning host applications determine how to incorporate resource context. Applications might expose resources through UI elements like a tree view, allow users to search and filter available resources, or implement automatic context inclusion based on their own heuristics.
1.4 What It Is NOT
- A
Resource
is NOT the data itself. It is a secure pointer (URI) to the data. - A
Resource
is NOT a mechanism for the AI to freely browse file systems. Access is strictly limited to what the application explicitly provides.
2. The Problem It Solves
2.1 The LLM-Era Catastrophe: The Token Budget Trap
The Resources
primitive was created to prevent the "Token Budget Trap," a set of four related failures that occur when AI agents try to interact with real-world data volumes.
- Catastrophic Economic Cost: LLMs are billed by the token. Feeding large files into a prompt is financially unsustainable, making routine analysis prohibitively expensive.
- Insurmountable Latency: Serializing, transmitting, and processing large data payloads can take minutes, destroying the possibility of real-time interaction required for tasks like fraud analysis or system monitoring.
- Context Window Obliteration: An LLM's context window is finite and minuscule compared to enterprise datasets. When confronted with a large dataset, the vast majority of the data is truncated and lost before the model can even see it, rendering any analysis incomplete and unreliable.
- Guaranteed System Crash: Attempting to load gigabyte-scale payloads into an application's memory will exhaust resources and cause a hard crash. The operation doesn't just get slow; it fails to complete entirely.
2.2 Why Traditional Approaches Fail
Before MCP, developers tried to solve this with brittle, ad-hoc solutions that could not scale.
- Manual Chunking: Writing custom "glue code" to split large files into smaller pieces. This is error-prone, loses long-range context within the data, and creates a massive maintenance burden, as custom code is needed for every new data type.
- Ad-Hoc Summarization Layers: Using one AI to pre-summarize data for another. This systematically fails when the crucial detail needed to answer a query is lost during the initial summarization step, leading to inaccurate or incomplete results.
These workarounds are temporary fixes that collapse under the complexity and scale of enterprise environments, leading to the unsustainable M×N integration crisis. Resources
provides a standardized, architectural solution.
Part II: Technical Architecture - The "How"
This is the technical deep dive, detailing the protocol mechanics, message structures, and the end-to-end lifecycle. This section is the source of truth for implementers.
3. Capabilities Declaration
Servers that support resources MUST declare the resources
capability during the initialization handshake. The capability supports two optional boolean features:
subscribe
: Whether the client can subscribe to be notified of changes to individual resources.listChanged
: Whether the server will emit notifications when the list of available resources changes.
Servers can support neither, either, or both features depending on their functionality.
Both Features Supported (Fully Dynamic):
{ "capabilities": { "resources": { "subscribe": true, "listChanged": true } } }
Only List Change Notifications Supported:
{ "capabilities": { "resources": { "listChanged": true } } }
Only Subscriptions Supported:
{ "capabilities": { "resources": { "subscribe": true } } }
Neither Feature Supported (Static Resources):
{ "capabilities": { "resources": {} } }
4. Protocol Specification
4.1 Request & Response Schemas
All operations are standard JSON-RPC 2.0 messages.
1. resources/list
: Discovering Available Resources
The client sends this to discover what data pointers are available from the server. This operation supports pagination.
Request:
{
"jsonrpc": "2.0",
"method": "resources/list",
"params": {
// Optional: A token provided in a previous response to fetch the next page of results.
"cursor": "optional-pagination-token"
},
"id": "req-1"
}
Response:
{
"jsonrpc": "2.0",
"id": "req-1",
"result": {
"resources": [
{
// The unique, addressable pointer to the data.
"uri": "hipaa://trial-OncoDrug-734/genomics.parquet",
// The name of the resource, often the filename.
"name": "genomics.parquet",
// Optional: A human-readable title for display purposes.
"title": "Phase III Genomics Data",
// Optional: A short description of the resource's content.
"description": "Complete genomic sequencing data for trial OncoDrug-734.",
// Optional: The standard MIME type, essential for correct client-side processing.
"mimeType": "application/parquet",
// Optional: The size of the resource in bytes.
"size": 5497558138880,
// Optional: Annotations providing hints to the client.
"annotations": {
"audience": ["assistant"],
"priority": 0.9,
"lastModified": "2024-10-21T18:30:00Z"
}
}
],
// If more resources are available, this token is used in the next request's `cursor`.
"nextCursor": "pagination-token-for-next-page"
}
}
2. resources/read
: Fetching Resource Content
The client sends this to retrieve the actual data a Resource
URI points to.
Request:
{
"jsonrpc": "2.0",
"method": "resources/read",
"params": {
// The URI of the resource to be read, obtained from a `resources/list` call or a Tool result.
"uri": "hipaa://trial-OncoDrug-734/genomics.parquet"
},
"id": "req-2"
}
Response:
{
"jsonrpc": "2.0",
"id": "req-2",
"result": {
"contents": [
{
"uri": "hipaa://trial-OncoDrug-734/genomics.parquet",
"mimeType": "application/parquet",
// For binary content, the data is Base64 encoded in the `blob` field.
"blob": "AAEAAAD... (base64 encoded data) ...AQAAAA=="
}
]
}
}
4.2 Data Types
Resource Object:
A Resource
definition includes the following fields:
Field | Type | Required | Description |
---|---|---|---|
uri |
string |
Yes | Unique identifier for the resource (RFC3986). |
name |
string |
Yes | The name of the resource (e.g., filename). |
title |
string |
No | A human-readable title for display purposes. |
description |
string |
No | A short description of the resource. |
mimeType |
string |
No | The MIME type of the resource content. |
size |
integer |
No | The size of the resource in bytes. |
annotations |
object |
No | Hints for client processing (see below). |
Resource Contents Object:
The contents
object returned by resources/read
represents the data itself. A Resource
can contain EITHER text OR binary data, not both.
Binary Content Example:
{
"uri": "file:///example.png",
"name": "example.png",
"title": "Example Image",
"mimeType": "image/png",
"blob": "iVBORw0KGgoAAAANSUhEUgAAAAUA..."
}
Text Content Example:
{
"uri": "file:///example.txt",
"name": "example.txt",
"title": "Example Text File",
"mimeType": "text/plain",
"text": "This is the resource content."
}
Annotations Object:
The optional annotations
object provides hints to clients:
Field | Type | Description |
---|---|---|
audience |
string[] |
Intended audience. Valid values: "user" , "assistant" . |
priority |
number |
Importance from 0.0 (least) to 1.0 (most). |
lastModified |
string |
ISO 8601 timestamp (e.g., "2025-01-12T15:00:58Z" ). |
4.3 Error Handling
Servers SHOULD return standard JSON-RPC errors for common failure cases.
Code | Meaning | Description | Client Action |
---|---|---|---|
-32601 | Method not found | The server does not support the requested method (e.g., resources/read ). |
Check server capabilities declared during initialization. |
-32602 | Invalid params | The request parameters are malformed (e.g., uri is missing). |
Fix the request payload and retry. |
-32002 | Resource Not Found | The requested uri does not exist on the server. |
Inform the user. Do not retry without confirming the URI is valid. |
-32003 | Permission Denied | The user/client is not authorized to access the requested uri . |
Inform the user. Trigger re-authentication if applicable. |
-32603 | Internal error | An unexpected error occurred on the server while processing the request. | Retry with exponential backoff. Contact support if the issue persists. |
5. The Lifecycle & Dynamic Behavior
5.1 Step-by-Step Communication Flow
The complete lifecycle can include discovery, reading, subscriptions, and updates.

5.2 Subscriptions and Notifications
For real-time data, the protocol supports subscriptions.
- Subscribe: The client sends a
resources/subscribe
request for a specific URI. The server confirms the subscription. - Unsubscribe: To stop receiving updates, clients send a
resources/unsubscribe
request with the same URI. - Content Update Notification: When the content of a subscribed resource changes, the server sends a
notifications/resources/updated
notification. - List Change Notification: If the server declared the
listChanged
capability, it SHOULD send anotifications/resources/list_changed
notification whenever the set of available resources is modified (e.g., a file is added or deleted). This signals to the client that it should re-runresources/list
to get the latest state.
List Changed Notification Format:
{
"jsonrpc": "2.0",
"method": "notifications/resources/list_changed"
}
5.3 Resource Templates
Resource templates allow servers to expose parameterized resources using URI Templates (RFC 6570). This is useful for exposing large sets of resources that follow a pattern, like files in a directory or records in a database.
Request:
{
"jsonrpc": "2.0",
"id": "req-3",
"method": "resources/templates/list"
}
Response:
{
"jsonrpc": "2.0",
"id": "req-3",
"result": {
"resourceTemplates": [
{
"uriTemplate": "file:///{path}",
"name": "Project Files",
"title": "📁 Project Files",
"description": "Access files in the project directory"
}
]
}
}
The client can use this template to construct valid URIs. The arguments (e.g., {path}
) may be auto-completed through the MCP completion API.
Part III: Implementation Patterns - The "Show Me"
This section makes concepts concrete through a progression of code-heavy examples, from a simple "hello world" to a complex, production-ready workflow.
6. Pattern 1: The Canonical "Golden Path"
Goal: To expose a simple, static file as a Resource
and allow a client to read it.
Implementation (Server-Side - Node.js/TypeScript):
import { Server, ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk";
import fs from "fs/promises";
import path from "path";
const server = new Server({ name: "config-server", version: "1.0.0" });
const CONFIG_URI = "file:///app/config.json";
const CONFIG_PATH = path.resolve("./config.json");
// Handle discovery
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const stats = await fs.stat(CONFIG_PATH);
return {
resources: [{
uri: CONFIG_URI,
name: "config.json",
title: "Application Configuration",
mimeType: "application/json",
size: stats.size,
}],
};
});
// Handle reads
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri !== CONFIG_URI) {
throw new Error("Resource not found");
}
const content = await fs.readFile(CONFIG_PATH, 'utf-8');
return {
contents: [{
uri: CONFIG_URI,
mimeType: "application/json",
text": content,
}],
};
});
server.listen();
Key Takeaway: The core pattern separates discovery (list
), which returns metadata, from access (read
), which returns content. This allows clients to understand the data landscape before committing to a potentially expensive transfer.
7. Pattern 2: The Compositional Workflow
Goal: To show how a Tool
and a Resource
work together in a "search-then-analyze" workflow.
Note: The resource_link
pattern shown here is a recommended convention, not part of the official MCP specification. It represents a best practice for Tools
to return references to Resources
they create.
Implementation (Conceptual - Python):
# SERVER-SIDE: A Tool that returns a pointer to a Resource.
@mcp.tool()
def search_flight_logs(query: str) -> dict:
"""Searches flight logs and returns a reference to the results."""
result_uri = perform_massive_search_and_get_uri(query)
# GOOD: Return a lightweight reference in the tool's response.
return {
"content": f"Found matching flight logs for query: '{query}'. Results are available.",
"structuredContent": {
"resource_link": { # This is a conventional structure
"uri": result_uri,
"title": "Search results for flight data."
}
}
}
# CLIENT-SIDE: The application orchestrates the flow.
async def run_flight_analysis(query: str):
# 1. The model invokes the tool.
tool_response = await mcp_client.call_tool("search_flight_logs", {"query": query})
# 2. The Host application gets the pointer from the response.
resource_uri = tool_response["result"]["structuredContent"]["resource_link"]["uri"]
# 3. The Host reads the data using the pointer, outside the LLM's context.
flight_data_content = await resource_client.read_resource(resource_uri)
# 4. The Host processes the data locally and creates a concise summary.
summary = process_flight_data(flight_data_content)
# 5. Send the summary back to the LLM for the final response.
final_response = await mcp_client.send_message(f"Here is a summary: {summary}")
return final_response
Key Takeaway: Tools
act, Resources
contain. This powerful combination allows agents to perform massive computations at the source and provide the AI with efficient, secure access to the results.
8. Pattern 3: The Domain-Specific Solution (Healthcare)
Goal: To provide access to sensitive patient data for a clinical trial in a HIPAA-compliant manner, complete with audit trails.
Implementation (Server-Side - Python with HIPAA Compliance):
# security.py
def is_authorized_for_trial(user_context, trial_id: str) -> bool:
# ⚠️ SECURITY CRITICAL: This MUST check against a real ACL/permission system.
return user_context.get("role") == "PI" and trial_id in user_context.get("approved_trials", [])
# audit.py
class HIPAAAuditLogger:
def log_access(self, user_context, resource_uri, action, outcome):
# ⚠️ COMPLIANCE CRITICAL: This log must be secure, immutable, and retained.
log_entry = { "timestamp": datetime.utcnow().isoformat(), "userId": user_context.get("user_id"), "resourceUri": resource_uri, "action": action, "outcome": outcome }
print(f"AUDIT_LOG: {json.dumps(log_entry)}")
# server.py
audit_logger = HIPAAAuditLogger()
@mcp.handler("resources/read")
async def handle_hipaa_read(request, user_context):
uri = request.params["uri"]
parsed_uri = urlparse(uri)
if parsed_uri.scheme != "hipaa":
raise PermissionError("Invalid URI scheme.")
trial_id = parsed_uri.netloc
# 1. Enforce Authorization
if not is_authorized_for_trial(user_context, trial_id):
audit_logger.log_access(user_context, uri, "read", "denied")
raise PermissionError(f"User not authorized for trial {trial_id}.")
# 2. Log Successful Access
audit_logger.log_access(user_context, uri, "read", "success")
# 3. Retrieve and return data
data = get_minimum_necessary_data(uri)
return {"contents": [{"uri": uri, "blob": base64.b64encode(data).decode('utf-8')}]}
Key Takeaway: The indirection provided by URIs is essential for compliance. It allows the server to act as a security gateway, enforcing authorization, generating audit logs, and applying data minimization principles before any sensitive data is returned.
Part IV: Production Guide - The "Build & Deploy"
9. Server-Side Implementation
9.1 Core Logic & Handlers
A minimal, runnable server in Python using a framework like FastMCP
.
from fastmcp import FastMCP
import os
app = FastMCP()
DATA_ROOT = "/var/data/safe_zone"
@app.on_event("startup")
def initialize_server():
# Announce capabilities, enabling features as needed.
app.capabilities.resources = {
"subscribe": False, # Set to True if implementing subscriptions
"listChanged": False # Set to True if the resource list is dynamic
}
@app.handler("resources/list")
async def list_files():
files = []
for filename in os.listdir(DATA_ROOT):
stats = os.stat(os.path.join(DATA_ROOT, filename))
files.append({
"uri": f"file://{filename}",
"name": filename,
"title": f"Data File: {filename}",
"size": stats.st_size
})
return {"resources": files}
@app.handler("resources/read")
async def read_file(uri: str):
filename = urlparse(uri).path.lstrip('/')
filepath = os.path.realpath(os.path.join(DATA_ROOT, filename))
# ⚠️ SECURITY CRITICAL: Path traversal prevention
if not filepath.startswith(os.path.realpath(DATA_ROOT)):
raise PermissionError("Access denied: Path traversal attempt detected.")
content = await aiofiles.open(filepath, 'rb').read()
return {"contents": [{"uri": uri, "blob": base64.b64encode(content).decode('utf-8')}]}
9.2 Production Hardening
Observability (Logging):
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
def log_read_event(uri, user_id, size):
logging.info({ "event": "resource_read", "uri": uri, "userId": user_id, "bytes": size })
Performance (Caching):
from cachetools import LRUCache
l1_cache = LRUCache(maxsize=100)
async def read_file_cached(uri: str):
if uri in l1_cache: return l1_cache[uri]
# ... fetch from origin ...
Security (Rate Limiting):
from slowapi import Limiter
limiter = Limiter(key_func=get_remote_address)
@app.handler("resources/read")
@limiter.limit("10/minute")
async def read_file_protected(request: Request, uri: str): #...
10. Client-Side Implementation
10.1 Core Logic & State Management
A robust Python client for discovering and reading resources, with caching.
import base64
class ResourceClient:
def __init__(self, mcp_client):
self.mcp_client = mcp_client
self.metadata_cache = {}
self.content_cache = {} # For small resources
async def discover_resources(self, force_refresh=False):
if not force_refresh and self.metadata_cache:
return list(self.metadata_cache.values())
response = await self.mcp_client.request("resources/list", {})
resources = response.get("resources", [])
for res in resources: self.metadata_cache[res["uri"]] = res
return resources
async def read_resource(self, uri: str):
if uri in self.content_cache: return self.content_cache[uri]
response = await self.mcp_client.request("resources/read", {"uri": uri})
content_obj = response["contents"][0]
data = base64.b64decode(content_obj["blob"]) if "blob" in content_obj else content_obj["text"].encode('utf-8')
if len(data) < (1024 * 1024): self.content_cache[uri] = data # Cache if < 1MB
return data
10.2 User Experience (UX) Patterns
When an AI proposes an action involving a large Resource
, the UI MUST clearly communicate the scale of the operation to the user before they grant consent.
Mockup Text: The Scale-Aware Consent Dialog
Confirm Action
The AI wants to runperform_genomic_analysis
.
This action will process the following Resource:📄 Phase III Genomics Data (hipaa://.../genomics.parquet
)📦 Size: A large dataset
This is a computationally intensive operation.
[ Confirm ] [ Cancel ]
Part V: Guardrails & Reality Check - The "Don't Fail"
11. Decision Framework
11.1 When to Use This Primitive
If you need to... | Then use Resources because... |
An alternative would be... |
---|---|---|
Provide context with large data (> 50KB) | It avoids catastrophic token costs and system crashes. | There is no viable alternative at scale. |
Handle binary content (images, PDFs, Parquet) | It provides a standard mechanism (blob ) for transport. |
Returning base64 content in a Tool (only for tiny files). |
Reference the same data multiple times in a session | It allows for efficient client-side caching. | Re-fetching with a Tool on every use (inefficient). |
Enforce strict access control and audit logging | The URI acts as a chokepoint for security/compliance checks. | Building custom, non-standard security for every Tool . |
11.2 Critical Anti-Patterns (The "BAD vs. GOOD")
1. The "Token-Stuffing" Resource
This is the most destructive anti-pattern.
- Why it's wrong: This negates the purpose of
Resources
, re-introducing the massive cost, latency, and crash risk.
GOOD CODE:
# Server returns raw content in the correct field, letting the Host process it.
return { "contents": [{ "uri": uri, "blob": base64.b64encode(data).decode('utf-8') }] }
BAD CODE:
# Server returns the full data in a `text` field instead of a `blob`.
return { "contents": [{ "uri": uri, "text": f"FULL DATA:\n{data}" }] } # 😱 CATASTROPHIC
2. The "Everything is a Resource" Anti-Pattern
Using Resources
for tiny, ephemeral data adds unnecessary complexity.
- Why it's wrong: This forces a second network call (
resources/read
) for a tiny piece of information.
GOOD CODE:
# Tool returns the simple status directly.
return { "content": "Server status: HEALTHY" }
BAD CODE:
# Tool returns a pointer to a simple status message.
uri = store_temp_resource("HEALTHY")
return { "structuredContent": { "resource_link": { "uri": uri } } }
12. The Reality Check
12.1 Known Limitations & Constraints
- Discovery Latency at Scale:
resources/list
on a server with tens of thousands of items can be slow. Servers with large numbers of resources MUST implement pagination viacursor
. - No Standard for Ranged Reads: The
resources/read
call fetches the entire resource. For very large files where only a small slice is needed, a customTool
is required as a workaround.
12.2 Security Considerations (Official Specification)
- Servers MUST validate all resource URIs to prevent attacks like path traversal.
- Access controls SHOULD be implemented for sensitive resources. The protocol does not provide this; it is the developer's responsibility.
- Binary data MUST be properly Base64 encoded to ensure safe transport via JSON.
- Resource permissions SHOULD be checked before performing any operation (
list
,read
,subscribe
).
12.3 The Trust Model: Enforced vs. Cooperative
- Enforced by Protocol: The JSON-RPC message format, the separation of metadata and content, and the existence of standard fields.
- Developer Responsibility (Cooperative):
- ⚠️ URI Validation: The server MUST sanitize all incoming URIs.
- ⚠️ Access Control: The server MUST implement its own authorization logic.
Part VI: Quick Reference - The "Cheat Sheet"
13. Common Operations & Snippets (Client-Side Python)
Discover and List All Resources:
resource_client = ResourceClient(mcp_client)
all_resources = await resource_client.discover_resources()
for res in all_resources: print(f"- {res['title']} ({res['uri']})")
Read a Text Resource:
content_bytes = await resource_client.read_resource("file:///app/config.json")
config = json.loads(content_bytes.decode('utf-8'))
14. Configuration & Error Code Tables
Common URI Schemes
Scheme | Purpose | Example | Notes |
---|---|---|---|
https:// |
Web resources | https://example.com/data.json |
SHOULD only be used if the client can fetch the URL directly. |
file:// |
Filesystem-like resources | file:///data/report.csv |
Can use XDG MIME types like inode/directory for non-regular files. |
git:// |
Version control integration | git://repo/main/src/app.js |
For code analysis. |
custom | Application-specific | hipaa://... , ledger://... |
MUST comply with RFC3986. |
Error Code Reference
Code | Meaning | Recommended Client Action |
---|---|---|
-32601 | Method not found | Verify server capabilities. Do not retry. |
-32002 | Resource Not Found | Inform user the data is unavailable. Check if URI is correct. |
-32003 | Permission Denied | Inform user they lack permissions. Prompt for login. |
-32603 | Internal error | Retry with backoff. Contact support if persistent. |