MCP Resources: The Overlooked Primitive (and Why That's a Problem)

AI MCP
On this page

Everyone talks about MCP Tools. The GitHub discussions focus on Tools. The token efficiency debates center on Tools. But MCP has another primitive that solves a different problem entirely, and ignoring it means teams are stuffing context where it doesn’t belong.

Resources are MCP’s forgotten primitive. Inconsistent host support is holding back patterns that could meaningfully improve how we build agent systems.

The control model difference

The distinction between Tools and Resources comes down to one question: who decides when to use it?

Tools are model-controlled. You describe a capability to the LLM, and the model decides when to invoke it based on conversation context. You give the model a hammer; it decides when to swing.

{
  "name": "search_files",
  "description": "Search files by glob pattern",
  "inputSchema": {
    "type": "object",
    "properties": {
      "pattern": { "type": "string" }
    }
  }
}

The model sees this tool definition, interprets user intent, and decides whether to call search_files. That decision-making consumes context and requires the tool description to be present in the context window.

Resources are application-controlled. The application or user explicitly decides what context to load before the model ever sees it. Resources are data the AI can read, not functions it can execute.

{
  "uri": "file:///project/README.md",
  "name": "README.md",
  "description": "Project documentation",
  "mimeType": "text/markdown"
}

The host application presents available resources to the user. The user selects what to include. The model receives the content directly, without needing to decide whether to fetch it.

This is a fundamental architectural difference. Tools require the LLM to reason about when to use them. Resources let humans or applications make that decision upfront.


Why this matters for token efficiency

In my previous post on MCP token efficiency, I covered how verbose tool schemas burn tokens before agents do any actual work. The community has developed solutions at every layer: host-side filtering, gateway aggregation, protocol proposals for lazy hydration.

But something keeps getting missed: some context doesn’t need the model to decide when to fetch it.

When Abdelhak Bahioui commented on that post, he mentioned offloading static context to MCP Resources rather than bloating tool descriptions. He’s onto something important.

If the user knows they want to include project documentation before asking a question, why should that documentation’s availability be described in a tool manifest? Why burn tokens on a tool description the model has to parse and reason about?

Resources flip the model:

ApproachToken CostDecision Maker
Tool that fetches docsTool description tokens + LLM reasoningModel decides when to call
Resource with docsJust the content tokensUser/app decides upfront

For static, reference-style context, Resources can be significantly more efficient. The model doesn’t need to see a tool description, doesn’t need to decide whether to invoke it, and doesn’t need to parse the response as a tool result. The content just is part of the context.

(To be clear: Resources still consume context tokens when loaded. The efficiency gain is in how they get there, not what gets loaded. No tool manifest overhead, no LLM reasoning overhead. The actual content tokens are the same either way.)


The MCP Resources API

The MCP specification defines Resources as a first-class primitive with its own protocol methods.

Capability declaration

Servers that support Resources must declare the capability:

{
  "capabilities": {
    "resources": {
      "subscribe": true,
      "listChanged": true
    }
  }
}

Both subscribe and listChanged are optional features.

Core methods

resources/list — Discover available resources:

{
  "method": "resources/list",
  "params": { "cursor": "optional-pagination-cursor" }
}

Returns a list of resources with URIs, names, descriptions, and MIME types.

resources/read — Fetch resource content:

{
  "method": "resources/read",
  "params": { "uri": "file:///project/config.json" }
}

Returns the actual content (text or base64-encoded binary).

resources/templates/list — Parameterized resources:

{
  "resourceTemplates": [
    {
      "uriTemplate": "db://tables/{table_name}/schema",
      "name": "Table Schema",
      "description": "Database table schema"
    }
  ]
}

Templates use RFC 6570 URI templates for dynamic resource patterns.

Subscriptions

Resources can be subscribed to for real-time updates:

{
  "method": "resources/subscribe",
  "params": { "uri": "file:///project/config.json" }
}

The server then sends notifications/resources/updated when the resource changes. This enables patterns like live config reloading or watching files for changes.

Resource structure

Each resource includes:

FieldRequiredDescription
uriYesUnique identifier (RFC 3986 compliant)
nameYesDisplay name
titleNoHuman-readable title
descriptionNoWhat this resource contains
mimeTypeNoContent type
sizeNoSize in bytes
annotationsNoHints for clients (audience, priority)

The annotations field is particularly interesting for prioritization:

{
  "annotations": {
    "audience": ["assistant"],
    "priority": 0.8,
    "lastModified": "2025-01-15T10:30:00Z"
  }
}

This lets servers hint which resources are most relevant for AI consumption versus human display.


Patterns where Resources win

Not everything should be a Tool. I’ve seen a few patterns where Resources are clearly better.

1. Reference documentation

User explicitly loads relevant docs before asking questions.

User: "I want to ask about this project"
App: [Shows available resources: README.md, ARCHITECTURE.md, API.md]
User: [Selects README.md and ARCHITECTURE.md]
App: [Loads resource content into context]
User: "How does the authentication system work?"

No tool description overhead. No LLM reasoning about whether to fetch docs. The user made the decision; the model just has the context.

2. Sensitive data opt-in

User explicitly chooses what data to expose. The model never sees options it shouldn’t have.

Consider a CRM integration. With Tools, you might expose:

{
  "name": "search_customers",
  "description": "Search customer records by name, email, or account ID"
}

The model now knows this capability exists and might try to use it inappropriately. With Resources, the user explicitly loads specific customer records into context. The model never sees a capability to search—it just has the data the user chose to include.

This is a security posture difference. Tools expose capabilities. Resources expose data the user has already authorized.

3. Large static context

Config files, schemas, templates, style guides. Load once at conversation start, reference throughout.

{
  "uri": "config://project/eslint",
  "name": "ESLint Configuration",
  "description": "Project linting rules",
  "mimeType": "application/json"
}

If the user is asking about code style, they can load the ESLint config as a Resource. The model doesn’t need a tool to “fetch linting rules”—the rules are just present.

4. State machine storage (experimental)

This is a pattern I’ve been experimenting with, though I’ll note it’s not widely adopted yet: using Resources as a kind of storage for state machines in multi-turn agent workflows.

Instead of passing state through tool results (which the model has to parse and manage), state lives in a Resource that gets updated. The model reads current state from a Resource, makes decisions, and actions update that Resource.

Resource: workflow://current-state
Content: { "step": "review", "items_processed": 47, "errors": [] }

This keeps intermediate state out of the conversation context while remaining accessible. The caveat: this pattern depends on subscription support, which (as we’ll see) is inconsistently implemented.


The host support problem

This is where it gets frustrating: most MCP hosts don’t support Resources well, if at all.

Claude Desktop

Claude Desktop’s Resource support has documented issues:

  • Resources aren’t automatically used. Claude Desktop calls resources/list on startup and displays available resources in Settings, but doesn’t call resources/read when answering questions. It often does a web search instead of reading registered resources.

  • Dynamic resources don’t work. Resource templates like greeting://{name} are broken. Only static resources function. This is tracked as a P0 issue.

  • Size limitations. Large resources can cause stack size errors that don’t occur with equivalent file attachments.

Claude Code

Claude Code has similar issues. MCP servers show as connected, but Resources exposed by those servers aren’t consumable. The same servers work with Claude Desktop (to the extent Desktop works) but not Code.

Other hosts

The pattern repeats across the ecosystem. Cursor, custom integrations, and other MCP hosts have varying levels of Resource support. Some implement resources/list but not resources/read. Some support read but not subscribe. The inconsistency makes Resources unreliable for production use.

Why this keeps happening

I think it comes down to this: Tools get attention because they’re model-controlled and feel more “agentic.” The LLM decides when to use them, which feels like intelligence. Resources require application UX to be useful. Someone has to build the interface for users to browse and select resources.

That’s harder. It requires product thinking, not just protocol implementation.


What good Resource support would look like

If you’re building an MCP host, comprehensive Resource support requires more than just implementing the protocol methods:

Discovery UI

Users need to browse available resources across connected servers. This means:

  • Tree or list views of resources by server
  • Search and filtering
  • Resource metadata display (size, type, description)
  • Template parameter input for dynamic resources

Selection mechanism

Users need ways to include resources in context:

  • Explicit selection (click to add)
  • Drag-and-drop into conversation
  • Keyboard shortcuts for power users
  • “Pin” frequently-used resources

Context management

Once selected, resources need context management:

  • Show what’s currently included
  • Allow removal mid-conversation
  • Handle size limits gracefully
  • Support subscription updates

Automatic inclusion (optional)

Some hosts may want heuristic-based inclusion:

  • Include resources with high priority annotations
  • Include resources matching conversation keywords
  • Let users configure “always include” rules

The spec explicitly supports all of these patterns. The hosts just need to implement them.


Building Resources into your MCP server

If you’re building an MCP server, consider what data makes sense as Resources versus Tools.

Good Resource candidates

  • Static reference content: Documentation, schemas, configuration
  • User-specific context: Profile data, preferences, history
  • Large read-only data: Datasets, logs, exports
  • State snapshots: Current workflow state, session context

Implementation example

Here’s a minimal TypeScript server exposing project documentation as Resources:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";

const server = new Server({
  name: "project-docs",
  version: "1.0.0"
}, {
  capabilities: {
    resources: {}
  }
});

server.setRequestHandler("resources/list", async () => ({
  resources: [
    {
      uri: "docs://readme",
      name: "README",
      description: "Project overview and setup instructions",
      mimeType: "text/markdown"
    },
    {
      uri: "docs://architecture",
      name: "Architecture",
      description: "System architecture documentation",
      mimeType: "text/markdown"
    }
  ]
}));

server.setRequestHandler("resources/read", async (request) => {
  const content = await loadDocumentation(request.params.uri);
  return {
    contents: [{
      uri: request.params.uri,
      mimeType: "text/markdown",
      text: content
    }]
  };
});

When to use Tools instead

Resources aren’t always the answer. Use Tools when:

  • The model needs to decide when to fetch data
  • The operation has side effects
  • Parameters require LLM reasoning to construct
  • The action is part of an agentic workflow

The primitives are complementary. Resources provide context; Tools provide capabilities.


Where this goes

Resources are underutilized because hosts haven’t invested in the UX to make them useful. But the protocol support is there, and the patterns are sound.

As the ecosystem matures and teams hit the token efficiency wall, I expect Resources to get more attention. When every tool description token counts, having an alternative primitive that skips tool reasoning entirely starts to look attractive.

A few things that would help:

  1. Host improvements. Claude Desktop, Claude Code, and other hosts need to actually implement resources/read and build selection UX. The P0 issues need fixing.

  2. Server adoption. More MCP servers should expose Resources alongside Tools. Documentation, configuration, and reference data are natural fits.

  3. Pattern documentation. The community needs more examples of Resource patterns: state machines, context management, user-controlled data exposure.

  4. Tooling. Debugging Resources is harder than debugging Tools because there’s no clear invocation to trace. Better observability would help.


Three things to remember

  1. Resources are application-controlled; Tools are model-controlled. If the user or application should decide what context to include, Resources are likely the right primitive.

  2. Host support is inconsistent. Don’t assume Resources will work reliably across hosts. Test your specific deployment target. The spec support is there; the implementations lag.

  3. Resources complement Tools. They’re not competing primitives. Resources provide context; Tools provide capabilities. Most real systems need both.


Further reading


Are you using MCP Resources in production? I’d love to hear about your patterns and what host support issues you’ve encountered. Reach out on LinkedIn.