stormdotcom/mcp-server
If you are the rightful owner of mcp-server and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to henry@mcphub.com.
This document provides a comprehensive guide to setting up and running a Model Context Protocol (MCP) server using Node.js and TypeScript.
- Node.js 22.14.0+ (22+ LTS recommended)
- A terminal + a code editor
1) What is MCP in simple terms?
A: MCP is a protocol that lets AI models interact with external tools and data through a standardized interface. It acts like a universal adapter so LLMs donāt have to hallucinateāthey can fetch accurate, real-time information.
2) Create the project
mkdir mcp-users && cd mcp-users
npm init -y
3) Install dependencies
# Core
npm i @modelcontextprotocol/sdk zod
# Dev tooling
npm i -D typescript tsx @types/node
# Fake data generator (so "random user" works without an AI model)
npm i @faker-js/faker
# (Optional, very handy) MCP Inspector for testing
npm i -D @modelcontextprotocol/inspector
4) Set up package.json
(scripts + ESM)
Open package.json
and replace with this (or merge with yours):
{
"name": "mcp-users",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "dist/server.js",
"scripts": {
"dev": "tsx src/server.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js",
"typecheck": "tsc --noEmit",
"inspect": "npx @modelcontextprotocol/inspector npm run dev"
},
"dependencies": {
"@faker-js/faker": "^9.0.0",
"@modelcontextprotocol/sdk": "^1.13.0",
"zod": "^3.25.0"
},
"devDependencies": {
"@modelcontextprotocol/inspector": "^0.14.3",
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.6.0"
}
}
type: "module"
lets us use modernimport
syntax, which MCP SDK uses.
5) Add TypeScript config
Create tsconfig.json
:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"outDir": "dist",
"types": ["node"],
"skipLibCheck": true
},
"include": ["src"]
}
6) Create folders and a ādatabaseā
mkdir -p src data
echo "[]" > data/users.json
- Weāll store users in
data/users.json
. - It starts as an empty array
[]
.
7) Write the MCP server
Create src/server.ts
and paste this:
import {
McpServer,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "node:fs/promises";
import { faker } from "@faker-js/faker";
// =======================
// Small data layer (JSON)
// =======================
const DATA_PATH = "./data/users.json";
async function readUsers(): Promise<any[]> {
try {
const raw = await fs.readFile(DATA_PATH, "utf-8");
return JSON.parse(raw);
} catch (e: any) {
if (e?.code === "ENOENT") return [];
throw e;
}
}
async function writeUsers(users: any[]) {
await fs.writeFile(DATA_PATH, JSON.stringify(users, null, 2));
}
function nextId(users: any[]) {
return (users.at(-1)?.id ?? 0) + 1; // safe even after deletions
}
// ==============
// Zod schemas
// ==============
const UserInputSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
address: z.string().min(1),
phone: z.string().min(3),
});
const UserUpdateSchema = z.object({
id: z.number(),
name: z.string().min(1).optional(),
email: z.string().email().optional(),
address: z.string().min(1).optional(),
phone: z.string().min(3).optional(),
});
// ======================
// Build the MCP server
// ======================
const server = new McpServer({
name: "users-mcp",
version: "1.0.0",
capabilities: { resources: {}, tools: {}, prompts: {} },
});
// -------------
// Resources
// -------------
// List all users
server.resource(
"users",
"users://all",
{
description: "Get all users",
title: "Users",
mimeType: "application/json",
},
async (uri) => {
const users = await readUsers();
return {
contents: [
{
uri: uri.href,
text: JSON.stringify(users),
mimeType: "application/json",
},
],
};
}
);
// Get one user by ID
server.resource(
"user-details",
new ResourceTemplate("users://{id}/profile", { list: undefined }),
{
description: "Get a single user by ID",
title: "User Details",
mimeType: "application/json",
},
async (uri, { id }) => {
const users = await readUsers();
const user = users.find((u) => u.id === Number(id));
return {
contents: [
{
uri: uri.href,
text: JSON.stringify(user ?? { error: "User not found" }),
mimeType: "application/json",
},
],
};
}
);
// ---------
// Tools
// ---------
// Create
server.tool(
"create-user",
"Create a new user",
UserInputSchema.shape, // the shape object (keys => zod types)
{
title: "Create User",
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
async (params) => {
const input = UserInputSchema.parse(params);
const users = await readUsers();
const id = nextId(users);
users.push({ id, ...input });
await writeUsers(users);
return { content: [{ type: "text", text: `User ${id} created` }] };
}
);
// Read (tool form)
server.tool(
"get-user",
"Get a user by ID",
{ id: z.number() },
{
title: "Get User",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
async ({ id }) => {
const users = await readUsers();
const user = users.find((u) => u.id === id);
return {
content: [
{
type: "text",
text: JSON.stringify(user ?? { error: "User not found" }),
},
],
};
}
);
// Update (partial)
server.tool(
"update-user",
"Update a user by ID (partial)",
UserUpdateSchema.shape,
{
title: "Update User",
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
async (params) => {
const update = UserUpdateSchema.parse(params);
const users = await readUsers();
const idx = users.findIndex((u) => u.id === update.id);
if (idx < 0) {
return {
content: [{ type: "text", text: `No user found with ID ${update.id}` }],
};
}
const current = users[idx];
const merged = {
...current,
...Object.fromEntries(
Object.entries(update).filter(([k, v]) => k !== "id" && v !== undefined)
),
};
users[idx] = merged;
await writeUsers(users);
return { content: [{ type: "text", text: `User ${update.id} updated` }] };
}
);
// Delete
server.tool(
"delete-user",
"Delete a user by ID",
{ id: z.number() },
{
title: "Delete User",
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: true,
},
async ({ id }) => {
const users = await readUsers();
const idx = users.findIndex((u) => u.id === id);
if (idx < 0) {
return {
content: [{ type: "text", text: `No user found with ID ${id}` }],
};
}
const [deleted] = users.splice(idx, 1);
await writeUsers(users);
return {
content: [{ type: "text", text: `Deleted user ${deleted.name ?? id}` }],
};
}
);
// Extra: delete by name (case-insensitive)
server.tool(
"delete-user-by-name",
"Delete all users matching a name (case-insensitive)",
{ name: z.string() },
{
title: "Delete User(s) by Name",
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: true,
},
async ({ name }) => {
const users = await readUsers();
const before = users.length;
const remaining = users.filter(
(u) => (u.name ?? "").toLowerCase() !== name.toLowerCase()
);
const deleted = before - remaining.length;
if (deleted === 0) {
return { content: [{ type: "text", text: `No user named "${name}"` }] };
}
await writeUsers(remaining);
return {
content: [
{ type: "text", text: `Deleted ${deleted} user(s) named "${name}"` },
],
};
}
);
// ----------
// Prompt
// ----------
server.prompt(
"generate-fake-user",
"A template to ask an AI to create a fake user",
{ name: z.string() },
({ name }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Generate a fake user named "${name}" and return ONLY JSON with fields: name, email, address, phone.`,
},
},
],
})
);
// ----------
// Random user tool (works offline using faker)
// ----------
server.tool(
"create-random-user",
"Create a random user (no AI needed)",
{
title: "Create Random User",
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
async () => {
const fake = {
name: faker.person.fullName(),
email: faker.internet.email(),
address: `${faker.location.streetAddress()}, ${faker.location.city()}`,
phone: faker.phone.number(),
};
const users = await readUsers();
const id = nextId(users);
users.push({ id, ...fake });
await writeUsers(users);
return { content: [{ type: "text", text: `User ${id} created (random)` }] };
}
);
// ==========
// Boot
// ==========
(async () => {
const transport = new StdioServerTransport();
await server.connect(transport);
// IMPORTANT: Do not console.log here (stdout is for MCP JSON). Use console.error for debug.
})();
Why no
console.log
? Because MCP speaks over stdout. If you print normal logs, the client will see garbage instead of JSON. Useconsole.error
for debugging.
8) Run it
Dev mode (TypeScript directly):
npm run dev
Build & run (compiled JS):
npm run build
npm start
The process now waits for an MCP client (like the Inspector) to connect over stdio.
9) Test it with MCP Inspector (GUI)
npm run inspect
In the Inspector:
-
Go to Resources ā open
users://all
(should be[]
at first). -
Go to Tools:
-
Run create-user with:
{ "name": "Ada Lovelace", "email": "ada@example.com", "address": "10 Analytical Engine Way", "phone": "+1-555-1234" }
-
Run get-user with
{ "id": 1 }
. -
Run update-user with
{ "id": 1, "phone": "+1-555-0000" }
. -
Run delete-user with
{ "id": 1 }
. -
Try create-random-user to generate fake users quickly.
-
10) Common gotchas (read this!)
- Donāt log to stdout from the server. Use
console.error
for debugging. - Donāt use
import("./data/users.json")
to read data after writes ā Node caches it. Usefs.readFile
/fs.writeFile
like above. - Keep read/write paths the same (
./data/users.json
). - If you change the data file by hand, make sure the JSON stays valid (no trailing commas, etc.).
11) Whatās next?
- Swap the JSON file for a real database (only your small data layer changes).
- Add more tools/resources by copy-pasting the patterns.
- Split this one file into folders (
resources/
,tools/
,services/
) when it grows.