Exposing Resources: URIs, Templates, and Data
- Define static and template-based resources using URIs and RFC 6570 URI templates
- Return resource content as TextResourceContents or BlobResourceContents with appropriate MIME types
- Choose between resources and tools based on whether the operation is read-only context retrieval or active computation
- Implement resource handlers in both TypeScript (low-level SDK) and Python (FastMCP) patterns
Resources vs Tools: The Core Distinction
MCP defines three primitives: tools, resources, and prompts. Of these, resources are the most conceptually distinct. The rule is simple: resources are read-only data sources that the client or model can pull context from. Tools perform actions.
- "Read a file from disk" → resource
- "Write a file to disk" → tool
- "Get the database schema for a table" → resource
- "Run a SQL query against a table" → tool
- "Return the current app configuration" → resource
- "Update the app configuration" → tool
If an operation has side effects, produces dynamic results requiring computation parameters beyond a URI, or changes state anywhere — it belongs as a tool. Resources are passive: they sit there waiting to be read, identified by a URI.
Two Resource Types
MCP supports two flavors of resources, differentiated by how their URI is specified.
Static resources have a fixed, known URI that never changes. They are always available and always return the same kind of data (though the content may change over time). Examples: config://app/settings, docs://readme, file:///etc/hosts.
Dynamic resources use URI templates following RFC 6570. A URI template contains one or more {placeholder} variables that the client fills in before making the read request. Examples: db://tables/{table_name}/schema, file://{path}, weather://{city}/current.
The two types are registered and listed separately: static resources appear in resources/list responses; URI templates appear in resources/templates/list responses.
URI Scheme Conventions
You define your own URI schemes when building an MCP server. There are no reserved schemes beyond the standard ones. Use descriptive custom schemes that communicate the data domain clearly:
config://— configuration datadb://— database contentdocs://— documentationfile://— filesystem contentweather://— weather datagithub://— GitHub content
The scheme is purely informational. The server parses the full URI string and routes it however it chooses — there is no DNS lookup or network resolution happening. Keep schemes short, lowercase, and domain-specific to your server's purpose.
The Three MCP Resource Requests
Clients interact with resources through three request types:
resources/list— returns the array of static resource descriptors the server exposesresources/templates/list— returns URI templates clients can fill in to address dynamic resourcesresources/read— takes a fully resolved URI and returns the resource content
A client that wants to browse available context will call resources/list and resources/templates/list on startup, then call resources/read with a specific URI when the user or model requests that content.
TypeScript: Static Resources (Low-Level SDK)
Using the low-level SDK, you register request handlers directly for each MCP request type:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { ListResourcesRequestSchema, ReadResourceRequestSchema }
from "@modelcontextprotocol/sdk/types.js";
const server = new McpServer({ name: "config-server", version: "1.0.0" });
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "config://app/settings",
name: "Application Settings",
description: "Current application configuration",
mimeType: "application/json",
},
],
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri === "config://app/settings") {
return {
contents: [{
uri,
mimeType: "application/json",
text: JSON.stringify({ theme: "dark", version: "1.2.0" }, null, 2),
}],
};
}
throw new Error(`Unknown resource: ${uri}`);
});
The contents array in the read response can contain multiple items — useful when one URI logically returns multiple pieces (e.g., a directory listing with individual file contents). Each item carries its own uri and mimeType.
TypeScript: URI Templates for Dynamic Resources
import { ListResourceTemplatesRequestSchema }
from "@modelcontextprotocol/sdk/types.js";
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
resourceTemplates: [
{
uriTemplate: "db://tables/{table_name}/schema",
name: "Table Schema",
description: "Returns the column schema for a database table",
mimeType: "application/json",
},
],
}));
When the client wants the schema for the users table, it fills in the template and calls resources/read with db://tables/users/schema. Your ReadResourceRequestSchema handler receives that fully-resolved URI, parses out the table name, queries your database, and returns the result.
Use a URL parsing approach to extract template variables from the incoming URI:
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
const match = uri.match(/^db://tables/([^/]+)/schema$/);
if (match) {
const tableName = match[1];
const schema = await db.getSchema(tableName);
return {
contents: [{
uri,
mimeType: "application/json",
text: JSON.stringify(schema, null, 2),
}],
};
}
throw new Error(`Unknown resource: ${uri}`);
});
Python FastMCP: Resources Are Simpler
FastMCP's decorator API makes resources feel like annotated functions:
from mcp.server.fastmcp import FastMCP
import json
mcp = FastMCP("config-server")
@mcp.resource("config://app/settings")
def get_app_settings() -> str:
"""Current application configuration."""
return json.dumps({"theme": "dark", "version": "1.2.0"}, indent=2)
@mcp.resource("db://tables/{table_name}/schema")
def get_table_schema(table_name: str) -> str:
"""Returns the column schema for a database table."""
schema = db.get_schema(table_name)
return json.dumps(schema)
FastMCP infers the resource URI from the decorator argument. Template variables in the URI map directly to function parameters — {table_name} becomes the table_name: str parameter. The return value becomes the resource text content.
TextResourceContents vs BlobResourceContents
Resource content has two representations:
TextResourceContents — use the text field for any text-based content: text/plain, text/html, application/json, text/csv. This is what you'll use for the vast majority of resources.
BlobResourceContents — use the blob field (base64-encoded string) for binary content: images (image/png, image/jpeg), PDFs (application/pdf), or any other binary format. The client decodes the base64 to get the raw bytes.
A resource item uses one or the other, never both. If your MIME type is a text format, use text. If it's binary, base64-encode it and use blob.
When to Use Resources vs Tools
Resources are the right choice when:
- The data is configuration, documentation, or schema — things the model reads to understand context before acting
- The operation is purely read-only with no side effects
- The data is addressable by a URI, with at most a few path parameters
- The content could reasonably be cached or pre-fetched
Reach for a tool instead when:
- The operation needs complex parameters that don't fit naturally into a URI
- You're doing computation, not retrieval (e.g., running a search query)
- There are side effects: writes, notifications, API mutations
- The result depends on state the client sends in the body, not just a URI
Client Support in 2026
Not all MCP clients fully implement resource browsing. Claude Desktop exposes resources in the attachment panel and lets users explicitly attach resource content to conversations. Some programmatic clients — particularly those using the SDK directly — only request resources when the model explicitly asks for them via tool calls or when the developer explicitly calls resources/read. Test your resources in MCP Inspector (covered in lesson 12) before assuming a client will surface them automatically.
What to Try Next
Add a resource to a server you've already built in this track:
- Define a static
config://app/settingsresource that returns a JSON object describing your server's configuration or capabilities - Define a URI template resource like
docs://{topic}that returns a short description for each topic your server handles - Connect MCP Inspector and verify both resources appear in the Resources tab — then click "Read" to confirm the content returns correctly
- Resources are read-only context sources — URIs identify them, resources/list enumerates them, resources/read fetches content
- URI templates follow RFC 6570 and use {placeholder} syntax to create parameterized resources (e.g., file://{path}, db://tables/{name}/schema)
- Use the text field for text content (JSON, plain text, HTML) and blob with base64 encoding for binary content like images or PDFs
- Resources suit configuration, documentation, and static data; tools suit anything requiring computation, side effects, or parameters beyond a URI
- Not all MCP clients fully implement resource browsing in 2026 — Claude Desktop supports it, but some clients only use resources when the model explicitly requests them