Build a Local MCP Server with Node.js, TypeScript, and Zod: The Complete Guide
Build a Local MCP Server with Node.js, TypeScript, and Zod: The Complete Guide
Part 1: First, Let's Understand MCP (The "Why")
Have you ever tried to make an AI Agent use a traditional API? It can be painful. The Agent needs to read documentation, handle complex authentication, and be custom-coded for every single endpoint.
Imagine you're that AI Agent. To get information from a tool, you must learn its specific "language"—its API docs, its data formats, its quirks. To use 10 different tools, you'd need to learn 10 different interfaces. What a nightmare!
But what if instead, your AI agent had a USB-C port?
Just like USB-C gives you one cable to connect chargers, displays, hard drives, and more, MCP gives your AI agent one interface to access all kinds of tools — from scraping websites to summarizing documents.
In technical terms, MCP (Model Context Protocol) is an open protocol that lets AI agents dynamically discover and invoke tools in a standardized way. Instead of writing glue code for every tool, you define them once inside an MCP server.
Part 2: Our Game Plan
Before we dive into the code, here's what we'll build:
- Set up a Node.js + TypeScript project
- Create a local MCP-style server with Express
- Understand how the server discovers and invokes tools
- Define tools using Zod for rock-solid validation
- Expose a
/mcp
endpoint compatible with clients like Cursor - Connect it to Cursor and test it live!
Part 3: Setting Up the Project
For this tutorial, you can get started instantly by cloning the pre-configured project.
Open your terminal and run:
git clone https://github.com/rajeshdh/local-mcp
cd local-mcp
npm install
npm run dev
Your server should now be running at http://localhost:8080. Keep this running in a terminal window for the rest of the tutorial.
Part 4: How the MCP Server Works (Under the Hood)
This isn't just a black box. The Express server has two key responsibilities, handled in src/router.ts
.
1. Discovery: The GET /mcp
Endpoint
How does a client like Cursor know what your server can do? It asks! When a client connects, it first makes a GET request to /mcp
. Our server responds with a JSON object that lists every available tool, including its name, description, and input schema.
This "menu" of tools is what allows the AI to decide which tool is right for the job.
2. Invocation: The POST /invoke/{toolName}
Endpoint
Once the AI chooses a tool, it makes a POST request to this dynamic endpoint, providing the tool's name in the URL and the input data in the request body.
This is where the magic happens:
- The server finds the tool by its name
- It uses the tool's
inputSchema
(our Zod schema) to validate the incoming data. If the data is invalid, it immediately rejects the request with a helpful error - If validation passes, it calls the tool's
handler
function with the sanitized input - It returns the result from the handler as a JSON response
This makes our server both discoverable and secure.
Part 5: Defining Your First MCP Tool
Now for the fun part. Each tool is a simple object that lives in its own file inside the src/tools/
directory. Let's look at src/tools/greeter.ts
:
import { z } from "zod";
export const greeterTool = {
// The unique name the AI will use to call the tool
name: "greeter",
// The MOST important part! This is how the AI understands what the tool does.
description: "Generates a friendly greeting for a given name.",
// The Zod schema defines the "contract" for the input.
inputSchema: z.object({
name: z.string().min(1, "Name cannot be empty."),
}),
// The actual logic that runs after input is validated.
handler: async (input) => {
// `input` is guaranteed to be safe and match the schema.
return { message: `Hello, ${input.name}!` };
},
};
To add this tool to the server, you simply import it in src/router.ts
and add it to the tools array. The server handles the rest!
Part 6: Use It in Cursor
Let's connect our running server to Cursor.
-
Create a
.cursor/mcp.json
file in the root of your project directory.
Alternatively, you can open Cursor's settings by clicking the settings button or pressing Cmd+Shift+J (Ctrl+Shift+J on Windows/Linux). You can add mcp config under Tools & Integrations.Cursor MCP Example
-
Add the following configuration, which tells Cursor where to find your server:
{
"mcpServers": {
"local-mcp-lite": {
"url": "http://localhost:8080"
}
}
}
- Restart Cursor (Cmd/Ctrl + Shift + P and search for "Reload Window")
- Open any chat panel (Cmd/Ctrl + K)
- Type
@
and you should see your local tools appear in the list!
Now, give it a command: greet Rajesh
Cursor will discover your tools, find the right one based on your prompt, and call it automatically.
Cursor MCP Usage Example
Part 7: Add Your Own Tools
The best part about this setup is how easy it is to extend. Let's add a new tool.
- Create a new file in
src/tools/
calledreverseText.ts
- Add this code:
import { z } from "zod";
export const reverseTextTool = {
name: "reverseText",
description: "Reverses any given string.",
inputSchema: z.object({
text: z.string(),
}),
handler: async (input) => {
const reversed = input.text.split("").reverse().join("");
return { reversedText: reversed };
},
};
- Open
src/router.ts
, import your new tool, and add it to the tools array
Your server (if running with npm run dev
) will automatically restart, and the new reverseText
tool will immediately be available in Cursor. No need to touch the core routing logic!
Final Thoughts
You now have a working MCP-style server, built with modern TypeScript and plugged into one of the most powerful AI dev tools out there: Cursor.
Your AI tools are now:
- Discoverable: Clients can ask what tools are available
- Input-safe: Zod validation prevents bad data
- Plug-and-play: Adding new tools is trivial
Stay tuned for Part 2 where we'll add OpenAI tools, GPT-generated summaries, and even plug into Google Sheets!
Repo: https://github.com/rajeshdh/local-mcp
Tag me if you build something cool with it!