MCP (Model Context Protocol) lets AI models call into your code. You define tools, resources, and prompts on a server. The model discovers them, decides when to use them, and calls them with structured input. This guide walks you through building a working MCP server in Node.js, registering a tool, and wiring it into Claude Desktop.

By the end you will have a running server that Claude can call directly from your local machine.

Prerequisites

  • Node.js 18 or later
  • npm
  • A code editor
  • Claude Desktop installed (for testing)

Verify your Node version:

node --version

If you are below v18, upgrade before continuing.

1. Project Setup

Create a new directory and initialize it:

mkdir mcp-weather-server && cd mcp-weather-server
npm init -y

Install the MCP SDK and Zod (used for input schema validation):

npm install @modelcontextprotocol/sdk zod

Install TypeScript as a dev dependency:

npm install -D typescript @types/node

2. Configure TypeScript and package.json

Open package.json and add the "type" field so Node treats .js files as ES modules:

{
  "type": "module"
}

Also add a build script while you are in there:

{
  "scripts": {
    "build": "tsc"
  }
}

Create a tsconfig.json at the project root:

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

The module and moduleResolution settings must both be "Node16". This is what the SDK expects.

3. Build the Server

Create the source directory and main file:

mkdir src

Write the following into 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: "weather-server",
  version: "1.0.0",
});

// Register a tool
server.tool(
  "get-weather",
  "Get current weather for a given city. Returns mock data.",
  {
    city: z.string().describe("City name, e.g. 'San Francisco'"),
  },
  async ({ city }) => {
    // Mock weather data. Replace this with a real API call.
    const conditions = ["sunny", "cloudy", "rainy", "windy", "snowy"];
    const condition = conditions[Math.floor(Math.random() * conditions.length)];
    const tempC = Math.floor(Math.random() * 35) + 5;
    const tempF = Math.round(tempC * 9 / 5 + 32);

    return {
      content: [
        {
          type: "text" as const,
          text: `Weather in ${city}: ${condition}, ${tempC}C (${tempF}F)`,
        },
      ],
    };
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error("Server failed to start:", error);
  process.exit(1);
});

A few things to note here. The server.tool() method takes four arguments: the tool name, a description, an input schema object built with Zod, and an async handler function. The handler receives the validated input and must return an object with a content array. Each content item has a type (usually "text") and the corresponding data.

We are using StdioServerTransport, which communicates over standard input/output. This is the simplest transport and the right choice for local development with Claude Desktop.

4. Build and Verify

Compile the TypeScript:

npm run build

This outputs dist/index.js. Verify it exists:

ls dist/index.js

You can do a quick smoke test by running the server directly. It will start and wait for input on stdin:

node dist/index.js

The process will hang waiting for MCP protocol messages. That is expected. Kill it with Ctrl+C.

5. Wire It into Claude Desktop

Claude Desktop discovers MCP servers through a config file.

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

Windows: %APPDATA%\Claude\claude_desktop_config.json

Open (or create) that file and add your server. Replace /absolute/path/to/mcp-weather-server with your actual project path:

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

The path must be absolute. Relative paths will not resolve correctly.

If you already have other MCP servers configured, add "weather-server" as a new key inside the existing "mcpServers" object.

6. Test It

Restart Claude Desktop. Open a new conversation and type something like:

What is the weather in Tokyo?

Claude will discover your get-weather tool, call it, and return the mock weather data. You should see Claude indicate it is using the “weather-server” tool before responding.

If the tool does not appear, check:

  • The path in claude_desktop_config.json is absolute and correct
  • You ran npm run build after your last code change
  • Node.js is available in your system PATH
  • You restarted Claude Desktop after editing the config

What to Build Next

This server has a single tool returning mock data. Here is where to go from here.

Add real API calls. Swap the mock weather logic for a call to OpenWeatherMap, WeatherAPI, or any HTTP service. The handler is a normal async function. Use fetch or any HTTP client.

Expose resources. MCP servers can also expose resources (read-only data the model can pull in as context). Use server.resource() to register them. Good candidates: file contents, database records, API documentation.

Define prompts. Use server.prompt() to register reusable prompt templates that the model can invoke with parameters.

Switch to Streamable HTTP transport. Stdio works for local use. For remote deployment, switch to Streamable HTTP transport so clients can connect over the network. The SDK provides StreamableHTTPServerTransport for this.

Add multiple tools. A single server can register many tools. Group related functionality into one server rather than spinning up separate servers for each tool.

FAQ

Do I need TypeScript, or can I use plain JavaScript?

You can use plain JavaScript. Skip the TypeScript setup, write your code as .js files with ES module syntax, and run directly with node. The SDK works with both. TypeScript gives you better type checking on the handler inputs and return types, which helps catch mistakes early.

Can I use MCP servers with models other than Claude?

Yes. MCP is an open protocol. Any client that implements the MCP specification can connect to your server. Claude Desktop is the most common client today, but other tools and editors are adding support. The server side is client-agnostic.

How do I debug when the tool is not showing up in Claude Desktop?

Check Claude Desktop’s MCP logs. On macOS, look in ~/Library/Logs/Claude/ for files named mcp*.log. These show connection attempts, errors from your server process, and protocol messages. Most issues come from incorrect paths in the config or missing dependencies. Running node dist/index.js manually in your terminal can also surface startup errors that would otherwise be hidden behind the stdio transport.