Blog Image

How to Implement an MCP Server in Node.js: Step-by-Step Guide

Artificial Intelligence
November 28, 20257 Min
Table of contents
Share blog:

TL;DR

  • MCP servers provide tools, resources, and prompts for AI agents like Claude, using a discovery-based interaction model instead of predefined API calls
  • Node.js isn't required for MCP, but the official MCP SDK offers excellent TypeScript support and developer experience
  • Build an MCP server by installing @modelcontextprotocol/sdk, creating a Server instance, implementing request handlers (ListTools, CallTool, etc.), and connecting via stdio or network transport
  • Connect Claude Desktop by adding your server to claude_desktop_config.json with the Node command and path to your compiled JavaScript file
  • Production MCP servers use network transports (SSE/WebSocket) instead of stdio for communication with Claude's API infrastructure

You've seen Claude's Extended Context window. You've heard about the Model Context Protocol. Now you're staring at the docs wondering where to start.

Here's the thing: building an MCP server in Node.js isn't rocket science, but the scattered documentation makes it feel harder than it should be. You're not looking for another surface-level explainer. You need a practical guide that shows you how to set up a working Node.js MCP server, connect it to Claude, and understand what makes MCP different from every other API you've built.

Let's break it down. By the end of this guide, you'll have a functioning MCP server that Claude can actually use, along with a clear understanding of when and why you'd choose this architecture over traditional REST APIs.

What Makes MCP Servers Different from Normal Servers

Before you write a single line of code, you need to understand what you're building.

A traditional server waits for HTTP requests, processes them, and sends responses. You've built dozens of these. An MCP server operates differently. It's designed specifically for AI agents like Claude, providing a standardized way to expose tools, resources, and prompts that the AI can discover and use autonomously.

Think of it like this: your REST API is a vending machine where the user knows exactly what they want and presses the right buttons. An MCP server in Node.js is more like a toolkit where Claude can see what's available, decide what it needs, and request the right tool for the job.

The Model Context Protocol server Node.js implementation handles three core capabilities:

  • Tools: Functions Claude can call (like "search database" or "fetch weather")
  • Resources: Data sources Claude can read (like file contents or API responses)
  • Prompts: Pre-built conversation templates Claude can use

This isn't just a different API format. It's a different interaction model designed for agentic AI workflows where the AI needs to understand what's possible before it acts.

Does MCP Require Node.js? Understanding Your Options

No, MCP doesn't require Node.js. You can build an MCP server in Python, TypeScript, or any language that supports the protocol specification.

But here's why Node.js makes sense for many teams:

The official MCP SDK has first-class TypeScript support, which means you get type safety and IntelliSense when building your server. The Node.js MCP SDK is actively maintained by Anthropic, so you're working with battle-tested code instead of reverse-engineering the spec. If your existing backend is already in Node.js or TypeScript, you can integrate MCP capabilities without adding a new runtime to your stack.

That said, if your team is Python-first or you need heavy data science libraries, the Python SDK works just as well. The protocol is language-agnostic. The choice comes down to your team's expertise and existing infrastructure.

For this guide, we're using TypeScript with Node.js because it offers the best developer experience and most comprehensive examples.

Setting Up Your Node.js Environment for MCP

Let's get your environment ready. You'll need Node.js 18+ installed (check with node --version). The MCP server SDK requires modern JavaScript features, so older versions won't work.

Create your project:

bash
mkdir my-mcp-server
cd my-mcp-server
npm init -y

Install the MCP SDK:

bash
npm install @modelcontextprotocol/sdk
npminstall-Dtypescript@types/nodetsx

Set up TypeScript:

bash

npx tsc --init

Update your tsconfig.json with these settings:

json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
}
}

Create a src folder for your code:

bash
mkdir src

Your environment is ready. Now let's build something.

Building Your First MCP Server in Node.js

Here's a minimal Node.js MCP server example that exposes a simple tool. Create src/index.ts:

typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

const server = new Server(
{
name: "my-first-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);

server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_current_time",
description: "Returns the current server time",
inputSchema: {
type: "object",
properties: {},
},
},
],
};
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "get_current_time") {
return {
content: [
{
type: "text",
text: new Date().toISOString(),
},
],
};
}

throw new Error(`Unknown tool: ${request.params.name}`);
});

async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running on stdio");
}

main().catch(console.error);

What's happening here:

  1. You create a Server instance with metadata (name, version)
  2. You declare capabilities (in this case, tools)
  3. You implement ListToolsRequestSchema to tell Claude what tools exist
  4. You implement CallToolRequestSchema to handle tool execution
  5. You connect the server to stdio transport (Claude communicates via stdin/stdout)

Run your server:

bash
npx tsx src/index.ts

You'll see "MCP server running on stdio" in your terminal. The server is now waiting for MCP protocol messages on stdin.

This is a working MCP server. It doesn't do much yet, but it follows the protocol correctly. Claude can discover the get_current_time tool and call it.

How to Connect Claude to Your Custom MCP Server

Your server is running, but Claude doesn't know it exists yet. Here's how to connect Claude to custom API using MCP.

For Claude Desktop (local development):

  1. Locate your Claude Desktop config file:
    Mac: ~/Library/Application Support/Claude/claude_desktop_config.json
    Windows: %APPDATA%\Claude\claude_desktop_config.json
  2. Add your server configuration:
