Build Your First MCP Server in 10 Minutes — Copy-Paste TypeScript Tutorial
MCP (Model Context Protocol) lets AI models call your code. Instead of copying data into prompts, you expose tools that models invoke directly. It's the difference between "here's my database schema" and "query my database yourself."
This tutorial gets you from zero to a working MCP server in 10 minutes. No theory dumps — just code.
What We're Building
A local MCP server that exposes two tools:
-
get_todos— Returns a list of todos from a JSON file -
add_todo— Adds a new todo
Simple, but it covers every concept you need for real MCP servers.
Prerequisites
- Node.js 18+
- npm or yarn
- Any MCP-compatible client (Claude Desktop, Cursor, etc.)
Step 1: Scaffold the Project
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Time elapsed: ~2 minutes.
Step 2: Define Your Tools
Create src/server.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { readFileSync, writeFileSync, existsSync } from "fs";
const TODO_FILE = "./todos.json";
// Helper: load todos from disk
function loadTodos(): { id: number; text: string; done: boolean }[] {
if (!existsSync(TODO_FILE)) return [];
return JSON.parse(readFileSync(TODO_FILE, "utf-8"));
}
// Helper: save todos to disk
function saveTodos(todos: { id: number; text: string; done: boolean }[]) {
writeFileSync(TODO_FILE, JSON.stringify(todos, null, 2));
}
// Create the MCP server
const server = new McpServer({
name: "todo-server",
version: "1.0.0",
});
// Tool 1: Get all todos
server.tool(
"get_todos",
"Returns all todo items",
{},
async () => {
const todos = loadTodos();
return {
content: [
{
type: "text",
text: JSON.stringify(todos, null, 2),
},
],
};
}
);
// Tool 2: Add a new todo
server.tool(
"add_todo",
"Adds a new todo item",
{
text: z.string().describe("The todo item text"),
},
async ({ text }) => {
const todos = loadTodos();
const newTodo = {
id: todos.length + 1,
text,
done: false,
};
todos.push(newTodo);
saveTodos(todos);
return {
content: [
{
type: "text",
text: `Added todo #${newTodo.id}: "${text}"`,
},
],
};
}
);
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Todo MCP Server running on stdio");
}
main().catch(console.error);
That's the entire server. Let's break down what matters:
-
McpServerhandles the protocol — you never touch JSON-RPC -
server.tool()registers tools with name, description, schema, and handler - Zod schemas define parameters — the SDK validates inputs automatically
-
StdioServerTransportuses stdin/stdout (how most MCP clients communicate)
Time elapsed: ~5 minutes.
Step 3: Add a Resource (Bonus)
Resources let models read data without calling a tool. Add this before main():
server.resource(
"todo-summary",
"todos://summary",
async (uri) => {
const todos = loadTodos();
const done = todos.filter((t) => t.done).length;
const summary = `${todos.length} total, ${done} completed, ${todos.length - done} pending`;
return {
contents: [
{
uri: uri.href,
mimeType: "text/plain",
text: summary,
},
],
};
}
);
Now models can check todo status without triggering a tool call.
Step 4: Connect to a Client
Add to your package.json:
{
"scripts": {
"start": "tsx src/server.ts"
}
}
Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"todos": {
"command": "npx",
"args": ["tsx", "src/server.ts"],
"cwd": "/path/to/my-mcp-server"
}
}
}
Cursor
Add to .cursor/mcp.json in your project:
{
"mcpServers": {
"todos": {
"command": "npx",
"args": ["tsx", "src/server.ts"],
"cwd": "/path/to/my-mcp-server"
}
}
}
Restart the client. Your tools appear automatically.
Time elapsed: ~8 minutes.
Step 5: Test It
Ask your AI client:
"What todos do I have?"
It calls get_todos and shows results. Then:
"Add a todo: Review MCP documentation"
It calls add_todo, creates the item, and confirms.
That's it. You have a working MCP server.
Time elapsed: ~10 minutes. ✅
Common Patterns for Real Servers
Once you have the basics, here's what production MCP servers typically add:
Database Tool
server.tool(
"query_db",
"Run a read-only SQL query",
{
query: z.string().describe("SQL SELECT query"),
},
async ({ query }) => {
if (!query.trim().toUpperCase().startsWith("SELECT")) {
return {
content: [{ type: "text", text: "Error: Only SELECT queries allowed" }],
isError: true,
};
}
const results = await db.query(query);
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}
);
API Wrapper
server.tool(
"search_issues",
"Search GitHub issues",
{
repo: z.string().describe("owner/repo"),
query: z.string().describe("Search terms"),
},
async ({ repo, query }) => {
const res = await fetch(
`https://api.github.com/search/issues?q=${query}+repo:${repo}`
);
const data = await res.json();
const summary = data.items.slice(0, 5).map((i: any) =>
`#${i.number}: ${i.title} (${i.state})`
);
return {
content: [{ type: "text", text: summary.join("\n") }],
};
}
);
What's Next
You now know enough to build MCP servers for:
- Databases — Let models query your data safely
- APIs — Wrap any REST/GraphQL API as tools
- File systems — Expose project files as resources
- DevOps — Deploy, monitor, manage infrastructure
The protocol handles the plumbing. You just write the logic.
This is part of the "AI Engineering in Practice" series — practical guides for developers building with AI. Follow for more.
Top comments (0)