Skip to main content
Glama
lvfast

weather-intelligence

by lvfast

Weather Intelligence Service

CI Node.js 24 License: MIT

A production-minded NestJS backend that exposes normalized WeatherAPI.com data and deterministic, explainable activity-risk assessments through REST, MCP stdio, and stateless MCP Streamable HTTP.

Architecture

One application core serves every interface. Ports-and-adapters keep dependencies pointing inward:

REST / MCP adapters
        |
Application services (location, weather, assessment)
        |
Domain models and the pure rule engine
        |
Ports: provider, cache, quota, clock, logging
        |
WeatherAPI, Redis, memory, and system adapters
  • REST controllers and MCP tool handlers call the same application services and share the same strict Zod request/output schemas.

  • Domain models and the rule engine import no NestJS, HTTP, MCP, Redis, or WeatherAPI DTO types.

  • Contract tests (test/contract/interface-parity.spec.ts) prove that REST and MCP return canonically equal results for identical inputs, including ambiguity, stale-data, quota-exhaustion, and upstream-outage cases.

Provider limitations

WeatherAPI.com Free plan is the only provider:

  • 100,000 calls per calendar month. The default service budget is 90,000 calls, reservable downward; above 95,000 requires UNSAFE_ALLOW_HIGH_PROVIDER_BUDGET=true.

  • Forecasts cover one to three days only.

  • Alerts have limited coverage. An empty alert list means the provider returned no alert; it does not guarantee that no hazard exists. Every alert response declares coverage: "limited".

Related MCP server: Open Meteo MCP Server

Quick start (memory cache, no Redis)

Requires Node.js 24.

npm ci
$env:WEATHERAPI_KEY='your-key'
npm run start:dev:http

The HTTP runtime listens on http://127.0.0.1:3000 by default.

Docker Compose (app + Redis)

$env:WEATHERAPI_KEY='your-key'
docker compose up -d --build
curl.exe --fail http://127.0.0.1:3000/health/live
curl.exe --fail http://127.0.0.1:3000/health/ready
docker compose down

Compose binds the app to 127.0.0.1:3000 and uses Redis for the shared cache, quota counter, and (in public mode) rate-limit state.

REST API

All endpoints live under /api/v1; success responses use a { data, meta } envelope with requestId, provider, fetchedAt, cached, stale, and warnings.

# 1. Resolve a free-form place name (ambiguity is a successful 200 result here)
curl.exe "http://127.0.0.1:3000/api/v1/locations/resolve?q=Paris&limit=5"

# 2. Current weather by query, opaque id, or coordinates
curl.exe "http://127.0.0.1:3000/api/v1/weather/current?q=Paris"
curl.exe "http://127.0.0.1:3000/api/v1/weather/current?locationId=2807"
curl.exe "http://127.0.0.1:3000/api/v1/weather/current?lat=48.8566&lon=2.3522"

# 3. Forecast (days 1-3, hourly intervals only when requested)
curl.exe "http://127.0.0.1:3000/api/v1/weather/forecast?q=Paris&days=3&includeHourly=true"

# 4. Alerts (always declares limited coverage)
curl.exe "http://127.0.0.1:3000/api/v1/weather/alerts?q=Paris"

# 5. Assessment for an activity with an optional explicit window (ISO 8601 with offset)
curl.exe -X POST "http://127.0.0.1:3000/api/v1/weather/assessments" ^
  -H "Content-Type: application/json" ^
  -d "{\"location\":{\"query\":\"Paris\"},\"activity\":\"running\",\"startTime\":\"2026-09-24T06:00:00+02:00\",\"endTime\":\"2026-09-24T08:00:00+02:00\"}"

Other routes: GET /health/live, GET /health/ready, Swagger UI at /docs, generated OpenAPI JSON at /docs-json.

Error envelope

{
  "error": {
    "code": "LOCATION_AMBIGUOUS",
    "message": "Multiple locations match the query.",
    "details": { "candidates": [] },
    "requestId": "01J..."
  }
}

No error ever contains the API key, upstream query strings, raw provider bodies, stack traces, or internal causes. Ambiguity outside /locations/resolve maps to HTTP 409; location resolution is the one endpoint where discovering ambiguity is the successful purpose.

Rule explanation example

{
  "activity": "running",
  "riskScore": 60,
  "riskLevel": "high",
  "recommendation": "avoid",
  "ruleVersion": "weather-activity-rules/1.0.0",
  "triggeredRules": [
    {
      "ruleId": "running.rain.high",
      "severity": "high",
      "points": 40,
      "evidence": {
        "metric": "precipitationMm",
        "observed": 6.2,
        "operator": ">=",
        "threshold": 5,
        "unit": "mm/h"
      },
      "window": {
        "startTime": "2026-09-24T06:00:00+02:00",
        "endTime": "2026-09-24T07:00:00+02:00"
      },
      "mitigation": "Wear rain protection and check drainage on your route."
    }
  ]
}

