Skip to main content
RapidDev - Software Development Agency
mcp-tutorial

How to define resources in an MCP server

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.

What you'll learn

  • How to register static resources with server.registerResource()
  • How to create resource templates with URI patterns
  • How to use the @mcp.resource() decorator in Python
  • The difference between resources and tools in MCP
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate8 min read20-30 minMCP TypeScript SDK 1.x, MCP Python SDK 1.xMarch 2026RapidDev Engineering Team
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.

Prerequisites

  • 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
1// TypeScript
2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4const server = new McpServer({ name: "resource-server", version: "1.0.0" });
5
6server.registerResource("app-config", "config://app", {
7 description: "Application configuration settings",
8 mimeType: "application/json",
9}, async (uri) => ({
10 contents: [{
11 uri: uri.href,
12 text: JSON.stringify({
13 appName: "My App",
14 version: "2.1.0",
15 features: ["auth", "billing", "notifications"],
16 }, null, 2),
17 }],
18}));
19
20# Python
21from mcp.server.fastmcp import FastMCP
22
23mcp = FastMCP("resource-server")
24
25@mcp.resource("config://app")
26async def get_app_config() -> str:
27 """Application configuration settings."""
28 return json.dumps({
29 "appName": "My App",
30 "version": "2.1.0",
31 "features": ["auth", "billing", "notifications"],
32 }, 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
1// TypeScript
2import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4server.registerResourceTemplate(
5 "user-profile",
6 new ResourceTemplate("db://users/{userId}", { list: undefined }),
7 {
8 description: "User profile data by ID",
9 mimeType: "application/json",
10 },
11 async (uri, { userId }) => {
12 const user = await db.query("SELECT * FROM users WHERE id = $1", [userId]);
13 return {
14 contents: [{
15 uri: uri.href,
16 text: JSON.stringify(user, null, 2),
17 }],
18 };
19 }
20);
21
22# Python
23@mcp.resource("db://users/{user_id}")
24async def get_user_profile(user_id: str) -> str:
25 """User profile data by ID."""
26 user = await db.fetchrow("SELECT * FROM users WHERE id = $1", user_id)
27 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
1// TypeScript
2import { readFile } from "fs/promises";
3import path from "path";
4
5const PROJECT_ROOT = "/home/user/project";
6
7server.registerResourceTemplate(
8 "project-file",
9 new ResourceTemplate("file://project/{filePath+}", { list: undefined }),
10 { description: "Project source files" },
11 async (uri, { filePath }) => {
12 // Prevent directory traversal
13 const resolved = path.resolve(PROJECT_ROOT, filePath as string);
14 if (!resolved.startsWith(PROJECT_ROOT)) {
15 throw new Error("Access denied: path outside project root");
16 }
17 const content = await readFile(resolved, "utf-8");
18 return {
19 contents: [{ uri: uri.href, text: content }],
20 };
21 }
22);

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
1// TypeScript
2import { readFile } from "fs/promises";
3
4server.registerResource("app-logo", "assets://logo.png", {
5 description: "Application logo image",
6 mimeType: "image/png",
7}, async (uri) => {
8 const buffer = await readFile("./assets/logo.png");
9 return {
10 contents: [{
11 uri: uri.href,
12 blob: buffer.toString("base64"),
13 }],
14 };
15});

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.

typescript
1// Resources for reading
2server.registerResource("schema", "db://schema", {
3 description: "Database schema definition",
4}, async (uri) => ({
5 contents: [{ uri: uri.href, text: await getSchemaSQL() }],
6}));
7
8// Tools for writing
9server.registerTool("run-query", {
10 description: "Execute a read-only SQL query",
11 inputSchema: { sql: z.string() },
12}, async ({ sql }) => {
13 const result = await pool.query(sql);
14 return { content: [{ type: "text", text: JSON.stringify(result) }] };
15});

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

Complete working example

src/index.ts
1import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3import { readFile } from "fs/promises";
4import path from "path";
5
6const server = new McpServer({
7 name: "resource-demo-server",
8 version: "1.0.0",
9});
10
11// Static resource: application config
12server.registerResource("app-config", "config://app", {
13 description: "Application configuration",
14 mimeType: "application/json",
15}, async (uri) => ({
16 contents: [{
17 uri: uri.href,
18 text: JSON.stringify({
19 appName: "Demo App",
20 version: "2.0.0",
21 database: "postgresql",
22 features: ["auth", "billing"],
23 }, null, 2),
24 }],
25}));
26
27// Static resource: API documentation
28server.registerResource("api-docs", "docs://api", {
29 description: "API endpoint documentation",
30 mimeType: "text/markdown",
31}, async (uri) => ({
32 contents: [{
33 uri: uri.href,
34 text: "# API Documentation\n\n## GET /users\nReturns all users...\n\n## POST /users\nCreates a new user...",
35 }],
36}));
37
38// Resource template: user profiles
39server.registerResourceTemplate(
40 "user-profile",
41 new ResourceTemplate("db://users/{userId}", { list: undefined }),
42 { description: "User profile by ID", mimeType: "application/json" },
43 async (uri, { userId }) => {
44 // Replace with actual database query
45 const user = { id: userId, name: "Example User", email: "user@example.com" };
46 return {
47 contents: [{ uri: uri.href, text: JSON.stringify(user, null, 2) }],
48 };
49 }
50);
51
52// Resource template: project files
53const PROJECT_ROOT = process.cwd();
54server.registerResourceTemplate(
55 "project-file",
56 new ResourceTemplate("file://project/{filePath+}", { list: undefined }),
57 { description: "Project source files" },
58 async (uri, { filePath }) => {
59 const resolved = path.resolve(PROJECT_ROOT, filePath as string);
60 if (!resolved.startsWith(PROJECT_ROOT)) {
61 throw new Error("Access denied");
62 }
63 const content = await readFile(resolved, "utf-8");
64 return { contents: [{ uri: uri.href, text: content }] };
65 }
66);
67
68const transport = new StdioServerTransport();
69await server.connect(transport);
70console.error("Resource demo server running on stdio");

Common mistakes when defining resources in an MCP server

Why it's a problem: Using resources for actions that modify state

How to avoid: Resources are read-only. If the operation writes data, creates files, or calls external APIs with side effects, use a tool instead.

Why it's a problem: Not validating file paths in resource handlers

How to avoid: Always resolve paths and check they stay within your allowed root directory to prevent directory traversal attacks.

Why it's a problem: Returning stale data without cache invalidation

How to avoid: If the underlying data changes, send a notifications/resources/list_changed notification so clients know to re-fetch.

Why it's a problem: Missing the mimeType in resource metadata

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building an MCP server in TypeScript. Show me how to register both a static resource with a fixed URI and a resource template with URI parameters using McpServer. Include proper metadata and content return format.

MCP Prompt

Add an MCP resource to my server that exposes [data description] at the URI [scheme://path]. Use server.registerResource() with a description, mimeType of [type], and a handler that returns the data as JSON text content.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.