json
{
"mcpServers": {
"my-first-server": {
"command": "node",
"args": [
"/absolute/path/to/your/project/dist/index.js"
]
}
}
}

3. Restart Claude Desktop

Your server will now appear in Claude's MCP servers list. When you ask Claude to check the current time, it can call your get_current_time tool.

For Claude API (production):

The MCP integration works differently in production. You'll typically run your MCP server as a persistent process and connect it to Claude via the API using the Extended Context feature. This requires configuring your server to accept connections over a network transport instead of stdio.

The key difference: local development uses stdio (direct process communication), while production typically uses SSE (Server-Sent Events) or WebSocket transports for network communication.

Adding Resources and Prompts to Your MCP Server

Tools are just one part of MCP. Let's add resources (readable data) and prompts (conversation templates).

Add resource capability:

Update your server initialization:

typescript
const server = new Server(
{
name: "my-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
resources: {},
prompts: {},
},
}
);

Implement resource listing:

typescript
import { ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";

server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "file:///config/settings.json",
name: "Application Settings",
mimeType: "application/json",
},
],
};
});

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === "file:///config/settings.json") {
return {
contents: [
{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify({ apiTimeout: 5000, retries: 3 }),
},
],
};
}

throw new Error(`Unknown resource: ${request.params.uri}`);
});

Add a prompt template:

typescript
import { ListPromptsRequestSchema, GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types.js";

server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "code_review",
description: "Review code for best practices",
},
],
};
});

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
if (request.params.name === "code_review") {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: "Review this code for security issues, performance problems, and best practices. Provide specific suggestions.",
},
},
],
};
}

throw new Error(`Unknown prompt: ${request.params.name}`);
});

Now your MCP server offers tools (actions), resources (data), and prompts (templates). Claude can discover all three and use them as needed during a conversation.

MCP Server TypeScript Best Practices

You've got a working server. Here's how to make it production-ready.

Use proper error handling:

typescript
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
// Your tool logic
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
});

Add input validation:

typescript
import { z } from "zod";

const WeatherInputSchema = z.object({
city: z.string().min(1),
units: z.enum(["celsius", "fahrenheit"]).optional(),
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "get_weather") {
const input = WeatherInputSchema.parse(request.params.arguments);
// Now TypeScript knows input.city exists and is a string
}
});
```

**Structure your code:**

Don't cram everything into one file. Split tools, resources, and prompts into separate modules:
```
src/
index.ts # Server setup and transport
tools/
weather.ts # Weather-related tools
database.ts # Database tools
resources/
files.ts # File system resources
prompts/
templates.ts # Prompt templates

Add logging:

typescript
import pino from "pino";

const logger = pino();

server.setRequestHandler(CallToolRequestSchema, async (request) => {
logger.info({ tool: request.params.name }, "Tool called");
// Handle request
});

Common Issues and How to Fix Them

Server not appearing in Claude Desktop:

Check your config file syntax. One misplaced comma breaks everything. Use a JSON validator before restarting Claude.

"Unknown tool" errors:

Your ListToolsRequestSchema handler must return tools that exactly match the names you check in CallToolRequestSchema. Case sensitivity matters.

TypeScript import errors:

The MCP SDK uses ESM imports. Make sure your tsconfig.json has "module": "Node16" or "NodeNext". CommonJS won't work.

Server crashes on startup:

Check that you're using Node.js 18+. The SDK requires modern JavaScript features not available in older versions.

Claude can't communicate with server:

If using stdio transport, make sure you're not writing to stdout (use stderr for logs instead). Stdout is reserved for MCP protocol messages.

Conclusion

You can see the big picture now that you've set up your first MCP server. This is more than just API wrappers and integration layers. It lets AI agents see your system for real, including what they can do, what data they can access, and how to act without you having to constantly guide them. This step makes MCP worth it, especially if you are making products where autonomy, context, and tool orchestration are crucial.

The sample in this lesson is enough to get you started with experimentation. You need a more careful design if you want to set up production environments, connect MCP tools to operational systems, or turn on AI features on your platform.

That's the kind of job we undertake at Codiste. If you need help making an MCP layer that fits in with your AI strategy, just ask. We'd be pleased to look at what you're making and help you get started.

Nishant Bijani
Nishant Bijani
CTO & Co-Founder | Codiste
Nishant is a dynamic individual, passionate about engineering and a keen observer of the latest technology trends. With an innovative mindset and a commitment to staying up-to-date with advancements, he tackles complex challenges and shares valuable insights, making a positive impact in the ever-evolving world of advanced technology.
Relevant blog posts
AI Powered Email Marketing: A Comprehensive Guide
Artificial Intelligence

AI Powered Email Marketing: A Comprehensive Guide

Know more
Top 6 Sports Use Cases of Generative AI in 2025
Artificial Intelligence

Top 6 Sports Use Cases of Generative AI in 2025

Know more
5 Key Use Cases of MCP in Fintech
Artificial Intelligence

5 Key Use Cases of MCP in Fintech: Payments, Lending, KYC & More

Know more
How AI Voice Agents Help Businesses Scale Customer Support Effortlessly
Artificial Intelligence

How AI Voice Agents Help Businesses Scale Customer Support Effortlessly

Know more

Working on a Project?

Share your project details with us, including its scope, deadlines, and any business hurdles you need help with.

Phone

29+
Countries Served Globally

68+
Technocrat Clients

96%
Repeat Client Rate