AI Catchup

How to Build Your Own MCP Server: A Beginner's Guide

By 7 min read

To build an MCP server in TypeScript, install the v2 SDK package @modelcontextprotocol/server with Zod v4, register each tool with server.registerTool and a Zod input schema, serve it over stdio, then add it to Claude Code with claude mcp add or a .mcp.json entry. A one-tool server is about 50 lines.

To build an MCP server in TypeScript, install @modelcontextprotocol/server (the v2 SDK, now the stable line) and Zod v4, register each tool with server.registerTool(), and serve it over stdio so Claude Code, Cursor or VS Code can launch it. Whether you want to connect your assistant to an internal API, a proprietary database, or a custom workflow, the shape is the same.

It is simpler than you might expect. A one-tool server is about 50 lines of TypeScript, and you can have one running in under an hour. (If you would rather use an existing server than build one, start with our top MCP server picks.)

Key Takeaways

  • MCP servers are just TypeScript (or Python) programs that follow a specific protocol. No special infrastructure required.
  • Use the v2 TypeScript SDK. It ships as @modelcontextprotocol/server and @modelcontextprotocol/client and implements the 2026-07-28 MCP spec. The v1 package, @modelcontextprotocol/sdk, only gets bug and security fixes.
  • The SDK handles the protocol and input validation. You write one Zod schema per tool; the SDK derives the JSON Schema the model sees and rejects bad arguments before your handler runs.
  • Local testing is fast. Run claude mcp add (or edit .mcp.json) to point Claude Code at your server and start using it immediately.
  • You can expose tools (actions), resources (data), and prompts (templates) from a single server. Start with one tool.

What You Will Build

In this guide, you will build a simple MCP server that provides a weather lookup tool. When finished, you will be able to ask your AI assistant "What is the weather in San Francisco?" and it will use your server to fetch and return live weather data from the free wttr.in service, which needs no API key.

This example is intentionally simple, but the patterns you learn apply to any integration: internal APIs, databases, file systems, cloud services, or any system you can call from code.

We built and ran this exact server on October 9, 2026 with @modelcontextprotocol/server 2.3.1, zod 4.6.5 and TypeScript 7.0.2.

Prerequisites

Before you start, make sure you have:

  • Node.js 20 or later installed (node --version to check). The v2 SDK requires it.
  • An AI tool that supports MCP -- Claude Code is the easiest to configure, but Cursor and VS Code with Copilot also work
  • Basic TypeScript familiarity -- you do not need to be an expert, but you should be comfortable with types and async/await
  • A text editor -- any editor will do, though your AI coding tool can help you write the server itself

Step 1: Set Up the Project

Create a new directory and initialize a TypeScript project:

mkdir my-weather-mcp
cd my-weather-mcp
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod
npm install -D typescript @types/node

The key dependency is @modelcontextprotocol/server, which provides the server framework and handles all MCP protocol communication. Tool schemas use Standard Schema, so any compatible library works; this guide uses Zod v4, the library the SDK's own examples use. The type=module line matters because the v2 SDK ships ES modules only.

Create a tsconfig.json:

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

The "types": ["node"] entry loads the Node.js type definitions the SDK's stdio transport relies on. Without it, TypeScript 7 reports Cannot find name 'Buffer'.

Create the source directory:

mkdir src

Step 2: Define Tools

Create src/index.ts -- this is your entire server:

import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";

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

