Build an MCP Server With Node.js and JavaScript
How to write a plain JavaScript MCP server on Node.js: define tools, wire up stdio transport, test each call with the Inspector, and keep API decisions and lessons for maintainers.
Updated 5 min read
An MCP server on Node.js is a small script that defines tools an AI assistant can call, listens on a transport, and returns structured responses. The official TypeScript SDK works equally well in plain JavaScript, so you do not need a TypeScript build step to get started. Tools like fenbs let you record the API decisions and hard-won troubleshooting lessons your team needs when they maintain or extend the server later.
Node.js is the most common starting point for MCP servers because the official SDK is TypeScript-first and the JavaScript ecosystem has broad JSON tooling. A stdio server that handles a handful of tools can be a single file. You can add a build step, types, and tests as the server grows.
Install the SDK
The official MCP SDK for JavaScript and TypeScript is published on npm as @modelcontextprotocol/sdk. Install it with your package manager of choice. The SDK exposes a McpServer class that handles the protocol layer, request routing, and transport management. You supply tool definitions and handlers; the SDK handles the rest.
npm install @modelcontextprotocol/sdk # or pnpm add @modelcontextprotocol/sdk
Define a tool
Each tool needs a name, a description, and an input schema defined with Zod or as a plain JSON Schema object. The description is what the AI reads to decide whether to call the tool, so precision matters. A tool named search_records with a description that says what records it searches and what it returns is far more useful to a model than a one-word description.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
server.tool(
'get_status',
'Returns the status of a named project.',
{ name: z.string().describe('Project name') },
async ({ name }) => ({ content: [{ type: 'text', text: await fetchStatus(name) }] })
);Keep handlers focused: one tool, one operation. A handler that does two things is harder to test and harder to describe accurately to a model. If you find yourself adding an optional parameter that changes what the tool does, split it into two tools instead.
Connect the stdio transport
For local use, connect the server to the stdio transport. The client process spawns the server and exchanges JSON-RPC messages over standard input and output. No network configuration is required. The SDK's StdioServerTransport class handles framing and buffering. Call server.connect(transport) and the server is ready.
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const transport = new StdioServerTransport();
await server.connect(transport);Test each tool before using an AI client
Run the MCP Inspector against your server before connecting it to Claude, Cursor, or any other AI client. The Inspector lets you list tools, send test calls, and inspect raw request and response objects. This saves time: a bad tool description or a missing required field in the schema is much easier to catch in the Inspector than mid-session with an AI.
- List tools and confirm names and descriptions match what you intended.
- Call each tool with valid input and check the response shape.
- Send invalid or missing arguments and confirm the server returns a proper error.
- Confirm the process exits cleanly when the transport closes.
Common issues and how to diagnose them
The most common problem is a tool the AI calls incorrectly because the description is ambiguous. Read every description as if you had no other context: is it clear what the tool returns, what each parameter means, and what happens if a parameter is missing? The second common issue is a handler that throws an unhandled exception, which causes the server to exit or return a malformed response. Wrap handler logic in try-catch and return a structured error.
If the Inspector connects but shows no tools, confirm the tool registrations run before server.connect(). If the server exits immediately, check that the stdio transport is not being closed by an uncaught promise rejection. Adding a top-level process.on('uncaughtException', ...) handler helps during development.
Use environment variables for configuration
Any value that differs between environments — API keys, base URLs, feature flags — should come from environment variables rather than being hardcoded. Node.js reads process.env at runtime; a .env file loaded with a library like dotenv keeps local values out of source control. This matters especially for MCP servers because they are often shared across team members who each have their own credentials. A server that reads from the environment works for everyone; one with hardcoded values works only for the person who wrote it.
Record decisions and lessons as the server evolves
A Node.js MCP server often starts as a quick prototype and ends up as a shared dependency. The decisions that made sense at the start — which tools to expose, how to handle authentication, why a particular schema shape was chosen — are forgotten within weeks. fenbs keeps those decisions next to the tasks, so a connected assistant can read them before the next session rather than making the same choices again from scratch.
How to save Node.js MCP server decisions with fenbs
Use fenbs to keep the decisions and lessons from day one. They cost nothing to write and prevent repeated questions from the next person who touches the server.
- Create a free account and add a project for the server. Describe what the server connects to and who consumes it.
- Write the first Decisions and rules: which tools are read-only, how errors are reported, and why the tool surface is shaped the way it is.
- When a session reveals a tricky edge case, add it as a Lesson learned. Name the cause and what to check before the same problem recurs.
- Connect your development assistant via Integrations so it reads current rules before each coding session.
- Ask the assistant to save progress before each session ends, noting what changed and what still needs review.
Keep API decisions and lessons where the next engineer finds them
fenbs stores your tool API decisions and troubleshooting lessons as Decisions and rules and Lessons learned. A connected assistant reads them at the start of each session, so it does not repeat a mistake the team already solved. Create a free account and write the first rule before the server is shared.
Create a free accountFree for one person · No credit card · Works with Claude, ChatGPT, Cursor and more