Tools vs Resources
In the previous module, you learned about tools — functions the AI model can call. Resources are the other side of the coin: data the application can read. Understanding the difference is essential.
The Control Axis:
Tools → Model-controlled → "I (the AI) decide to call this"
Resources → Application-controlled → "The app decides to fetch this"
Prompts → User-controlled → "The human chooses this template"
Example:
Tool: The AI calls "search_files" to find something
Resource: The app reads "file:///config.json" to provide context
Prompt: The user picks the "code-review" prompt templateThink of resources like files in a file system. The application (or user) browses what is available, selects what to include, and the content gets added to the conversation context. The AI model does not decide to “fetch a resource” — the host application does, often based on configuration or user action.
Why does this distinction matter? Because it affects who controls the data flow. Tools are active — the model invokes them. Resources are passive — they are offered as context. A client might automatically include certain resources in every conversation (like a project's README), while tools are invoked dynamically based on what the user asks.
Resource URIs
Every resource is identified by a URI (Uniform Resource Identifier). MCP does not mandate a specific URI scheme — you choose one that makes sense for your server. Common patterns:
URI Scheme Examples:
file:///home/user/project/README.md — File system path
postgres://localhost/mydb/users/schema — Database resource
github://repos/owner/repo/issues/42 — GitHub issue
config://app/settings — Application config
memory://session/conversation-history — In-memory data
s3://bucket-name/path/to/object — Cloud storage
The scheme (before "://") tells the client what kind of
resource this is. The rest identifies the specific item.URIs must be unique within your server. Two resources cannot share the same URI. The URI should be descriptive enough that a human or AI can understand what it points to without additional context.
A good URI is like a good file path: self-documenting. Compare db://data/123 (meaningless) to postgres://myapp/users/schema (immediately clear).
Static Resources
The simplest type of resource is a static resource — a fixed piece of data with a known URI. When the client asks for a list of resources, your server returns all static resources with their URIs, names, and descriptions.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({
name: "config-server",
version: "1.0.0",
});
// Register a static resource
// .resource() takes: URI, name (for display), and a handler
server.resource(
// The unique URI for this resource
"config://app/settings",
// Human-readable name — clients display this in their UI
"Application Settings",
// The handler — called when a client reads this resource
async (uri) => {
// Read the actual data (from a file, database, etc.)
const settings = await readSettingsFile();
return {
contents: [
{
uri: uri.href, // Echo back the requested URI
mimeType: "application/json",
text: JSON.stringify(settings, null, 2),
},
],
};
}
);When a client connects and calls resources/list, it gets back:
{
"resources": [
{
"uri": "config://app/settings",
"name": "Application Settings",
"mimeType": "application/json"
}
]
}
The client can then call resources/read with that URI
to get the actual content.Resource Templates
Static resources work when you know all resources upfront. But what about dynamic data? You cannot pre-register a resource for every row in a database or every file in a directory. That is where resource templates come in.
A resource template is a URI pattern with placeholders. Clients fill in the placeholders to request a specific resource.
// Register a resource template
// The URI contains {placeholders} that clients fill in
server.resource(
// Template URI — {table} is a placeholder
"postgres://mydb/{table}/schema",
// Name template — can also use the placeholder
"Schema for {table}",
// Handler receives the resolved URI with the actual value
async (uri) => {
// Extract the table name from the URI
// The SDK resolves the template and passes the full URI
const tableName = uri.pathname.split("/")[1];
const schema = await getTableSchema(tableName);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(schema, null, 2),
},
],
};
}
);
// Now clients can request:
// postgres://mydb/users/schema → schema for "users" table
// postgres://mydb/orders/schema → schema for "orders" table
// postgres://mydb/products/schema → schema for "products" table
// All handled by the same template.When a client calls resources/templates/list, it gets the template pattern. It can then construct specific URIs and call resources/read with them. This is how one handler serves an entire category of resources.
Template Listing Response:
{
"resourceTemplates": [
{
"uriTemplate": "postgres://mydb/{table}/schema",
"name": "Schema for {table}",
"description": "Returns the column definitions for a database table"
}
]
}MIME Types and Content
Resources carry a MIME type so clients know how to handle the content. The content itself can be either text or binary:
// Text content — returned in the "text" field
{
contents: [{
uri: "config://app/settings",
mimeType: "application/json", // Client knows it is JSON
text: '{ "theme": "dark" }', // Actual content as string
}],
}
// Binary content — returned in the "blob" field as base64
{
contents: [{
uri: "file:///images/logo.png",
mimeType: "image/png", // Client knows it is a PNG
blob: "iVBORw0KGgoAAAAN...", // Base64-encoded binary data
}],
}
Common MIME types you will use:
text/plain → Plain text files
text/markdown → Markdown documents
application/json → JSON data (configs, API responses)
text/csv → Tabular data
text/html → HTML content
image/png, image/jpeg → Images (use blob, not text)
application/pdf → PDFs (use blob, not text)Rule of thumb: if you can represent it as a string, use text. If it is binary (images, PDFs, compressed files), use blob with base64 encoding.
Reading Resources
From the protocol perspective, reading resources is a two-step process:
Step 1: Discovery — client asks "what resources exist?"
Client → Server: resources/list
Server → Client: [{ uri: "config://app/settings", name: "App Settings" }, ...]
Client → Server: resources/templates/list
Server → Client: [{ uriTemplate: "db://{table}/schema", name: "..." }, ...]
Step 2: Reading — client asks "give me this specific resource"
Client → Server: resources/read { uri: "config://app/settings" }
Server → Client: { contents: [{ uri: "...", text: "...", mimeType: "..." }] }
For templates, the client constructs the URI first:
Client → Server: resources/read { uri: "db://users/schema" }
Server → Client: { contents: [{ uri: "...", text: "...", mimeType: "..." }] }
The key insight: resources/read returns a "contents" array.
A single read can return multiple content items. This handles
cases where a resource is a collection.Subscriptions and Change Notifications
Resources can change over time. A config file gets updated, new data arrives in a database, a log file grows. MCP supports subscriptions so clients can be notified when a resource changes.
Subscription Flow:
1. Client subscribes to a resource URI
Client → Server: resources/subscribe { uri: "config://app/settings" }
2. Server acknowledges
Server → Client: (success response)
3. Later, when the resource changes...
Server → Client: notification/resources/updated { uri: "config://app/settings" }
4. Client re-reads the resource to get new content
Client → Server: resources/read { uri: "config://app/settings" }
5. When done, client unsubscribes
Client → Server: resources/unsubscribe { uri: "config://app/settings" }The server does not push the new content in the notification — it just says “this resource changed.” The client then decides whether to re-read it. This keeps notifications lightweight and lets the client control when it actually fetches data.
Your server can also notify that its resource list has changed (new resources added, old ones removed):
// Notify clients that the available resources have changed
// (e.g., a new config file was created)
server.notification({
method: "notifications/resources/list_changed",
});
// Clients that care will call resources/list again
// to discover the new resources.Complete Example: Database Schema Resource
Here is a practical example: a server that exposes database table schemas as resources. This is genuinely useful — an AI coding assistant that can read your database schema makes much better SQL suggestions.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({
name: "db-schema-server",
version: "1.0.0",
});
// --- Static resource: list all tables ---
server.resource(
"postgres://myapp/tables",
"Database Table List",
async (uri) => {
// In a real server, query: SELECT table_name FROM information_schema.tables
const tables = ["users", "orders", "products", "categories"];
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
database: "myapp",
tables: tables.map(t => ({
name: t,
schemaUri: `postgres://myapp/${t}/schema`,
})),
}, null, 2),
}],
};
}
);
// --- Template resource: schema for any table ---
server.resource(
"postgres://myapp/{table}/schema",
"Schema for {table}",
async (uri) => {
// Parse table name from the URI path
const segments = uri.pathname.split("/").filter(Boolean);
const tableName = segments[0]; // "users", "orders", etc.
// In a real server, query information_schema.columns
const schema = await getSchemaForTable(tableName);
if (!schema) {
// Return an empty contents array for not found
return { contents: [] };
}
// Format as a clear, readable schema description
const formatted = [
`Table: ${tableName}`,
`Columns:`,
...schema.columns.map(col =>
` ${col.name} ${col.type}${col.nullable ? " (nullable)" : ""}${col.primaryKey ? " PRIMARY KEY" : ""}`
),
``,
`Indexes:`,
...schema.indexes.map(idx =>
` ${idx.name} ON (${idx.columns.join(", ")})${idx.unique ? " UNIQUE" : ""}`
),
].join("\n");
return {
contents: [{
uri: uri.href,
mimeType: "text/plain",
text: formatted,
}],
};
}
);
// --- Helper function (simulated) ---
async function getSchemaForTable(name: string) {
// This would be a real database query in production
const schemas: Record<string, { columns: Array<{ name: string; type: string; nullable: boolean; primaryKey: boolean }>; indexes: Array<{ name: string; columns: string[]; unique: boolean }> }> = {
users: {
columns: [
{ name: "id", type: "uuid", nullable: false, primaryKey: true },
{ name: "email", type: "varchar(255)", nullable: false, primaryKey: false },
{ name: "name", type: "varchar(100)", nullable: true, primaryKey: false },
{ name: "created_at", type: "timestamptz", nullable: false, primaryKey: false },
],
indexes: [
{ name: "users_pkey", columns: ["id"], unique: true },
{ name: "users_email_idx", columns: ["email"], unique: true },
],
},
};
return schemas[name] ?? null;
}This server exposes two resources: a static listing of all tables, and a template that serves the schema for any specific table. An AI assistant connected to this server could read the table list to understand the database structure, then read individual schemas when asked to write queries.
Try It Yourself
Design a resource server for one of these scenarios. Write out the resource URIs, names, MIME types, and what the content would look like:
- A config file server that exposes application config files from a directory. You need: a static resource listing all config files, and a template resource to read any specific file. Think about what URI scheme to use and how to handle nested directories.
- A Git repository server that exposes repository metadata. Resources might include: current branch, recent commits, modified files, and the content of any file at HEAD. Which of these are static and which are templates?
- A monitoring dashboard server that exposes system metrics. Think about: CPU usage, memory, disk space, and recent error logs. Which resources would benefit from subscriptions?
Key Takeaway
Resources expose data for applications to read and include as context. They are identified by URIs, can be static or templated, carry MIME types, and support subscriptions for change notifications. Unlike tools (which the AI model calls), resources are controlled by the application or user. Design resources around what context an AI assistant would benefit from having.
Common Mistakes
- Using a tool when you should use a resource. If the data is read-only context that the application fetches, it is a resource. If the AI model needs to actively decide to retrieve it based on user intent, it is a tool. A database schema is a resource. A database query is a tool.
- Non-descriptive URIs.
data://1tells nobody anything.postgres://myapp/users/schemais self-documenting. Treat URIs like file paths — they should be readable. - Returning huge resources without pagination. A resource that returns 100MB of data will choke the client. For large datasets, provide summaries or paginated templates.
- Forgetting the MIME type. Clients use MIME types to decide how to render content. JSON without
application/jsonmight be displayed as plain text without syntax highlighting. - Pushing content in change notifications. Notifications say “something changed” — they do not include the new content. The client re-reads the resource if it wants the update. Sending content in notifications violates the protocol design.
- Using text field for binary data. If the content is an image or PDF, use the
blobfield with base64 encoding. Putting binary data in thetextfield will corrupt it.