Skip to main content
Glama
README.md
# Mist MCP Server

An [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) server that exposes the
[Juniper Mist](https://www.mist.com/) REST API (`api.mist.com/api/v1`, Bearer token auth) as a
curated set of MCP tools for AI agents.

## Overview

This project bridges AI assistants (such as Claude, Copilot, or any MCP-compatible agent) with
Juniper Mist's REST API. At startup, the server:

1. Authenticates with a permanent Bearer API token
2. Registers a static, hand-written set of 22 curated MCP tools
3. Serves the tools over the `streamable-http` MCP transport

The result: an AI agent can query organizations, sites, devices, WLANs, RF templates, networks,
alarms, events, maps, and insights, execute Marvis AI queries, and perform targeted write
operations, all through natural-language conversations — without any hand-written API glue.

> **Why curated tools?** The Mist REST API is a large OpenAPI surface. The curated tool set
> provides safe, conveniently-shaped wrappers for the most common operations. Unlike other MCP
> servers in this family, there are **no escape hatches** — the 22 curated tools are the complete
> surface. There is also **no write gating** — the Mist API token's RBAC permissions are the sole
> access control.

## Prerequisites

Before using this server, you must configure Juniper Mist for API access:

1. **Create an API token**
   - Navigate to **Organization > Administrators > API Tokens** in the Mist Console.
   - Create a new token and copy the generated value — this is your `MIST_API_TOKEN`.

2. **Find your Organization ID**
   - Navigate to **Organization > Settings** in the Mist Console.
   - The organization ID appears in the URL or on the settings page — this is your `MIST_ORG_ID`.

> **Important:** The token's effective permissions are entirely determined by the Mist RBAC
> profile assigned to the token. The MCP server does not perform any additional access control.
> Write tools are exposed by the server but their actual reach is enforced by Mist via the
> token's RBAC permissions. Mist validates all write operations and rejects unauthorized ones.

## How It Works

```
┌──────────────┐     streamable-http      ┌──────────────────┐
│  AI Agent    │ ◄──────────────────────► │  Mist MCP        │
│  (Claude,    │     tools/list           │  Server           │
│   Copilot…)  │     tools/call           │  (FastMCP)        │
└──────────────┘                          └────────┬─────────┘
                                                   │
                                        Authorization: Bearer <token>
                                                   │
                                                   ▼
                                        ┌──────────────────┐
                                        │  Juniper Mist    │
                                        │  REST API        │
                                        │  api.mist.com    │
                                        │  /api/v1         │
                                        └──────────────────┘
```

1. **Bearer token auth** — The server sends a permanent API token as
   `Authorization: Bearer <token>` on every request to `api.mist.com/api/v1`. The token is
   registered for log redaction so it never appears in logs or error messages.

2. **Curated tool set** — The server registers a fixed, hand-written set of 22 tools defined in
   `src/mist_mcp/curated_tools.py`. The set is:

   - **Read tools (15):** `mist_orgs`, `mist_sites`, `mist_site`, `mist_devices`, `mist_device`,
     `mist_device_stats`, `mist_inventory`, `mist_wlans`, `mist_rf_templates`, `mist_networks`,
     `mist_alarms`, `mist_events`, `mist_maps`, `mist_insights`, `mist_marvis_query`
   - **Write tools (7):** `mist_marvis_suggest`, `mist_marvis_action`, `mist_create_wlan`,
     `mist_update_wlan`, `mist_delete_wlan`, `mist_claim_devices`, `mist_restart_device`

   All 22 tools are always registered and always available. There are **no escape hatches** —
   no raw request tool, no introspection tool, no generic "pull anything" tool. Access control
   is enforced entirely by the Mist API token's RBAC permissions.

3. **Retry & error handling** — HTTP 429 and 5xx responses are retried with exponential backoff
   (1s, 2s, 4s). Timeouts are retried once. The API token is redacted from all log output and
   error messages.

## Tool Catalog

### Read tools (15)

| Tool | Description |
|------|-------------|
| `mist_orgs` | Return all organizations accessible with the configured API token |
| `mist_sites` | Return all sites in an organization |
| `mist_site` | Return a single site's full details by its site ID |
| `mist_devices` | Return all devices (APs, switches, gateways) at a site |
| `mist_device` | Return a single device's full details by its device ID |
| `mist_device_stats` | Return real-time device statistics and health for a site |
| `mist_inventory` | Return the device inventory for an organization |
| `mist_wlans` | Return all WLAN configurations at a site |
| `mist_rf_templates` | Return all RF templates in an organization |
| `mist_networks` | Return all network definitions at a site |
| `mist_alarms` | Return current alarms for an organization |
| `mist_events` | Return events for a site, optionally filtered by event type |
| `mist_maps` | Return all maps (floor plans) at a site |
| `mist_insights` | Return wireless or client insights for an organization |
| `mist_marvis_query` | Ask a natural language question to Marvis, the Mist AI engine |

### Write tools (7)

| Tool | Description |
|------|-------------|
| `mist_marvis_suggest` | Return suggested questions that Marvis can answer |
| `mist_marvis_action` | Ask Marvis for an actionable remediation recommendation |
| `mist_create_wlan` | Create a new WLAN at a site with the given SSID and JSON configuration |
| `mist_update_wlan` | Update an existing WLAN configuration at a site (partial update) |
| `mist_delete_wlan` | Delete a WLAN from a site by its WLAN ID |
| `mist_claim_devices` | Claim and assign devices to an organization's inventory using a claim code or MAC addresses |
| `mist_restart_device` | Restart (reboot) a device by its device ID |

> **Note:** There are no escape hatches (no `mist_get`, no `mist_raw_request`). The 22 curated
> tools are the complete surface. Write tools have no server-side gate — Mist RBAC is the sole
> access control. Mist validates all writes and rejects unauthorized operations.

## Environment Variables

| Variable | Required | Default | Description |
|---|---|---|---|
| `MIST_API_TOKEN` | **Yes** | — | Mist API token from Organization > Administrators > API Tokens |
| `MIST_ORG_ID` | **Yes** | — | Default organization ID from Organization > Settings |
| `MIST_HTTP_TIMEOUT` | No | `30` | HTTP request timeout in seconds for outbound calls to `api.mist.com` |
| `MIST_INSECURE_SKIP_VERIFY` | No | *(unset)* | Set to `true` to skip TLS verification (development only; Mist cloud API always uses valid TLS) |
| `MIST_HOST` | No | `0.0.0.0` (container) / `127.0.0.1` (direct run) | Server bind address. The container image sets `0.0.0.0` so rootless Podman's `pasta` network can forward into the container on WSL2/macOS; the host-side port publish stays loopback-only. Direct host runs (`python -m mist_mcp`) default to `127.0.0.1`; set `0.0.0.0` only when running inside a container. |
| `MIST_PORT` | No | `8935` | Server bind port |
| `MIST_LOG_LEVEL` | No | `INFO` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR` |

See [`env.example`](./env.example) for a documented template.

## Build

Build the container image with Podman:

```bash
podman build --format docker -t ghcr.io/adam5isalive/mist-mcp-server:latest -f Containerfile .
```

> **Note:** Build with `--format docker`. The default OCI image format does not preserve the
> `HEALTHCHECK` directive defined in the Containerfile; `docker` format keeps it for standalone
> `podman run`. The quadlet unit supplies its own `HealthCmd`, so this only matters outside
> quadlet.

## Deploy with Podman Quadlet

[Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) lets you run
containers as systemd user services. This is the recommended deployment method for production
use.

### Step-by-step

1. **Create the systemd user directory** (if it does not already exist):

   ```bash
   mkdir -p ~/.config/containers/systemd/
   ```

2. **Copy the quadlet file** into that directory:

   ```bash
   cp mist-mcp.container ~/.config/containers/systemd/
   ```

3. **Create the environment file** with your credentials:

   ```bash
   mkdir -p ~/.config/mist-mcp
   ```

   Edit `~/.config/mist-mcp/env` with the required variables (see [`env.example`](./env.example)):

   ```bash
   MIST_API_TOKEN=your-api-token
   MIST_ORG_ID=org_abc123
   ```

4. **Reload systemd** and start the service:

   ```bash
   systemctl --user daemon-reload
   systemctl --user enable --now mist-mcp
   ```

5. **Check status**:

   ```bash
   systemctl --user status mist-mcp
   ```

6. **View logs**:

   ```bash
   journalctl --user -u mist-mcp
   ```

### Example quadlet file (`mist-mcp.container`)

```ini
[Unit]
Description=Mist MCP Server
Documentation=https://github.com/adam5isalive/mist-mcp-server

[Container]
Image=localhost/mist-mcp:latest
EnvironmentFile=%h/.config/mist-mcp/env
PublishPort=127.0.0.1:8935:8935
Restart=always
HealthCmd=/usr/bin/curl -fsS -o /dev/null -X POST http://127.0.0.1:8935/mcp -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"healthcheck","version":"1"}}}'
HealthInterval=30s
HealthTimeout=10s
HealthStartPeriod=30s
HealthRetries=3
AutoUpdate=registry
Label=io.containers.autoupdate=registry

[Service]
Restart=on-failure
TimeoutStartSec=60

[Install]
WantedBy=default.target
```

> **Host binding / pasta:** The container binds `0.0.0.0` (baked into the image via
> `ENV MIST_HOST=0.0.0.0`), which is required so rootless Podman's `pasta` network can forward
> into the container on WSL2/macOS. The security boundary is the host-side port publish, pinned to
> loopback with `PublishPort=127.0.0.1:8935:8935`. The server is reachable only from the host at
> `http://127.0.0.1:8935/mcp`. Never publish the port on `0.0.0.0` host-side.

## Verify

List the available tools using any MCP client, or test directly with `curl`. The server is
reachable at `http://127.0.0.1:8935/mcp` from the host. The MCP `streamable-http` transport
requires a full handshake (`initialize` → capture `Mcp-Session-Id` → `notifications/initialized`
→ `tools/list`) and the proper `Accept` header.

For a manual check, first establish a session and capture the session ID:

```bash
curl -i -X POST http://127.0.0.1:8935/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
```

Then, using the `Mcp-Session-Id` header from the response, list the tools:

```bash
curl -X POST http://127.0.0.1:8935/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: <session-id>" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
```

A successful response lists all 22 tools, for example:

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "mist_orgs",
        "description": "Return all organizations accessible with the configured API token, including org id, name, and metadata.",
        "inputSchema": { "type": "object", "properties": {} }
      },
      {
        "name": "mist_sites",
        "description": "Return all sites in an organization, including site id, name, address, and configuration.",
        "inputSchema": { "type": "object", "properties": { "org_id": { "type": "string" } } }
      },
      {
        "name": "mist_devices",
        "description": "Return all devices (APs, switches, gateways) at a site, including id, mac, model, name, type, and status.",
        "inputSchema": { "type": "object", "properties": { "site_id": { "type": "string" } } }
      },
      {
        "name": "mist_marvis_query",
        "description": "Ask a natural language question to Marvis, the Mist AI engine. Supports questions about network health, client experience, device status, and more.",
        "inputSchema": { "type": "object", "properties": { "query": { "type": "string" }, "org_id": { "type": "string" } } }
      },
      {
        "name": "mist_create_wlan",
        "description": "Create a new WLAN at a site with the given SSID and JSON configuration.",
        "inputSchema": { "type": "object", "properties": { "site_id": { "type": "string" }, "ssid": { "type": "string" }, "config": { "type": "string" } } }
      },
      …
    ]
  }
}
```

## Hermes Registration

To register this MCP server with an AI agent that supports the Hermes MCP configuration format,
add the following to the agent's MCP config:

```json
{
  "mcpServers": {
    "mist-mcp": {
      "url": "http://127.0.0.1:8935/mcp"
    }
  }
}
```

## Security Notes

- **Secrets come from environment variables only** — The API token and org ID are never baked
  into the container image. They are provided at runtime via the `EnvironmentFile` in the quadlet
  (or set directly in the shell for development).
- **The API token is redacted from all logs and error messages** — `MistClient` registers the
  token with a log redaction filter, and outgoing request headers are logged with the authorization
  header replaced by `Bearer ***`.
- **No escape hatches** — Unlike other MCP servers in this family, this server has no raw request
  tool, no introspection tool, and no generic "pull anything" tool. The 22 curated tools are the
  complete surface. Agents cannot bypass the curated tool set.
- **No write gating** — All 7 write tools are always registered and always available. There is no
  `ALLOW_WRITE` flag. Access control is enforced entirely by the Mist API token's RBAC
  permissions. Mist validates all writes and rejects unauthorized operations.
- **Loopback-only exposure** — The security boundary is the host-side port publish, pinned to
  loopback (`PublishPort=127.0.0.1:8935:8935` in the quadlet). The server is reachable only from
  the host at `http://127.0.0.1:8935/mcp`. Inside the container the server binds `0.0.0.0`
  (baked into the image via `ENV MIST_HOST=0.0.0.0`) so rootless Podman's `pasta` network can
  forward into the container on WSL2/macOS. Never publish the port on `0.0.0.0` host-side. For
  direct host runs the bind defaults to `127.0.0.1`.
