Skip to main content
Glama
AlexanderBuiko

Jarvis MCP Server

README.md
# jarvis-mcp-server

Standalone MCP server(s) for Jarvis. It currently hosts a **time** server (in the
`time_server/` package) that reports the current local time for a city, and is
structured to grow more tool domains over time — each added as its own package
alongside `time_server/`.

Unlike a stdio MCP server (which a client launches as a subprocess), it runs as an
**independent network process** over **Streamable HTTP**, so it has its own
lifecycle and can be reached by the MCP Inspector, a CLI client, or a Cloud Run
deployment.

## Tools

| Tool | Args | Returns |
|---|---|---|
| `get_current_time` | `city: str` | Current local time + timezone for the city |
| `get_weather_digest` | `period: str` (e.g. `24h`, `7d`), `city: str` (default Tokyo) | Aggregated weather digest for a city (JSON) |
| `get_weather_readings` | `city: str` (default Tokyo), `period: str` (≤ 7 days) | Compact per-day weather report (JSON) — input to the anomaly pipeline |
| `detect_weather_anomalies` | `weather_report: str` (a `get_weather_readings` report) | Deterministic anomaly report (JSON) |
| `send_telegram_alert` | `anomaly_report: str` (a `detect_weather_anomalies` report), `notify_when_clear: bool` (default `false`) | Sends a Telegram alert if anomalies exist; with `notify_when_clear=true` also sends an "all clear" message when none (JSON result) |
| `add_city` | `city: str` | Start collecting weather for a city; returns the tracked-city list |
| `remove_city` | `city: str` | Stop collecting a city (its history is kept); returns the tracked-city list |
| `list_cities` | — | The cities currently being collected |
| `echo` | `text: str` | The input unchanged (connectivity smoke-test) |

`get_current_time` is a two-hop pipeline: **Open-Meteo geocoding** (city → lat/lon)
→ **TimeAPI.io** (lat/lon → local time). Both APIs are free and need no key. If the
network is unavailable it falls back to the system UTC clock, so a call never
hard-fails.

### Tokyo weather digest (a scheduled agent)

The [`weather_digest/`](weather_digest) package runs a **continuous agent**: while
the server is up, a background thread collects the current weather **once an hour**
for **every tracked city** (Open-Meteo, with a mock fallback when offline) and
stores each reading in **SQLite** (`weather_measurements` table). The scheduler
starts with the server (via the ASGI lifespan) and stops cleanly on shutdown — it
runs independently of any tool call.

**Tracked cities are managed at runtime.** Tokyo is tracked by default; `add_city`
/ `remove_city` / `list_cities` change the set live (no restart). `add_city` does
one immediate collection so a new city has data right away; `remove_city` only
stops future collection — the city's stored history is preserved (re-adding
resumes with it intact). The set is persisted in a `tracked_cities` table, so it
survives restarts.

`get_weather_digest(period, city)` aggregates the stored readings over a window
(`city` defaults to Tokyo):

```json
{
  "city": "Tokyo", "period": "24h", "sample_count": 36,
  "average_temperature": 19.0, "min_temperature": 12.7, "max_temperature": 24.1,
  "most_common_condition": "overcast", "rainfall_occurrences": 7,
  "temperature_trend": "rising",
  "window_start": "...", "window_end": "..."
}
```

On a **fresh/empty database** the store auto-seeds **7 days of realistic hourly
mock readings** (multiple conditions, rainfall, diurnal temperature swing), so a
digest is demoable immediately — before the hourly scheduler has collected
anything live.

| Env var | Default | Purpose |
|---|---|---|
| `WEATHER_DB_PATH` | `weather_digest/weather.db` | SQLite file location |
| `WEATHER_CITY` | `Tokyo` | Default city (always tracked + seeded); add more via `add_city` |
| `WEATHER_COLLECT_INTERVAL_S` | `3600` | Seconds between collections (lower for demos) |

> **Cloud Run note:** with `--min-instances 0` the service scales to zero when
> idle, so the hourly collection only runs while an instance is alive. The seed
> data keeps the digest meaningful regardless; set `--min-instances 1` for
> uninterrupted hourly collection.

### Weather-anomaly pipeline (three chained tools)

A realistic workflow built from three tools the LLM chains automatically:

```
get_weather_readings → detect_weather_anomalies → send_telegram_alert
```

> *"Analyze Tokyo weather for the last week. If unusual weather conditions are
> detected, send a Telegram alert."*

1. **`get_weather_readings(city, period)`** reads the stored measurements and rolls
   them up **server-side** into a compact per-day report (mean/min/max temperature
   and rainy fraction per UTC day). The raw rows never leave the server, so the data
   the model relays to the next tool stays tiny. `period` is capped at **7 days**.

