~ $ man build-your-first-mcp-server

Build your first MCP server in plain Node

10 min · updated

The fastest way to understand MCP is to build a server yourself. This guide walks through a complete, dependency-free HTTP MCP server in a single Node file. By the end you'll have a working server you can test with curl and register with Claude Code.

What we're building

A minimal MCP server that handles the three required interactions:

No npm, no TypeScript compilation, no framework. Just node:http and JSON-RPC.

The server: mcp-server.mjs

import { createServer } from "node:http";

const PORT = 8765;

const TOOLS = [
  {
    name: "echo",
    description: "Return the text you send.",
    inputSchema: {
      type: "object",
      properties: {
        text: { type: "string", description: "The text to echo." },
      },
      required: ["text"],
    },
  },
];

function handleRequest(body) {
  const { jsonrpc, id, method, params } = body;

  if (method === "initialize") {
    return {
      jsonrpc,
      id,
      result: {
        protocolVersion: "2025-03-26",
        capabilities: { tools: {} },
        serverInfo: { name: "my-first-mcp", version: "0.1.0" },
        instructions: "A minimal echo server for learning MCP.",
      },
    };
  }

  if (method === "tools/list") {
    return { jsonrpc, id, result: { tools: TOOLS } };
  }

  if (method === "tools/call") {
    const { name, arguments: args } = params;
    if (name !== "echo") {
      return {
        jsonrpc,
        id,
        error: { code: -32601, message: `Unknown tool: ${name}` },
      };
    }
    return {
      jsonrpc,
      id,
      result: {
        content: [{ type: "text", text: args.text }],
      },
    };
  }

  return {
    jsonrpc,
    id,
    error: { code: -32601, message: `Unknown method: ${method}` },
  };
}

const server = createServer((req, res) => {
  if (req.method !== "POST") {
    res.writeHead(405).end("Method Not Allowed");
    return;
  }

  let raw = "";
  req.on("data", (chunk) => (raw += chunk));
  req.on("end", () => {
    let body;
    try {
      body = JSON.parse(raw);
    } catch {
      res.writeHead(400, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: "Invalid JSON" }));
      return;
    }

    const response = handleRequest(body);
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(response));
  });
});

server.listen(PORT, () => {
  console.log(`MCP server listening on http://localhost:${PORT}`);
});

Save this as mcp-server.mjs anywhere on your machine. Run it:

node mcp-server.mjs
# MCP server listening on http://localhost:8765

Test with curl

Verify each method before touching Claude Code. Open a second terminal.

Initialize:

curl -s -X POST http://localhost:8765 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}'

Expected response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-03-26",
    "capabilities": { "tools": {} },
    "serverInfo": { "name": "my-first-mcp", "version": "0.1.0" },
    "instructions": "A minimal echo server for learning MCP."
  }
}

List tools:

curl -s -X POST http://localhost:8765 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

Call the echo tool:

curl -s -X POST http://localhost:8765 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hello from curl"}}}'

Expected:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "hello from curl" }]
  }
}

Test an unknown method to confirm error handling:

curl -s -X POST http://localhost:8765 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":4,"method":"nonexistent","params":{}}'

Expected:

{ "jsonrpc": "2.0", "id": 4, "error": { "code": -32601, "message": "Unknown method: nonexistent" } }

Register with Claude Code

With the server running, register it using the HTTP transport:

claude mcp add --transport http my-first-mcp http://localhost:8765

Confirm it's registered:

claude mcp list

Start a new Claude Code session. Claude Code will handshake with your server at startup and make the echotool available. In the session, ask Claude to use it: "use the echo tool to repeat: hello world". Claude Code will call tools/call with { "name": "echo", "arguments": { "text": "hello world" } } and display the result.

JSON-RPC error codes

The spec defines standard error codes. The one you'll use most often:

CodeMeaning
-32700Parse error — invalid JSON.
-32600Invalid request — missing required fields.
-32601Method not found — unknown method or tool name.
-32602Invalid params — wrong argument types or missing required args.
-32603Internal error — unexpected server exception.

Always return structured JSON-RPC errors rather than HTTP 4xx/5xx for application-level failures. HTTP status codes are for transport-level problems (wrong method, auth failure). The JSON-RPC error field handles everything inside a valid request.

What to add next

This server is intentionally stripped down. The natural next steps:

When you're ready to see what a production HTTP MCP server looks like, read Inside the promenow-tools MCP server for real transcripts from a live deployment. To use it directly, visit the Terminal Pro builder.