Chapter 5: Resources
The Read-Only Sibling
If tools are verbs (“do this”), resources are nouns (“here’s this”). Resources represent data that an MCP server exposes for clients to read. Files, database records, API responses, live system metrics, log outputs—anything a model might want to look at.
Resources are application-controlled, meaning the host application (not the LLM) decides when to read them and how to incorporate them into the conversation. This is different from tools, which are model-controlled. The distinction matters: a tool is “the model calls this when it decides to,” while a resource is “the application fetches this and hands it to the model.”
In practice, resources often end up in the system prompt or get attached to the conversation when the user references them. Think of them as contextual data rather than actions.
Anatomy of a Resource
Every resource has a URI and some metadata:
{
"uri": "file:///home/user/project/src/main.rs",
"name": "main.rs",
"description": "Primary application entry point",
"mimeType": "text/x-rust",
"size": 4096,
"annotations": {
"audience": ["user", "assistant"],
"priority": 0.8,
"lastModified": "2025-03-15T10:30:00Z"
}
}
uri (required)
The resource’s unique identifier. URIs follow the standard format: scheme://authority/path. Common schemes include:
file://— Local filesystem pathshttps://— Web URLspostgres://— Database connectionsgit://— Git repositories- Custom schemes — Servers can define their own (e.g.,
slack://channel/general)
name (required)
A human-readable name for the resource. This is what gets displayed in UIs.
description (optional)
Explains what the resource contains or why it’s useful.
mimeType (optional)
The MIME type of the resource’s content (text/plain, application/json, image/png, etc.). Helps clients decide how to render or process the data.
size (optional)
The size in bytes. Useful for clients that want to warn before loading large resources.
annotations (optional)
Metadata about the resource’s intended use:
| Field | Type | Purpose |
|---|---|---|
audience | string[] | Who should see this? "user", "assistant", or both |
priority | number | How important is this? 0.0 (low) to 1.0 (high) |
lastModified | string | ISO 8601 timestamp of last modification |
The audience field is particularly useful. Some resources are for the model’s context (API docs, schema definitions), some are for the user’s eyes (rendered HTML, images), and some are for both.
Discovering Resources
Clients discover resources with resources/list:
{
"jsonrpc": "2.0",
"id": 1,
"method": "resources/list",
"params": {}
}
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resources": [
{
"uri": "file:///project/README.md",
"name": "README.md",
"mimeType": "text/markdown"
},
{
"uri": "file:///project/src/index.ts",
"name": "index.ts",
"mimeType": "text/typescript"
},
{
"uri": "postgres://localhost/mydb/schema",
"name": "Database Schema",
"description": "Current database schema definition",
"mimeType": "application/json"
}
]
}
}
This list is paginated (see Chapter 3) for servers with many resources.
Reading Resources
To read a resource’s contents:
{
"jsonrpc": "2.0",
"id": 2,
"method": "resources/read",
"params": {
"uri": "file:///project/README.md"
}
}
Response:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"contents": [
{
"uri": "file:///project/README.md",
"mimeType": "text/markdown",
"text": "# My Project\n\nThis is a cool project that does cool things."
}
]
}
}
A single resources/read request can return multiple content items (for example, if the URI represents a directory or a compound resource). Each item in the contents array has either a text field (for text data) or a blob field (for base64-encoded binary data).
Text Resources
{
"uri": "file:///config.yaml",
"mimeType": "text/yaml",
"text": "database:\n host: localhost\n port: 5432"
}
Binary Resources
{
"uri": "file:///logo.png",
"mimeType": "image/png",
"blob": "iVBORw0KGgoAAAANSUhEUg..."
}
Resource Templates
Sometimes a server doesn’t have a fixed list of resources—it has a pattern for resources that can be generated on demand. Resource templates solve this using URI templates (RFC 6570).
Discovering templates:
{
"jsonrpc": "2.0",
"id": 3,
"method": "resources/templates/list",
"params": {}
}
Response:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resourceTemplates": [
{
"uriTemplate": "postgres://localhost/mydb/tables/{table}/schema",
"name": "Table Schema",
"description": "Get the schema for a specific database table",
"mimeType": "application/json"
},
{
"uriTemplate": "github://repos/{owner}/{repo}/issues/{issue_number}",
"name": "GitHub Issue",
"description": "A specific GitHub issue"
}
]
}
}
The {table}, {owner}, {repo}, and {issue_number} are template variables. The client fills them in to construct a concrete URI, then uses resources/read to fetch the data.
For example, to get the schema for the users table:
{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/read",
"params": {
"uri": "postgres://localhost/mydb/tables/users/schema"
}
}
Resource templates are powerful because they let servers expose an infinite number of resources without listing them all upfront. A GitHub server doesn’t need to list every issue in every repository—it provides a template and the client fills in the blanks.
Subscriptions
For resources that change over time (log files, live metrics, collaborative documents), MCP supports subscriptions. The client asks to be notified when a resource changes:
Subscribe:
{
"jsonrpc": "2.0",
"id": 5,
"method": "resources/subscribe",
"params": {
"uri": "file:///var/log/app.log"
}
}
When the resource changes, the server sends a notification:
{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": {
"uri": "file:///var/log/app.log"
}
}
Note: the notification doesn’t include the new content—just the URI. The client should re-read the resource to get the updated data. This design keeps notifications lightweight and avoids sending large payloads for every change.
Unsubscribe:
{
"jsonrpc": "2.0",
"id": 6,
"method": "resources/unsubscribe",
"params": {
"uri": "file:///var/log/app.log"
}
}
Subscription support is opt-in. The server declares it in capabilities:
{
"capabilities": {
"resources": {
"subscribe": true
}
}
}
Resources vs. Tools: When to Use Which
This is one of the most common design questions when building MCP servers. Here’s the mental model:
Use Resources When:
- The data is read-only — You’re exposing something to read, not executing an action
- The data is addressable — It has a natural URI (a file path, a URL, a database record)
- The data is relatively static — It doesn’t change on every request
- You want application-controlled access — The host/user decides when to fetch it
- The data is useful as context — It belongs in the system prompt or conversation background
Examples: files, configuration, database schemas, documentation, API specs, templates
Use Tools When:
- You’re performing an action — Creating, updating, deleting, searching
- The operation has side effects — It changes state somewhere
- The result is dynamic — It depends on input parameters and the current moment
- You want model-controlled access — The LLM decides when to invoke it
- The operation is a computation — Calculate, transform, analyze
Examples: search queries, API calls, file writes, data analysis, code execution
The Gray Area
Some things could go either way. Reading a file? Could be a resource (file:///path) or a tool (read_file(path)). Both work.
The practical guideline: if the model needs to decide to fetch the data based on conversation context, make it a tool. If the data should always be available as background context, make it a resource.
Many servers expose both. A filesystem server might expose the current directory’s files as resources (for context) and provide tools like read_file, write_file, search_files (for actions the model can take).
Common URI Schemes
MCP doesn’t mandate specific URI schemes, but conventions have emerged:
| Scheme | Example | Use Case |
|---|---|---|
file:// | file:///home/user/doc.txt | Local files |
https:// | https://api.example.com/data | Web resources |
postgres:// | postgres://host/db/table | Database resources |
git:// | git://repo/branch/path | Git repository contents |
s3:// | s3://bucket/key | AWS S3 objects |
slack:// | slack://channel/general | Slack channels/messages |
custom:// | custom://anything/you/want | Server-defined schemes |
Servers can use any scheme they want. The URI is opaque to the client—it just passes it back to the server for reading. The scheme is a hint for humans, not a routing mechanism.
Resource Annotations in Practice
Annotations help clients make intelligent decisions about how to use resources:
Audience Targeting
{
"uri": "file:///project/api-schema.json",
"name": "API Schema",
"annotations": {
"audience": ["assistant"],
"priority": 0.9
}
}
This schema is for the LLM’s context—it helps the model understand the API. The user doesn’t need to see raw JSON Schema.
{
"uri": "file:///project/architecture-diagram.png",
"name": "Architecture Diagram",
"annotations": {
"audience": ["user"],
"priority": 0.7
}
}
This diagram is for the user to look at. The LLM (unless it’s multimodal) can’t process it.
{
"uri": "file:///project/README.md",
"name": "README",
"annotations": {
"audience": ["user", "assistant"],
"priority": 0.8
}
}
Both the user and the model benefit from seeing the README.
Priority-Based Loading
When a host has limited context window space, priority helps it decide what to include:
- Priority 1.0: Critical context, always include
- Priority 0.7-0.9: Important, include if there’s room
- Priority 0.3-0.6: Nice to have
- Priority 0.0-0.2: Include only if specifically requested
Best Practices
1. Use Meaningful URIs
URIs should be descriptive and follow conventions. file:///project/src/auth/login.ts is better than resource://r42.
2. Set MIME Types
Always set mimeType when you know it. This helps clients render content correctly and helps the LLM understand what it’s looking at.
3. Use Templates for Parameterized Resources
If your resources follow a pattern, expose templates. Don’t list every possible database table as a separate resource—provide a template with a {table} parameter.
4. Keep Resources Focused
A resource should represent one coherent piece of data. Don’t concatenate your entire database into a single resource. Don’t split a single config file into 20 resources. Use your judgment.
5. Implement Subscriptions for Live Data
If your resource changes (log files, metrics, live data), implement subscriptions. This lets clients stay up-to-date without polling.
6. Consider Size
Large resources consume context window space. If a resource could be very large, consider providing a summary resource alongside the full version, or document the expected size so clients can make informed decisions.
Summary
Resources are MCP’s read-only data primitive. They have URIs, metadata, and content. They support templates for parameterized access and subscriptions for live updates. They’re application-controlled (unlike tools, which are model-controlled), making them ideal for providing contextual data to the LLM.
Resources and tools are complementary. Together, they give an LLM both the context it needs to reason (resources) and the ability to act on that reasoning (tools).
Next: the third and final primitive—prompts.