🤖 AI Tools
· 3 min read

How to Build an MCP Server in TypeScript — Step-by-Step Tutorial


This tutorial walks you through building a complete MCP server in TypeScript — from setup to connecting it to Claude Code and Cursor.

By the end, you’ll have a server that exposes tools, serves resources, and works with any MCP-compatible host.

Prerequisites

Step 1: Project setup

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

Update tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true
  }
}

Update package.json:

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

Step 2: Create the server

// src/index.ts
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-dev-tools",
  version: "1.0.0",
});

Step 3: Add a tool

Tools are actions the AI can execute. Let’s add a tool that checks if a website is up:

server.tool(
  "check_website",
  "Check if a website is responding",
  { url: z.string().url().describe("The URL to check") },
  async ({ url }) => {
    try {
      const start = Date.now();
      const response = await fetch(url);
      const latency = Date.now() - start;
      
      return {
        content: [{
          type: "text",
          text: `${url}: ${response.status} (${latency}ms)`
        }]
      };
    } catch (error) {
      return {
        content: [{
          type: "text",
          text: `${url}: UNREACHABLE — ${error.message}`
        }],
        isError: true
      };
    }
  }
);

Step 4: Add a resource

Resources are data the AI can read. Let’s expose the project’s package.json:

import { readFileSync } from "fs";

server.resource(
  "package_json",
  "file://package.json",
  "The project's package.json",
  async () => {
    const content = readFileSync("package.json", "utf-8");
    return {
      contents: [{
        uri: "file://package.json",
        mimeType: "application/json",
        text: content
      }]
    };
  }
);

Step 5: Add a prompt template

Prompts are reusable templates users can invoke:

server.prompt(
  "review_dependencies",
  "Review project dependencies for security and updates",
  {},
  () => ({
    messages: [{
      role: "user",
      content: "Review the package.json dependencies. Check for:\n1. Known security vulnerabilities\n2. Outdated packages\n3. Unnecessary dependencies\n4. Missing important packages\n\nProvide specific recommendations."
    }]
  })
);

Step 6: Connect the transport

// At the bottom of src/index.ts
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running on stdio");

Step 7: Build and test

npm run build

Connect to Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS):

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

Restart Claude Desktop. You should see your tools available in the tools menu.

Connect to Claude Code

claude mcp add my-dev-tools node /absolute/path/to/dist/index.js

Now you can ask Claude: “Check if aimadetools.com is up” and it will use your check_website tool.

Step 8: Add error handling

Production servers need proper error handling:

server.tool(
  "query_database",
  "Run a read-only SQL query",
  { query: z.string().describe("SQL SELECT query") },
  async ({ query }) => {
    // Validate: only allow SELECT
    if (!query.trim().toUpperCase().startsWith("SELECT")) {
      return {
        content: [{ type: "text", text: "Error: Only SELECT queries are allowed" }],
        isError: true
      };
    }
    
    try {
      const results = await db.query(query);
      return {
        content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
      };
    } catch (error) {
      return {
        content: [{ type: "text", text: `Query failed: ${error.message}` }],
        isError: true
      };
    }
  }
);

Common patterns

Environment variables for secrets

const API_KEY = process.env.MY_API_KEY;
if (!API_KEY) throw new Error("MY_API_KEY required");

Pass env vars in the host config:

{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["dist/index.js"],
      "env": { "MY_API_KEY": "sk-..." }
    }
  }
}

Multiple tools in one server

Group related tools in a single server. A “dev-tools” server might have: check_website, query_database, read_logs, run_tests.

Publishing to npm

npm publish

Users install with:

npx my-mcp-server

Next steps

Related: What is MCP? · MCP vs A2A vs ACP · What is Tool Calling?