Skip to content
|13 min read

How to Build an MCP Server for Claude

Step-by-step MCP server tutorial. Build custom Claude tools with TypeScript, connect AI to your APIs, databases, and business systems.

MCPClaudetutorialAI tools
How to Build an MCP Server for Claude

You've been copy-pasting data into Claude. Screenshots of dashboards. Database query results. Customer records from your CRM. Every time Claude needs context about your business, you're the middleman — fetching data from one system and pasting it into another.

MCP fixes that. Instead of you being the bridge between Claude and your systems, you build a server that lets Claude query those systems directly. It asks for the data it needs, gets it, and keeps working.

This guide walks you through building your first MCP server in TypeScript. By the end, you'll have a working server that gives Claude access to a custom tool — and you'll understand the architecture well enough to build whatever you need next.

What Is MCP?

MCP stands for Model Context Protocol. It's an open protocol created by Anthropic that standardizes how AI models connect to external tools and data sources.

The easiest analogy: MCP is USB-C for AI. Before USB-C, every device had its own charger, its own cable, its own connector. MCP does the same thing for AI integrations — one standard interface instead of custom code for every tool you want to connect.

The protocol defines a simple relationship:

  • MCP Client — The AI application (Claude Desktop, Claude Code, Cursor, or any app that implements the protocol)
  • MCP Server — Your code that exposes tools, data, and capabilities to the client

The client discovers what the server offers, and the AI model decides when and how to use those capabilities based on the conversation. You don't hardcode when tools get called. Claude reads the tool descriptions, understands what they do, and invokes them when they're relevant.

Why Build an MCP Server?

Because right now, Claude is cut off from everything that makes your business run.

It can't check your CRM for a customer's order history. It can't query your database for last month's revenue. It can't look up a ticket in your support system. It can't search your internal documentation. Every piece of business context requires you to manually fetch it and paste it in.

An MCP server changes that. You define what Claude can access, and it pulls the data it needs mid-conversation. Some real examples:

  • "What's the status of the Johnson account?" — Claude queries your CRM and tells you, instead of you opening Salesforce.
  • "Show me all open support tickets over 48 hours old." — Claude hits your ticketing API and returns the results.
  • "Generate an invoice for the Smith project." — Claude calls a tool that creates the invoice in your billing system.
  • "What does our internal policy say about remote work?" — Claude searches your knowledge base and quotes the relevant section.

The key distinction: you're not building a chatbot. You're giving an existing AI model — one that already understands natural language, reasons about problems, and generates useful output — direct access to your systems. The AI is already smart. MCP makes it informed.

Architecture: How MCP Works

The architecture is straightforward. Here's the flow:

  1. You start an MCP client (Claude Desktop, Claude Code, etc.) configured to connect to your server
  2. The client launches your MCP server and asks it what capabilities it offers
  3. Your server responds with a list of tools, resources, and prompts it provides
  4. During a conversation, when Claude determines it needs external data or needs to take an action, it calls one of your server's tools
  5. Your server executes the logic — queries a database, calls an API, reads a file — and returns the result
  6. Claude incorporates the result into its response to you

MCP servers can expose three types of capabilities:

  • Tools — Functions that Claude can call. These do things: query databases, call APIs, create records, run calculations. Tools are the most common and most powerful capability.
  • Resources — Read-only data that Claude can access. Think configuration files, documentation, or reference data that doesn't change between requests.
  • Prompts — Reusable prompt templates that users can invoke. Useful for standardizing complex workflows.

For most use cases, tools are what you want. That's what we'll build.

Transport: How Client and Server Communicate

MCP supports two transport mechanisms:

  • stdio — The client launches your server as a child process and communicates over standard input/output. This is the simplest option and what Claude Desktop uses. No networking, no ports, no authentication headaches. Your server is just a local process.
  • Streamable HTTP — The server runs as an HTTP endpoint. This is better for remote deployments, shared servers, or when multiple clients need to connect to the same server instance.

For your first server, use stdio. It's simpler, and it's what Claude Desktop expects by default.

Step by Step: Build a Customer Lookup MCP Server

Let's build something useful. We'll create an MCP server that gives Claude a lookup_customer tool. When Claude needs information about a customer, it calls this tool with a search query, and the server returns matching customer records.

In a real deployment, this would hit your CRM API or database. For this tutorial, we'll use a mock data store so you can focus on the MCP mechanics without needing external services.

