Tutorials9 min read

How to Build Your First MCP Server for Claude (Step-by-Step, 2026)

Learn how to build a custom MCP server for Claude in TypeScript. Step-by-step tutorial covering tools, resources, prompts, and connecting to Claude Code.

How to Build Your First MCP Server for Claude (Step-by-Step)

You've seen the lists of MCP servers — filesystem access, GitHub integration, Slack, databases. They're powerful. But what if Claude needs to talk to your API, your internal tool, or your proprietary data source? That's when you stop consuming MCP servers and start building them.

This tutorial walks you through building a custom MCP server from scratch using TypeScript, connecting it to Claude Code or Claude Desktop, and exposing your first real tool. No hand-waving. Working code by the end.

What Is an MCP Server, Exactly?

The Model Context Protocol (MCP) is an open standard that defines how AI models like Claude communicate with external tools and data sources. Think of it as a USB-C port for AI: instead of every app building its own custom integration, everything speaks the same protocol.

An MCP server can expose three types of capabilities:

CapabilityWhat It DoesExample
ToolsFunctions Claude can callsearch_database(), send_email()
ResourcesFile-like data Claude can readA project's README, a user's profile
PromptsReusable prompt templates"Summarize this ticket in one sentence"

Most production MCP servers focus on tools — they're the most powerful because they let Claude take real actions.

The protocol runs over stdio (standard input/output) locally or HTTP with SSE for remote servers. For this tutorial we'll use stdio, which is the simplest setup and works perfectly with Claude Code and Claude Desktop.

Prerequisites

Before you start, make sure you have:

  • Node.js 18+ (check with node --version)
  • npm or pnpm (pnpm is faster, but either works)
  • Claude Code installed, or Claude Desktop with developer mode enabled
  • Basic TypeScript familiarity — you don't need to be an expert

If you haven't installed Claude Code yet, it's available via npm install -g @anthropic-ai/claude-code.

Step 1 — Scaffold the Project

Create a new directory and initialize it:

bashmkdir my-mcp-server
cd my-mcp-server
npm init -y

Install the MCP SDK and TypeScript tooling:

bashnpm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

Create a tsconfig.json:

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

Update your package.json scripts:

json{
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts",
    "start": "node dist/index.js"
  },
  "type": "module"
}

Step 2 — Write Your First MCP Server

Create src/index.ts. We'll build a product lookup server — a simple but realistic example that queries a local product catalog. You can swap in your own data source later.

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

// Sample in-memory product catalog (replace with your real data source)
const products = [
  { id: "P001", name: "Claude API Access", price: 29, category: "subscription" },
  { id: "P002", name: "CCA Practice Test Bank", price: 19.99, category: "certification" },
  { id: "P003", name: "AI Prompt Pack", price: 9.99, category: "digital" },
];

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

// Register a tool: search products
server.tool(
  "search_products",
  "Search the product catalog by name or category",
  {
    query: z.string().describe("Search term — product name or category"),
    max_results: z.number().optional().default(5).describe("Max results to return"),
  },
  async ({ query, max_results }) => {
    const results = products
      .filter(
        (p) =>
          p.name.toLowerCase().includes(query.toLowerCase()) ||
          p.category.toLowerCase().includes(query.toLowerCase())
      )
      .slice(0, max_results);

    if (results.length === 0) {
      return {
        content: [{ type: "text", text: `No products found for "${query}"` }],
      };
    }

    const formatted = results
      .map((p) => `• ${p.name} — ${p.price} (${p.category}) [ID: ${p.id}]`)
      .join("\n");

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

// Register a tool: get product by ID
server.tool(
  "get_product",
  "Get full details for a specific product by ID",
  {
    product_id: z.string().describe("The product ID (e.g. P001)"),
  },
  async ({ product_id }) => {
    const product = products.find((p) => p.id === product_id);

    if (!product) {
      return {
        content: [{ type: "text", text: `Product ${product_id} not found` }],
        isError: true,
      };
    }

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(product, null, 2),
        },
      ],
    };
  }
);

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

main().catch(console.error);

A few important notes:

  • Never use console.log() in an MCP server. It writes to stdout, which is reserved for the JSON-RPC protocol. Use console.error() for debugging — it goes to stderr and doesn't corrupt messages.
  • Zod schemas define your tool's input parameters. MCP uses these to generate the JSON Schema that Claude reads when deciding how to call your tool.
  • The isError: true flag tells Claude that a result represents a failure, not a success.

Step 3 — Add a Resource

Resources let Claude read structured data without calling a function. Add a product catalog resource after your tools:

typescript// Register a resource: full product catalog
server.resource(
  "product-catalog",
  "catalog://products",
  async (uri) => {
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify(products, null, 2),
        },
      ],
    };
  }
);

