MCPizy
MarketplaceRecipesGuidesPricingDocs
Log inGet Started
All guides
Guide · EN
13 min read
April 22, 2026

Building Your First MCP Server in Node and Python

Step-by-step tutorial for shipping your first MCP server — tool schema, error handling, testing with the MCP inspector, and deploying to Fly.io. Node and Python variants.

tutorialmcpnodepythontypescript

The fastest way to learn MCP is to build one. This guide walks through a real, useful server — a weather tool — in both Node and Python, then shows how to test and deploy it. Total time: ~30 minutes.

What we will build

An MCP server with two tools:

  • get_forecast(city, days) — returns a weather forecast
  • get_current(city) — returns the current weather

Backed by the free Open-Meteo API (no key needed, rate-limited politely). Transport: stdio for local, HTTP for deploy.

Node / TypeScript variant

Scaffold:

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

The server (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-mcp", version: "0.1.0" }); async function geocode(city: string) { const r = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`); const j = await r.json(); const hit = j.results?.[0]; if (!hit) throw new Error(`Unknown city: ${city}`); return { lat: hit.latitude, lon: hit.longitude, name: hit.name }; } server.tool( "get_forecast", { city: z.string().describe("City name, any language"), days: z.number().min(1).max(14).default(3).describe("Days of forecast, 1-14"), }, async ({ city, days }) => { const { lat, lon, name } = await geocode(city); const r = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&daily=temperature_2m_max,temperature_2m_min,precipitation_sum&forecast_days=${days}&timezone=auto`); const j = await r.json(); return { content: [{ type: "text", text: JSON.stringify({ location: name, daily: j.daily }, null, 2) }] }; } ); server.tool( "get_current", { city: z.string() }, async ({ city }) => { const { lat, lon, name } = await geocode(city); const r = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,wind_speed_10m,weather_code&timezone=auto`); const j = await r.json(); return { content: [{ type: "text", text: JSON.stringify({ location: name, current: j.current }, null, 2) }] }; } ); const transport = new StdioServerTransport(); await server.connect(transport);

Run it:

npx tsx src/index.ts

It will sit silently on stdio. That is correct — MCP servers log to stderr, never stdout (stdout is the protocol channel).

Python variant

Same logic:

pip install "mcp[cli]" httpx # server.py import httpx from mcp.server.fastmcp import FastMCP mcp = FastMCP("weather-mcp") async def geocode(city: str): async with httpx.AsyncClient() as c: r = await c.get(f"https://geocoding-api.open-meteo.com/v1/search?name={city}&count=1") j = r.json() hit = (j.get("results") or [None])[0] if not hit: raise ValueError(f"Unknown city: {city}") return hit["latitude"], hit["longitude"], hit["name"] @mcp.tool() async def get_forecast(city: str, days: int = 3) -> dict: """Weather forecast for a city, 1-14 days.""" lat, lon, name = await geocode(city) async with httpx.AsyncClient() as c: r = await c.get( "https://api.open-meteo.com/v1/forecast", params={ "latitude": lat, "longitude": lon, "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum", "forecast_days": days, "timezone": "auto", }, ) return {"location": name, "daily": r.json()["daily"]} @mcp.tool() async def get_current(city: str) -> dict: """Current weather for a city.""" lat, lon, name = await geocode(city) async with httpx.AsyncClient() as c: r = await c.get( "https://api.open-meteo.com/v1/forecast", params={ "latitude": lat, "longitude": lon, "current": "temperature_2m,wind_speed_10m,weather_code", "timezone": "auto", }, ) return {"location": name, "current": r.json()["current"]} if __name__ == "__main__": mcp.run()

Run: python server.py

Testing with the MCP inspector

The MCP Inspector is the equivalent of curl for MCP. Use it before wiring your server into an agent — it surfaces schema issues that are painful to debug through Claude's error messages.

npx @modelcontextprotocol/inspector npx tsx src/index.ts # opens http://localhost:5173

In the web UI you can list tools, call them with arbitrary args, see the raw JSON-RPC traffic, and verify your tool schema matches what an agent would see. Fix anything that looks off here; fixing it later is 10x slower.

Wiring into Claude Code

Add to .claude.json in your project root:

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

Restart Claude Code. Try "what's the weather in Aix-en-Provence this week?" — the agent should call get_forecast and answer with real data.

Deploying to Fly.io (HTTP transport)

Switch transport from stdio to streamable HTTP. The Node SDK:

import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import express from "express"; const app = express(); app.use(express.json()); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); await server.connect(transport); app.post("/mcp", (req, res) => transport.handleRequest(req, res, req.body)); app.get("/mcp", (req, res) => transport.handleRequest(req, res)); app.get("/healthz", (_req, res) => res.send("ok")); app.listen(process.env.PORT || 3000);

Dockerfile:

FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY . . RUN npx tsc EXPOSE 3000 CMD ["node", "dist/index.js"]

Deploy:

fly launch --no-deploy fly deploy

Your server is now available at https://weather-mcp.fly.dev/mcp and any MCP client can connect to it. Add OAuth before production (see the deployment guide).

Next steps

  • Add auth: OAuth 2.1 with PKCE against your existing auth provider.
  • Add observability: OpenTelemetry traces on each tool call.
  • Publish: submit to the MCPizy marketplace so other developers can install your server in one command.

Full source for both variants: github.com/mcpizy-examples/weather-mcp.

Try MCPizy → /pricing

Running MCP in production?

Centralised auth, cost analytics, and the APC optimization layer — free tier included.

Try MCPizy
All guidesPricing →
MCPizy— The MCP Platform
MarketplaceDocsPrivacyTermsCookiesMentions légalesContact

© 2026 MCPizy. All rights reserved.