Prerequisites

  • Node.js 18+
  • TypeScript
  • Claude Desktop (to test the server)

1. Initialize the Project

mkdir mcp-customer-server
cd mcp-customer-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init

The two key dependencies:

  • @modelcontextprotocol/sdk — Anthropic's official SDK for building MCP servers (and clients)
  • zod — Schema validation library used to define tool input schemas

2. Configure TypeScript

Update your tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

3. Build the Server

Create src/index.ts. Here's the complete server:

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

// Mock customer database — replace with your real data source
const customers = [
  {
    id: "C001",
    name: "Sarah Johnson",
    email: "sarah@acmecorp.com",
    company: "Acme Corp",
    plan: "Enterprise",
    mrr: 2400,
    status: "active",
    lastContact: "2026-02-01",
  },
  {
    id: "C002",
    name: "Mike Chen",
    email: "mike@startupxyz.io",
    company: "Startup XYZ",
    plan: "Growth",
    mrr: 800,
    status: "active",
    lastContact: "2026-01-15",
  },
  {
    id: "C003",
    name: "Lisa Park",
    email: "lisa@bigretail.com",
    company: "Big Retail Inc",
    plan: "Enterprise",
    mrr: 5200,
    status: "churned",
    lastContact: "2025-11-30",
  },
]

// Create the MCP server
const server = new McpServer({
  name: "customer-lookup",
  version: "1.0.0",
})

// Define the lookup_customer tool
server.tool(
  "lookup_customer",
  "Search for customers by name, email, or company. Returns matching customer records with their plan, MRR, status, and last contact date.",
  {
    query: z.string().describe("Search term — matches against name, email, or company"),
    status: z
      .enum(["active", "churned", "all"])
      .optional()
      .default("all")
      .describe("Filter by customer status"),
  },
  async ({ query, status }) => {
    const normalizedQuery = query.toLowerCase()

    let results = customers.filter(
      (c) =>
        c.name.toLowerCase().includes(normalizedQuery) ||
        c.email.toLowerCase().includes(normalizedQuery) ||
        c.company.toLowerCase().includes(normalizedQuery)
    )

    if (status !== "all") {
      results = results.filter((c) => c.status === status)
    }

    if (results.length === 0) {
      return {
        content: [
          {
            type: "text" as const,
            text: `No customers found matching "${query}"`,
          },
        ],
      }
    }

    const formatted = results
      .map(
        (c) =>
          `**${c.name}** (${c.id})
  Company: ${c.company}
  Email: ${c.email}
  Plan: ${c.plan}
  MRR: $${c.mrr}
  Status: ${c.status}
  Last Contact: ${c.lastContact}`
      )
      .join("\n\n")

    return {
      content: [
        {
          type: "text" as const,
          text: `Found ${results.length} customer(s):\n\n${formatted}`,
        },
      ],
    }
  }
)

// Start the server
async function main() {
  const transport = new StdioServerTransport()
  await server.connect(transport)
  console.error("Customer Lookup MCP Server running on stdio")
}

main().catch(console.error)

Let's break down what's happening:

McpServer — The main class from the SDK. You give it a name and version, then register tools on it.

server.tool() — Registers a tool with four arguments: the tool name, a description (Claude reads this to decide when to call it), a Zod schema defining the input parameters, and an async handler function that executes when the tool is called.

The Zod schema — This defines what arguments the tool accepts. Zod handles validation automatically — if Claude sends malformed input, the SDK rejects it before your handler runs. The .describe() calls on each field are important: Claude reads these descriptions to understand what to pass.

The handler — Your business logic. It receives the validated input, does whatever work is needed (in this case, filtering an array — in production, this would be an API call or database query), and returns a result object with a content array.

StdioServerTransport — Connects the server to stdin/stdout so Claude Desktop can communicate with it.

4. Build and Test

Add a build script to your package.json:

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Build the project:

npm run build

5. Connect to Claude Desktop

Open your Claude Desktop configuration file. On macOS it's at ~/Library/Application Support/Claude/claude_desktop_config.json. On Windows, %APPDATA%\Claude\claude_desktop_config.json.

Add your server:

{
  "mcpServers": {
    "customer-lookup": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-customer-server/dist/index.js"]
    }
  }
}

Replace the path with the actual absolute path to your built JavaScript file. Restart Claude Desktop.

You should see a hammer icon in the Claude Desktop interface indicating that MCP tools are available. Now try asking Claude:

  • "Look up the customer from Acme Corp"
  • "Which customers have churned?"
  • "What's Sarah Johnson's current MRR?"

