dohkoBuild Your First MCP Server in 10 Minutes — Copy-Paste TypeScript Tutorial MCP (Model...
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.
A local MCP server that exposes two tools:
get_todos — Returns a list of todos from a JSON fileadd_todo — Adds a new todoSimple, but it covers every concept you need for real MCP servers.
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.
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:
McpServer handles the protocol — you never touch JSON-RPCserver.tool() registers tools with name, description, schema, and handlerStdioServerTransport uses stdin/stdout (how most MCP clients communicate)Time elapsed: ~5 minutes.
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.
Add to your package.json:
{
"scripts": {
"start": "tsx src/server.ts"
}
}
Add to claude_desktop_config.json:
{
"mcpServers": {
"todos": {
"command": "npx",
"args": ["tsx", "src/server.ts"],
"cwd": "/path/to/my-mcp-server"
}
}
}
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.
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. ✅
Once you have the basics, here's what production MCP servers typically add:
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) }],
};
}
);
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") }],
};
}
);
You now know enough to build MCP servers for:
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.