Build Your First MCP Server in 10 Minutes — Copy-Paste TypeScript Tutorial

# ai# typescript# programming# tutorial
Build Your First MCP Server in 10 Minutes — Copy-Paste TypeScript Tutorialdohko

Build Your First MCP Server in 10 Minutes — Copy-Paste TypeScript Tutorial MCP (Model...

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:

  1. get_todos — Returns a list of todos from a JSON file
  2. 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
Enter fullscreen mode Exit fullscreen mode

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

That's the entire server. Let's break down what matters:

  • McpServer handles 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
  • StdioServerTransport uses 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,
        },
      ],
    };
  }
);
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "todos": {
      "command": "npx",
      "args": ["tsx", "src/server.ts"],
      "cwd": "/path/to/my-mcp-server"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Cursor

Add to .cursor/mcp.json in your project:

{
  "mcpServers": {
    "todos": {
      "command": "npx",
      "args": ["tsx", "src/server.ts"],
      "cwd": "/path/to/my-mcp-server"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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) }],
    };
  }
);
Enter fullscreen mode Exit fullscreen mode

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") }],
    };
  }
);
Enter fullscreen mode Exit fullscreen mode

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.