Skip to main content
Glama
WYRE-AI

saas-alerts-mcp

by WYRE-AI
README.md
# saas-alerts-mcp

Model Context Protocol server for [Kaseya SaaS Alerts](https://saasalerts.com/) — a SaaS security monitoring platform for M365 and Google Workspace events.

Part of the [WYRE Technology MCP fleet](https://github.com/WYRE-AI).

## Features

Exposes the full SaaS Alerts External Partner API (v0.20.0) through 30 MCP tools organized by domain:

| Domain | Tools |
|--------|-------|
| **events** | Query/count security events by severity, customer, user, event type; advanced Elasticsearch queries; scroll pagination; recommended actions |
| **customers** | List, get, create, update, delete customers; set IP/country and account whitelists |
| **users** | MSP user info, partner users, customer users |
| **devices** | Unify mapped/unmapped/ignored devices, device organizations |
| **billing** | Billing details by date, billing date history |
| **reports** | List/get/create/delete scheduled reports |
| **partner** | Partner profile, branding settings |

Write/destructive tools require user confirmation via MCP elicitation (fail-open: proceeds if client doesn't support elicitation).

## Authentication

SaaS Alerts uses an API key sent as the `api_key` HTTP header.

Generate your API key in the SaaS Alerts UI under **Settings → API Keys**.

Copy [`.env.example`](.env.example). Leave `CONDUIT_S2S_SECRET` empty in that file and never commit a real value.

| Variable | Required | Description |
|----------|----------|-------------|
| `SAAS_ALERTS_API_KEY` | For stdio and `AUTH_MODE=env` | SaaS Alerts API key. Gateway mode does not read it. |
| `MCP_TRANSPORT` | No | `stdio` (default) or `http` |
| `AUTH_MODE` | No | `gateway` (Docker image and Compose default) or `env`. Gateway mode requires `X-SaaS-Alerts-API-Key` on each `/mcp` request and never falls back to `SAAS_ALERTS_API_KEY`. |
| `CONDUIT_S2S_SECRET` | Yes for HTTP | Service-to-service secret. The HTTP server logs an error and exits non-zero when this is empty or a known placeholder (`changeme`, `secret`, `your-s2s-secret`, and the other example values). Compose will not start when it is unset. The value is never logged. |
| `MCP_ALLOW_INSECURE_DEV` | No | Set to `1` only for local development to start HTTP without `CONDUIT_S2S_SECRET`. The process logs a warning, does not enforce `X-Gateway-S2S`, and listens on `127.0.0.1` only. A wider `MCP_HTTP_HOST` (including `0.0.0.0`) is overridden. A placeholder secret is still rejected. |
| `MCP_HTTP_HOST` | No | Bind address when a real `CONDUIT_S2S_SECRET` is set. Defaults to `127.0.0.1`. The container image sets `0.0.0.0`. Without a real secret the listener is forced to `127.0.0.1`. |
| `MCP_HTTP_PORT` | No | HTTP port. Defaults to `8080`. |

`/health` and `/healthz` stay unauthenticated and do not read credentials. The stdio transport does not use `CONDUIT_S2S_SECRET`.

### X-Gateway-S2S signing

With `MCP_TRANSPORT=http` and a real `CONDUIT_S2S_SECRET`, every `/mcp` request must include `X-Gateway-S2S`. A missing or invalid header returns 401 before any vendor credential is read.

The header value is `t=<unix seconds>,v1=<hex hmac-sha256>`:

1. Set `t` to the current unix time in seconds.
2. Sign the ASCII message `t=<unix seconds>` with HMAC-SHA256, using `CONDUIT_S2S_SECRET` as the key.
3. Hex-encode the digest (64 lowercase hex characters) and send `t=<unix seconds>,v1=<hex>`.

The timestamp must be within 300 seconds of the server clock. Example:

```js
import { createHmac } from 'node:crypto';

const t = Math.floor(Date.now() / 1000);
const v1 = createHmac('sha256', process.env.CONDUIT_S2S_SECRET).update(`t=${t}`).digest('hex');
const xGatewayS2s = `t=${t},v1=${v1}`;
```

### Local / stdio mode

```bash
export SAAS_ALERTS_API_KEY=your-api-key
node dist/index.js
```

### HTTP mode

`CONDUIT_S2S_SECRET` is required. If it is empty, the process logs an error and exits non-zero.

```bash
export CONDUIT_S2S_SECRET="$(openssl rand -hex 32)"
export SAAS_ALERTS_API_KEY=your-api-key
export MCP_TRANSPORT=http
export AUTH_MODE=env
export MCP_HTTP_PORT=8080
node dist/http.js
```

Local HTTP without a secret is development-only. It binds `127.0.0.1` even if `MCP_HTTP_HOST` is `0.0.0.0`, and logs a warning:

```bash
MCP_TRANSPORT=http MCP_ALLOW_INSECURE_DEV=1 npm run start:http
```

### WYRE Gateway mode

When running behind the WYRE MCP Gateway, set `AUTH_MODE=gateway` (the image and Compose default). The gateway signs each request with `X-Gateway-S2S` and injects the API key via `X-SaaS-Alerts-API-Key`. A request missing that credential header returns 401 and does not use `SAAS_ALERTS_API_KEY` from the environment.

```js
await fetch('http://127.0.0.1:8080/mcp', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    accept: 'application/json, text/event-stream',
    'x-gateway-s2s': xGatewayS2s,
    'x-saas-alerts-api-key': apiKey,
  },
  body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }),
});
```

## Running with Docker

The image listens on `0.0.0.0` inside the container and defaults to `AUTH_MODE=gateway`. Publish the port on loopback and pass the S2S secret. In gateway mode the SaaS Alerts API key arrives on each request; it is not read from the environment.

```bash
docker build -t saas-alerts-mcp .
docker run \
  -e CONDUIT_S2S_SECRET="$CONDUIT_S2S_SECRET" \
  -p 127.0.0.1:8080:8080 \
  saas-alerts-mcp
```

Compose refuses to start when `CONDUIT_S2S_SECRET` is unset, sets `AUTH_MODE=gateway`, publishes `127.0.0.1:8080:8080`, and does not pass `SAAS_ALERTS_API_KEY` into the gateway service:

```bash
CONDUIT_S2S_SECRET="$CONDUIT_S2S_SECRET" docker compose up
```

Single-tenant HTTP (`AUTH_MODE=env`) still requires the S2S secret, then uses `SAAS_ALERTS_API_KEY`:

```bash
docker run \
  -e CONDUIT_S2S_SECRET="$CONDUIT_S2S_SECRET" \
  -e AUTH_MODE=env \
  -e SAAS_ALERTS_API_KEY=your-key \
  -p 127.0.0.1:8080:8080 \
  ghcr.io/wyre-ai/saas-alerts-mcp:latest
```

## Development

```bash
npm install
npm run build
npm test
npm run lint
node scripts/lint-destructive-warnings.mjs src
```

## Destructive tool confirmation

Tools that permanently delete data (customer delete, scheduled report delete) carry:
- `⚠ DESTRUCTIVE — IRREVERSIBLE` description prefix
- `annotations.destructiveHint: true`
- Elicitation confirmation guard (additive — proceeds if client doesn't support elicitation)

Always confirm with the user before invoking these tools.

## License

Apache-2.0 — see [LICENSE](LICENSE).