server.registerTool(
  "get_weather",
  {
    description: "Get the current weather for a city",
    inputSchema: z.object({
      city: z.string().describe("The city name to look up weather for"),
    }),
  },
  async ({ city }) => {
    // Fetch live conditions from the free wttr.in JSON API (no key needed).
    const res = await fetch(
      `https://wttr.in/${encodeURIComponent(city)}?format=j1`
    );
    if (!res.ok) {
      return {
        content: [{ type: "text", text: `Weather lookup failed: HTTP ${res.status}` }],
        isError: true,
      };
    }
    const data = await res.json();
    const current = data.current_condition[0];

    return {
      content: [
        {
          type: "text",
          text: `Weather in ${city}: ${current.temp_F}F, ${current.weatherDesc[0].value}. Humidity: ${current.humidity}%. Wind: ${current.windspeedMiles} mph.`,
        },
      ],
    };
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("weather MCP server running on stdio");
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

Here is what each piece does:

  1. McpServer creates your server instance with a name and version.
  2. server.registerTool(name, config, handler) registers a tool. The config holds the description and an inputSchema written as a Zod object; the SDK turns that schema into the JSON Schema the model sees and validates every call against it.
  3. The handler receives validated, typed inputs and returns content, a list of typed blocks. isError: true marks a failed result the model can read and react to.
  4. StdioServerTransport connects the server over standard input/output, which is how local AI tools talk to MCP servers. The SDK's own tutorial uses a shortcut from the same /stdio import instead: serveStdio(createServer), which takes a function that builds the server.

The tool description is important -- this is what the AI reads to decide when to use your tool. Make it clear and specific.

If a caller passes the wrong type, say { "city": 42 }, the SDK rejects it before your handler runs and returns an isError result reading "Input validation error: Invalid arguments for tool get_weather: city: Invalid input: expected string, received number".

Adding more tools

You can register as many tools as you need. Each server.registerTool() call adds another capability. For example, add a forecast tool alongside the current weather tool, above main():

server.registerTool(
  "get_forecast",
  {
    description: "Get a weather forecast of up to 3 days for a city",
    inputSchema: z.object({
      city: z.string().describe("The city name"),
      days: z.number().int().min(1).max(3).default(3).describe("Number of days"),
    }),
  },
  async ({ city, days }) => {
    const res = await fetch(
      `https://wttr.in/${encodeURIComponent(city)}?format=j1`
    );
    if (!res.ok) {
      return {
        content: [{ type: "text", text: `Forecast lookup failed: HTTP ${res.status}` }],
        isError: true,
      };
    }
    const data = await res.json();
    const lines = data.weather
      .slice(0, days)
      .map((d: any) => `${d.date}: ${d.mintempF}F to ${d.maxtempF}F`);
    return { content: [{ type: "text", text: lines.join("\n") }] };
  }
);

Step 3: Test Locally

First, build the project:

npx tsc

Before involving an AI tool, you can call your tools directly with the MCP Inspector, a local web app that launches your server and lets you run each tool by hand:

npx @modelcontextprotocol/inspector node dist/index.js

Now connect Claude Code. The quickest route is one command, where everything after -- is the launch command:

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

That adds the server at local scope, just for you. To share it with your team instead, commit a .mcp.json file at the root of the project where you use Claude Code:

{
  "mcpServers": {
    "weather": {
      "type": "stdio",
      "command": "node",
      "args": ["/absolute/path/to/my-weather-mcp/dist/index.js"]
    }
  }
}

A stdio entry takes command, args, and an optional env object for environment variables such as API keys. Claude Code asks you to approve a project-scoped server from .mcp.json the first time you start an interactive session. Then run /mcp inside Claude Code: weather shows as connected with get_weather listed. Try asking: "What is the weather in New York?"

The AI will recognize that this question can be answered by your get_weather tool, call it with city: "New York", and present the results in its response.

Debugging tips

If the server does not work immediately, here are the most common issues:

  • Path errors: Make sure the path in args is the absolute path to your built dist/index.js.
  • Run it by hand first: node dist/index.js should print weather MCP server running on stdio to stderr and then wait. If it exits or crashes, fix that before changing any host config.
  • Build errors: Run npx tsc and fix any TypeScript errors before testing. The server must compile cleanly.
  • Console output: Avoid using console.log() in your server -- MCP communicates over stdio, so any stray output on stdout corrupts the protocol. Use console.error() for debug logging, as stderr does not interfere.

Next steps

Once your basic server works, consider these enhancements:

  • Add resources to expose static data or configuration that the AI can read without calling a tool.
  • Add prompts to provide pre-built templates that guide the AI's use of your tools.
  • Call your tools from hooks. Claude Code's mcp_tool hook type runs a tool on a configured MCP server at a lifecycle event; our Claude Code hooks guide covers the setup.
  • Ship it in a plugin. A Claude Code plugin can bundle an MCP server alongside skills and a mod; see how to build a Claude Code mod.
  • Publish to npm so others can install and use your server with a single command.
  • Add error handling with clear error messages that help the AI understand what went wrong and suggest fixes to the user.

Every server you build makes your AI assistant a little more capable. Start with the v2 SDK, one registerTool() call and a stdio transport, then grow from there.

Corrections and Updates

  • October 9, 2026: This page taught the v1 TypeScript SDK (@modelcontextprotocol/sdk with server.tool()); the code now uses v2, the stable line since July 27, 2026 (@modelcontextprotocol/server, server.registerTool() and zod/v4), and needs Node.js 20 or later.
  • October 9, 2026: The .mcp.json example used a cwd field that Claude Code's MCP docs do not document, and a code comment said the tool returned mock data while it fetched live data from wttr.in. The example now uses an absolute path in args, and the comment describes the live fetch.

Sources

More skills and workflows

Frequently Asked Questions

What programming languages can I use to build an MCP server?

Official MCP SDKs cover TypeScript, Python, C#, Go, Rust and Ruby at Tier 1, Java at Tier 2, and Swift, PHP and Kotlin at Tier 3. The tier reflects feature completeness, protocol support and maintenance commitment. TypeScript and Python have the most tutorials and examples, which makes them the easiest starting points.

Which version of the TypeScript MCP SDK should I use?

Use v2, the stable line, published as @modelcontextprotocol/server and @modelcontextprotocol/client. It implements the 2026-07-28 MCP spec. The older v1 package, @modelcontextprotocol/sdk, gets bug fixes and security updates for at least six months after the July 27, 2026 v2 release, so plan a migration.

Do I need to deploy my MCP server to use it?

No. Most MCP servers run locally on your machine. Your AI tool starts the server process automatically when it needs to use it. Deployment is only necessary if you want to share the server across a team or run it in a CI/CD environment.

Get the weekly AI Catchup

Tools, practices, and what matters, in your inbox every week.