Day 5 of 90 ๐ŸŒฑ Spark Phase MCP Resources

Resources Deep Dive โ€”
URIs, Templates & Subscriptions

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.

Think of resources as the filing cabinet next to the AI's desk. Tools are the hands that do work โ€” resources are the folders the hands can open and read. A well-designed resource layer means the model always has the right context without you having to cram everything into the system prompt.
Table of Contents
01 โ€” What Are Resources

What Are Resources?

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.

URIUnique address per resource
2Resource types (static + template)
text|blobContent formats
โˆžCustom URI schemes allowed
๐Ÿ—‚๏ธ

Resources Are a Shared Filing Cabinet

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
๐Ÿ”‘
Resources vs Tool Results โ€” a subtle but critical distinction

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.

02 โ€” URI Design

Resource URI Design

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.

URI Structure
github :// acme-corp / repos/backend/issues ? state=open
Scheme Your custom protocol or https/file
Authority Namespace / org / host
Path Hierarchical resource identifier
Query Optional filters / parameters

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:

textURI scheme conventions by domain
โ”€โ”€ File System Resources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ file:///home/user/project/src/main.ts standard file URI file:///C:/Users/user/docs/report.pdf Windows path โ”€โ”€ Database Resources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ postgres://mydb/users/42 record by primary key postgres://mydb/products?category=electronics filtered query sqlite:///app.db/schema schema introspection โ”€โ”€ API / Service Resources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ github://acme-corp/repos/backend/issues/123 GitHub issue jira://PROJ-456 Jira ticket notion://page/abc123def456 Notion page โ”€โ”€ App-Specific Resources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ config://app/settings app configuration log://app/errors?last=100 last 100 error logs memory://context/session-abc conversation memory
๐Ÿ“
URI Design Rules of Thumb

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.

03 โ€” Static Resources

Static Resources

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.

๐Ÿ“Œ
Static Resource
Fixed URI ยท Always in resources/list
URI defined once at server.resource() call
Always appears in resources/list response
Content can be dynamic โ€” only URI is fixed
Best for: config, schema, readme, global data
Registration: server.resource(name, uri, meta, handler)
config://app/settings
weather://current/london
schema://database/tables
๐Ÿงฉ
Template Resource
URI Pattern ยท Generated on demand
URI is an RFC 6570 template with {variable} slots
Appears in resources/list as a template entry
Client fills in variables, reads the expanded URI
Best for: per-user, per-ID, parameterised records
Registration: server.resource(name, template, meta, handler)
github://{owner}/{repo}/issues
user://{userId}/profile
typescriptRegistering static resources
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // โ”€โ”€ Static resource: fixed URI, dynamic content โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ server.resource( "app-config", // resource name (shown in list) "config://app/settings", // fixed URI { mimeType: "application/json", description: "Current application configuration" }, async (uri) => ({ // handler receives the URI object contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify(await loadConfig(), null, 2) }] }) ); // โ”€โ”€ Static resource: binary content (blob) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ server.resource( "company-logo", "assets://images/logo.png", { mimeType: "image/png", description: "Company logo (PNG)" }, async (uri) => { const bytes = await fs.readFile("./assets/logo.png"); return { contents: [{ uri: uri.toString(), mimeType: "image/png", blob: bytes.toString("base64") // binary โ†’ base64 string }] }; } );
๐Ÿ’ก
text vs blob โ€” which field to use

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.

04 โ€” URI Templates

URI Templates โ€” Dynamic Resources

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.

URI Template Expansion โ€” How Variables Fill In
github://{owner}/{repo}/issues/{number}
{owner:"acme", repo:"api", number:"42"} โ†’ github://acme/api/issues/42
{owner:"nasa", repo:"apollo", number:"7"} โ†’ github://nasa/apollo/issues/7
{owner:"me", repo:"blog", number:"1"} โ†’ github://me/blog/issues/1
typescriptRegistering URI template resources
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; // โ”€โ”€ Template: one registration handles ALL user profiles โ”€โ”€โ”€โ”€โ”€ server.resource( "user-profile", new ResourceTemplate("user://{userId}/profile", { list: undefined // no pre-enumerable list }), { mimeType: "application/json", description: "User profile by ID. Replace {userId} with the user's ID." }, async (uri, { userId }) => { // 2nd arg: extracted variables! const user = await db.users.findById(userId); if (!user) throw new Error(`User ${userId} not found`); return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify({ id: user.id, name: user.name, email: user.email, role: user.role }) }] }; } ); // โ”€โ”€ Template with optional list (enumerate known resources) โ”€โ”€ server.resource( "github-issue", new ResourceTemplate("github://{owner}/{repo}/issues/{number}", { list: async () => ({ // optional: pre-enumerate known URIs resources: (await github.listRecentIssues()).map(issue => ({ uri: `github://${issue.owner}/${issue.repo}/issues/${issue.number}`, name: `#${issue.number}: ${issue.title}`, mimeType: "application/json" })) }) }), { mimeType: "application/json" }, async (uri, { owner, repo, number }) => { const issue = await github.getIssue({ owner, repo, number: +number }); return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: JSON.stringify(issue) }] }; } );
๐Ÿ”
The list callback in ResourceTemplate

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).

