Tools let the model act. Resources let the model read. Today you master the second MCP primitive โ from designing clean URI schemes to dynamic URI templates, MIME types, subscription change notifications, and the six real-world resource patterns that power production MCP servers.
In MCP, a Resource is any piece of data your server exposes at a URI that a client can read. Files, database records, API responses, configuration objects, log excerpts, code snippets โ if it can be represented as text or binary bytes and addressed by a URI, it can be a resource.
Resources are application-controlled. Unlike Tools (which the model decides to call) and Prompts (which the user selects), Resources are surfaced by the host application โ Claude Desktop decides when to attach a resource to context. The model can request to read a specific resource, but the host ultimately controls what the user sees and what gets included.
Imagine a physical filing cabinet in an office. Each drawer has a label (the URI). Anyone with access can open a drawer and read what's inside (resources/read). Some drawers always contain the same document โ your company handbook (static resource). Other drawers are labelled with a template โ "Client [name] Contract" โ and you fill in the client name to find the right file (URI template resource). The filing cabinet doesn't change the documents; it just makes them findable and readable.
sequenceDiagram participant H as Host App
(Claude Desktop) participant C as MCP Client participant S as MCP Server H->>C: User opens chat C->>S: resources/list S-->>C: [{uri, name, mimeType}, ...] H->>H: Display resource list to user Note over H: User or model selects a resource C->>S: resources/read { uri } S-->>C: { contents: [{ uri, text }] } H->>H: Attach content to model context
When a tool runs and returns text, that text goes directly into the model's context as a tool result. When a resource is read, the host decides whether and how to include it โ it might show it as an attachment, inject it into context, or let the user preview it separately. Resources are a more deliberate, user-visible data access pattern.
A Resource URI is the unique address that identifies a piece of data in your server. MCP is deliberately flexible โ you can use https://, file://, or completely custom schemes like github://, postgres://, or myapp://. The only requirement: URIs must be unique within your server and stable across calls.
MCP doesn't mandate any specific URI scheme โ that's intentional. Your choice of scheme should communicate the data's origin and type at a glance. Here are the conventions that have emerged in the MCP ecosystem:
1. Hierarchical paths โ nest logically: github://org/repo/issues/123 not github://issue-123-in-backend-repo. 2. Stable across restarts โ URIs should be deterministic. Don't use timestamps or random IDs as path segments. 3. Descriptive scheme โ postgres:// tells the reader more than db:// or myapp://. 4. No auth in the URI โ never embed API keys or passwords in the URI; use server-side env vars instead.
A static resource has a fixed, known URI defined at registration time. When the server responds to resources/list, static resources appear verbatim in the list. The client reads them with resources/read using that exact URI. The content can still change dynamically โ "static" refers to the URI being fixed, not the data it returns.
server.resource(name, uri, meta, handler){variable} slotsserver.resource(name, template, meta, handler)Use text for anything string-based: JSON, Markdown, CSV, plain text, XML, TypeScript source. Use blob for binary data that cannot be represented as UTF-8 text: images, PDFs, compiled binaries, audio. The blob value must be base64-encoded. Never mix both fields in the same content block.
What if you have thousands of users, each with a profile? You can't register a static URI for every user. That's where URI Templates shine. Based on RFC 6570, a URI template is a pattern with {variable} placeholders. The server registers the pattern once; the client fills in variables and reads the expanded URI.
The list callback inside ResourceTemplate is optional. If provided, it returns a set of known concrete URIs that the SDK merges into the resources/list response โ giving the model a starting set of readable resources for that template. Set it to undefined if the namespace is too large to pre-enumerate (e.g., all possible user IDs).
Every resource content block must declare a mimeType. This tells the host and model what format the content is in, how to render it, and whether it's text or binary. Getting MIME types right is not optional โ a host displaying a text/markdown resource will render it as formatted markdown; the same content tagged as text/plain will show raw asterisks and hashes.
| MIME Type | Field | Use Case | Notes |
|---|---|---|---|
| text/plain | text | Logs, raw output, simple strings | Default choice when unsure |
| text/markdown | text | Documentation, README, articles | Host may render as formatted HTML |
| application/json | text | API responses, config, structured data | Must be valid JSON string |
| text/html | text | Web pages (sanitized) | Host decides whether to render |
| text/csv | text | Tabular data, spreadsheet exports | Model can parse columns well |
| application/xml | text | SOAP, RSS, configuration files | Include encoding declaration |
| image/png | blob | Screenshots, diagrams, logos | Base64-encode binary bytes |
| image/jpeg | blob | Photos, compressed images | Base64-encode binary bytes |
| application/pdf | blob | Reports, documents | Large files may exceed context limits |
Most resource reads are pull-based โ the client asks for a resource when it needs it. But what if a resource changes frequently? Polling with resources/read every few seconds is wasteful. MCP's subscription system solves this: the client subscribes to a resource URI, and the server notifies it whenever that resource's content changes.
This is entirely opt-in. Your server must declare subscription support in its capabilities during the handshake, and you must emit notifications/resources/updated when the resource changes. Without both, subscriptions silently fail.
resources/subscribe with the URI to watch. Server acknowledges with an empty result.resources/read to get the new content โ the notification doesn't include the content itself.resources/unsubscribe. Good practice to clean up subscriptions when resources are no longer needed.A common mistake: trying to embed the new resource content inside the notifications/resources/updated payload. The spec defines this notification with only a uri parameter. When the client receives it, the client calls resources/read separately to fetch the new content. Think of it as a "cache invalidation signal", not a data push.
Both resources and tools can return data to the model. Beginners often implement everything as a tool because tools are simpler. This works, but it misses the architectural intent of the protocol. Choosing correctly between the two has real consequences for performance, user experience, and how the host application presents your server's capabilities.
| Dimension | ๐ฆ Resources | ๐ง Tools |
|---|---|---|
| Control | App-controlled โ host decides when to attach | Model-controlled โ AI decides when to call |
| Direction | Pull only โ client reads on demand | Call/response โ model invokes, server acts |
| Side effects | Read-only by convention โ no mutations | Can read AND write, call APIs, delete data |
| Discoverability | Listed in resources/list โ user can browse | Listed in tools/list โ model browses at inference |
| Caching | Host can cache aggressively, subscribe to updates | No caching protocol โ every call is fresh |
| UX in host | Shown as attachments/tabs user can preview | Shown as "Claude used a tool" in chat |
| Best for | Config, docs, schemas, user data, files | Search, create, update, delete, compute |
A patient's medical records are a resource โ readable, stable-ish, the doctor pulls them up before the appointment. Running a blood test is a tool โ an action with a result, potentially with side effects, executed on demand. You wouldn't implement "view patient chart" as a tool (it just reads), and you wouldn't implement "order MRI" as a resource (it takes action). The right primitive makes the model smarter about what it's doing and why.
These are the six resource patterns you'll implement in nearly every production MCP server. Each one maps a common data domain to a clean URI design and content strategy.
file:// URI. Use ResourceTemplate with file://{+path} to accept any path. Return text/plain for source code, detect MIME type by extension for other files. Never expose paths outside a sandboxed root directory.postgres://db/{table}/{id}. Return rows as JSON. For schema introspection, add a static postgres://db/schema resource that returns all table definitions โ invaluable for letting the model write correct queries.text/markdown for READMEs, application/json for structured data.application/json.text/plain with one entry per line โ easy for the model to count, scan, and reason over.save_memory tool for write-back. Return as application/json.The most powerful MCP servers pair resources with complementary tools. A user://{id}/profile resource (read) pairs with an update_user_profile tool (write). A github://{owner}/{repo}/issues resource (browse) pairs with create_github_issue and close_github_issue tools (act). This pattern โ browse with resources, act with tools โ is the natural MCP architecture for any CRUD domain.
Five questions covering everything from today's deep dive.
Select one answer per question, then submit to see your score.
{variable} placeholders (RFC 6570). The client fills these in when requesting a specific resource. One template registration handles potentially millions of individual addressable resources.notifications/resources/updated notification is received. What must the client do to get the new content?resources/read with that URI to fetch the new content. The spec intentionally separates notification (lightweight) from content delivery (potentially large).blob field with base64-encoded bytes. Text content (JSON, Markdown, plain text) uses the text field. The MIME type image/png correctly identifies the content format. There is no data field in MCP resource content blocks.config://app/settings), read-only by convention, and benefit from being host-visible (user can inspect them). Search (A) takes dynamic parameters and belongs as a Tool. Creating tickets (B) and sending messages (D) are write operations โ always Tools.list: undefined tells the SDK not to pre-enumerate (since you can't enumerate 10,000 users in a list response). The client looks up individual users by constructing the URI with a known ID.