Building your first MCP server.

If you want Claude to interact with your APIs, databases, or internal tools, MCP is the answer. This is the practical guide we wished existed when we built our first one.

What you're building.

An MCP server is a small service that runs alongside your application. Claude connects to it using the Model Context Protocol, which tells it what tools are available, what arguments those tools take, and what they return. Claude calls your tools the same way it would use any function — with proper types, error handling, and no API keys floating in the prompt.

The result: Claude can query your database, call your APIs, read your CMS content, or trigger workflows — with you controlling exactly what it can and cannot touch.

Prerequisites.

Step 1 — Install the SDK.

npm install @modelcontextprotocol/sdk

The MCP SDK handles the transport layer, JSON-RPC protocol, and connection management. You write the tools; the SDK handles the communication.

Step 2 — Define a tool.

A tool has a name, description, input schema, and handler. Here is a minimal example that lets Claude query a user by ID:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "my-api-server", version: "1.0.0" });

server.tool(
  "get_user",
  "Retrieve a user by their ID",
  { userId: z.string().describe("The user's unique identifier") },
  async ({ userId }) => {
    const user = await db.users.findOne({ id: userId });
    if (!user) return { content: [{ type: "text", text: "User not found" }] };
    return { content: [{ type: "text", text: JSON.stringify(user) }] };
  }
);

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

Step 3 — Handle errors properly.

Claude needs to understand when something went wrong. Do not throw raw errors — return structured error responses:

server.tool("get_order", "Get an order by ID", { orderId: z.string() }, async ({ orderId }) => {
  try {
    const order = await db.orders.findOne({ id: orderId });
    if (!order) {
      return { content: [{ type: "text", text: `Order ${orderId} not found` }], isError: true };
    }
    return { content: [{ type: "text", text: JSON.stringify(order) }] };
  } catch (err) {
    return { content: [{ type: "text", text: `Database error: ${err.message}` }], isError: true };
  }
});

Step 4 — Connect Claude to your server.

In Claude Desktop or any MCP-compatible client, add your server to the configuration:

{
  "mcpServers": {
    "my-api": {
      "command": "node",
      "args": ["/path/to/your/server.js"]
    }
  }
}

Claude will now have access to all tools your server defines. It will call them when they are relevant to the task at hand.

What to do next.

A basic server is easy. A production server is where the real work starts: authentication (never put API keys in the Claude config — use environment variables and validate them server-side), rate limiting (Claude can and will hammer your tools), logging (you want to know what Claude is calling and why), and tool scope (fewer, well-named tools outperform many ambiguous ones).

"The biggest mistake we see: defining too many tools with overlapping purposes. Claude gets confused about which to call. Keep tools focused, names specific, descriptions clear."

If you are building something complex — multi-system integrations, custom auth flows, tools that need to persist state — we build these for teams at 404 Technologies. See the service page.