- **TLS verification is on by default** — `MIST_INSECURE_SKIP_VERIFY=true` should only be used in
  development or test environments. The Mist cloud API always uses valid TLS certificates.
- **Rootless operation** — The quadlet runs under `systemctl --user`, meaning the container runs
  without root privileges. This follows the principle of least privilege.

## Development

Run the server locally without a container. The curated tool set is registered by
`register_curated_tools()` in `src/mist_mcp/curated_tools.py`:

```bash
# Create and activate a virtual environment
python -m venv .venv && . .venv/bin/activate

# Install the package in editable mode
pip install -e .

# Run with required environment variables
MIST_API_TOKEN=your-api-token \
MIST_ORG_ID=org_abc123 \
python -m mist_mcp
```

Or use the installed console script:

```bash
MIST_API_TOKEN=your-api-token \
MIST_ORG_ID=org_abc123 \
mist-mcp
```

The server will start on `127.0.0.1:8935` by default. You can override the bind address, port,
log level, timeout, and TLS verification via the environment variables listed above.

## Updating

If the container image is published to a registry, and the quadlet includes `AutoUpdate=registry`,
you can update to the latest image with:

```bash
podman auto-update
```

This pulls the latest image and restarts the container gracefully. Check the status afterwards:

```bash
systemctl --user status mist-mcp
```