Now Claude can reference catalog://products directly in conversations — useful for giving it the full dataset upfront when context is cheap.

Step 4 — Build and Test Locally

Compile the TypeScript:

bashnpm run build

You can test the server manually by running it and sending a JSON-RPC handshake:

bashecho '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | node dist/index.js

You should see a JSON response with your server's capabilities. If you get an error, check for any console.log() calls — those will break the JSON parsing.

Step 5 — Connect to Claude Code

Claude Code reads MCP server configuration from .claude/mcp.json in your project directory, or from your global Claude settings.

To add your server to a specific project, create .claude/mcp.json in your working directory:

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

Or add it globally so it's available in every Claude Code session:

bashclaude mcp add product-lookup node /absolute/path/to/my-mcp-server/dist/index.js

Restart Claude Code and you should see your server listed under available tools. You can verify with:

bashclaude mcp list

Step 6 — Connect to Claude Desktop

If you're using Claude Desktop, open the configuration file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add your server under mcpServers:

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

Restart Claude Desktop. Your tools will appear with a hammer icon in the toolbar.

Step 7 — Test It With a Real Claude Conversation

In Claude Code (or Desktop), try these prompts to verify your tools work:

Search for certification products in my catalog.

Get the full details for product P002.

Read the product catalog resource and tell me the cheapest item.

If Claude calls your tools and returns the right data, you're done. If tools aren't appearing, check:

  • The path in your MCP config is an absolute path, not relative
  • You ran npm run build after the last code change
  • No console.log() calls exist in your server code
  • Common Mistakes to Avoid

    Using relative paths in MCP config. Claude spawns your server as a subprocess — it doesn't inherit your terminal's working directory. Always use absolute paths. Forgetting to rebuild after changes. TypeScript needs compilation. Add a file watcher for active development: npm install -D nodemon and use tsx --watch src/index.ts during development. Overly broad tool descriptions. Claude reads your tool name and description to decide when to use it. Vague descriptions like "do stuff" will get ignored. Be specific: "Search product catalog by name or category, returns up to 5 matches." Not handling errors gracefully. Always return an isError: true result on failure rather than throwing. Uncaught exceptions crash the MCP server process, which breaks the entire session.

    Extending This to Real Use Cases

    The pattern above scales to any data source. A few real-world adaptations:

    Connect to a database: Replace the in-memory products array with a Prisma or pg query. Your tool becomes a natural language SQL interface for Claude. Wrap a REST API: Call fetch() inside your tool handler. Claude can now interact with any API that doesn't have a native MCP server. Read the filesystem: Use Node's fs module to expose project files, log files, or config files as resources. This is exactly what the official Filesystem MCP server does. Authentication: For remote HTTP servers (not stdio), the MCP SDK supports OAuth 2.1 with a dedicated auth helper. For local stdio servers, environment variables via the env key in your MCP config work fine.

    Python Alternative

    Prefer Python? The official Python SDK (pip install mcp) offers an identical API:

    pythonfrom mcp.server.fastmcp import FastMCP
    
    mcp = FastMCP("product-lookup")
    
    @mcp.tool()
    def search_products(query: str, max_results: int = 5) -> str:
        """Search the product catalog by name or category."""
        # Your logic here
        return f"Found results for: {query}"
    
    if __name__ == "__main__":
        mcp.run()

    FastMCP infers JSON Schemas from your Python type hints and docstrings — less boilerplate than the TypeScript version for simple tools.

    Key Takeaways

    • MCP servers expose tools, resources, and prompts to Claude over a standard protocol
    • TypeScript and Python both have official SDKs — pick whichever your team knows
    • Always use absolute paths in MCP config and console.error() instead of console.log()
    • Zod schemas (TypeScript) and type hints (Python) define the parameters Claude sees when calling your tools
    • A working MCP server can connect Claude to any API, database, or internal system in under 100 lines of code

    Next Steps

    Once your first MCP server is working, the natural next step is understanding which MCP servers are already production-ready so you don't reinvent the wheel. Check out our guide to the best MCP servers for Claude Code in 2026 — it covers filesystem, GitHub, databases, and 10+ others with setup instructions.

    If you're going deeper into the Claude ecosystem and want to validate your skills formally, the Claude Certified Architect (CCA-F) exam tests your knowledge of Claude APIs, MCP, agents, and prompt engineering. Our CCA Practice Test Bank includes 200+ questions with detailed explanations — the fastest way to prep.


    Sources: Model Context Protocol official docs · MCP TypeScript SDK on GitHub · MCP server development guide

    Ready to Start Practicing?

    300+ scenario-based practice questions covering all 5 CCA domains. Detailed explanations for every answer.

    Free CCA Study Kit

    Get domain cheat sheets, anti-pattern flashcards, and weekly exam tips. No spam, unsubscribe anytime.