How to Build an MCP Server from Scratch (Step-by-Step for Beginners)
MCP servers let Claude call your tools. This guide covers the simplest path: a local MCP server using stdio transport, connected to Claude Desktop. Follow five steps: create a project, register a tool, compile, connect, and test.

Article focus
MCP
the open protocol giving AI assistants real-world capabilities
Key takeaways
- MCP is a protocol that lets Claude call tools you expose from a server process.
- An MCP server is a program (often Node.js) that speaks the MCP protocol over a transport like stdio.
- The description matters most - Claude reads it to decide whether to use your tool. Vague descriptions = Claude ignores your tool.
- For local testing, you can run your server without deployment. Production/remote setups add hosting and auth concerns.
- Best MCP servers wrap things Claude cannot reach: private APIs, databases, files on your computer, custom scripts.
What Is MCP?
MCP stands for Model Context Protocol. It is a standard way for Claude (or other MCP clients) to discover and call tools exposed by a server.
Right now Claude knows only what it learned during training. It cannot check your database, call your private API, or read files on your computer. MCP fixes this: you create a server, list your tools, Claude can now use them.
Think of it like this: you are a restaurant. Claude is a customer. Without MCP, Claude can only talk. With MCP, Claude can order food, check the menu, leave a review - because you have a way for Claude to actually DO things.
Scope note: this guide covers a local MCP server using stdio transport (the easiest setup). Remote MCP servers and production deployments are possible, but more complex.
See It: MCP Runtime Loop
One diagram. Full request → decision → tool call (optional) → result loop → final response.
Runtime loop. Claude decide. Server execute. Tool optional.
MCP runtime flow (conceptual)
Build It: Step-by-Step (Local Stdio)
Click step, copy snippet, follow same order.
Visual flow. Click step. Copy snippet. Build fast.
mkdir my-first-mcp-server cd my-first-mcp-server npm init -y npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node npx tsc --init --target ES2022 --module Node16 --moduleResolution Node16 --outDir dist --rootDir src --strict true
Tool description checklist
- Do: what tool do + when use + what return.
- Bad: "fetch data".
- Good: "Fetch GitHub user info: name, bio, followers, repos. Use when user ask about GitHub profile."
Step 0: What You Need Before Starting
You need Node.js installed (version 18 or newer), a code editor, and about 15 minutes. That is it. No special tools. No deployment needed.
Check if Node.js is installed: open your terminal and type node --version. If you see a version number like v18.17.0 or higher, you are ready. If not, download it from nodejs.org.
You will write code in TypeScript, but do not worry - it is just JavaScript with type hints. We will set up the compiler for you, and you just copy-paste the code.
You will also need an MCP client such as Claude Desktop or Claude Code. Availability and setup can vary by version and OS, so treat client installation as a prerequisite check.
Step 1: Create Your Project (5 minutes)
Make a folder, run npm init, install two packages. Copy-paste these commands into your terminal.
We are setting up a standard Node.js project. There is nothing weird or complicated. The only special part is the @modelcontextprotocol/sdk package - that is the library that handles all the protocol stuff so you do not have to.
Zod is for validating inputs. When Claude calls your tool with arguments, Zod checks that they are the right type. If Claude sends bad data, Zod catches it.
After these commands run, you will have a src/ folder for your code and a dist/ folder for the compiled output.
bash
# Create and enter the project folder
mkdir my-first-mcp-server
cd my-first-mcp-server
# Initialize npm (hit enter for all defaults)
npm init -y
# Install the packages you need
npm install @modelcontextprotocol/sdk zod
# Install the tools for development
npm install -D typescript @types/node
# Create folders
mkdir src
# Create the TypeScript config
npx tsc --init --target ES2022 --module Node16 --moduleResolution Node16 --outDir dist --rootDir src --strict trueStep 2: Write Your First Tool (10 minutes)
Create src/index.ts and write a simple tool that Claude can use. Copy this code exactly.
This code creates a server with one tool: it takes your name and says hello. Nothing fancy, but it is a complete, working MCP server.
Key parts: server.tool() registers a tool, the description is what Claude reads to decide if it should use this tool, and the Zod schema defines what inputs it accepts.
The return statement always follows this format: { content: [{ type: "text", text: "your response" }] }. Copy this pattern and swap out your response text.
typescript
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Create the server
const server = new McpServer({
name: "hello-world-server",
version: "1.0.0",
});
// Register one tool: greeting
server.tool(
"greet",
"Say hello to someone. Give a friendly greeting with their name.",
{
name: z.string().describe("The person's name"),
language: z
.enum(["english", "spanish", "french"])
.optional()
.default("english")
.describe("What language to greet in"),
},
async ({ name, language }) => {
const greetings = {
english: `Hello, ${name}! Nice to meet you.`,
spanish: `¡Hola, ${name}! Encantado de conocerte.`,
french: `Bonjour, ${name}! Ravi de vous rencontrer.`,
};
return {
content: [{ type: "text", text: greetings[language] }],
};
}
);
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[Server] Hello World MCP server running");Step 3: Compile Your Code
Run the TypeScript compiler. This turns your .ts file into .js.
TypeScript is just JavaScript with type checking. The compiler reads your .ts file and outputs .js that Node.js can run.
Run tsc and it will create dist/index.js. That is the file Claude will run.
bash
# Compile TypeScript to JavaScript
npx tsc
# Check it worked - you should see dist/index.js now
ls dist/Step 4: Test Your Server in Claude Desktop (3 minutes)
Tell Claude Desktop about your server, restart it, and ask Claude to use your tool.
Claude Desktop reads a config file that tells it which MCP servers to run. You add your server to that config, restart Claude Desktop, and boom - your tool appears.
The config file path and format can vary by client version and OS. The paths below are common defaults, but if yours differs, follow your client documentation.
Use absolute paths - the full path from the root of your drive. Do not use ~ or relative paths, Claude will not find your server.
json
// On Mac: ~/Library/Application Support/Claude/claude_desktop_config.json
// On Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/absolute/path/to/my-first-mcp-server/dist/index.js"]
}
}
}
// Example on Mac:
// {
// "mcpServers": {
// "my-server": {
// "command": "node",
// "args": ["/Users/yourname/my-first-mcp-server/dist/index.js"]
// }
// }
// }Generate Your Claude Desktop Config
Pick OS, paste your project path, copy JSON into your config file.
Pick OS. Paste project folder path. Copy config JSON.
Claude Desktop config path
%APPDATA%\Claude\claude_desktop_config.json
Note: config path + format can vary by Claude Desktop version. If this path does not exist, search for claude_desktop_config.json on your machine or follow your client docs.
Paste folder that contains dist/. We auto-append dist/index.js.
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/absolute/path/to/my-first-mcp-server/dist/index.js"]
}
}
}Step 5: Use Your Tool in Claude
Restart Claude Desktop. Now ask Claude to greet you using your tool.
Close Claude Desktop completely. Then open it again. This forces Claude to read the config file and discover your server.
Now ask Claude something like "can you greet me in Spanish using the greet tool?" Claude will see your tool and use it.
If Claude says it does not have access to the tool, check: (1) The path in your config is correct, (2) Claude Desktop was closed and reopened, (3) Your code compiles without errors.
bash
# If something is broken, test your server directly
node dist/index.js
# If it crashes, you will see the error. Fix it and re-run.
# If it starts successfully, you will see:
# [Server] Hello World MCP server running
# Then you can close it with Ctrl+CA Real Example: Fetch Data from an API
Now that your first tool works, let us build something useful: a tool that fetches weather or GitHub data.
The pattern is the same: define inputs with Zod, fetch data, return it as text. The main difference is you actually call an API instead of just returning a hardcoded string.
Often, returning a short summary is more usable than dumping raw JSON. But for workflows that need structure, returning JSON can also be a good choice - pick the format that matches how you want Claude to reason about the result.
For APIs that need authentication (like GitHub), read the token from environment variables. Never put tokens in your code.
typescript
// Add this tool to your server
server.tool(
"get_github_user",
"Fetch GitHub user information: username, bio, location, repos, followers",
{ username: z.string().describe("GitHub username") },
async ({ username }) => {
try {
const res = await fetch(`https://api.github.com/users/${username}`);
if (res.status === 404) {
throw new Error(`User ${username} not found on GitHub`);
}
if (!res.ok) {
throw new Error(`GitHub API error: ${res.status}`);
}
const user = await res.json();
// Return clean text, not raw JSON
const summary = `
GitHub User: ${user.login}
Name: ${user.name || "Not set"}
Bio: ${user.bio || "No bio"}
Location: ${user.location || "Not set"}
Followers: ${user.followers}
Public Repos: ${user.public_repos}
Profile: ${user.html_url}
`;
return { content: [{ type: "text", text: summary }] };
} catch (error) {
throw new Error(`Failed to fetch user: ${error.message}`);
}
}
);Troubleshooting: 3 Common Problems & Fixes
Your tool is not showing up? Here are the three most common reasons and how to fix them.
Problem 1: "The path in my config is wrong". Fix: Use the full absolute path. On Mac, type pwd in the folder to get it. On Windows, right-click the folder and copy the path.
Problem 2: "Claude Desktop is not seeing my changes". Fix: Close Claude Desktop completely, wait 2 seconds, open it again. Just closing the chat is not enough.
Problem 3: "My tool is registered but Claude won't use it". Fix: Check your tool description. If it says "does stuff", Claude has no idea what it does. Write a clear description like "Fetch GitHub user information: shows username, followers, public repos".
bash
# Debug checklist
# 1. Does your code compile?
npx tsc --noEmit
# If you get errors, fix them. Do not move forward until this passes.
# 2. Can you run your server directly?
node dist/index.js
# Should see: [Server] Hello World MCP server running
# If it crashes, read the error message.
# 3. Is the path in your config correct?
ls /absolute/path/to/dist/index.js
# Should show: /absolute/path/to/dist/index.js
# If not found, path is wrong.
# 4. Did you restart Claude Desktop?
# Close it completely, wait 2 seconds, reopen it.Fix It Fast: Troubleshooting Decision Tree
Click symptom. Follow checks in order. Most issues fixed in 2 minutes.
Pick symptom. Get exact fix path.
Tool not showing in Claude
- 1. Restart Claude Desktop fully
Close app completely. Reopen. (Not just close chat.)
- 2. Verify config file path + JSON valid
Config file exists, JSON parses, server entry inside mcpServers.
- 3. Verify dist file path exists
ls "/absolute/path/to/my-first-mcp-server/dist/index.js"
Command prints that file path (no "No such file").
Next Steps: Build Your Own Server
You now know how to build an MCP server. Here are ideas for your next tool.
Query your own database: write a tool that runs SQL and returns results. Claude can ask questions about your data.
Wrap a private API: if you have an internal API, wrap it in a tool. Claude can now access your company data.
Read/write files: create a tool that Claude can use to read config files or write generated code.
Run scripts: let Claude trigger shell commands, deploy code, or run tests.
The pattern is always the same: define inputs with Zod, do the work, return text. You already know this.
Recommended blogs
Continue reading

How I Use Claude Code to Ship Features 10× Faster
A real-world workflow guide for using Claude Code CLI to build features, debug bugs, refactor legacy code, and run custom automations - with actual examples from building this portfolio.

JavaScript Closures Explained: Why Your Functions Remember Everything
Learn JavaScript closures with interactive demos. Covers lexical scope, the var vs let loop bug, stale React hooks, memory leak patterns, and closure interview questions.
Reference photo by Asad Photo on Pexels
Read article