05 โ€” MIME Types

MIME Types & Content Formats

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 TypeFieldUse CaseNotes
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
typescriptChoosing text vs blob correctly
// โœ… JSON config โ€” string content, use text field { uri: "config://app/settings", mimeType: "application/json", text: JSON.stringify({ debug: false, maxRetries: 3 }) } // โœ… Markdown doc โ€” string content, use text field { uri: "docs://api/getting-started", mimeType: "text/markdown", text: "# Getting Started\n\nInstall with `npm install`..." } // โœ… PNG image โ€” binary, base64 encode into blob field const imgBytes = await fs.readFile("./chart.png"); { uri: "assets://charts/revenue-2024.png", mimeType: "image/png", blob: imgBytes.toString("base64") // โ† blob, not text } // โŒ WRONG โ€” never use text for binary data { mimeType: "image/png", text: imgBytes.toString("base64") // โ† wrong field for images }
06 โ€” Subscriptions

Resource Subscriptions

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.

Subscription Lifecycle โ€” 4 Steps
1
Declare Capability
Server includes subscription support in the initialize handshake response. Without this, the client won't attempt to subscribe.
capabilities: { resources: { subscribe: true } }
2
Client Subscribes
Client sends resources/subscribe with the URI to watch. Server acknowledges with an empty result.
resources/subscribe โ†’ { uri: "log://app/errors" }
3
Server Notifies on Change
When the resource's content changes, the server sends a notification. The client then calls resources/read to get the new content โ€” the notification doesn't include the content itself.
notifications/resources/updated โ†’ { uri: "log://app/errors" }
4
Client Unsubscribes
When done, the client sends resources/unsubscribe. Good practice to clean up subscriptions when resources are no longer needed.
resources/unsubscribe โ†’ { uri: "log://app/errors" }
typescriptServer-side subscription implementation
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Step 1: Declare subscription capability in server options const server = new McpServer({ name: "live-log-server", version: "1.0.0" }, { capabilities: { resources: { subscribe: true, // โ† enables resources/subscribe method listChanged: true // โ† enables notifications/resources/list_changed } } }); // Step 2: Register the resource with live data const activeSubscribers = new Set<string>(); server.resource( "error-log", "log://app/errors", { mimeType: "text/plain", description: "Live application error log" }, async (uri) => ({ contents: [{ uri: uri.toString(), mimeType: "text/plain", text: (await getRecentErrors(50)).join("\n") }] }) ); // Step 3: Watch the data source and send notifications watchErrorLog((newError) => { // Notify all clients subscribed to this URI server.server.sendNotification({ method: "notifications/resources/updated", params: { uri: "log://app/errors" } }); });
โš ๏ธ
Notifications don't carry content โ€” they just say "something changed"

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.

07 โ€” Resources vs Tools

Resources vs Tools โ€” The Decision Framework

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
๐Ÿฅ

The Doctor's Office Analogy

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.

textDecision guide โ€” resource or tool?
Ask these questions: 1. Does this operation CHANGE anything (write, delete, send)? โ†’ YES โ†’ Tool (always) โ†’ NO โ†’ continue... 2. Is this data the USER should see/preview in the host UI? โ†’ YES โ†’ Resource (lets host render it as an attachment) โ†’ NO โ†’ Tool is fine 3. Does this data change frequently and need live updates? โ†’ YES โ†’ Resource + Subscription โ†’ NO โ†’ continue... 4. Is this a named, addressable piece of content (file, record)? โ†’ YES โ†’ Resource (give it a URI) โ†’ NO โ†’ Tool (compute something on the fly) Examples: config/settings โ†’ Resource (readable, addressable, no mutation) user/42/profile โ†’ Resource (addressable record, display in UI) search GitHub issues โ†’ Tool (action with parameters, returns results) create Jira ticket โ†’ Tool (mutates external state) latest error logs โ†’ Resource (addressable, subscribe for updates) calculate tax amount โ†’ Tool (computation, not stored data)
08 โ€” Real-World Patterns

Real-World Resource Patterns

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 System Resources
file:///path/to/file.ts
Expose local files at their 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.
๐Ÿ—„๏ธ
Database Record Resources
postgres://db/users/{id}
Map database tables to URI templates: 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.
๐ŸŒ
External API Resources
github://{owner}/{repo}/readme
Mirror external API endpoints as resources when they're read-only and addressable. Cache responses with a TTL to avoid hammering rate limits. Return the original MIME type where possible โ€” text/markdown for READMEs, application/json for structured data.
โš™๏ธ
Configuration Resources
config://app/{section}
Expose your server's own configuration as a resource. Lets the model understand its own context โ€” feature flags, environment info, supported capabilities. Always sanitize: strip API keys, tokens, and secrets before returning. Return application/json.
๐Ÿ“
Log & Event Resources
log://app/errors?last=50
Expose time-windowed logs as resources with optional query parameters for filtering. Combine with subscriptions to push change notifications when new errors appear. Return text/plain with one entry per line โ€” easy for the model to count, scan, and reason over.
๐Ÿง 
Memory & Context Resources
memory://session/{id}/facts
Store conversation facts, user preferences, and learned context in memory resources. The model can read them at the start of a session to restore context without re-learning from scratch. Combine with a save_memory tool for write-back. Return as application/json.
๐Ÿš€
Combine resources + tools for maximum power

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.

typescriptComplete resource + tool pairing example
// READ โ€” resource for browsing notes server.resource( "note", new ResourceTemplate("notes://{noteId}", { list: async () => ({ resources: (await db.notes.findAll()).map(n => ({ uri: `notes://${n.id}`, name: n.title, mimeType: "text/markdown" })) }) }), { mimeType: "text/markdown" }, async (uri, { noteId }) => { const note = await db.notes.find(noteId); return { contents: [{ uri: uri.toString(), mimeType: "text/markdown", text: note.content }] }; } ); // WRITE โ€” tools for mutating notes server.tool( "create_note", "Create a new note. Returns the new note's URI.", { title: z.string(), content: z.string() }, async ({ title, content }) => { const note = await db.notes.create({ title, content }); // Notify subscribers that the resource list changed server.server.sendNotification({ method: "notifications/resources/list_changed", params: {} }); return { content: [{ type: "text" as const, text: `Created. URI: notes://${note.id}` }] }; } );
09 โ€” Knowledge Check

Test Your Understanding

Five questions covering everything from today's deep dive.

Day 5 โ€” Resources Deep Dive Quiz

Select one answer per question, then submit to see your score.

Q1 What is the fundamental difference between a static resource and a URI template resource?
A Static resources return fixed data; template resources always return dynamic data from an API
B Static resources have a fixed URI defined at registration; template resources define a URI pattern with variables the client fills in at read time
C Static resources support subscriptions; template resources do not
D Static resources use the text field; template resources use the blob field
โœ… B is correct. "Static" refers to the URI being fixed, not the content โ€” a static resource can still return dynamic data. A URI template resource uses {variable} placeholders (RFC 6570). The client fills these in when requesting a specific resource. One template registration handles potentially millions of individual addressable resources.
Q2 A notifications/resources/updated notification is received. What must the client do to get the new content?
A The notification's params field already contains the new content โ€” extract it directly
B Call resources/read with the URI from the notification to fetch the updated content separately
C Re-subscribe to the resource โ€” the subscription resets and sends the new content automatically
D Wait for the server to push a resources/push message with the new content
โœ… B is correct. The notification is purely a cache invalidation signal โ€” it only contains the URI, not the content. After receiving it, the client calls resources/read with that URI to fetch the new content. The spec intentionally separates notification (lightweight) from content delivery (potentially large).
Q3 You're building a server that returns PNG screenshots. Which content block field and MIME type are correct?
A Field: text, MIME: image/png
B Field: blob, MIME: image/png, value is base64-encoded bytes
C Field: data, MIME: image/png
D Field: blob, MIME: text/plain, since all MCP content is text
โœ… B is correct. Binary content uses the 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.
Q4 Which scenario is best handled by a Resource rather than a Tool?
A Searching GitHub issues by keyword
B Creating a new Jira ticket with title and description
C Reading the application's current configuration settings
D Sending a Slack message to a channel
โœ… C is correct. Configuration settings are addressable (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.
Q5 Your server has 10,000 user records. How should you expose them as resources?
A Register 10,000 static resources, one per user โ€” resources/list will return all of them
B Use a URI template user://{userId}/profile with list: undefined โ€” one registration handles all 10,000 users, client addresses them individually
C Create a single static resource that returns all 10,000 users in one JSON blob
D Resources can only handle up to 100 items โ€” use a Tool instead
โœ… B is correct. URI templates are designed exactly for this scenario. One template registration covers every possible user ID. Setting 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.
๐ŸŽ‰
Score: 5/5
Resources mastered. Ready for Day 6!
โ† Previous Day
Day 4: Tools in Depth
Design, errors, and real-world patterns
Next Day โ†’
Day 6: Prompts & Sampling
Reusable prompt templates and model sampling