📚Academy
likeone
online

Your First Server

Build a working MCP server from scratch. By the end of this lesson, you will have a real server running on your machine that Claude can talk to. Every line of code is explained.

What You Will Build

We are building a note-taking MCP server — a tool that lets Claude create, read, list, and search notes stored on your machine. It is simple enough to understand in one sitting, but complex enough to teach every pattern you need for production servers.

📝 create_note

Save a new note with a title and content to disk

📄 read_note

Retrieve a specific note by its title

🔍 search_notes

Search all notes by keyword and return matches

Prerequisites

You need Node.js 18+ installed. Check with node --version in your terminal. If you do not have it, install from nodejs.org.

Terminalbash
# Create a project folder and install dependencies mkdir my-notes-server && cd my-notes-server npm init -y npm install @modelcontextprotocol/sdk zod # Create the server file touch server.ts

The Complete Server

Here is the entire server. Read it top to bottom — every section is annotated. After the code, we break down each part in detail.

server.tsTypeScript
// ── Imports ────────────────────────────────────────────── // McpServer: the main class that handles protocol messages // StdioServerTransport: connects via stdin/stdout (local) import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs"; import { join } from "path"; // ── Storage Setup ──────────────────────────────────────── // Notes are stored as individual .txt files in a "notes" folder // next to the server. Simple, inspectable, no database needed. const NOTES_DIR = join(process.cwd(), "notes"); if (!existsSync(NOTES_DIR)) mkdirSync(NOTES_DIR); // ── Create the Server ──────────────────────────────────── // name: shows up in Claude's tool list as the server identity // version: used during capability negotiation with the client const server = new McpServer({ name: "notes-server", version: "1.0.0", }); // ── Tool 1: create_note ────────────────────────────────── // server.tool() takes three arguments: // 1. name — what Claude calls to invoke this tool // 2. schema — Zod schema defining required inputs // 3. handler — async function that does the actual work server.tool( "create_note", { title: z.string().describe("The note title (used as filename)"), content: z.string().describe("The note body text"), }, async ({ title, content }) => { // Sanitize the title to prevent path traversal attacks const safe = title.replace(/[^a-zA-Z0-9_-]/g, "_"); const path = join(NOTES_DIR, `${safe}.txt`); writeFileSync(path, content, "utf-8"); return { content: [{ type: "text", text: `Note "${title}" saved.` }], }; } ); // ── Tool 2: read_note ──────────────────────────────────── server.tool( "read_note", { title: z.string().describe("The title of the note to read"), }, async ({ title }) => { const safe = title.replace(/[^a-zA-Z0-9_-]/g, "_"); const path = join(NOTES_DIR, `${safe}.txt`); if (!existsSync(path)) { return { content: [{ type: "text", text: `Note "${title}" not found.` }], isError: true, }; } const text = readFileSync(path, "utf-8"); return { content: [{ type: "text", text }], }; } ); // ── Tool 3: search_notes ───────────────────────────────── server.tool( "search_notes", { query: z.string().describe("Keyword to search for in note contents"), }, async ({ query }) => { const files = readdirSync(NOTES_DIR).filter(f => f.endsWith(".txt")); const matches = []; for (const file of files) { const text = readFileSync(join(NOTES_DIR, file), "utf-8"); if (text.toLowerCase().includes(query.toLowerCase())) { matches.push({ title: file.replace(".txt", ""), preview: text.slice(0, 100) }); } } if (matches.length === 0) { return { content: [{ type: "text", text: `No notes found matching "${query}".` }] }; } const result = matches.map(m => `• ${m.title}: ${m.preview}...`).join("\n"); return { content: [{ type: "text", text: `Found ${matches.length} match(es):\n${result}` }], }; } ); // ── Start the Server ───────────────────────────────────── // StdioServerTransport reads from stdin and writes to stdout. // Claude Desktop launches this process and communicates // through these streams — no network, no ports, no config. const transport = new StdioServerTransport(); await server.connect(transport);

Line-by-Line Breakdown

Let us walk through the four sections of this server so you understand the pattern deeply enough to build your own.

🔒

This lesson is for Pro members

Unlock all 520+ lessons across 52 courses with Academy Pro.

Already a member? Sign in to access your lessons.

Academy
Built with soul — likeone.ai