Claude will call your lookup_customer tool, get the results, and incorporate them into its response. You didn't tell Claude when to call the tool — it figured that out from the tool description and the conversation context.

Real-World Examples

The customer lookup server is a starting point. Here's what production MCP servers look like:

CRM integration — Instead of a mock array, your lookup_customer tool calls the HubSpot or Salesforce API. Add tools for create_contact, update_deal_stage, log_activity. Now Claude is a CRM assistant that can look up accounts, update pipelines, and log notes — all from a conversation.

Invoice generator — A create_invoice tool that takes a customer ID, line items, and payment terms, then calls your billing API (Stripe, QuickBooks, Xero) to generate an actual invoice. Claude handles the conversational part — "Create an invoice for Acme Corp, 40 hours of consulting at $150/hour, net-30 terms" — and your server handles the API call.

Knowledge base search — A search_docs tool that takes a query, runs it against your internal documentation (using vector search, Elasticsearch, or even a simple full-text search), and returns relevant passages. This is essentially RAG as a tool — letting the model decide when to retrieve, rather than retrieving on every query. Claude gets grounded answers from your actual docs instead of making things up.

Database query tool — A query_database tool that accepts natural language, converts it to SQL (or accepts raw SQL if you're feeling adventurous), runs it against your database, and returns results. Add guardrails: read-only access, query timeouts, row limits. Claude becomes a natural language interface to your data warehouse.

Multi-tool servers — A single MCP server can expose dozens of tools. An operations server might have check_inventory, create_purchase_order, get_shipping_status, update_warehouse_location, and generate_report — all in one server. Claude picks the right tool based on what you ask.

Tips for Building Production MCP Servers

After building these systems for real workloads, here's what we've learned:

Write tool descriptions like documentation. Claude decides when to call your tool based entirely on its description and parameter descriptions. "Looks up customers" is too vague. "Search for customers by name, email, or company name. Returns matching records with plan tier, monthly revenue, account status, and last contact date. Use this when the user asks about a specific customer or wants to find accounts matching certain criteria." — that's what Claude needs to make good decisions.

Handle errors gracefully. Your tool handler should never throw unhandled exceptions. Catch errors and return them as text content: "Error: Could not connect to CRM API. The service may be temporarily unavailable." Claude will relay this to the user and can suggest alternatives. An unhandled crash kills the server process and breaks the entire session.

Use resources for reference data, tools for actions. If you have data that doesn't change within a session — a product catalog, a configuration file, a list of team members — expose it as a resource. Tools are for dynamic operations: queries, writes, calculations. This distinction helps Claude understand what's available and how to use it.

Keep tool responses focused. Don't dump 500 rows of raw JSON into a tool response. Summarize, format, and limit. If a query returns 200 customer records, return the top 10 with a count: "Showing 10 of 200 results. Refine your search to narrow down." Claude works better with structured, concise data.

Add input validation beyond Zod. Zod validates types and shapes. You should also validate business logic. If someone asks to delete all records, your handler should check permissions before executing. If a query parameter looks like SQL injection, reject it. The MCP server is a trust boundary between Claude and your systems.

Test with Claude Desktop before deploying anywhere. The stdio transport and Claude Desktop give you the fastest feedback loop. Get the tool working locally first. Worry about remote deployment, authentication, and multi-user access later.

What's Next

MCP is still young, but the ecosystem is growing fast. There are already community-built MCP servers for GitHub, Slack, PostgreSQL, Google Drive, Notion, Linear, and dozens of other services. The official MCP repository has a growing directory.

Anthropic is actively developing the protocol. Recent additions include streamable HTTP transport for remote servers, authentication support, and expanded resource capabilities. The trajectory is clear — MCP is becoming the standard way AI models interact with the outside world.

The practical move is this: identify the three systems you copy-paste from most when working with Claude. Build an MCP server that connects to those systems. Start with read-only tools — lookups, searches, status checks. Once that's working, add write capabilities. You'll be surprised how much faster your workflows get when Claude can access your data directly instead of relying on you to be the intermediary.

If you want help building MCP integrations for your business systems — or any AI tooling that connects to your infrastructure — get in touch. We build these systems end to end and can tell you straight whether an MCP server, a chatbot, or a different approach is the right fit for your use case.

Ready to Build Your AI System?

Let's discuss your use case and build something that works. AI solutions from Alabama to the Gulf.