The engine is pure, deterministic, versioned, and fully boundary-tested with 100% branch coverage. Findings aggregate per hourly slot (capped at 100), the overall score is the worst slot, official alert severities apply risk floors, and stale inputs floor results to moderate risk with dataQuality: "degraded".

MCP

Five tools are exposed over both stdio and stateless Streamable HTTP:

Tool

Purpose

resolve_location

Resolve a query into normalized candidates

get_current_weather

Normalized current conditions

get_weather_forecast

One-to-three-day forecast, optional hourly

get_weather_alerts

Alerts with limited-coverage metadata

assess_weather_conditions

Deterministic activity-risk assessment

Ambiguity is a workflow result, not a tool error: other tools return status: "location_resolution_required" with candidates. Not-found is a structured tool error (LOCATION_NOT_FOUND). Every result carries canonical structuredContent plus a short text summary.

Streamable HTTP

# Endpoint (stateless; GET/DELETE session operations answer 405 by design)
http://127.0.0.1:3000/mcp

Stdio clients

Point the client at the built entrypoint (absolute paths shown) and keep the key in client-managed environment configuration — never in arguments or committed files.

Codex (~/.codex/config.toml):

[mcp_servers.weather-intelligence]
command = "node"
args = ["C:\\path\\to\\weather-intelligence-mcp\\dist\\entrypoints\\stdio.js"]
env = { WEATHERAPI_KEY = "your-key", CACHE_BACKEND = "memory" }

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "weather-intelligence": {
      "command": "node",
      "args": ["C:\\path\\to\\weather-intelligence-mcp\\dist\\entrypoints\\stdio.js"],
      "env": { "WEATHERAPI_KEY": "your-key", "CACHE_BACKEND": "memory" }
    }
  }
}

OpenClaw / generic MCP client:

{
  "mcp": {
    "weather-intelligence": {
      "transport": "stdio",
      "command": "node",
      "args": ["C:\\path\\to\\weather-intelligence-mcp\\dist\\entrypoints\\stdio.js"],
      "env": { "WEATHERAPI_KEY": "your-key" }
    }
  }
}

Build first with npm run build. Stdio reserves stdout for MCP protocol traffic; all logs go to stderr.

Configuration

See .env.example for every key, default, and constraint. Highlights:

  • EXPOSURE_MODE=public requires Redis, a non-empty HOST_ALLOWLIST, and an explicit non-loopback HOST.

  • Default HTTP protection: 60 requests per rolling minute per IP, 64 KiB request bodies, 10-second processing deadline, Host and Origin validation on every route including /mcp.

  • Provider budget: reserve before every upstream attempt including retries; exhaustion fails fast with PROVIDER_BUDGET_EXHAUSTED.

  • Redis outage: readiness reports 503 and provider traffic is gated by an emergency limiter (five attempts per rolling minute per process by default).

Testing

npm test                 # unit + contract tests (offline, no provider key)
npm run test:coverage    # coverage thresholds: 85% statements/lines/functions, 80% branches; 100% rule branches
npm run test:e2e         # REST, MCP HTTP, and spawned stdio end-to-end tests
npm run test:all         # coverage + integration + e2e

Redis-backed integration tests need a disposable Redis:

docker run --rm -d --name wis-test-redis -p 6379:6379 redis:8.2.1-alpine
$env:TEST_REDIS_URL='redis://127.0.0.1:6379'
npm run test:integration
docker stop wis-test-redis

Opt-in live smoke test

$env:RUN_LIVE_WEATHERAPI_TESTS='true'
$env:WEATHERAPI_KEY='your-key'
npm run test:live

The live test makes one quota-guarded provider call for London, prints no secret, and is skipped by default and in CI.

Security limitations

  • HTTP authentication is not part of the MVP. An Internet deployment without authentication is suitable only as a controlled portfolio demo; keep every safeguard enabled (Redis, host allowlist, CORS allowlist, rate limits, budget).

  • The WeatherAPI base URL is fixed configuration; tool input cannot redirect upstream traffic (no SSRF through inputs).

  • API keys exist only in server or client-managed environment configuration.

Phase 2 roadmap

  • Authentication and multi-tenant keys.

  • PostgreSQL-backed users, saved locations, and assessment history.

  • Metrics (Prometheus) and tracing.

  • Additional providers behind the existing WeatherProvider port.

  • Runtime-editable rule configuration with versioned rollout.

License

MIT — see LICENSE.

Related MCP Connectors

Related MCP Servers

  • F
    license
    B
    quality
    D
    maintenance
    Enables users to get weather alerts and forecasts through natural language interactions. Provides real-time weather information and alert notifications via MCP tools.
    2
    -
  • F
    license
    A
    quality
    D
    maintenance
    A comprehensive MCP server providing tools for real-time, forecast, and historical weather data, alongside air quality, marine conditions, and climate projections. It also includes geocoding services to search for locations and retrieve precise coordinates for environmental analysis.
    7
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables to interact with comprehensive weather data through the MCP protocol, including current conditions, multi-day forecasts, hourly forecasts, and geocoding.
    7 npm
    MIT