## Architecture

```
mist-mcp-server/
├── env.example              # Documented environment variable template
├── pyproject.toml           # Project metadata, dependencies, entrypoint
├── src/
│   └── mist_mcp/
│       ├── __main__.py      # Entrypoint: env loading, init, server start
│       ├── rest_client.py   # Async REST client for api.mist.com/api/v1
│       └── curated_tools.py # Static, hand-written MCP tool set (22 tools)
└── tests/                   # Test suite (see test files)
```

### Module responsibilities

| Module | Role |
|---|---|
| `__main__.py` | Reads environment variables, validates required ones (`MIST_API_TOKEN`, `MIST_ORG_ID`), initialises `MistClient`, creates the `FastMCP` server, calls `register_curated_tools()`, and starts the streamable-http transport. |
| `rest_client.py` | Async HTTP client for `api.mist.com/api/v1`. Sends `Authorization: Bearer <token>`, provides `get()`, `post()`, `put()`, `delete()` methods, retries on 429/5xx with exponential backoff (1s, 2s, 4s), retries once on timeout, redacts the API token from all log output, and raises typed exceptions (`MistError`, `MistHttpError`). |
| `curated_tools.py` | Registers the static, hand-written tool set (`register_curated_tools(mcp, mist_client)`): 15 read tools and 7 write tools. No escape hatches, no write gating. Returns the number of tools registered (22). |

## Skills

- [`skills/mist-tools.skill.md`](./skills/mist-tools.skill.md) — Per-tool reference for all 22
  tools: exact names, argument signatures, return shapes, and read/write status.
- [`skills/mist-api.skill.md`](./skills/mist-api.skill.md) — Reference for the Juniper Mist REST
  API itself: endpoint, auth, URL paths, and RBAC.
- [`skills/podman-deploy.skill.md`](./skills/podman-deploy.skill.md) — Rootless Podman quadlet
  build and deployment of the server.