Skip to main content
Glama
README.md
# IT Operations Hub

เกตเวย์ MCP รวมศูนย์สำหรับงาน IT Operations และบัญชี Express — เอเจนต์ AI คุยกับ **Zabbix 7**, **MeshCentral**, และ **Express Accounting** (express.co.th) ผ่าน SSE และ Streamable HTTP หลัง Cloudflare Tunnel และ Nginx RBAC

A production Docker Compose stack:

`Cloudflare Tunnel → Nginx (Bearer RBAC + OAuth DCR, SSE/WebSocket) → mcp-hub-it | mcp-hub-admin | mcp-hub-accounting → sub-mcp-zabbix | sub-mcp-meshcentral | sub-mcp-express → Zabbix / MeshCentral / Express books`

All services share a single bridge network, `infra-net`. MCP hubs and databases are not published on the host. Only LAN/VPN ports for the gateway, Zabbix, and MeshCentral agents are bound.

## Architecture

```
AI agents (Claude Desktop, Cursor, ChatGPT, Grok)
        |  HTTPS  (+ OAuth DCR token page, or Authorization: Bearer)
        v
Cloudflare Zero Trust  ──tunnel──► cloudflared
        |
        v
Nginx :80 (internal) / MCP_LAN_PORT on the host
  OAuth  /authorize /token /register /.well-known/*  → mcp-oauth (token-paste page)
  Bearer IT_TOKEN          → role it          → /mcp/it/*          (it + admin)
  Bearer ADMIN_TOKEN       → role admin       → /mcp/admin/*       (admin only)
  Bearer ACCOUNTING_TOKEN  → role accounting  → /mcp/accounting/*  (accounting only)
        |
        +--> mcp-hub-it:3000           Zabbix + MeshCentral inventory
        +--> mcp-hub-admin:3000        same + meshcentral_run_shell
        +--> mcp-hub-accounting:3000   Express Accounting read-only
                    |
                    +--> sub-mcp-zabbix / sub-mcp-meshcentral
                    +--> sub-mcp-express   fixture | http adapter | DBF
                              |
                              +--> zabbix-web / zabbix-server / zabbix-db
                              +--> meshcentral
                              +--> Express books (sample, sidecar REST, or .DBF)
```

### Tools

| Tool | IT hub | Admin hub | Accounting hub |
| --- | --- | --- | --- |
| `zabbix_get_active_problems(severity_min?)` | yes | yes | no |
| `zabbix_get_device_status(group_name?)` | yes | yes | no |
| `zabbix_get_metrics(host_name, item_keys)` | yes | yes | no |
| `meshcentral_get_inventory()` | yes | yes | no |
| `meshcentral_run_shell(node_id, command)` | no | yes | no |
| `express_get_status()` | no | no | yes |
| `express_list_customers(query?, limit?)` | no | no | yes |
| `express_list_vendors(query?, limit?)` | no | no | yes |
| `express_list_items(query?, limit?)` | no | no | yes |
| `express_list_ar_invoices(status?, query?, limit?)` | no | no | yes |
| `express_list_gl_accounts(query?, limit?)` | no | no | yes |

Nginx rejects an IT token on `/mcp/admin/` and `/mcp/accounting/` with HTTP 403. An accounting token cannot call IT or admin paths. The IT hub process does not register the shell tool. Express tools are read-only; see [docs/EXPRESS.md](docs/EXPRESS.md).

## Requirements

- Docker Engine 24+ with Compose v2
- A host that can reach ESXi / switches / NVR / Windows agents on the LAN (Zabbix server port `10051`, MeshCentral `443`/`4433`)
- Optional: Cloudflare account for the public MCP hostname

## Quick start

```bash
git clone <this-repo>
cd itops-mcp-hub
cp .env.example .env
```

Edit `.env`:

1. Generate unique RBAC tokens (URL-safe, no spaces or quotes):

   ```bash
   openssl rand -hex 32   # IT_TOKEN
   openssl rand -hex 32   # ADMIN_TOKEN
   openssl rand -hex 32   # ACCOUNTING_TOKEN
   ```

