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, and mimeType — these help clients understand and display the resource
  • ReadResourceResult is the return type of the handler, containing a contents array
  • 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.

typescript
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' }) }]
  })
);

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.

typescript
server.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.

typescript
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...' }]
  })
);

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 uri in content entries: each item in the contents array must include the uri field, even though the handler already receives the URI as a parameter
  • Mixing text and blob: a single content entry should use either text or blob, 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 mimeType in 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 title and description in 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

typescript
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' }) }]
  })
);
typescript
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...' }]
  })
);
✓ Completed