Introduction
Resources are one of the three core MCP primitives. They allow your server to expose data — files, configuration, database records, API responses — that LLMs and clients can read. Unlike tools, resources are not actions; they are data sources that provide context to the model.
In the TypeScript SDK, you register resources using server.registerResource(), providing a name, a URI, options, and a handler function that returns the data.
Key Concepts
- Static resources have a fixed URI (e.g.,
config://app) and always point to the same logical piece of data server.registerResource()takes four arguments: a unique name, a URI string, an options object, and an async handler- Options include
title,description, andmimeType— these help clients understand and display the resource ReadResourceResultis the return type of the handler, containing acontentsarray- Content entries use
{ uri, text }for text data or{ uri, blob }for base64-encoded binary data
Real World Context
Static resources are ideal for exposing data that has a single, well-known location:
- Application configuration (
config://app) - Server health status (
status://health) - Environment metadata (
env://current) - Database schema definitions (
schema://main)
When an LLM needs context about your system, it reads these resources just like a developer would open a config file.
Deep Dive
The server.registerResource() method is the primary way to expose data. Here is a complete example that registers a static configuration resource.
typescriptimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; const server = new McpServer({ name: 'my-server', version: '1.0.0' }); server.registerResource( 'config', 'config://app', { title: 'App Config', description: 'Application configuration', mimeType: 'application/json' }, async (uri): Promise<ReadResourceResult> => ({ contents: [{ uri: uri.href, text: JSON.stringify({ debug: true, version: '1.0.0' }) }] }) );
The first argument 'config' is the resource name — it must be unique across all registered resources. The second argument is the URI string that clients use to request this resource. The options object provides metadata, and the handler is called whenever a client reads the resource.
The handler receives the parsed URI and must return a ReadResourceResult. The contents array can contain one or more entries. For text content, use the text field.
typescriptserver.registerResource( 'readme', 'file://readme', { title: 'README', description: 'Project readme file', mimeType: 'text/markdown' }, async (uri): Promise<ReadResourceResult> => ({ contents: [{ uri: uri.href, text: '# My Project\nWelcome to the project.' }] }) );
For binary content such as images or PDFs, use the blob field with base64-encoded data instead of text.
typescriptserver.registerResource( 'logo', 'file://logo.png', { title: 'Company Logo', description: 'Logo image', mimeType: 'image/png' }, async (uri): Promise<ReadResourceResult> => ({ contents: [{ uri: uri.href, blob: 'iVBORw0KGgoAAAANSUhEUgAA...' }] }) );
The mimeType in options tells clients how to interpret the content. Common values include application/json, text/plain, text/markdown, image/png, and application/pdf.
Common Pitfalls
- Forgetting the
uriin content entries: each item in thecontentsarray must include theurifield, even though the handler already receives the URI as a parameter - Mixing
textandblob: a single content entry should use eithertextorblob, never both - Using non-unique names: resource names must be unique; registering two resources with the same name will cause errors
- Omitting
mimeType: while optional in the spec, omitting it forces clients to guess the content type, which can lead to incorrect rendering
Best Practices
- Use descriptive URI schemes that indicate the data domain:
config://,schema://,status:// - Always set
mimeTypein options so clients can render content correctly - Keep resource handlers fast — avoid long-running operations that block reads
- Return structured data (JSON) when the LLM needs to parse the content, and use plain text or markdown for human-readable context
- Include both
titleanddescriptionin options to help clients display meaningful labels
Summary
Static resources expose fixed, addressable data through server.registerResource(). Each resource has a unique name, a URI, metadata options, and a handler that returns ReadResourceResult. Text content uses the text field, binary content uses blob, and the mimeType option tells clients how to interpret the data.
Code Examples
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
server.registerResource(
'config',
'config://app',
{ title: 'App Config', description: 'Application configuration', mimeType: 'application/json' },
async (uri): Promise<ReadResourceResult> => ({
contents: [{ uri: uri.href, text: JSON.stringify({ debug: true, version: '1.0.0' }) }]
})
);server.registerResource(
'logo',
'file://logo.png',
{ title: 'Company Logo', description: 'Logo image', mimeType: 'image/png' },
async (uri): Promise<ReadResourceResult> => ({
contents: [{ uri: uri.href, blob: 'iVBORw0KGgoAAAANSUhEUgAA...' }]
})
);