MCP Resources: The Overlooked Primitive (and Why That's a Problem)
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:
| Approach | Token Cost | Decision Maker |
|---|---|---|
| Tool that fetches docs | Tool description tokens + LLM reasoning | Model decides when to call |
| Resource with docs | Just the content tokens | User/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:
| Field | Required | Description |
|---|---|---|
uri | Yes | Unique identifier (RFC 3986 compliant) |
name | Yes | Display name |
title | No | Human-readable title |
description | No | What this resource contains |
mimeType | No | Content type |
size | No | Size in bytes |
annotations | No | Hints 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/liston startup and displays available resources in Settings, but doesn’t callresources/readwhen 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
priorityannotations - 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:
-
Host improvements. Claude Desktop, Claude Code, and other hosts need to actually implement
resources/readand build selection UX. The P0 issues need fixing. -
Server adoption. More MCP servers should expose Resources alongside Tools. Documentation, configuration, and reference data are natural fits.
-
Pattern documentation. The community needs more examples of Resource patterns: state machines, context management, user-controlled data exposure.
-
Tooling. Debugging Resources is harder than debugging Tools because there’s no clear invocation to trace. Better observability would help.
Three things to remember
-
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.
-
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.
-
Resources complement Tools. They’re not competing primitives. Resources provide context; Tools provide capabilities. Most real systems need both.
Further reading
- MCP Resources Specification
- MCP Tool Schema Bloat: The Hidden Token Tax
- MCP Resources Explained (Medium)
- Claude Desktop Resource Issues (GitHub)
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.