Adding MCP Resources
Guide for creating custom resources in Seed to expose structured data and configuration to MCP clients.
Overview
MCP resources provide structured, read-only access to data. Unlike tools (which perform actions) and prompts (which generate text), resources expose existing data and configuration for clients to read.
Use resources when you want to:
- Expose configuration settings
- Provide user profile data
- Share application state
- Offer structured documentation
- Deliver real-time data feeds
Resource Concepts
Resource URIs
Resources are identified by URIs following a custom scheme:
scheme://pathExamples:
config://server- Server configurationuser://profile- User profile dataapp://status- Application statusdocs://getting-started- Documentation
Resource Types
Resources can provide different content types:
- Text - Plain text or markdown
- JSON - Structured data
- Binary - Images, files (base64 encoded)
Resource Updates
Resources can notify clients when data changes using MCP's resource update protocol. This enables real-time data synchronization.
Creating a Resource
Step 1: Create Resource File
Create a new file in src/mcp/resources/:
// src/mcp/resources/my-resource.ts
import {Server as McpServer} from "@modelcontextprotocol/sdk/server/index.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
export function registerMyResource(server: McpServer): void {
// Resource implementation will go here
}Step 2: Define Resource Metadata
Add resource to the resources/list handler:
export function registerMyResource(server: McpServer): void {
// List resources handler
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "myapp://data",
name: "Application Data",
description: "Current application state and configuration",
mimeType: "application/json",
},
],
};
});
}Resource metadata:
uri- Unique resource identifiername- Human-readable namedescription- What the resource containsmimeType- Content type (text/plain, application/json, image/png, etc.)
Step 3: Implement Read Handler
Add resource content handler:
export function registerMyResource(server: McpServer): void {
// ... list handler from above ...
// Read resource handler
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
if (uri === "myapp://data") {
const data = {
version: "1.0.0",
environment: process.env.NODE_ENV || "development",
features: {
authentication: true,
rateLimit ing: true,
},
timestamp: new Date().toISOString(),
};
return {
contents: [
{
uri: uri,
mimeType: "application/json",
text: JSON.stringify(data, null, 2),
},
],
};
}
throw new Error(`Unknown resource: ${uri}`);
});
}Step 4: Register Resource
Add to src/mcp/resources/index.ts:
import {Server as McpServer} from "@modelcontextprotocol/sdk/server/index.js";
import {registerConfigResource} from "./config.js";
import {registerUserResource} from "./user.js";
import {registerMyResource} from "./my-resource.js"; // Add import
export function registerAllResources(server: McpServer): void {
registerConfigResource(server);
registerUserResource(server);
registerMyResource(server); // Add registration
}Complete Example: Database Stats Resource
Here's a complete example exposing database statistics:
// src/mcp/resources/database-stats.ts
import {Server as McpServer} from "@modelcontextprotocol/sdk/server/index.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import {getRedisClient} from "../../services/redis.js";
interface DatabaseStats {
redis: {
connected: boolean;
memory: {
used: string;
peak: string;
};
keys: number;
clients: number;
};
sessions: {
active: number;
total: number;
};
}
async function getStats(): Promise<DatabaseStats> {
const redis = getRedisClient();
const info = await redis.info("memory");
const dbSize = await redis.dbsize();
const clients = await redis.clientList();
const sessions = await redis.keys("seed:session:*");
// Parse memory info
const usedMemory = info.match(/used_memory_human:(.+)/)?.[1] || "unknown";
const peakMemory = info.match(/used_memory_peak_human:(.+)/)?.[1] || "unknown";
return {
redis: {
connected: true,
memory: {
used: usedMemory.trim(),
peak: peakMemory.trim(),
},
keys: dbSize,
clients: clients.length,
},
sessions: {
active: sessions.length,
total: sessions.length,
},
};
}
export function registerDatabaseStatsResource(server: McpServer): void {
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "database://stats",
name: "Database Statistics",
description: "Real-time Redis and session statistics",
mimeType: "application/json",
},
],
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === "database://stats") {
try {
const stats = await getStats();
return {
contents: [
{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(stats, null, 2),
},
],
};
} catch (error) {
throw new Error(`Failed to fetch database stats: ${error.message}`);
}
}
throw new Error(`Unknown resource: ${request.params.uri}`);
});
}Resource Patterns
1. Static Configuration
Expose server configuration:
export function registerConfigResource(server: McpServer): void {
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === "config://server") {
const config = {
version: "1.0.0",
auth: {
required: process.env.AUTH_REQUIRED !== "false",
provider: process.env.OIDC_ISSUER,
},
features: {
tools: true,
prompts: true,
resources: true,
},
};
return {
contents: [
{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(config, null, 2),
},
],
};
}
});
}2. User-Specific Data
Expose authenticated user information:
export function registerUserResource(server: McpServer): void {
server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => {
if (request.params.uri === "user://profile") {
// Access user context from request
const user = (extra?.req as any)?.user;
if (!user) {
throw new Error("Authentication required");
}
const profile = {
id: user.sub,
email: user.email,
name: user.name,
groups: user.groups || [],
};
return {
contents: [
{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(profile, null, 2),
},
],
};
}
});
}3. Dynamic Data Feeds
Provide real-time data:
export function registerMetricsResource(server: McpServer): void {
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === "metrics://current") {
const metrics = {
timestamp: new Date().toISOString(),
system: {
uptime: process.uptime(),
memory: {
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
},
cpu: process.cpuUsage(),
},
requests: {
total: global.requestCount || 0,
rate: global.requestRate || 0,
},
};
return {
contents: [
{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(metrics, null, 2),
},
],
};
}
});
}4. Documentation Resources
Expose structured documentation:
export function registerDocsResource(server: McpServer): void {
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const path = uri.replace("docs://", "");
const docs: Record<string, string> = {
"getting-started": `# Getting Started\\n\\nWelcome to Seed...`,
"authentication": `# Authentication\\n\\nSeed uses OAuth 2.0...`,
"tools": `# Available Tools\\n\\n1. healthcheck...`,
};
if (path in docs) {
return {
contents: [
{
uri: uri,
mimeType: "text/markdown",
text: docs[path],
},
],
};
}
throw new Error(`Documentation not found: ${path}`);
});
}5. Binary Resources
Expose images or files:
import fs from "fs/promises";
import path from "path";
export function registerImageResource(server: McpServer): void {
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri.startsWith("image://")) {
const imageName = request.params.uri.replace("image://", "");
const imagePath = path.join(__dirname, "../../assets", imageName);
try {
const data = await fs.readFile(imagePath);
const base64 = data.toString("base64");
return {
contents: [
{
uri: request.params.uri,
mimeType: "image/png",
blob: base64,
},
],
};
} catch (error) {
throw new Error(`Image not found: ${imageName}`);
}
}
});
}Resource Updates
Resources can notify clients when data changes:
export function registerRealtimeResource(server: McpServer): void {
// Store current value
let currentValue = {
counter: 0,
lastUpdate: new Date().toISOString(),
};
// Update every 5 seconds
setInterval(() => {
currentValue = {
counter: currentValue.counter + 1,
lastUpdate: new Date().toISOString(),
};
// Notify clients of update
server.notification({
method: "notifications/resources/updated",
params: {
uri: "realtime://counter",
},
});
}, 5000);
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === "realtime://counter") {
return {
contents: [
{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(currentValue, null, 2),
},
],
};
}
});
}Multiple Resources in One File
You can handle multiple related resources in a single file:
export function registerAppResources(server: McpServer): void {
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "app://status",
name: "Application Status",
description: "Current application health and status",
mimeType: "application/json",
},
{
uri: "app://version",
name: "Application Version",
description: "Version and build information",
mimeType: "application/json",
},
{
uri: "app://config",
name: "Application Configuration",
description: "Current runtime configuration",
mimeType: "application/json",
},
],
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
switch (uri) {
case "app://status":
return {
contents: [
{
uri: uri,
mimeType: "application/json",
text: JSON.stringify({status: "ok", uptime: process.uptime()}, null, 2),
},
],
};
case "app://version":
return {
contents: [
{
uri: uri,
mimeType: "application/json",
text: JSON.stringify({version: "1.0.0", build: "12345"}, null, 2),
},
],
};
case "app://config":
return {
contents: [
{
uri: uri,
mimeType: "application/json",
text: JSON.stringify({env: process.env.NODE_ENV}, null, 2),
},
],
};
default:
throw new Error(`Unknown resource: ${uri}`);
}
});
}Testing Resources
Manual Testing
Test resources using curl:
# Initialize session
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {"resources": {}},
"clientInfo": {"name": "test", "version": "1.0"}
},
"id": 1
}'
# List resources
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"jsonrpc": "2.0",
"method": "resources/list",
"id": 2
}'
# Read resource
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"jsonrpc": "2.0",
"method": "resources/read",
"params": {"uri": "config://server"},
"id": 3
}'Unit Testing
Create tests in src/mcp/resources/*.test.ts:
import {describe, it, expect, beforeEach} from "vitest";
import {Server as McpServer} from "@modelcontextprotocol/sdk/server/index.js";
import {registerMyResource} from "./my-resource.js";
describe("My Resource", () => {
let server: McpServer;
beforeEach(() => {
server = new McpServer({name: "test", version: "1.0"}, {capabilities: {}});
registerMyResource(server);
});
it("should list resources", async () => {
const response = await server.request({
method: "resources/list",
}, {});
expect(response.resources).toContainEqual(
expect.objectContaining({
uri: "myapp://data",
name: "Application Data",
})
);
});
it("should read resource", async () => {
const response = await server.request(
{
method: "resources/read",
params: {uri: "myapp://data"},
},
{}
);
expect(response.contents).toHaveLength(1);
expect(response.contents[0].uri).toBe("myapp://data");
expect(response.contents[0].mimeType).toBe("application/json");
const data = JSON.parse(response.contents[0].text);
expect(data).toHaveProperty("version");
expect(data).toHaveProperty("environment");
});
it("should throw error for unknown resource", async () => {
await expect(
server.request(
{
method: "resources/read",
params: {uri: "unknown://resource"},
},
{}
)
).rejects.toThrow("Unknown resource");
});
});Best Practices
1. Use Descriptive URIs
// Good
"config://server"
"user://profile"
"metrics://system/memory"
"docs://getting-started"
// Avoid
"resource1"
"data"
"myresource"2. Provide Clear Descriptions
{
uri: "metrics://requests",
name: "Request Metrics",
description: "Real-time HTTP request statistics including rate, latency, and error counts",
mimeType: "application/json"
}3. Handle Errors Gracefully
try {
const data = await fetchData();
return {contents: [{uri, mimeType: "application/json", text: JSON.stringify(data)}]};
} catch (error) {
throw new Error(`Failed to fetch resource: ${error.message}`);
}4. Sanitize Sensitive Data
// Remove sensitive fields before exposing
const config = {
version: "1.0.0",
database: {
host: process.env.DB_HOST,
// Don't expose password!
},
};5. Use Appropriate MIME Types
// JSON data
mimeType: "application/json"
// Markdown documentation
mimeType: "text/markdown"
// Plain text
mimeType: "text/plain"
// Images
mimeType: "image/png"
mimeType: "image/jpeg"6. Document Your Resources
Add comments explaining what each resource provides:
/**
* Exposes current system metrics including:
* - Memory usage (used, total, percentage)
* - CPU usage (user, system)
* - Uptime
* - Request statistics
*
* Updates every 5 seconds via resource update notifications.
*/
export function registerMetricsResource(server: McpServer): void {
// ...
}Common Pitfalls
1. Not Handling Unknown URIs
// Bad - falls through
if (uri === "known://resource") {
return {...};
}
// Returns undefined
// Good - explicit error
if (uri === "known://resource") {
return {...};
}
throw new Error(`Unknown resource: ${uri}`);2. Exposing Sensitive Data
// Bad - exposes secrets
const config = {
apiKey: process.env.API_KEY, // Don't do this!
};
// Good - sanitize
const config = {
features: {apiEnabled: !!process.env.API_KEY},
};3. Slow Resource Handlers
// Bad - blocks for long time
const data = await verySlowOperation(); // 10+ seconds
// Good - cache or optimize
const cachedData = cache.get("resource") || await fetchAndCache();Related Documentation
- MCP Server Design - Overall MCP architecture
- Adding MCP Tools - Creating custom tools
- Testing Guide - Writing tests
- Resources Overview - User documentation for resources
Examples in Seed
Study existing resources:
src/mcp/resources/config.ts- Server configuration resourcesrc/mcp/resources/user.ts- User profile resource
Next Steps
- Plan your resource - Define URI scheme and data structure
- Implement resource - Create file and handlers
- Register resource - Add to index.ts
- Test thoroughly - Write unit tests and manual tests
- Document - Add user documentation in
/resources/ - Deploy - Rebuild and restart server