Skip to main content
Tutorial7 min read

Build your first MCP server (Node.js tutorial, 50 lines)

Walk through a minimal MCP server that exposes one tool: get the current weather. Runs in Claude Desktop and Cursor. Under 50 lines of TypeScript.

You have the MCP client side figured out. Now let us build the server side. Goal: a minimal Node.js MCP server exposing one tool, consumable by any MCP host. Under 50 lines of TypeScript.

What we are building

An MCP server with a single tool get_weather that takes a city name and returns a fake forecast. Simple, but covers every concept you need to extend to real integrations.

Setup

mkdir weather-mcp && cd weather-mcp
npm init -y
npm i @modelcontextprotocol/sdk zod
npm i -D typescript tsx @types/node
npx tsc --init

Open package.json and add:

"bin": { "weather-mcp": "./dist/index.js" },
"scripts": { "build": "tsc", "dev": "tsx src/index.ts" }

The server

Create src/index.ts:

#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';

const server = new Server({ name: 'weather-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });

const GetWeatherSchema = z.object({ city: z.string().describe('City name, e.g. "Berlin"') });

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'get_weather',
    description: 'Get the current weather for a given city',
    inputSchema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
  }],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name !== 'get_weather') throw new Error('Unknown tool');
  const { city } = GetWeatherSchema.parse(request.params.arguments);
  // TODO: call a real weather API. For now, fake it.
  const temp = 15 + Math.floor(Math.random() * 20);
  return { content: [{ type: 'text', text: `${city}: ${temp}°C, partly cloudy` }] };
});

await server.connect(new StdioServerTransport());

Build and test

npm run build
chmod +x dist/index.js

Smoke-test it with the MCP inspector:

npx @modelcontextprotocol/inspector node dist/index.js

Opens a browser UI. List Tools → you should see get_weather. Call it with { "city": "Berlin" } → you get a fake forecast back.

Use it in Claude Desktop

Edit claude_desktop_config.json:

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

Restart Claude, click the hammer icon — get_weather shows up. Ask Claude “what is the weather in Tokyo?” and it calls your tool.

Making it real

Swap the fake body for an actual HTTP call, e.g. openweathermap.org. Two additions:

  • Read API key from process.env.OPENWEATHER_API_KEY. Pass it via env in the client config.
  • Call fetch(), parse JSON, return a formatted content block.

Adding more tools

Each tool is an entry in ListToolsRequestSchema plus a branch in CallToolRequestSchema. For larger servers, factor each tool into its own module with (args: z.infer<T>) => Promise<string>.

Publishing

Two options:

  • npm publish — users run npx your-package. Easiest for JS-based servers.
  • Docker image on GHCR — language-agnostic. Users run docker run. What GitHub, Playwright etc. do.

Submit it to Loadout

Once your server is on npm or GHCR, submit it to the directory. Free review, typically live within 24 hours. You pick up users from Claude Desktop, Cursor and Windsurf folks searching for exactly what you built.

Loadout

Build your AI agent loadout

Directory
Contact
© 2026 Loadout. Built on Angular 21 SSR.