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.

Install the SDK
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.

Simple tool definition in JavaScript
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.

Start with stdio transport
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.

  1. Create a free account and add a project for the server. Describe what the server connects to and who consumes it.
  2. 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.
  3. 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.
  4. Connect your development assistant via Integrations so it reads current rules before each coding session.
  5. Ask the assistant to save progress before each session ends, noting what changed and what still needs review.
How fenbs helps

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 account

Free for one person · No credit card · Works with Claude, ChatGPT, Cursor and more

Questions people ask.

Do I need TypeScript to build a Node.js MCP server?

No. The official SDK is distributed as compiled JavaScript with type declarations. You can import it in a plain JavaScript file without a TypeScript compiler or build step.

Can I run a Node.js MCP server over HTTP instead of stdio?

Yes. The SDK includes an HTTP-based transport using server-sent events. Use it when the server needs to be reached from multiple clients or run on a shared host. Stdio is simpler for local use.

How do I add authentication to a Node.js MCP server?

For remote deployments using the HTTP transport, verify a bearer token or session credential in each request handler before running tool logic. For local stdio servers, the operating system controls who can spawn the process.

What happens if my handler throws an error?

Unhandled exceptions can crash the server process or return a malformed response to the client. Wrap handler logic in try-catch and return a structured error object so the client receives a readable message.

How many tools should a single MCP server expose?

There is no hard limit, but a focused server with five to ten tools on one topic is easier to describe, test, and maintain than a large server that mixes unrelated functionality. Split large servers when concerns diverge.

More in Connect your AI tools

All Connect your AI tools articles

Start with one thing.

Free for one person · No credit card · Works with Claude, ChatGPT, Cursor and more

There is nothing to set up first. Write one line and you’ve started.