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.
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 forecastget_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.
Running MCP in production?
Centralised auth, cost analytics, and the APC optimization layer — free tier included.