2. **`detect_weather_anomalies(weather_report)`** applies **deterministic rules** to
   that report and lists any anomalies. It accepts **only** a `get_weather_readings`
   report — a raw-readings array, a missing marker, too many day-buckets, or an
   oversized payload are rejected **in code** (not just by the prompt), so a large
   array can never be relayed in. Rules (thresholds overridable via `ANOMALY_*` env
   vars):

   | Anomaly | Trigger | Default |
   |---|---|---|
   | `rapid_temperature_rise` / `_drop` | max/min day-over-day mean Δ | ±6 °C |
   | `high_temperature_variability` | window max − min | 18 °C |
   | `high_rainfall_frequency` | mean daily rainy fraction | 40 % |
   | `unusually_dry` | mean daily rainy fraction (≥ 3 days) | 5 % |
   | `prolonged_bad_weather` | consecutive mostly-rainy days | 3 days |
   | `warming_trend` / `cooling_trend` | first-half vs second-half mean Δ | ±4 °C |

3. **`send_telegram_alert(anomaly_report, notify_when_clear=false)`** sends a
   Telegram message **only if anomalies were found** (otherwise it skips). Pass
   `notify_when_clear=true` to also send a reassuring "all clear" message when no
   anomalies were detected — handy for a scheduled all-is-well check-in. If Telegram
   is not configured it reports that without failing. Uses the Telegram Bot API over
   stdlib `urllib` (no new dependency); the bot token is never logged or returned.

| Env var | Default | Purpose |
|---|---|---|
| `TELEGRAM_BOT_TOKEN` | _(unset)_ | Bot token from [@BotFather](https://t.me/BotFather). Unset → sends are skipped with `not configured`. |
| `TELEGRAM_CHAT_ID` | _(unset)_ | Chat/channel id to deliver alerts to. |
| `ANOMALY_*` | _(rule defaults)_ | Override any threshold above, e.g. `ANOMALY_RAPID_TEMP_DELTA_C=8`. |

To get a `TELEGRAM_CHAT_ID`: message your bot, then open
`https://api.telegram.org/bot<token>/getUpdates` and read `result[].message.chat.id`.

## Run locally

```bash
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python -m time_server.server          # serves http://0.0.0.0:8080/mcp
```

### Configuration (env vars)

| Var | Default | Purpose |
|---|---|---|
| `HOST` | `0.0.0.0` | Bind address |
| `PORT` | `8080` | Bind port (Cloud Run injects this) |
| `TRANSPORT` | `streamable-http` | `streamable-http` \| `sse` \| `stdio` |
| `MCP_API_KEY` | _(unset)_ | If set, every request (except `/healthz`) must send a matching `X-API-Key`. Unset → **open** server (local/Inspector only). |
| `LOG_LEVEL` | `INFO` | Server log verbosity |

### Security

- **Auth:** set `MCP_API_KEY`; clients must send it as the `X-API-Key` header.
  Comparison is constant-time and the key is never logged. Leaving it unset logs a
  loud warning and runs the server open — fine for local Inspector testing, never
  for a public deployment.
- **Health:** `GET /healthz` is unauthenticated (for Cloud Run / curl probes).

## Test with the MCP Inspector

In a second terminal (server still running):

```bash
npx @modelcontextprotocol/inspector
```

In the Inspector UI:
1. **Transport Type:** `Streamable HTTP`
2. **URL:** `http://localhost:8080/mcp`
3. **Connect** → open the **Tools** tab → run `get_current_time` with `city = London`.

## Automated checks

```bash
python tests/smoke_http.py      # end-to-end over Streamable HTTP (server must be up)
pytest -q                       # offline unit tests (no network, no server)
```

## Deployment

See [DEPLOY.md](DEPLOY.md) for the Cloud Run build/deploy commands, secret setup,
and how scaling / cold starts work.

## Roadmap

- [x] Standalone Streamable HTTP server + tools (`get_current_time`, `get_weather_digest`)
- [x] Scheduled weather-digest agent (hourly collection → SQLite → aggregation)
- [x] Runtime-managed multi-city collection (`add_city` / `remove_city` / `list_cities`)
- [x] Weather-anomaly pipeline (`get_weather_readings` → `detect_weather_anomalies` → `send_telegram_alert`)
- [x] Local test via MCP Inspector
- [x] JarvisCLI client wired (auth-aware, degrades cleanly when down/unauthorized)
- [x] API-key auth middleware (pure-ASGI, constant-time, `/healthz` exempt)
- [x] Dockerfile (slim, non-root, port 8080)
- [x] Cloud Run deploy guide