Try any skill Free: connect your AI agent and get 3 free runs on every skill

    Tutorials
    mcp
    tutorial
    typescript

    How to Build Your First MCP Server in 2026

    Build a working MCP server in under 30 minutes. This tutorial walks through setup, tool definition, testing, and connecting to Claude Code.

    April 11, 20268 min read
    Share:

    Building an MCP server is simpler than most developers expect. If you can write a basic Node.js app, you can build an MCP server. This tutorial walks through creating a functional server from scratch, connecting it to Claude Code, and testing it.

    Quick Answer: You can build your first Model Context Protocol (MCP) server by setting up a Node.js project, defining tools with @modelcontextprotocol/sdk and zod, building the project with TypeScript, and connecting it to an AI agent like Claude Code via stdio or HTTP/SSE transport.

    What you're building

    A simple MCP server that exposes two tools to AI agents:

    1. get_weather: returns current weather for a city (using a free API)
    2. get_time: returns the current time in a given timezone

    These are intentionally simple so you can focus on the MCP patterns. Once you understand the structure, you can replace these with any tools you want.

    Prerequisites

    You need Node.js 18+ and npm installed. You also need an AI agent that supports MCP (Claude Code, Cursor, or Codex CLI).

    Step 1: Set up the project

    mkdir my-mcp-server
    cd my-mcp-server
    npm init -y
    npm install @modelcontextprotocol/sdk zod
    

    The MCP SDK handles the protocol implementation. Zod is used for input validation.

    Create a tsconfig.json:

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

    Step 2: Define your tools

    Create src/server.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-first-mcp",
      version: "1.0.0",
    });
    
    // Tool 1: Get current time in a timezone
    server.tool(
      "get_time",
      "Get the current time in a specific timezone",
      {
        timezone: z.string().describe(
          "IANA timezone name, e.g. America/New_York, Europe/London"
        ),
      },
      async ({ timezone }) => {
        try {
          const time = new Date().toLocaleString("en-US", {
            timeZone: timezone,
          });
          return {
            content: [
              {
                type: "text",
                text: `Current time in ${timezone}: ${time}`,
              },
            ],
          };
        } catch (error) {
          return {
            content: [
              {
                type: "text",
                text: `Invalid timezone: ${timezone}`,
              },
            ],
            isError: true,
          };
        }
      }
    );
    
    // Tool 2: Get weather (using wttr.in, no API key needed)
    server.tool(
      "get_weather",
      "Get current weather for a city",
      {
        city: z.string().describe("City name, e.g. London, Tokyo"),
      },
      async ({ city }) => {
        try {
          const response = await fetch(
            `https://wttr.in/${encodeURIComponent(city)}?format=j1`
          );
          const data = await response.json();
          const current = data.current_condition[0];
    
          return {
            content: [
              {
                type: "text",
                text: [
                  `Weather in ${city}:`,
                  `Temperature: ${current.temp_C}°C / ${current.temp_F}°F`,
                  `Condition: ${current.weatherDesc[0].value}`,
                  `Humidity: ${current.humidity}%`,
                  `Wind: ${current.windspeedKmph} km/h`,
                ].join("\n"),
              },
            ],
          };
        } catch (error) {
          return {
            content: [
              {
                type: "text",
                text: `Could not fetch weather for ${city}`,
              },
            ],
            isError: true,
          };
        }
      }
    );
    
    // 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);
    

    Each tool has four parts: a name, a description (which the agent reads to decide when to use it), an input schema (validated with Zod), and a handler function that executes the logic.

    Step 3: Build and test locally

    Add build and start scripts to package.json:

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

    Build it:

    npm run build
    

    Step 4: Connect to Claude Code

    Add your server to Claude Code's config. Edit ~/.claude.json:

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

    This uses stdio transport (the server runs as a local process). For remote deployment, you would use HTTP/SSE transport instead.

    Restart Claude Code and type /mcp to verify the connection. You should see your server listed with the two tools.

    Step 5: Test the tools

    Ask Claude Code something that should trigger your tools:

    "What's the weather like in Amsterdam right now?"

    Claude should recognize that the get_weather tool is relevant, call it with "Amsterdam", and include the weather data in its response.

    Try: "What time is it in Tokyo?"

    Claude should use the get_time tool.

    Step 6: Add more tools

    Adding a new tool follows the same pattern. Here's a third tool that counts words in a file:

    server.tool(
      "count_words",
      "Count the number of words in a file",
      {
        filepath: z.string().describe("Path to the file to count"),
      },
      async ({ filepath }) => {
        const fs = await import("fs/promises");
        const content = await fs.readFile(filepath, "utf-8");
        const words = content.split(/\s+/).filter(Boolean).length;
        return {
          content: [
            {
              type: "text",
              text: `${filepath} contains ${words} words`,
            },
          ],
        };
      }
    );
    

    Deploying remotely

    For production use, you'll want to deploy your MCP server as a web service instead of running it locally. This requires switching from stdio transport to HTTP/SSE transport:

    import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
    import express from "express";
    
    const app = express();
    
    app.get("/sse", async (req, res) => {
      const transport = new SSEServerTransport("/message", res);
      await server.connect(transport);
    });
    
    app.post("/message", async (req, res) => {
      // Handle messages from the transport
    });
    
    app.listen(3000);
    

    Deploy this to Railway, Render, or Fly.io, and agents can connect to it remotely using the URL instead of a local command.

    The lower-level TypeScript API

    McpServer (used above) is the ergonomic wrapper. If you need full control over the protocol, the raw Server class lets you answer tools/list and tools/call yourself:

    import { Server } from "@modelcontextprotocol/sdk/server/index.js";
    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
    
    const server = new Server(
      { name: "my-mcp-server", version: "1.0.0" },
      { capabilities: { tools: {} } }
    );
    
    server.setRequestHandler("tools/list", async () => ({
      tools: [
        {
          name: "get_weather",
          description: "Get current weather for a city",
          inputSchema: {
            type: "object",
            properties: { city: { type: "string", description: "City name" } },
            required: ["city"],
          },
        },
      ],
    }));
    
    server.setRequestHandler("tools/call", async (request) => {
      if (request.params.name === "get_weather") {
        const city = request.params.arguments?.city;
        return { content: [{ type: "text", text: `Weather in ${city}: 22°C, sunny` }] };
      }
      throw new Error(`Unknown tool: ${request.params.name}`);
    });
    
    const transport = new StdioServerTransport();
    await server.connect(transport);
    

    Either API produces the same wire protocol. Use McpServer unless you need custom request handling.

    Patterns for real servers

    The examples above return computed or hardcoded data. Real servers usually fall into one of four shapes:

    API wrapper. Call an external REST API and return formatted results. Handle authentication, rate limits, and error cases explicitly — the agent will retry if you return a vague failure.

    Database query. Connect to a database, run a query, return rows. Expose read-only access unless writes are genuinely required, and never interpolate agent input into SQL.

    File processor. Read files, transform them, return the result. Useful for format conversion, extraction, and analysis.

    Operational read. Surface logs, error trackers, or CI status so the agent can answer "is production healthy?" from real data.

    Publishing and distribution

    Once your server works, publish it as an npm package so others can run it with npx. Keep the tool descriptions precise: they are the only thing the agent uses to decide whether to call you. To make it discoverable by the agent community, list it on the Agensi marketplace, where it is security-scanned before publication and available to every MCP-compatible agent. Background reading: what is MCP and the MCP server security guide.

    What to build next

    Now that you understand the pattern, think about what tools would be most useful in your workflow. Some ideas:

    A server that queries your project's database so your agent can answer questions about your data. A server that reads your monitoring dashboard so your agent can check system health. A server that integrates with your CI/CD pipeline so your agent can trigger deployments.

    The best MCP servers solve a specific problem well. Start small, test with your agent, and iterate.

    For the other side of agent customization (teaching your agent procedures and expertise rather than connecting it to tools), see our guide on what is SKILL.md. For understanding when to use MCP vs skills, see MCP vs SKILL.md skills. For real-world examples of combining both, read how MCP and SKILL.md work together.


    Frequently Asked Questions