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
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);
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"]
}
}
]
};
});
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
};
}
});
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"]
}
}
}
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
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"]
}
}
}
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
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)