Skip to main content
← Back to Articles
TutorialTypeScriptMCP Server

How to Build Your First MCP Server: Step-by-Step TypeScript Tutorial (2026)

Build a working MCP server from scratch in TypeScript: project setup, tool and resource definitions, testing with the MCP Inspector, and connecting it to Claude Desktop or Cursor.

By Web MCP GuideJuly 25, 20268 min read


How do you build an MCP server? Install the @modelcontextprotocol/sdk, create a server instance, define one or more tools with a name, description, and input schema, connect it over stdio transport, and point an MCP host (Claude Desktop, Cursor) at the compiled script. A minimal working server is under 30 lines of TypeScript — this tutorial builds one step by step, then adds resources and prompts.

If you're new to MCP as a concept rather than as something you're about to build, start with our introduction to the Model Context Protocol first — this page assumes you already know what tools, resources, and a host/server relationship mean.

Before You Build One, Check Someone Hasn't Already

This is worth five minutes before you write any code. The MCP ecosystem already has servers for most common integrations — GitHub, Postgres, Slack, filesystem access, and dozens more (see Top 10 MCP Servers). Building your own makes sense when you're integrating something proprietary or internal, when you want to learn the protocol hands-on, or when an existing server doesn't expose the specific operation you need. It doesn't make sense as the first thing you reach for if a maintained server for the same API already exists — you'll spend more time on config validation, error handling, and edge cases than the actual "wrap an API call" part.

Prerequisites

Before we start, make sure you have:

  • Node.js 18 or later installed

  • Basic TypeScript knowledge

  • A code editor (VS Code recommended)

  • npm or yarn package manager
  • Step 1: Project Setup

    First, create a new directory and initialize your project:

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

    Create a tsconfig.json:

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

    Step 2: Create the Server

    Create src/index.ts:

    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
    import { z } from "zod";
    
    // Create the MCP server
    const server = new McpServer({
      name: "my-first-server",
      version: "1.0.0",
    });
    
    // Define a simple tool
    server.tool(
      "greet",
      "Greet someone by name",
      {
        name: z.string().describe("The name to greet"),
      },
      async ({ name }) => {
        return {
          content: [
            {
              type: "text",
              text: `Hello, ${name}! Welcome to MCP.`,
            },
          ],
        };
      }
    );
    
    // Define a calculation tool
    server.tool(
      "calculate",
      "Perform basic math operations",
      {
        operation: z.enum(["add", "subtract", "multiply", "divide"]),
        a: z.number().describe("First number"),
        b: z.number().describe("Second number"),
      },
      async ({ operation, a, b }) => {
        let result: number;
        switch (operation) {
          case "add":
            result = a + b;
            break;
          case "subtract":
            result = a - b;
            break;
          case "multiply":
            result = a * b;
            break;
          case "divide":
            result = b !== 0 ? a / b : NaN;
            break;
        }
        return {
          content: [
            {
              type: "text",
              text: `${a} ${operation} ${b} = ${result}`,
            },
          ],
        };
      }
    );
    
    // Start the server
    async function main() {
      const transport = new StdioServerTransport();
      await server.connect(transport);
      console.error("MCP Server running on stdio");
    }
    
    main().catch(console.error);
    

    Step 3: Understanding the Code

    Let's break down what we just wrote:

    Server Initialization:

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

    This creates a new MCP server with a name and version that clients can identify.

    Tool Definition:

    server.tool(
      "greet",           // Tool name
      "Greet someone",   // Description
      { name: z.string() }, // Input schema using Zod
      async ({ name }) => { /* handler */ }
    );
    

    Tools are functions that AI can call. We define the name, description, input schema, and handler function.

    Transport:

    const transport = new StdioServerTransport();
    

    STDIO transport means communication happens through standard input/output — perfect for local servers.

    Step 4: Test Your Server

    Add a script to package.json:

    {
      "scripts": {
        "start": "tsx src/index.ts"
      }
    }
    

    You can test the server using the MCP Inspector:

    npx @modelcontextprotocol/inspector tsx src/index.ts
    

    This opens a web interface where you can interact with your server and test your tools.

    Step 5: Connect to Claude Desktop

    To use your server with Claude Desktop, add it to your configuration. On macOS, edit ~/Library/Application Support/Claude/claude_desktop_config.json:

    {
      "mcpServers": {
        "my-first-server": {
          "command": "npx",
          "args": ["tsx", "/path/to/my-mcp-server/src/index.ts"]
        }
      }
    }
    

    Restart Claude Desktop, and your tools will be available!

    Adding Resources

    MCP servers can also expose resources — read-only data that provides context:

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

    Adding Prompts

    Prompts are reusable templates for common interactions:

    server.prompt(
      "code-review",
      "Template for code review requests",
      { language: z.string() },
      ({ language }) => ({
        messages: [
          {
            role: "user",
            content: {
              type: "text",
              text: `Please review the following ${language} code...`,
            },
          },
        ],
      })
    );
    

    Best Practices

    1. Validate all inputs: Use Zod schemas to ensure type safety
    2. Write clear descriptions: AI uses these to understand when to call your tools
    3. Handle errors gracefully: Return meaningful error messages
    4. Keep tools focused: Each tool should do one thing well
    5. Log for debugging: Use console.error (not console.log) for debug output

    Frequently Asked Questions

    Q: Do I have to use TypeScript, or can I write an MCP server in another language?
    A: TypeScript and Python have the most mature official SDKs, and this tutorial uses TypeScript because that's what most MCP hosts' own examples target. If you're more comfortable in Python, the concepts (tools, resources, prompts, stdio transport) carry over directly — see Getting Started with MCP in Python.

    Q: What's the difference between a tool and a resource, and how do I know which to build?
    A: A tool is an action the AI can invoke (run a calculation, send a request, write data). A resource is data the AI can read passively, more like a file the model can pull into context. If your integration mostly answers "what is X," lean resource; if it mostly does "go do Y," lean tool. Most real servers expose both — see MCP Tools vs Resources vs Prompts for the full breakdown.

    Q: How do I test my server before connecting it to Claude Desktop or Cursor?
    A: Use the official MCP Inspector (npx @modelcontextprotocol/inspector tsx src/index.ts, shown in Step 4). It gives you a web UI to call your tools directly and see raw responses — much faster than restarting Claude Desktop after every code change to check if something works.

    Q: My server works with the Inspector but doesn't show up in Claude Desktop — why?
    A: Almost always a path or JSON syntax issue in claude_desktop_config.json. Use an absolute path to your script (not a relative one), confirm the JSON is valid (a trailing comma will silently break the whole file), and fully quit and reopen Claude Desktop — it only reads that config on startup.

    Q: Should I use stdio or SSE/HTTP transport for my own server?
    A: Stdio (what this tutorial builds) is the right default for a server that runs locally alongside the AI host. Switch to SSE/HTTP transport only once you need the server to run remotely or be shared across multiple users — see Local vs. Remote MCP Servers for that trade-off in more depth.

    Next Steps


  • MCP Tools vs Resources vs Prompts — Understand the core primitives in depth

  • MCP Security Best Practices — Keep your integrations secure

  • Local vs. Remote MCP Servers — stdio vs. SSE/HTTP transport trade-offs

  • Top 10 MCP Servers — See how others have built their servers

  • MCP Architecture Deep Dive — Understand the protocol internals
  • Prefer Python? Check out Getting Started with MCP in Python.

    ---


    Related guides