AI applications are becoming more powerful, but connecting them to real-world tools, APIs, databases, and business systems remains a challenge. Model Context Protocol (MCP) provides a standardized way for AI applications to discover and interact with external capabilities without requiring every integration to be built from scratch.
With Node.js, you can build MCP servers that expose JavaScript functions, business data, and external services as tools that compatible AI clients can discover and invoke. This makes MCP particularly useful for developers building AI agents, automation workflows, and intelligent developer tools.
In this guide, we will build a working MCP server using JavaScript and the official MCP SDK. You will learn how to register tools, validate inputs, return structured results, handle errors, and connect the server to an MCP-compatible client.
Building an MCP Server with Node.js
What Is Model Context Protocol?
Model Context Protocol is an open protocol that standardizes how AI applications communicate with external tools and data sources. An MCP server can expose tools that perform actions, resources that provide contextual data, and prompts that offer reusable interaction templates. The protocol separates the AI client from the implementation details of the systems it uses.
For example, a customer support AI agent could use an MCP server to look up orders, check delivery status, or retrieve product information. Instead of hardcoding every integration into the AI application, developers can expose these capabilities through well-defined MCP interfaces.
Why Use Node.js?
Node.js is a practical choice for MCP servers because it supports asynchronous operations, has a large ecosystem of API clients, and works naturally with JavaScript-based applications. You can integrate existing Express services, MongoDB repositories, PostgreSQL queries, and third-party APIs into MCP tools while keeping most of your application logic reusable.
The official TypeScript SDK can also be used from JavaScript projects, although some advanced integrations may benefit from TypeScript's static type checking. The example below uses standard JavaScript with ES modules and the official SDK's stdio transport, which is useful when an MCP client launches the server as a local subprocess.
How an MCP Server Works
An MCP client establishes a connection to the server through a supported transport. With stdio, the client launches the server process and exchanges protocol messages through standard input and standard output. The server advertises available tools, receives tool calls, validates their arguments, executes the corresponding functions, and returns results in the expected format.
It is important to keep standard output reserved for protocol communication when using stdio. Use console.error() for diagnostic logs instead of console.log(), which could interfere with the protocol stream. For production deployments, also validate all inputs, enforce authorization at the appropriate layer, and avoid exposing unrestricted database or shell operations.
Example: Create a Task Management MCP Server
Our example exposes three tools: creating a task, listing tasks, and retrieving a task by ID. It stores tasks in memory to keep the demonstration easy to run, but you can replace the array with a database repository or an external API integration.
The server uses the official @modelcontextprotocol/sdk package and Zod to define input schemas. Each tool returns a structured result alongside readable text, making the response useful to both MCP clients and people inspecting the interaction. The example also demonstrates input validation, consistent error handling, and step-by-step diagnostic logging.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { randomUUID } from "node:crypto";
// Keep demonstration data in memory; use a database in production.
const tasks = new Map();
// Create the MCP server with a descriptive identity and version.
const server = new McpServer({
name: "node-task-manager",
version: "1.0.0",
});
// Register a tool that creates a task.
server.registerTool(
"create_task",
{
title: "Create Task",
description: "Create a task with a title and optional priority.",
inputSchema: {
title: z.string().trim().min(1).max(200),
priority: z.enum(["low", "medium", "high"]).default("medium"),
},
},
async ({ title, priority }) => {
// Generate a unique identifier for this task.
const id = randomUUID();
const task = {
id,
title,
priority,
completed: false,
createdAt: new Date().toISOString(),
};
// Persist the task in the in-memory collection.
tasks.set(id, task);
console.error(`[create_task] Created task ${id}`);
// Return human-readable content and machine-readable data.
return {
content: [
{ type: "text", text: `Created task: ${task.title} (${task.id})` },
],
structuredContent: task,
};
},
);
// Register a tool that lists all existing tasks.
server.registerTool(
"list_tasks",
{
title: "List Tasks",
description: "List tasks, optionally filtering by completion status.",
inputSchema: {
completed: z.boolean().optional(),
},
},
async ({ completed }) => {
console.error("[list_tasks] Reading stored tasks");
// Apply the optional filter without modifying stored data.
const results = [...tasks.values()].filter((task) =>
completed === undefined ? true : task.completed === completed
);
return {
content: [
{
type: "text",
text: results.length
? JSON.stringify(results, null, 2)
: "No matching tasks found.",
},
],
structuredContent: { tasks: results, count: results.length },
};
},
);
// Register a tool that retrieves a single task by its identifier.
server.registerTool(
"get_task",
{
title: "Get Task",
description: "Retrieve a task using its unique identifier.",
inputSchema: {
id: z.string().uuid(),
},
},
async ({ id }) => {
console.error(`[get_task] Looking up task ${id}`);
const task = tasks.get(id);
// Return a clear error when the requested task does not exist.
if (!task) {
return {
isError: true,
content: [{ type: "text", text: `Task not found: ${id}` }],
};
}
return {
content: [{ type: "text", text: JSON.stringify(task, null, 2) }],
structuredContent: task,
};
},
);
// Start the server over stdio for an MCP-compatible client.
async function main() {
console.error("[startup] Initializing Node.js MCP server...");
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[startup] MCP server connected through stdio.");
}
// Report startup failures without writing to protocol stdout.
main().catch((error) => {
console.error("[startup] Failed to start MCP server:", error);
process.exitCode = 1;
});
Conclusion
Building MCP servers with Node.js gives you a reusable way to connect AI applications with the tools and services your business already depends on. By exposing capabilities through clearly defined tools and input schemas, you can keep integrations modular while allowing compatible AI clients to discover and invoke them through a standardized protocol.
To run the example, create a Node.js project configured for ES modules, install the official MCP SDK and Zod, and save the code in a file such as server.js. For example, use npm install @modelcontextprotocol/sdk zod and run it with node server.js; the process will wait for an MCP client to communicate over standard input and output rather than presenting an interactive terminal interface.
For your next step, replace the in-memory task collection with MongoDB or PostgreSQL, add authentication and authorization where appropriate, and expose a carefully scoped business operation as another tool. Remember that an MCP server makes capabilities available to a client; it does not automatically make every tool call safe, authorized, or reliable. What would you build first with MCP and Node.js: a database assistant, a customer support agent, or an automated developer workflow?
Top comments (0)