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_tasktool β create a new taskcomplete_tasktool β mark a task donelist_tasksresource β read all tasks as JSONdaily_planprompt β 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.