In this section
How to build Node.js MCP Servers that Expose Data Resources to LLMs
MCPs are famous for exposing Tools that augment Large Language Models (LLMs) with the ability to perform actions and invoke generic functions that MCP Servers can implement, but exposing data resources to LLMs is just as important.

What are MCP Server resources?
Resources allow MCP Servers to expose data as read-only information that LLMs can use as context and query for data points. For example, an MCP Server can define database records as resources, an application’s status, or any other form of text or binary data content.
Practical examples of MCP Server resources include:
An organizational org chart for a company.
A documentation server exposing API data.
Product FAQ and troubleshooting guidelines.

MCP Server resources are defined using a unique URI and will often contain their own custom protocol scheme. Building on the examples above, an MCP Server resource for an organizational org-chart could be org-chart://company-wide, org-chart://engineering, org-chart://cto
and so on.
These URI requests are sent from the MCP Client to the MCP Server, which needs to define specific routes for these as part of the server.setRequestHandler
MCP Server API.
Building a Node.js MCP Server for resources
Let’s build an MCP Server in JavaScript to run with a Node.js runtime and define Resources as part of this MCP Server data points.
Begin by creating the file ./resources/index.js
, which hosts the MCP Server Resources definition and to which we will begin by importing the necessary code from the Model Context Protocol and a handy logger utility:
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { initLogger } from "../utils/logger.js";
const logger = initLogger();
Next, we will define our main exported function that creates the resource. In this function, our Resource is going to be an open source SVG representation of the Node.js runtime release schedule. We can then query for questions like when is the next LTS version of the Node.js runtime planned for?
Let’s implement the logic to fetch this data:
export async function initializeResources(server) {
logger.info({ msg: "Initializing resources..." });
const resourceNodejsReleasesChartURL =
"https://raw.githubusercontent.com/nodejs/Release/main/schedule.svg?sanitize=true";
const resourceNodejsReleasesChart = await fetch(
resourceNodejsReleasesChartURL
);
if (!resourceNodejsReleasesChart.ok) {
logger.error({
msg: `Failed to fetch Node.js releases chart: ${resourceNodejsReleasesChart.status} ${resourceNodejsReleasesChart.statusText}`,
});
throw new Error(
`Failed to fetch Node.js releases chart: ${resourceNodejsReleasesChart.status} ${resourceNodejsReleasesChart.statusText}`
);
}
const resourceNodejsReleasesChartSVGText =
await resourceNodejsReleasesChart.text();
The above code simply fetches the data, includes generic error handling, and creates the resource as text data from the SVG file.
Next, we define an array of resources. Each MCP Server Resource definition has the following properties:
A URI.
A name.
A description.
A mime type.
We also defined a function handler so we can dynamically resolve all the resources. Following is the continuation of our initializeResources
function, which uses the server.setRequestHandler
API to list the resources as well as implement them by terminating the route on the request.params.uri
from the incoming request (originating from the MCP Client) and then invoking the function handler in our resource:
const resources = [
{
uri: "nodejs://releases-schedule-chart.svg",
name: "Node.js Releases Schedule Chart",
description: "A chart showing the release schedule of Node.js versions.",
mimeType: "image/svg+xml",
func: async (request) => {
logger.info({ msg: "Resource URI Access:", uri: request.params.uri });
return {
contents: [
{
uri: request.params.uri,
text: resourceNodejsReleasesChartSVGText,
},
],
};
},
},
];
// Implement dynamic resource definition for listing and implementation by
// looping over the `resources` array and defining each resource:
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resourcesList = resources.map((resource) => {
return {
uri: resource.uri,
name: resource.name,
description: resource.description,
mimeType: resource.mimeType || "text/plain",
};
});
return {
resources: resourcesList,
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const resource = resources.find((resource) => {
return resource.uri === request.params.uri;
});
if (resource) {
return await resource.func(request);
}
throw new Error(`Resource not found: ${request.params.uri}`);
});
}
Now let’s put this together via two files: index.js
and server.js
, which will expose the MCP Server, and then wrap all of this up as a generic Node.js application process:
The server.js
, as described above, will declare the MCP Server as STDIO transport and expose it:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { initLogger } from "./utils/logger.js";
import { initializeResources } from "./resources/index.js";
const logger = initLogger();
export async function createMcpServer() {
const server = new Server(
{
name: "nodejs-module-api-documentation",
description:
"Search built-in core Node.js modules API Documentation. Use whenever the user asks questions about Node.js API, Node.js modules or Node.js functions.",
version: "1.0.0",
},
{
capabilities: {
resources: {},
},
}
);
// await initializeDocumentationServer(server);
await initializeResources(server);
// Start receiving messages on stdin and sending messages on stdout
logger.info({
msg: "MCP Server instance created",
name: server.name,
version: server.version,
});
return server;
}
Then the index.js
file imports the MCP Server definition and starts:
#!/usr/bin/env node
import { createMcpServer } from "./server.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { initLogger } from './utils/logger.js';
const logger = initLogger();
// Initialize the server with Node.js API documentation
async function startServer() {
let server
try {
server = await createMcpServer();
} catch (error) {
logger.error({ err: error, msg: 'Failed to create MCP server' });
console.error(`Fatal error during server creation.`);
process.exit(1);
}
try {
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info({ msg: 'Server connected to transport. Ready.' });
} catch (error) {
logger.error({ err: error, msg: 'Failed to initialize server' });
console.error(`Fatal error during server transport init.`);
process.exit(1);
}
}
startServer();
Building MCP Servers
If you want to gain a better understanding of how MCP architecture works, its main components, and the differences between MCP to REST and MCP to Function Calls, then I highly recommend you follow up with the beginner's guide to visually understanding MCP architecture.
If you’re entirely new to MCPs, check our article What is MCP in AI? Everything you wanted to ask. It is a great start to gain fundamental knowledge of what MCPs solve.
My last recommendation is that if you choose AI coding tools to write code, like Cursor and others, I invite you to add the Snyk CLI MCP Server, which will help provide context to your agentic code workflow about insecure coding patterns and vulnerable dependencies that can mitigate security issues in an automated way.
Start securing AI-generated code
Create your free Snyk account to start securing AI-generated code in minutes. Or book an expert demo to see how Snyk can fit your developer security use cases.