DEV Community

Profiterole
Profiterole

Posted on

Build Your First MCP Server in Under 100 Lines of JavaScript

What is MCP?

Model Context Protocol (MCP) is an open standard by Anthropic that lets AI assistants like Claude connect to external tools, data sources, and services. Think of it as a USB-C port for AI — one standard connector that works with any compatible device.

Before MCP, every AI integration was bespoke. Want Claude to query your database? Custom plugin. Want it to call your API? Another custom integration. MCP standardizes all of this with a clean protocol that any AI client can speak.

The killer insight: you write the server once, and it works with Claude Desktop, Cursor, Continue, and any other MCP-compatible client.


The Protocol in 60 Seconds

An MCP server exposes three primitives:

  • Tools — functions the AI can call (e.g. search_database, send_email)
  • Resources — data the AI can read (e.g. files, database records)
  • Prompts — reusable prompt templates

For most integrations, tools are all you need.

Communication happens over stdio (subprocess) or SSE (HTTP). The client discovers your tools by calling tools/list, then invokes them via tools/call. That's it.


Building a Real MCP Server

Let's build something genuinely useful: a server that gives Claude developer utility tools — UUID generation, hashing, base64, JWT decoding, and more. This is the exact code behind the mcp-devutils npm package.

Step 1: Setup

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk
Enter fullscreen mode Exit fullscreen mode

Set "type": "module" in package.json.

Step 2: The Server Skeleton

Every MCP server starts the same way:

#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";

const server = new Server(
  { name: "my-mcp-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

// ... register tools here ...

const transport = new StdioServerTransport();
await server.connect(transport);
Enter fullscreen mode Exit fullscreen mode

Two handlers do all the work:

  • ListToolsRequestSchema — tells clients what tools exist
  • CallToolRequestSchema — actually runs a tool

Step 3: Registering Tools

Each tool needs a name, description, and a JSON Schema for its inputs:

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "uuid",
        description: "Generate a UUID v4",
        inputSchema: {
          type: "object",
          properties: {
            count: {
              type: "number",
              description: "Number of UUIDs to generate (default: 1, max: 10)"
            }
          }
        }
      },
      {
        name: "hash",
        description: "Hash text using md5, sha1, or sha256",
        inputSchema: {
          type: "object",
          properties: {
            text: { type: "string", description: "Text to hash" },
            algorithm: {
              type: "string",
              enum: ["md5", "sha1", "sha256"],
              description: "Hash algorithm (default: sha256)"
            }
          },
          required: ["text"]
        }
      },
      {
        name: "base64",
        description: "Encode or decode base64",
        inputSchema: {
          type: "object",
          properties: {
            text: { type: "string", description: "Text to encode or decode" },
            action: {
              type: "string",
              enum: ["encode", "decode"],
              description: "Action: encode or decode (default: encode)"
            }
          },
          required: ["text"]
        }
      }
    ]
  };
});
Enter fullscreen mode Exit fullscreen mode

The descriptions matter a lot — they're what Claude uses to decide when to call your tool. Be specific.

Step 4: Implementing the Tools

import crypto from "crypto";

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

  try {
    switch (name) {
      case "uuid": {
        const count = Math.min(Math.max(1, args?.count || 1), 10);
        const uuids = Array.from({ length: count }, () => crypto.randomUUID());
        return {
          content: [{ type: "text", text: uuids.join("\n") }]
        };
      }

      case "hash": {
        const { text, algorithm = "sha256" } = args;
        const hash = crypto.createHash(algorithm).update(text, "utf8").digest("hex");
        return {
          content: [{ type: "text", text: `${algorithm}: ${hash}` }]
        };
      }

      case "base64": {
        const { text, action = "encode" } = args;
        const result = action === "encode"
          ? Buffer.from(text, "utf8").toString("base64")
          : Buffer.from(text, "base64").toString("utf8");
        return {
          content: [{ type: "text", text: result }]
        };
      }

      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  } catch (error) {
    return {
      content: [{ type: "text", text: `Error: ${error.message}` }],
      isError: true
    };
  }
});
Enter fullscreen mode Exit fullscreen mode

The return format is always { content: [{ type: "text", text: "..." }] }. For errors, set isError: true.


Connecting to Claude Desktop

Add your server to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "node",
      "args": ["/absolute/path/to/index.js"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Restart Claude Desktop. Your tools will appear in the tools panel.


Use a Pre-Built One: mcp-devutils

If you just want developer utilities without building your own, install mcp-devutils directly:

npx mcp-devutils
Enter fullscreen mode Exit fullscreen mode

It provides 9 tools out of the box:

Tool What it does
uuid Generate 1–10 UUID v4s
hash Hash text with md5/sha1/sha256
base64 Encode or decode base64
timestamp Convert Unix ↔ ISO 8601
jwt_decode Decode JWT payload (no verification)
random_string Generate random strings/passwords
url_encode URL encode/decode
json_format Pretty-print or minify JSON
regex_test Test regex patterns with match details

Claude Desktop config:

{
  "mcpServers": {
    "devutils": {
      "command": "npx",
      "args": ["mcp-devutils"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Then you can say things like "generate 5 UUIDs", "hash this string with sha256", or "decode this JWT" directly in Claude.


Key Patterns

Defensive input handling. Always validate required fields and provide sane defaults:

const count = Math.min(Math.max(1, args?.count || 1), 10); // clamp 1–10
Enter fullscreen mode Exit fullscreen mode

Error isolation. Wrap everything in try/catch and return isError: true rather than crashing the server. A crashed server means Claude can't use any of your tools.

Good descriptions beat good code. Claude decides which tool to use based on the description. Write them like you're explaining to a colleague what the function does and when to reach for it.

Zero dependencies where possible. Node's built-in crypto module handles UUID, hashing, random bytes, and base64. Fewer dependencies = faster npx startup.


What to Build Next

Once you've got the pattern down, MCP servers are a great fit for:

  • Internal tooling — wrap your company's APIs so Claude can query them
  • Dev environment bridges — expose git operations, file system helpers, or database queries
  • Workflow automation — let Claude trigger pipelines, send notifications, or update records
  • Specialized utilities — anything you find yourself copy-pasting into ChatGPT regularly

The full source for mcp-devutils is ~100 lines. MCP really is that lightweight.


If this was useful, you can buy me a coffee. Happy building!

Top comments (0)