Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 paths
  • https:// — Web URLs
  • postgres:// — Database connections
  • git:// — 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:

FieldTypePurpose
audiencestring[]Who should see this? "user", "assistant", or both
prioritynumberHow important is this? 0.0 (low) to 1.0 (high)
lastModifiedstringISO 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:

SchemeExampleUse Case
file://file:///home/user/doc.txtLocal files
https://https://api.example.com/dataWeb resources
postgres://postgres://host/db/tableDatabase resources
git://git://repo/branch/pathGit repository contents
s3://s3://bucket/keyAWS S3 objects
slack://slack://channel/generalSlack channels/messages
custom://custom://anything/you/wantServer-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.