Build Your First MCP Server in 30 Minutes

A working MCP server, end to end, in 30 minutes. We'll expose two tools, run it locally, and connect it to Claude Desktop. TypeScript and Python versions, complete code, no glossing over the boring parts.

This tutorial gets you from zero to a working MCP server connected to Claude Desktop in roughly 30 minutes. We’ll build the server in either TypeScript or Python (both included), expose two tools, and walk through running it end to end.

If you don’t yet know what MCP is, start with What is MCP? first.

What we’re building

A small notes server that exposes:

  • add_note — append a note to an in-memory list
  • search_notes — find notes whose body contains a substring

It’s the simplest thing that’s actually useful and exercises both a write and a read tool.

Prerequisites

  • Node 18+ or Python 3.10+
  • Claude Desktop installed (for testing)
  • A terminal you’re comfortable in

Path A — TypeScript

Set up

bash
mkdir notes-mcp && cd notes-mcp
npm init -y
npm i @modelcontextprotocol/sdk zod
npm i -D typescript tsx @types/node
npx tsc --init

The server

Create src/index.ts:

typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const notes: { id: number; body: string; ts: number }[] = [];
let nextId = 1;

const server = new Server(
  { name: "notes", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.tool(
  "add_note",
  "Append a note. Returns the created note's id.",
  { body: z.string().min(1).describe("Note body") },
  async ({ body }) => {
    const note = { id: nextId++, body, ts: Date.now() };
    notes.push(note);
    return { content: [{ type: "text", text: JSON.stringify(note) }] };
  }
);

server.tool(
  "search_notes",
  "Find notes whose body contains the query (case-insensitive).",
  { query: z.string().describe("Substring to match") },
  async ({ query }) => {
    const q = query.toLowerCase();
    const hits = notes.filter(n => n.body.toLowerCase().includes(q));
    return { content: [{ type: "text", text: JSON.stringify(hits) }] };
  }
);

const transport = new StdioServerTransport();
server.connect(transport);

Run it

bash
npx tsx src/index.ts

The server starts, listens on stdio, and waits for an MCP client to connect.

Path B — Python

Set up

bash
mkdir notes-mcp && cd notes-mcp
python -m venv .venv && source .venv/bin/activate
pip install mcp

The server

Create server.py:

python
from mcp.server.fastmcp import FastMCP
import time

app = FastMCP("notes")
notes: list[dict] = []
next_id = {"v": 1}

@app.tool()
def add_note(body: str) -> dict:
    """Append a note. Returns the created note."""
    note = {"id": next_id["v"], "body": body, "ts": int(time.time())}
    next_id["v"] += 1
    notes.append(note)
    return note

@app.tool()
def search_notes(query: str) -> list[dict]:
    """Find notes whose body contains the query (case-insensitive)."""
    q = query.lower()
    return [n for n in notes if q in n["body"].lower()]

if __name__ == "__main__":
    app.run()

Run it

bash
python server.py

Connect it to Claude Desktop

Open Claude Desktop’s config file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add an entry under mcpServers:

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

Or for the Python version:

json
{
  "mcpServers": {
    "notes": {
      "command": "/absolute/path/to/notes-mcp/.venv/bin/python",
      "args": ["/absolute/path/to/notes-mcp/server.py"]
    }
  }
}

Restart Claude Desktop. You should see a small tools indicator showing 2 tools available from notes.

Try it

In Claude, ask: “Add a note that says ‘meet with Mira on Thursday’, then search my notes for ‘mira’.” The model picks the tools, calls them in order, and returns the matches.

Debug tip. If tools don’t show up, check the JSON config for typos and restart the host fully. The error logs are in Claude Desktop’s log directory; tailing them while you test catches almost every issue.

Where to go next

  • Persist notes to disk (or a real DB) so they survive restarts
  • Add a delete_note tool
  • Expose notes as MCP resources so the model can read them passively, not only by tool call
  • Read the protocol overview for deeper concepts (resources, prompts, sampling)