# How to define resources in an MCP server

- Tool: MCP
- Difficulty: Intermediate
- Time required: 20-30 min
- Compatibility: MCP TypeScript SDK 1.x, MCP Python SDK 1.x
- Last updated: March 2026

## TL;DR

Resources in MCP servers expose read-only data to AI clients through URI-based access. Define static resources with fixed URIs for configuration or reference data, and resource templates with URI patterns for dynamic content like database records or API responses. Resources let the AI read context without triggering actions.

## Defining Resources in Your MCP Server

Resources are the read-only data primitive in MCP. While tools perform actions, resources expose data that AI clients can read as context. Each resource has a URI (like config://app or db://users/123), a name, a MIME type, and a handler that returns content.

MCP supports two kinds of resources: static resources with fixed URIs, and resource templates with parameterized URI patterns. This tutorial covers both patterns in TypeScript and Python, showing how to expose configuration, database records, and file content to AI clients.

## Before you start

- MCP TypeScript SDK or Python SDK installed
- A working MCP server instance created with McpServer or FastMCP
- Understanding of MCP tools (resources complement tools in your server)
- Basic knowledge of URI patterns

## Step-by-step guide

### 1. Register a static resource with a fixed URI

Static resources have a fixed URI that always returns the same type of content. Use server.registerResource() with a name, URI string, optional metadata, and an async handler. The handler receives the parsed URI and must return a contents array with text or binary data. Static resources are ideal for configuration, documentation, or reference data.

```
// TypeScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const server = new McpServer({ name: "resource-server", version: "1.0.0" });

server.registerResource("app-config", "config://app", {
  description: "Application configuration settings",
  mimeType: "application/json",
}, async (uri) => ({
  contents: [{
    uri: uri.href,
    text: JSON.stringify({
      appName: "My App",
      version: "2.1.0",
      features: ["auth", "billing", "notifications"],
    }, null, 2),
  }],
}));

# Python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("resource-server")

@mcp.resource("config://app")
async def get_app_config() -> str:
    """Application configuration settings."""
    return json.dumps({
        "appName": "My App",
        "version": "2.1.0",
        "features": ["auth", "billing", "notifications"],
    }, indent=2)
```

**Expected result:** The resource appears in the server's resource list and can be read by any MCP client using the config://app URI.

### 2. Create a resource template with URI parameters

Resource templates use URI patterns with placeholders (like db://users/{userId}) to handle dynamic content. The McpServer class provides registerResourceTemplate() which accepts a URI template with curly-brace parameters. When a client requests a matching URI, the handler receives the extracted parameters. In Python, use f-string style URIs in the decorator.

```
// TypeScript
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";

server.registerResourceTemplate(
  "user-profile",
  new ResourceTemplate("db://users/{userId}", { list: undefined }),
  {
    description: "User profile data by ID",
    mimeType: "application/json",
  },
  async (uri, { userId }) => {
    const user = await db.query("SELECT * FROM users WHERE id = $1", [userId]);
    return {
      contents: [{
        uri: uri.href,
        text: JSON.stringify(user, null, 2),
      }],
    };
  }
);

# Python
@mcp.resource("db://users/{user_id}")
async def get_user_profile(user_id: str) -> str:
    """User profile data by ID."""
    user = await db.fetchrow("SELECT * FROM users WHERE id = $1", user_id)
    return json.dumps(dict(user), indent=2)
```

**Expected result:** Clients can read any user profile by requesting db://users/123, db://users/456, etc.

### 3. Expose file system content as resources

A common pattern is exposing project files as MCP resources. This lets AI clients read source code, documentation, or data files from your project. Use a resource template with a file path parameter and read the file in the handler. Always validate paths to prevent directory traversal attacks.

```
// TypeScript
import { readFile } from "fs/promises";
import path from "path";

const PROJECT_ROOT = "/home/user/project";

server.registerResourceTemplate(
  "project-file",
  new ResourceTemplate("file://project/{filePath+}", { list: undefined }),
  { description: "Project source files" },
  async (uri, { filePath }) => {
    // Prevent directory traversal
    const resolved = path.resolve(PROJECT_ROOT, filePath as string);
    if (!resolved.startsWith(PROJECT_ROOT)) {
      throw new Error("Access denied: path outside project root");
    }
    const content = await readFile(resolved, "utf-8");
    return {
      contents: [{ uri: uri.href, text: content }],
    };
  }
);
```

**Expected result:** Clients can read any file within the project by requesting file://project/src/index.ts.

### 4. Return binary content from resources

Resources can return binary data using base64 encoding. Use the blob field instead of text in the contents array. This is useful for images, PDFs, or other binary files that the AI client needs as context.

```
// TypeScript
import { readFile } from "fs/promises";

server.registerResource("app-logo", "assets://logo.png", {
  description: "Application logo image",
  mimeType: "image/png",
}, async (uri) => {
  const buffer = await readFile("./assets/logo.png");
  return {
    contents: [{
      uri: uri.href,
      blob: buffer.toString("base64"),
    }],
  };
});
```

**Expected result:** The client receives the binary image data encoded as base64 and can display or process it.

### 5. Connect resources with tools for a complete server

In practice, resources and tools work together. Resources provide read-only context (data the AI can reference), while tools provide actions (things the AI can do). A well-designed MCP server uses resources for data the AI reads frequently and tools for operations that change state. For complex server architectures combining resources and tools, the RapidDev engineering team can help plan the right design.

```
// Resources for reading
server.registerResource("schema", "db://schema", {
  description: "Database schema definition",
}, async (uri) => ({
  contents: [{ uri: uri.href, text: await getSchemaSQL() }],
}));

// Tools for writing
server.registerTool("run-query", {
  description: "Execute a read-only SQL query",
  inputSchema: { sql: z.string() },
}, async ({ sql }) => {
  const result = await pool.query(sql);
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
});
```

**Expected result:** The AI client reads the schema resource for context, then uses the query tool to fetch specific data.

## Complete code example

File: `src/index.ts`

```typescript
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readFile } from "fs/promises";
import path from "path";

const server = new McpServer({
  name: "resource-demo-server",
  version: "1.0.0",
});

// Static resource: application config
server.registerResource("app-config", "config://app", {
  description: "Application configuration",
  mimeType: "application/json",
}, async (uri) => ({
  contents: [{
    uri: uri.href,
    text: JSON.stringify({
      appName: "Demo App",
      version: "2.0.0",
      database: "postgresql",
      features: ["auth", "billing"],
    }, null, 2),
  }],
}));

// Static resource: API documentation
server.registerResource("api-docs", "docs://api", {
  description: "API endpoint documentation",
  mimeType: "text/markdown",
}, async (uri) => ({
  contents: [{
    uri: uri.href,
    text: "# API Documentation\n\n## GET /users\nReturns all users...\n\n## POST /users\nCreates a new user...",
  }],
}));

// Resource template: user profiles
server.registerResourceTemplate(
  "user-profile",
  new ResourceTemplate("db://users/{userId}", { list: undefined }),
  { description: "User profile by ID", mimeType: "application/json" },
  async (uri, { userId }) => {
    // Replace with actual database query
    const user = { id: userId, name: "Example User", email: "user@example.com" };
    return {
      contents: [{ uri: uri.href, text: JSON.stringify(user, null, 2) }],
    };
  }
);

// Resource template: project files
const PROJECT_ROOT = process.cwd();
server.registerResourceTemplate(
  "project-file",
  new ResourceTemplate("file://project/{filePath+}", { list: undefined }),
  { description: "Project source files" },
  async (uri, { filePath }) => {
    const resolved = path.resolve(PROJECT_ROOT, filePath as string);
    if (!resolved.startsWith(PROJECT_ROOT)) {
      throw new Error("Access denied");
    }
    const content = await readFile(resolved, "utf-8");
    return { contents: [{ uri: uri.href, text: content }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Resource demo server running on stdio");
```

## Common mistakes

- **Using resources for actions that modify state** — undefined Fix: Resources are read-only. If the operation writes data, creates files, or calls external APIs with side effects, use a tool instead.
- **Not validating file paths in resource handlers** — undefined Fix: Always resolve paths and check they stay within your allowed root directory to prevent directory traversal attacks.
- **Returning stale data without cache invalidation** — undefined Fix: If the underlying data changes, send a notifications/resources/list_changed notification so clients know to re-fetch.
- **Missing the mimeType in resource metadata** — undefined Fix: Always specify mimeType so clients know how to render the content (application/json, text/markdown, text/plain, etc.).

## Best practices

- Use meaningful URI schemes (config://, db://, file://, docs://) to organize resources by category
- Set the mimeType on every resource so clients can render content appropriately
- Use static resources for data that rarely changes and resource templates for dynamic, parameterized data
- Validate all URI parameters in resource template handlers to prevent injection or traversal attacks
- Keep resource content focused — return only what the AI needs, not entire database dumps
- Send notifications/resources/list_changed when your resource catalog changes dynamically
- Use the blob field with base64 encoding for binary content instead of trying to return raw bytes as text
- Combine resources (for context) with tools (for actions) for a complete server design

## Frequently asked questions

### What is the difference between resources and tools in MCP?

Resources are read-only data endpoints identified by URIs — the AI reads them for context. Tools are callable functions that perform actions and can have side effects. Use resources for data the AI needs to reference, and tools for operations the AI needs to execute.

### Can a resource handler make API calls or database queries?

Yes. Resource handlers are async functions that can do anything to produce the content. The key distinction is that resources should not have side effects — they should only read and return data, not modify state.

### How does an AI client discover available resources?

Clients call the resources/list method to get all static resources and resource templates. They then call resources/read with a specific URI to fetch content. Clients may also subscribe to resource change notifications.

### Can I return multiple content items from a single resource?

Yes. The contents array can contain multiple items, each with its own URI and content. This is useful for returning related pieces of data from a single resource request.

### How do I handle resources that are expensive to compute?

Add caching in your resource handler. Store computed results with a TTL and return cached data when fresh. The MCP protocol itself does not have built-in caching, so implement it server-side.

### Can I use resources with Python's FastMCP?

Yes. Use the @mcp.resource("uri://pattern") decorator on an async function. For templates, include parameters in the URI like @mcp.resource("db://users/{user_id}") and add matching function parameters. The RapidDev team has published several example Python MCP servers as open-source references.

---

Source: https://www.rapidevelopers.com/mcp-tutorial/how-to-define-resources-in-mcp-server
© RapidDev — https://www.rapidevelopers.com/mcp-tutorial/how-to-define-resources-in-mcp-server