2. Set `ZABBIX_DB_PASSWORD` and MeshCentral `MESHCENTRAL_PASSWORD` / `MESHCENTRAL_API_KEY`.
3. Leave `CLOUDFLARE_TUNNEL_TOKEN` empty until Zero Trust is configured.

Bring the stack up (LAN/VPN mode, no tunnel):

```bash
docker compose up -d
```

First boot of Zabbix Postgres schema can take a couple of minutes. Watch:

```bash
docker compose ps
docker compose logs -f zabbix-server zabbix-web nginx mcp-hub-it
```

Gateway status page (LAN): `http://<compose-host>:9080/`

| Service | Default LAN port | Notes |
| --- | --- | --- |
| MCP + status page | `9080` | Nginx. MCP paths require a Bearer token |
| Zabbix UI | `9443` | First login `Admin` / `zabbix` — change immediately |
| Zabbix server | `10051` | Agents, ESXi, SNMP traps path into the server |
| MeshCentral HTTPS | `9444` | Create the first admin when `ALLOW_NEW_ACCOUNTS=true` |
| MeshCentral agent | `4433` | Agent/MPS |

### Cloudflare Tunnel (public MCP URL)

```bash
docker compose --profile tunnel up -d
```

`cloudflared` is distroless and expects a real `CLOUDFLARE_TUNNEL_TOKEN`. Do not start this profile until the token is set.

## First-run: Zabbix API token

The MCP Zabbix server authenticates with a Zabbix **API token** (Bearer), not the UI password.

1. Open `http://<host>:9443` and log in as `Admin`.
2. Change the Admin password; set `ZABBIX_WEB_PASSWORD` in `.env` to match.
3. User menu → **API tokens** → Create token `mcp-gateway`.
4. Put the secret into `ZABBIX_API_TOKEN` and recreate the Zabbix MCP container:

   ```bash
   docker compose up -d --force-recreate sub-mcp-zabbix
   ```

Alternatively, after the UI is up:

```bash
node scripts/create-zabbix-api-token.mjs
# paste the printed value into ZABBIX_API_TOKEN
```

Create host groups that match how you filter devices (`ESXi`, `Switches`, `CCTV`, `Windows servers`, …). `zabbix_get_device_status` searches group names.

## First-run: MeshCentral

1. Open `https://<host>:9444` (self-signed certificate on a fresh volume).
2. Create the account that matches `MESHCENTRAL_USER` / `MESHCENTRAL_PASSWORD`.
3. Set `MESHCENTRAL_ALLOW_NEW_ACCOUNTS=false` and recreate `meshcentral` after the first admin exists.
4. Install MeshAgents on Windows / Linux / ESXi management jump hosts as needed.
5. `meshcentral_get_inventory` reads the **cached** node list over `/control.ashx` (`action: "nodes"`). CPU/RAM are returned when MeshCentral already stored them on the node object; they are not probed live.
6. `meshcentral_run_shell` uses `runcommands` (PowerShell on Windows, POSIX shell otherwise) and is exposed only on the admin hub.

`MESHCENTRAL_API_KEY` is used as the control-channel password when `MESHCENTRAL_PASSWORD` is empty. Prefer a dedicated MeshCentral service user. Authentication is the MeshCentral `x-meshauth` header (`base64(user),base64(pass)`).

Internal TLS to `https://meshcentral:443` uses a self-signed cert. Keep `MESHCENTRAL_TLS_INSECURE=true` unless you mounted a real certificate into `meshcentral-data`.

## Cloudflare Zero Trust

This is the public path for AI clients. MeshCentral agents and Zabbix pollers stay on the LAN; only the MCP gateway is published.

### 1. Tunnel

1. Zero Trust → **Networks** → **Tunnels** → Create a locally managed tunnel.
2. Copy the token into `CLOUDFLARE_TUNNEL_TOKEN`.
3. Public hostname, for example `mcp.example.com`:
   - Type: HTTP
   - URL: `http://nginx:80`
   - The connector runs **inside** `infra-net`, so it must use the Compose service name `nginx`, not a host port.
4. `docker compose --profile tunnel up -d`

Set `PUBLIC_MCP_ORIGIN=https://mcp.example.com` (the same public hostname) so ChatGPT/Grok OAuth discovery advertises the real URL. Then recreate `mcp-oauth` and `nginx`.

Optional origin settings in the hostname:

- HTTP Host Header: `mcp.example.com`
- Disable chunked encoding: **off** (SSE needs chunked transfer)
- No extra origin TLS (nginx listens HTTP on 80)

### 2. Access application (service tokens)

1. Zero Trust → **Access** → **Applications** → Add **Self-hosted**.
2. Application domain: `mcp.example.com` (include `/mcp*` if you split policies).
3. Identity: **Service Auth** (and optionally your IdP for humans).
4. Create a **Service Token** (`Client ID` + `Client Secret`).
5. Policy: Service Token is valid, then allow.

Cloudflare consumes the service token headers at the edge. Configure the application to **forward** these headers to origin (Access → Application → Settings / Overview, depending on the dashboard version):

| Header | Purpose |
| --- | --- |
| `CF-Access-Client-Id` | Service token id (validated by Cloudflare, then forwarded) |
| `CF-Access-Client-Secret` | Service token secret |
| `Authorization` | **Must pass through unmodified** — Nginx maps this to `it` / `admin` |
| `CF-Access-Jwt-Assertion` | Set by Cloudflare after a successful Access login |

If Access strips `Authorization`, Nginx will 401 every MCP call. Add `Authorization` to the allowed/forwarded header list, or put the MCP Bearer token in a second Access-approved header and change Nginx — this repo expects `Authorization: Bearer <IT_TOKEN|ADMIN_TOKEN|ACCOUNTING_TOKEN>`.

Create **two** Access service tokens if you want to rotate IT and Admin Cloudflare identities independently of the MCP RBAC tokens.

### 3. SSE through Cloudflare

Nginx already sets `proxy_buffering off`, `gzip off`, `X-Accel-Buffering: no`, and 1-hour proxy timeouts. In the tunnel hostname, do not enable extra buffering or “HTTP/2 to origin” if SSE stalls; HTTP/1.1 to nginx is the safe origin protocol.

## Team trial: ChatGPT / Grok (read-only)

ขณะรอ **ai-tools-mcp** (ชั้นอนุมัติคำสั่ง privileged) ทีมทดลองบน ChatGPT / Grok ได้ **เฉพาะเส้น IT**

- URL: `https://<tunnel-host>/mcp/it/mcp` — เลือก **OAuth** (อย่าเลือก Token ใน ChatGPT ถ้าต้องการหน้าเว็บ)
- ครั้งแรกเบราว์เซอร์เปิด `https://<tunnel-host>/authorize` ให้วาง `IT_TOKEN` เหมือน ai-collaboration-mcp
- ตั้ง `PUBLIC_MCP_ORIGIN=https://<tunnel-host>` ใน `.env` แล้ว recreate `mcp-oauth` + `nginx`
- ห้าม `ADMIN_TOKEN` และห้าม `/mcp/admin/` สำหรับทีมทดลอง
- ทีมบัญชีใช้เส้นแยก `https://<tunnel-host>/mcp/accounting/mcp` + `ACCOUNTING_TOKEN` + scope `mcp:accounting` — ดู [docs/EXPRESS.md](docs/EXPRESS.md)
- ขั้นตอนละเอียดอยู่ที่ [docs/TEAM-CONNECT.md](docs/TEAM-CONNECT.md)

`meshcentral_run_shell` ยังปิดสำหรับทีมทดลองจนกว่าจะมี payload-hash approval + human queue + audit

## MCP client configuration

Replace host, tokens, and Cloudflare service-token values. Claude Desktop still uses the legacy SSE transport (`/sse`). Newer clients can use Streamable HTTP (`/mcp`).

### Claude Desktop / SSE (IT role)

`claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "itops-it": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://mcp.example.com/mcp/it/sse",
        "--header",
        "Authorization: Bearer ${IT_TOKEN}",
        "--header",
        "CF-Access-Client-Id: ${CF_ACCESS_CLIENT_ID}",
        "--header",
        "CF-Access-Client-Secret: ${CF_ACCESS_CLIENT_SECRET}"
      ]
    }
  }
}
```

If your client supports URL + headers natively:

```json
{
  "mcpServers": {
    "itops-it": {
      "url": "https://mcp.example.com/mcp/it/sse",
      "transport": "sse",
      "headers": {
        "Authorization": "Bearer IT_TOKEN_HERE",
        "CF-Access-Client-Id": "CLIENT_ID.access",
        "CF-Access-Client-Secret": "CLIENT_SECRET"
      }
    },
    "itops-admin": {
      "url": "https://mcp.example.com/mcp/admin/sse",
      "transport": "sse",
      "headers": {
        "Authorization": "Bearer ADMIN_TOKEN_HERE",
        "CF-Access-Client-Id": "CLIENT_ID.access",
        "CF-Access-Client-Secret": "CLIENT_SECRET"
      }
    }
  }
}
```

### Streamable HTTP

```json
{
  "mcpServers": {
    "itops-accounting": {
      "url": "https://mcp.example.com/mcp/accounting/mcp",
      "headers": {
        "Authorization": "Bearer ACCOUNTING_TOKEN_HERE"
      }
    }
  }
}
```

### LAN test without Cloudflare

```bash
curl -sS http://127.0.0.1:9080/healthz
curl -sS -D- -o /dev/null \
  -H "Authorization: Bearer $IT_TOKEN" \
  http://127.0.0.1:9080/mcp/it/
curl -sS -D- -o /dev/null \
  -H "Authorization: Bearer $IT_TOKEN" \
  http://127.0.0.1:9080/mcp/admin/
# expect 403 on the admin path
curl -sS -D- -o /dev/null \
  -H "Authorization: Bearer $IT_TOKEN" \
  http://127.0.0.1:9080/mcp/accounting/
# expect 403 — books are not an IT role
curl -sS -D- -o /dev/null \
  -H "Authorization: Bearer $ACCOUNTING_TOKEN" \
  http://127.0.0.1:9080/mcp/accounting/
```

## Repository layout

```
docker-compose.yml
nginx/                    # RBAC reverse proxy + status page
packages/
  Dockerfile              # shared Node 22 image, ARG SERVICE=
  mcp-common/             # Streamable HTTP + SSE helper
  mcp-zabbix/             # Zabbix JSON-RPC tools
  mcp-meshcentral/        # MeshCentral control.ashx tools
  mcp-express/            # Express Accounting (fixture / HTTP / DBF)
  mcp-hub/                # aggregator; HUB_ROLE=it|admin|accounting
  mcp-oauth/              # OAuth 2.1 + DCR; /authorize asks for site token
scripts/create-zabbix-api-token.mjs
docs/EXPRESS.md           # Express Accounting backends and RBAC
```

## Operations notes

- Rotate `IT_TOKEN` / `ADMIN_TOKEN` / `ACCOUNTING_TOKEN` by changing `.env` and `docker compose up -d --force-recreate nginx mcp-oauth`.
- Do not publish `mcp-hub-*`, `sub-mcp-*`, or `zabbix-db` to the internet.
- `meshcentral_run_shell` runs as SYSTEM/root (`runAsUser: 0`) on the agent. Treat `ADMIN_TOKEN` like production break-glass.
- After changing MeshCentral hostname or published HTTPS port, update `config.json` in the `meshcentral-data` volume (`aliasPort` / `cert`) so agent download URLs stay correct.
- Logs are JSON lines from the Node services and json-file rotated at 10 MB × 3.

### Nested Docker / CI hosts

If containers start but Nginx cannot reach `mcp-hub-*` (SSE hangs after a 200 auth, ping between containers fails), the kernel is filtering bridged traffic:

```bash
sudo sysctl -w net.bridge.bridge-nf-call-iptables=0
sudo sysctl -w net.bridge.bridge-nf-call-ip6tables=0
```

This is a host setting, not a Compose service setting. Normal bare-metal or VM Docker installs already have working inter-container connectivity.

## License

Internal operations tooling. Review Zabbix and MeshCentral licenses for the upstream images.

Maintenance

ActivityMaintained
ResponsivenessNo issues