πŸ€– HarDojo
Log in Sign up

Build a Complete MCP Server

From zero to a working MCP server with tools, resources, and a real use case.

In this project you'll build a complete MCP server that manages a to-do list β€” with tools to add/complete tasks, a resource to read all tasks, and a prompt template for daily planning.

We'll use the TypeScript SDK and you'll have a working server you can connect to Claude Desktop, Cursor, or Claude Code.

What you'll build

A todo-mcp server exposing:

  • add_task tool β€” create a new task
  • complete_task tool β€” mark a task done
  • list_tasks resource β€” read all tasks as JSON
  • daily_plan prompt β€” generate a daily plan from tasks

Step 1: Scaffold the project

mkdir todo-mcp && cd todo-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node
npx tsc --init --outDir dist --rootDir src

Create src/index.ts:

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: "todo-mcp",
  version: "1.0.0",
});

// In-memory task store
let tasks: { id: number; title: string; done: boolean }[] = [];
let nextId = 1;

Step 2: Add the tools

server.tool(
  "add_task",
  "Create a new to-do task",
  { title: z.string().describe("The task description") },
  async ({ title }) => {
    const task = { id: nextId++, title, done: false };
    tasks.push(task);
    return {
      content: [{ type: "text", text: `Created task #${task.id}: ${task.title}` }],
    };
  }
);

server.tool(
  "complete_task",
  "Mark a task as done",
  { id: z.number().describe("The task ID to complete") },
  async ({ id }) => {
    const task = tasks.find((t) => t.id === id);
    if (!task) {
      return { content: [{ type: "text", text: `Task #${id} not found.` }] };
    }
    task.done = true;
    return {
      content: [{ type: "text", text: `Completed task #${id}: ${task.title}` }],
    };
  }
);

Step 3: Add the resource

server.resource(
  "list_tasks",
  "todo://tasks",
  async (uri) => ({
    contents: [{
      uri: uri.href,
      mimeType: "application/json",
      text: JSON.stringify(tasks, null, 2),
    }],
  })
);

Step 4: Add the prompt

server.prompt(
  "daily_plan",
  "Generate a daily plan from incomplete tasks",
  () => {
    const pending = tasks.filter((t) => !t.done);
    return {
      messages: [{
        role: "user",
        content: {
          type: "text",
          text: `Here are my pending tasks:\n${pending.map((t) => `- [ ] #${t.id} ${t.title}`).join("\n")}\n\nHelp me prioritize and plan my day.`,
        },
      }],
    };
  }
);

Step 5: Connect and run

async function main() {
  await server.connect(new StdioServerTransport());
  console.error("todo-mcp server running on stdio");
}
main();

Add to package.json:

{ "scripts": { "start": "npx tsx src/index.ts" } }

Run it: npm start β€” the server starts silently on stdin. That's correct for an MCP server.

Step 6: Connect it to Claude Desktop

Add to your claude_desktop_config.json:

{
  "mcpServers": {
    "todo": {
      "command": "npx",
      "args": ["tsx", "/full/path/to/todo-mcp/src/index.ts"]
    }
  }
}

Restart Claude Desktop. Ask: "Add a task to finish the report, then list all my tasks."

Checkpoint

You should see Claude call add_task, then list_tasks, and respond with your task list. πŸŽ‰

What you learned

  • Registering tools with server.tool() and zod schemas.
  • Exposing a resource at a URI with server.resource().
  • Creating reusable prompts with server.prompt().
  • Connecting via stdio transport.
  • How a real MCP server connects to a real client.
";