Module 3: Three Primitives — Resources, Tools, Prompts
Resources: Contextual Data the Model Can Read
Resources: Contextual Data the Model Can Read
Capsule description
The first MCP primitive is the simplest to understand: Resources. A resource is a piece of data that the MCP server exposes so the model can read it. Think of files, database records, API responses, configurations — any data the model needs as context to help you better.
The key characteristic of a resource is that it's pull-based and without side effects. The client asks, the server responds with data. Nothing is modified. It's like consulting a catalog: you look at what's available, ask for what you need, and receive the information. The catalog doesn't change because you consulted it.
In this capsule you're going to understand what resources are, how they're identified with URIs, how they're implemented in TypeScript and Python, and when to choose them over tools or prompts.
What is a Resource?
Formal definition
A Resource in MCP is a unit of data identified by a URI that the server exposes to the client. The client can:
- List the available resources (
resources/list) - Read a specific resource (
resources/read) - Subscribe to changes in a resource (
resources/subscribe)
Practical definition
A resource is the answer to: "What data can the model see through this server?"
Examples of resources:
├── file:///project/src/main.ts → A file's content
├── db://users/123 → A database record
├── api://weather/madrid → An external API response
├── config://app/settings → The app's configuration
├── logs://app/2024-01-15 → Logs from a specific date
└── metrics://server/cpu → System metrics
Key characteristics
| Characteristic | Detail |
|---|---|
| Identification | Each resource has a unique URI |
| Read-only | Doesn't modify state — only returns data |
| Pull-based | The client asks, the server responds |
| Typed | Each resource declares its MIME type (text/plain, application/json, etc.) |
| Listable | The client can discover which resources are available |
| Subscribable | Optionally, the client can receive change notifications |
Anatomy of a Resource
Data structure
Each resource the server exposes has this structure:
interface Resource {
uri: string; // Unique identifier (e.g., "file:///path/to/file")
name: string; // Human-readable name
description?: string; // Optional description
mimeType?: string; // Content type (e.g., "text/plain", "application/json")
}
When the client reads a resource, it receives:
interface ResourceContent {
uri: string; // The URI of the read resource
mimeType?: string; // Content type
text?: string; // Text content
blob?: string; // Binary content (base64)
}
URIs: the resource's identity
URIs are the resource addressing system. Each URI identifies a unique resource:
Scheme Authority Path
│ │ │
▼ ▼ ▼
file:// /project /src/main.ts
db:// users /123
api:// weather /madrid
You can use any URI scheme that makes sense for your server. The most common ones:
file:// → Filesystem files
db:// → Database records
api:// → Data from external APIs
config:// → Configurations
logs:// → System logs
metrics:// → Metrics and statistics
Resource Templates: dynamic URIs
In addition to static resources (with fixed URIs), MCP supports resource templates — URIs with parameters the client can fill in.
Key difference
Static resource:
uri: "config://app/settings"
→ Always returns the same configuration
Resource template:
uriTemplate: "db://users/{id}"
→ The client fills in {id} to read a specific user
When to use templates
- Static: When there's a finite and known number of resources (configuration, server state, fixed list)
- Template: When the resources are dynamic or there are many possible ones (users by ID, files by path, logs by date)
Structure of a template
interface ResourceTemplate {
uriTemplate: string; // URI with placeholders: "db://users/{id}"
name: string; // Readable name
description?: string; // Description
mimeType?: string; // Content type
}
Implementation in TypeScript
Basic setup
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
Static resource
server.resource(
"config-app",
"config://app/settings",
{
description: "The application's current configuration",
mimeType: "application/json",
},
async (uri) => {
const config = {
appName: "My App",
version: "2.1.0",
environment: "development",
debug: true,
maxConnections: 100,
};
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(config, null, 2),
},
],
};
}
);
Resource template (dynamic)
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
server.resource(
"user-by-id",
new ResourceTemplate("db://users/{id}", {
list: async () => {
const users = await getUsers();
return users.map((u) => ({
uri: `db://users/${u.id}`,
name: `User: ${u.name}`,
description: `Profile of ${u.name}`,
}));
},
}),
{
description: "A user's profile by their ID",
mimeType: "application/json",
},
async (uri, params) => {
const id = params.id;
const user = await getUserById(id);
if (!user) {
throw new Error(`User ${id} not found`);
}
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(user, null, 2),
},
],
};
}
);
Resource that lists files
import * as fs from "fs/promises";
import * as path from "path";
server.resource(
"project-files",
"file:///project/structure",
{
description: "List of the project's files",
mimeType: "application/json",
},
async (uri) => {
const projectDir = "/path/to/your/project";
const files = await fs.readdir(projectDir, { recursive: true });
const structure = files.map((file) => ({
name: file,
path: path.join(projectDir, file.toString()),
}));
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(structure, null, 2),
},
],
};
}
);
Implementation in Python
Basic setup
from mcp.server.fastmcp import FastMCP
server = FastMCP("my-server")
Static resource
@server.resource("config://app/settings")
async def get_config() -> str:
"""The application's current configuration."""
import json
config = {
"appName": "My App",
"version": "2.1.0",
"environment": "development",
"debug": True,
"maxConnections": 100,
}
return json.dumps(config, indent=2)
Resource template (dynamic)
@server.resource("db://users/{user_id}")
async def get_user(user_id: str) -> str:
"""A user's profile by their ID."""
import json
user = await get_user_by_id(user_id)
if not user:
raise ValueError(f"User {user_id} not found")
return json.dumps(user, indent=2)
Resource that lists files
import os
import json
@server.resource("file:///project/structure")
async def list_files() -> str:
"""List of the project's files."""
project_dir = "/path/to/your/project"
files = []
for root, dirs, filenames in os.walk(project_dir):
for f in filenames:
full_path = os.path.join(root, f)
files.append({
"name": f,
"path": full_path,
"size": os.path.getsize(full_path),
})
return json.dumps(files, indent=2)
Common Resource patterns
Pattern 1: Current system state
server.resource(
"system-status",
"system://status",
{ description: "The system's current state", mimeType: "application/json" },
async (uri) => {
const status = {
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString(),
version: "1.0.0",
};
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(status, null, 2),
}],
};
}
);
Pattern 2: Aggregated data
server.resource(
"sales-summary",
"analytics://sales/summary",
{ description: "The month's sales summary", mimeType: "application/json" },
async (uri) => {
const sales = await db.query("SELECT SUM(total), COUNT(*) FROM sales WHERE month = $1", [currentMonth]);
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
month: currentMonth,
totalSales: sales.sum,
transactionCount: sales.count,
}, null, 2),
}],
};
}
);
Pattern 3: Rich text content
server.resource(
"api-documentation",
"docs://api/endpoints",
{ description: "Documentation of the API's endpoints", mimeType: "text/markdown" },
async (uri) => {
const docs = `# API Endpoints
## GET /users
Returns a list of users.
## POST /users
Creates a new user.
- Body: { "name": string, "email": string }
## GET /users/:id
Returns a user by ID.
`;
return {
contents: [{
uri: uri.href,
mimeType: "text/markdown",
text: docs,
}],
};
}
);
When to use Resources vs Tools
This is a design decision you'll make constantly:
| Scenario | Resource | Tool |
|---|---|---|
| Read the current configuration | ✅ | ❌ |
| Modify the configuration | ❌ | ✅ |
| Query data from a DB | ✅ | ❌ |
| Insert data into a DB | ❌ | ✅ |
| Read a file | ✅ | ❌ |
| Write a file | ❌ | ✅ |
| Get system metrics | ✅ | ❌ |
| Restart a service | ❌ | ✅ |
Simple rule: If the operation is idempotent and without side effects → Resource. If it modifies state → Tool.
Gray case: what about complex queries?
"Find users with more than 10 purchases in the last month"
This is read-only (doesn't modify anything), but requires complex parameters. You can do it in two ways:
- Resource template:
db://users/active/{month}— if the parameters are simple - Search tool: If you need complex parameters with validation, a tool can be better
In practice, the decision depends on the complexity of the parameters. If they fit in a URI template, use a resource. If not, consider a read-only tool.
Subscriptions: Resources in real time
MCP supports subscriptions to resources — the client subscribes to changes and receives notifications when the resource updates:
// The server notifies of changes
server.notification({
method: "notifications/resources/updated",
params: {
uri: "metrics://server/cpu",
},
});
This is useful for:
- Real-time metrics
- Files that change frequently
- System state that updates
Subscriptions are optional and advanced — you don't need them for this module's mini-project. You'll see them in detail in module 4.
Troubleshooting
"The resource doesn't appear in the list"
Cause: The resource wasn't registered correctly in the server.
Solution:
// Verify that the resource() function is called before connecting the server
server.resource("my-resource", "my://uri", { ... }, async (uri) => { ... });
// The registration must occur BEFORE starting the transport
const transport = new StdioServerTransport();
await server.connect(transport);
"Error: URI not found"
Cause: The client is asking for a URI that doesn't match any registered resource.
Solution:
# Verify the registered URIs with MCP Inspector
npx @modelcontextprotocol/inspector
# Make sure the request's URI matches exactly
# the registered URI (including scheme and path)
"The resource returns empty data"
Cause: The async function that generates the data has an error or returns undefined.
Solution:
// Add logging to debug
server.resource("debug-resource", "debug://test", {}, async (uri) => {
console.error("Generating resource for:", uri.href);
const data = await getData();
console.error("Data obtained:", JSON.stringify(data));
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(data, null, 2),
}],
};
});
"Resource template doesn't resolve the parameters"
Cause: The placeholders in the URI template don't match the parameters the function receives.
Solution:
// The {id} placeholder maps automatically to the `params.id` parameter
server.resource(
"user",
new ResourceTemplate("db://users/{id}", { list: undefined }),
{},
async (uri, params) => {
console.error("Params received:", params);
const id = params.id; // ← must match {id} in the template
// ...
}
);
"Incorrect MIME type causes rendering problems"
Cause: You declared a mimeType that doesn't match the actual format of the data.
Solution:
// If you return JSON, use application/json
mimeType: "application/json"
// If you return plain text, use text/plain
mimeType: "text/plain"
// If you return markdown, use text/markdown
mimeType: "text/markdown"
// If you're not sure, text/plain is always safe
Exercises
Exercise 1: Identify Resources (Easy)
From the following list, identify which would be Resources and which wouldn't:
- Read a project's README.md
- Send an email
- Get the list of open GitHub issues
- Create a new issue in GitHub
- Query Bitcoin's current price
- Restart a Docker container
See solution
- ✅ Resource — Read-only of a file
- ❌ Not a Resource — Has side effects (sends an email) → Tool
- ✅ Resource — Read-only of GitHub data
- ❌ Not a Resource — Modifies state (creates an issue) → Tool
- ✅ Resource — Read-only of external API data
- ❌ Not a Resource — Modifies state (restarts a container) → Tool
Rule: If it modifies state or has side effects → Tool. If it only reads data → Resource.
Exercise 2: Design URIs (Medium)
Design URIs for the following resources of an MCP server for a development team:
- The project's configuration (package.json)
- The current environment variables
- A specific log by date
- A Git commit by hash
- The server's memory usage metrics
See solution
1. config://project/package-json
→ Scheme: config, path: project/package-json
2. env://app/variables
→ Scheme: env, path: app/variables
3. logs://app/{date}
→ Resource template with a date placeholder
→ Example: logs://app/2024-01-15
4. git://commits/{hash}
→ Resource template with a hash placeholder
→ Example: git://commits/abc123f
5. metrics://server/memory
→ Scheme: metrics, path: server/memory
URIs 3 and 4 are resource templates because they have dynamic parameters. The others are static resources.
Exercise 3: Implement a Resource in TypeScript (Medium)
Implement a resource that exposes the application's environment variables (only those starting with "APP_"):
See solution
server.resource(
"env-vars",
"env://app/variables",
{
description: "The application's environment variables (APP_*)",
mimeType: "application/json",
},
async (uri) => {
const appEnvVars: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith("APP_") && value !== undefined) {
appEnvVars[key] = value;
}
}
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(appEnvVars, null, 2),
},
],
};
}
);
We filter only variables with the "APP_" prefix for security — we don't want to expose tokens, passwords, or system variables.
Exercise 4: Implement a Resource Template in Python (Medium)
Implement a resource template in Python that returns a product's information by its SKU:
See solution
import json
PRODUCTS = {
"SKU001": {"name": "Laptop Pro", "price": 1299.99, "stock": 15},
"SKU002": {"name": "Ergonomic Mouse", "price": 49.99, "stock": 230},
"SKU003": {"name": "4K Monitor", "price": 599.99, "stock": 42},
}
@server.resource("products://catalog/{sku}")
async def get_product(sku: str) -> str:
"""A product's information by its SKU."""
product = PRODUCTS.get(sku)
if not product:
raise ValueError(f"Product with SKU '{sku}' not found")
return json.dumps({
"sku": sku,
**product,
}, indent=2)
The @server.resource decorator with {sku} automatically creates a resource template. FastMCP extracts the sku parameter from the URI and passes it to the function.
Exercise 5: Resource with aggregated data (Hard)
Implement a resource in TypeScript that returns a statistical summary of a directory: number of files per extension, total size, and largest file.
See solution
import * as fs from "fs/promises";
import * as path from "path";
server.resource(
"dir-stats",
new ResourceTemplate("stats://directory/{dirPath}", { list: undefined }),
{
description: "A directory's statistics",
mimeType: "application/json",
},
async (uri, params) => {
const dirPath = decodeURIComponent(params.dirPath as string);
const files = await fs.readdir(dirPath);
const extensions: Record<string, number> = {};
let totalSize = 0;
let largestFile = { name: "", size: 0 };
for (const file of files) {
const filePath = path.join(dirPath, file);
const stat = await fs.stat(filePath);
if (stat.isFile()) {
const ext = path.extname(file) || "(no extension)";
extensions[ext] = (extensions[ext] || 0) + 1;
totalSize += stat.size;
if (stat.size > largestFile.size) {
largestFile = { name: file, size: stat.size };
}
}
}
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
directory: dirPath,
totalFiles: files.length,
totalSize: `${(totalSize / 1024).toFixed(2)} KB`,
largestFile,
extensions,
}, null, 2),
}],
};
}
);
This resource combines filesystem operations to generate a useful summary. Note that it's still read-only — it doesn't modify anything.
Exercise 6: Decide Resource vs Tool (Hard)
For each scenario, decide whether you'd use a Resource, a Tool, or both. Justify your decision:
- An endpoint that returns a repo's last 10 commits
- An endpoint that runs
git pullin a repo - An endpoint that shows the diff between two branches
- An endpoint that searches files by content (grep)
- An endpoint that formats code with Prettier
See solution
-
Resource — Read-only of data (commit history). URI:
git://repo/commits/recent -
Tool — Has side effects (modifies the local repo's state). Tools are for actions that change something.
-
Resource template — Read-only (the diff is a derived piece of data, doesn't modify anything). URI:
git://repo/diff/{branch1}/{branch2} -
Gray case → Tool — Although it's read-only, grep requires complex parameters (search pattern, directory, options) that don't fit well in a URI template. A tool with a validated schema is more appropriate.
-
Tool — Modifies files (rewrites the formatted code). Definitely a tool with side effects.
The key is: does it modify state? → Tool. Does it only read data with simple parameters? → Resource. Does it only read data but with complex parameters? → Evaluate both options.
Summary
In this capsule you learned:
- Resources are contextual data the MCP server exposes for reading
- They're identified with URIs — MCP's addressing system
- They can be static (fixed URI) or dynamic (URI template with parameters)
- They're pull-based — the client asks, the server responds
- Without side effects — reading a resource never modifies state
- They support MIME types to indicate the format of the data
- They optionally support subscriptions for change notifications
- The decision rule: if it only reads data → Resource; if it modifies state → Tool
Next capsule: Tools — executable functions the model can invoke. The most powerful primitive and the one you'll use most frequently.
Additional resources
- MCP Specification — Resources - Official Resources specification
- MCP TypeScript SDK — Resources - Resources implementation in TypeScript
- MCP Python SDK — Resources - Resources implementation in Python
- URI Template RFC 6570 - URI templates specification
- MIME Types Reference - List of common MIME types
- Filesystem MCP Server Source - Real example of Resources in an MCP server