Build Your First MCP Server: Beginner Tutorial

What You'll Build in 20 Minutes
By the end of this tutorial, you will have a working MCP server written in TypeScript that exposes a single tool to any MCP-compatible client. The tool accepts a name and returns a greeting — simple on purpose, because the goal is to nail the wiring, not the business logic. Once this server runs, you can swap in any logic you want: call an API, query a database, trigger a browser action, or generate a report.
If you are not yet clear on what MCP servers are and why they exist, read that overview first. This tutorial assumes you understand that an MCP server is a process that exposes tools, resources, and prompts to AI agents over a standardized protocol. As of March 2026, the official @modelcontextprotocol/sdk package provides the McpServer class and transport layers you will use here. Everything in this guide follows official SDK patterns — no third-party wrappers or deprecated APIs.
What You Need Before Starting
You need Node.js 18 or later, a terminal, and a text editor. That is the entire dependency list. No Docker, no cloud accounts, no paid tools. If you have built anything with npm before, this will feel familiar. If you want a broader setup walkthrough covering multiple editors and runtimes, see our guide on getting started with MCP.
Install Node.js and Create the Project
Open your terminal and scaffold a new project directory. The npm init -y command generates a default package.json so you can start installing packages immediately.
mkdir my-first-mcp-server && cd my-first-mcp-server
npm init -y
Before installing dependencies, open package.json and add "type": "module" at the top level. The MCP SDK ships as ESM, and ts-node needs this flag to resolve the imports correctly. Without it, you will hit ERR_REQUIRE_ESM errors before your server even starts.
Add the MCP SDK, Zod, and TypeScript
Install the three runtime packages plus the development tooling in one pass:
npm install @modelcontextprotocol/sdk zod
npm install -D typescript ts-node @types/node
The @modelcontextprotocol/sdk package contains McpServer, all transport classes, and the protocol types. Zod handles schema validation for tool parameters — the SDK uses it natively. TypeScript and ts-node let you run .ts files directly without a separate build step during development.
Create Your First MCP Server File
Create a tsconfig.json at the project root and a src/ directory for your server code. The tsconfig below is the minimal configuration that works with the MCP SDK's ESM output:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
The key settings are "module": "NodeNext" and "moduleResolution": "NodeNext". These tell TypeScript to respect Node's native ESM resolution, which the SDK requires. Using "module": "commonjs" here will break imports.
Now create src/index.ts. This single file is your entire MCP server:
Import McpServer, Stdio Transport, and Zod
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
Three imports, three responsibilities. McpServer is the high-level server class that manages tool registration, protocol negotiation, and client communication. StdioServerTransport connects the server to stdin/stdout — the simplest transport for local development. z from Zod defines the input schema for each tool.
Register Your First Tool with server.tool()
const server = new McpServer({
name: "my-first-mcp-server",
version: "1.0.0",
});
server.tool(
"greet",
{ name: z.string().describe("Name of the person to greet") },
async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}! Welcome to the world of MCP.` }],
})
);
The server.tool() method takes three arguments: a tool name string, a Zod schema object describing the parameters, and an async handler function that returns MCP-formatted content. The handler receives validated, typed parameters — Zod parses and validates the input before your function runs, so you never need to manually check types. The return value must be an object with a content array containing one or more content blocks. Each block has a type (here "text") and the corresponding data.
Important: As of March 2026, the recommended API is server.tool(). Some older tutorials reference alternative method names like registerTool(), createMcpServer(), or addTool() — always check the official SDK documentation for your installed version. The current correct pattern is McpServer plus server.tool().
Connect the Server with Stdio
const transport = new StdioServerTransport();
await server.connect(transport);
Two lines. Create the transport, pass it to server.connect(). The server now listens on stdin for JSON-RPC messages and writes responses to stdout. This is the standard pattern for local MCP servers — the client launches your process and communicates over stdio pipes.
Here is the complete src/index.ts in one block for easy copy-paste:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-first-mcp-server",
version: "1.0.0",
});
server.tool(
"greet",
{ name: z.string().describe("Name of the person to greet") },
async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}! Welcome to the world of MCP.` }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
That is 18 lines of code. Your MCP server is done. Everything from here is testing and connecting it to clients.
Test It with MCP Inspector
Before wiring your server into Claude Desktop or VS Code, test it in isolation. The MCP Inspector is an official debugging tool that connects to your server, lists its tools, and lets you call them interactively. This step catches 90% of issues — wrong imports, missing await, broken schemas — before a client ever touches your code.
Run the Inspector Command
npx @modelcontextprotocol/inspector npx ts-node src/index.ts
This single command does two things: it starts your MCP server via ts-node, and it launches the Inspector client that connects to it over stdio. The Inspector opens a web UI (usually at http://localhost:5173) where you can see every tool your server exposes, the parameter schemas, and the raw JSON-RPC messages flowing back and forth.
If the command fails immediately, check three things: (1) "type": "module" is in your package.json, (2) your tsconfig uses "module": "NodeNext", and (3) you have a src/index.ts file with the correct imports. The error messages from ts-node are usually specific enough to pinpoint the issue.
Trigger the Tool and Inspect the Response
In the Inspector UI, you will see the greet tool listed with its parameter schema. Click it, enter a name like "Alice", and send the request. The response panel should show:
{
"content": [
{
"type": "text",
"text": "Hello, Alice! Welcome to the world of MCP."
}
]
}
If you see this response, your server works. The tool is registered, the schema validates correctly, and the handler returns properly formatted content. You are ready to connect a real client.
See CamoFox for Browser AutomationConnect It to Claude Desktop or VS Code
MCP clients like Claude Desktop and VS Code (with the Copilot MCP extension) discover servers through a local configuration file. You point the client at your server's startup command, and it launches the process automatically when you open a conversation or workspace.
Add the Local MCP Config
For Claude Desktop, edit the config file at ~/Library/Application Support/Claude/claude_desktop_config.json on macOS or %APPDATA%\Claude\claude_desktop_config.json on Windows. Add your server entry:
{
"mcpServers": {
"my-first-mcp-server": {
"command": "npx",
"args": ["ts-node", "/absolute/path/to/src/index.ts"]
}
}
}
Replace /absolute/path/to/ with the actual path to your project's src/index.ts. Relative paths do not work here — Claude Desktop launches the process from its own working directory, so the path must be absolute. For VS Code, the MCP config lives in .vscode/mcp.json in your workspace root using a similar format.
Confirm the Tool Appears in the Client
Restart Claude Desktop (or reload the VS Code window) after editing the config. In Claude Desktop, open a new conversation and look for the hammer icon — it indicates available MCP tools. Click it to see the greet tool listed. Type a prompt like "Use the greet tool to say hello to Bob" and Claude will call your server, pass the parameter, and display the response inline.
If the tool does not appear, open the Claude Desktop logs (Developer → View Logs) and look for connection errors. The most common problem is an incorrect path in the config file. The second most common is a missing "type": "module" in package.json, which causes the process to crash on startup before the MCP handshake completes.
Common Mistakes Beginners Hit
After helping developers build their first MCP servers, the same mistakes appear repeatedly. These are not edge cases — they are the issues you are most likely to encounter in your first hour.
Wrong API Names, Missing await, Bad tsconfig
Using the wrong API names. Search results and AI-generated code sometimes suggest createMcpServer(), registerTool(), or server.addTool(). As of March 2026, these are not part of the official SDK — the correct pattern is new McpServer() for instantiation and server.tool() for tool registration. API surfaces can change between versions, so always verify method names against the official TypeScript SDK repository for your installed version.
Forgetting await on server.connect(). The connect() call is async. Without await, your script may exit before the server finishes initializing. In a top-level module context (ESM with "type": "module"), top-level await works natively. If you wrap it in a function, make sure that function is async and properly awaited.
Wrong tsconfig module settings. Setting "module": "commonjs" or "moduleResolution": "node" will cause import resolution failures. The SDK ships with .js extensions in its import paths (e.g., @modelcontextprotocol/sdk/server/mcp.js), which require "module": "NodeNext" and "moduleResolution": "NodeNext" to resolve correctly. This is the single most confusing error for beginners because the error message mentions files that clearly exist on disk but Node refuses to load.
Mixing Transport and Tool Logic
Keep transport setup and tool registration separate. A common anti-pattern is embedding console.log() calls inside the tool handler for debugging. Since the stdio transport uses stdout for protocol messages, any stray console.log() in your server code will inject garbage into the JSON-RPC stream and crash the client connection. Use console.error() instead — stderr is safe because the transport only reads stdout.
Similarly, do not perform transport-specific logic inside tool handlers. Your tool handler should be a pure function of its inputs. If you need to support multiple transports later (adding SSE or HTTP alongside stdio), clean separation means your tools work unchanged — you only swap the transport layer.
Where to Go Next After Your First Server
You have a running MCP server, a tested tool, and a client connection. Here is where to expand. Add more tools: a second server.tool() call with different parameters and logic. Add resources with server.resource() to expose data that clients can read without calling a tool. Add prompts with server.prompt() to provide reusable templates.
For real-world projects, explore MCP browser automation options to see how production MCP servers control browsers. CamoFox MCP shows what a fully built anti-detection browser MCP looks like. If you are working with automation workflows beyond the browser, MCP is the layer that lets AI agents orchestrate multiple tools in a single conversation. For a broader view on browser tooling, see our anti-detection browser guide.
Turn Your MCP Tool into a Browser WorkflowFAQ
What is the fastest way to build an MCP server in TypeScript?
Install @modelcontextprotocol/sdk and zod, create a single .ts file with McpServer, register a tool with server.tool(), connect via StdioServerTransport, and run with ts-node. The minimal working server is under 20 lines of code. As of March 2026, this is the official recommended approach from the MCP TypeScript SDK.
Do I need Zod to create MCP tools?
Yes, when using the high-level McpServer API. The server.tool() method expects a Zod schema object for parameter validation. Zod schemas are automatically converted to JSON Schema for the MCP protocol, and the SDK validates incoming parameters against them before calling your handler. You could use the lower-level Server class and write raw JSON Schema instead, but McpServer with Zod is simpler and catches type errors at compile time.
Should beginners use stdio, SSE, or HTTP transport first?
Start with stdio. It requires zero network configuration, works immediately with local clients like Claude Desktop and VS Code, and has the simplest debugging model — your server is a process that reads stdin and writes stdout. SSE and the newer Streamable HTTP transport are designed for remote servers, which add network, authentication, and deployment complexity. Master stdio first, then migrate when you have a reason to go remote.
How do I test an MCP server before connecting Claude Desktop?
Use the MCP Inspector: npx @modelcontextprotocol/inspector npx ts-node src/index.ts. It connects to your server, displays all registered tools, and lets you call them interactively through a web UI. The Inspector shows raw JSON-RPC traffic, which helps debug schema issues, missing tools, and malformed responses without the overhead of a full client application.
Why isn't my MCP tool showing up in Claude Desktop or VS Code?
Check five things in order: (1) the path in your config file is absolute, not relative; (2) "type": "module" is present in package.json; (3) tsconfig uses "module": "NodeNext"; (4) you restarted the client after editing the config; (5) no console.log() statements are polluting stdout in your server code. Open the client's developer logs to see the actual error — it almost always points directly to the problem.
Try It Yourself
Follow along on your preferred platform — Bot, Web, iOS, or Android.
Free to try • No credit card required