← Back to blog MCP

Building a Custom MCP Server: Practical Guide

Jul 2, 20268 min

MCP (Model Context Protocol) is Anthropic’s protocol that gives Claude custom tools: API calls, database queries, file manipulation. Here’s how to build your own MCP server.

What an MCP Server Does

An MCP server exposes tools (functions Claude can call) and optionally resources (readable data) and prompts (pre-defined templates).

Claude decides when to call a tool, with what parameters, and how to use the result — like the API tool use, but via a standardized protocol.

Minimal Setup

npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript tsx @types/node

Basic MCP Server

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-mcp-server",
  version: "1.0.0",
});

// Define a tool
server.tool(
  "search_customer",
  "Search a customer in the database by name or email",
  {
    query: z.string().describe("Customer name or email"),
    limit: z.number().optional().default(5).describe("Max results"),
  },
  async ({ query, limit }) => {
    // Your logic here — database, internal API, etc.
    const results = await searchDatabase(query, limit);
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(results, null, 2),
        },
      ],
    };
  }
);

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

Adding a Resource

Resources are data Claude can read on demand — not callable functions.

server.resource(
  "config",
  "config://app/settings",
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({ env: "production", version: "2.1.0" }),
      },
    ],
  })
);

Error Handling

An MCP tool must always return something readable — not throw a bare exception.

server.tool("external_api_call", "...", { url: z.string() }, async ({ url }) => {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      return {
        content: [{ type: "text", text: `HTTP error ${response.status}: ${response.statusText}` }],
        isError: true,
      };
    }
    const data = await response.json();
    return { content: [{ type: "text", text: JSON.stringify(data) }] };
  } catch (err) {
    return {
      content: [{ type: "text", text: `Network error: ${err instanceof Error ? err.message : String(err)}` }],
      isError: true,
    };
  }
});

Configuration in Claude Code

Add your server to ~/.claude/settings.json (or the project’s settings.json):

{
  "mcpServers": {
    "my-server": {
      "command": "npx",
      "args": ["tsx", "/path/to/server.ts"],
      "env": {
        "DATABASE_URL": "postgresql://..."
      }
    }
  }
}

Real-World Use Cases

Internal MCP server I use: access to a PostgreSQL database + calls to internal business APIs. Claude can query data directly in natural language without me coding a new endpoint every time.

Recommended pattern: keep your MCP server simple and domain-specific. One server per business domain (CRM, billing, support) rather than one omniscient server — easier to maintain and secure.

SC

Stéphanie Caumont

AI Product Owner · Learn more