M
MeshWorld.
AI MCP Claude Tutorial Node.js 4 min read

Build Your First MCP Server for Claude in 15 Minutes

By Vishnu Damwala

The fastest way to understand MCP is to build something with it. This tutorial creates a minimal MCP server that exposes one tool — a simple note-taking API — and connects it to Claude.

You need Node.js 18+ and an Anthropic API key.

What we are building

A tiny MCP server that exposes a save_note tool. Claude can call it to store notes during a conversation. Simple enough to finish in 15 minutes, real enough to teach the core pattern.

1. Set up the project

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node tsx

Add a tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true
  }
}

2. Write the server

Create src/server.ts:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fs from "fs";
import path from "path";

const NOTES_FILE = path.join(process.cwd(), "notes.json");

// Load or initialize notes storage
function loadNotes(): Record<string, string> {
  if (fs.existsSync(NOTES_FILE)) {
    return JSON.parse(fs.readFileSync(NOTES_FILE, "utf-8"));
  }
  return {};
}

function saveNotes(notes: Record<string, string>) {
  fs.writeFileSync(NOTES_FILE, JSON.stringify(notes, null, 2));
}

// Create the MCP server
const server = new Server(
  { name: "notes-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "save_note",
      description: "Save a note with a title and content",
      inputSchema: {
        type: "object",
        properties: {
          title: { type: "string", description: "The note title" },
          content: { type: "string", description: "The note content" },
        },
        required: ["title", "content"],
      },
    },
    {
      name: "get_note",
      description: "Retrieve a saved note by title",
      inputSchema: {
        type: "object",
        properties: {
          title: { type: "string", description: "The note title to retrieve" },
        },
        required: ["title"],
      },
    },
  ],
}));

// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "save_note") {
    const { title, content } = args as { title: string; content: string };
    const notes = loadNotes();
    notes[title] = content;
    saveNotes(notes);
    return {
      content: [{ type: "text", text: `Note "${title}" saved successfully.` }],
    };
  }

  if (name === "get_note") {
    const { title } = args as { title: string };
    const notes = loadNotes();
    const content = notes[title];
    if (!content) {
      return {
        content: [{ type: "text", text: `No note found with title "${title}".` }],
      };
    }
    return {
      content: [{ type: "text", text: `**${title}**\n\n${content}` }],
    };
  }

  throw new Error(`Unknown tool: ${name}`);
});

// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Notes MCP server running on stdio");

3. Run it

npx tsx src/server.ts

The server starts and waits for MCP clients to connect over stdio. You will not see much in the terminal — it communicates through structured JSON over stdin/stdout.

4. Connect it to Claude Desktop

Open your Claude Desktop config file:

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

Add your server:

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

Restart Claude Desktop. You should see “notes” appear in the MCP tools panel.

Now test it — ask Claude:

“Save a note called ‘MCP ideas’ with the content: build a calendar MCP server next”

Claude will call save_note with the right arguments. Check notes.json in your project root — the note will be there.

5. Connect it to Claude Code

Add the server to your Claude Code config (~/.claude/mcp.json):

{
  "servers": {
    "notes": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/my-mcp-server/src/server.ts"],
      "transport": "stdio"
    }
  }
}

Now inside any Claude Code session, Claude can use save_note and get_note as tools.

What just happened

You defined a server with:

  1. ListToolsRequestSchema handler — tells MCP clients what tools exist and what parameters they take
  2. CallToolRequestSchema handler — receives tool calls and executes the logic
  3. StdioServerTransport — the communication channel (stdin/stdout for local tools)

That is the entire MCP server pattern. For a production server, you would swap StdioServerTransport for an HTTP transport and deploy it as a service.

Going further

Add more tools. Each tool follows the same pattern: define it in ListTools, handle it in CallTool.

Switch to HTTP transport. For a shared server multiple clients can connect to:

import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
// ... attach to an Express/Fastify route

Expose resources. Resources let Claude read data without calling a tool — good for static content, file listings, or read-only APIs.

Next steps