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
Register a static resource with a fixed URI
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.
1// TypeScript2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";34const server = new McpServer({ name: "resource-server", version: "1.0.0" });56server.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}));1920# Python21from mcp.server.fastmcp import FastMCP2223mcp = FastMCP("resource-server")2425@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.
Create a resource template with URI parameters
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.
1// TypeScript2import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";34server.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);2122# Python23@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.
Expose file system content as resources
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.
1// TypeScript2import { readFile } from "fs/promises";3import path from "path";45const PROJECT_ROOT = "/home/user/project";67server.registerResourceTemplate(8 "project-file",9 new ResourceTemplate("file://project/{filePath+}", { list: undefined }),10 { description: "Project source files" },11 async (uri, { filePath }) => {12 // Prevent directory traversal13 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.
Return binary content from resources
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.
1// TypeScript2import { readFile } from "fs/promises";34server.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.
Connect resources with tools for a complete server
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.
1// Resources for reading2server.registerResource("schema", "db://schema", {3 description: "Database schema definition",4}, async (uri) => ({5 contents: [{ uri: uri.href, text: await getSchemaSQL() }],6}));78// Tools for writing9server.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
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";56const server = new McpServer({7 name: "resource-demo-server",8 version: "1.0.0",9});1011// Static resource: application config12server.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}));2627// Static resource: API documentation28server.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}));3738// Resource template: user profiles39server.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 query45 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);5152// Resource template: project files53const 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);6768const 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation