Skip to main content
Glama
coevin

qb-mcp-server

by coevin
README.md
# qb-mcp-server

A local [MCP](https://modelcontextprotocol.io) server that exposes the
[Qbee](https://qbee.io) device-management REST API as tools over the
**streamable HTTP transport**. It lets MCP clients (Claude Code, Claude
Desktop, …) answer questions about your Qbee-managed device fleet and perform
tiered write operations: staged configuration commits, file management, device
metadata, enrollment approvals, IAM, and bootstrap keys.

## ⚠️ Caveat Emptor (Buyer Beware)

You are about to hand an LLM the keys to a device fleet. That is powerful —
and occasionally hilarious — right up until a slightly misread request renames
every device in a group or commits a config you meant to keep as a draft.

The guardrails are real but not magic: consequential tools are confirm-gated
(the target must be restated exactly), every mutation is audited to stderr,
irreversible deletions aren't exposed at all, and
`QBEE_MCP_ALLOW_WRITES=false` takes every write tool off the table. What no
guardrail can do is decide whether a request was a *good idea* — that part
stays with you. Sensible habits:

- Learn on a scratch device (a `hello-world`, if you will) before pointing
  this at production fleets.
- Review `qb_changes_list` before every `qb_config_commit` — commit applies to
  real hardware.
- Give the account a Qbee role with the narrowest permissions that still work.
- Remember: the undo button for a commit is usually another commit.

## Requirements

- Node.js ≥ 20
- A Qbee account **without** 2FA enabled (see [Troubleshooting](#troubleshooting))

## Configuration

Credentials and the API base URL live in `~/.config/qbee-fleet/config.json`
(override the path with `QBEE_CONFIG_PATH`):

```json
{
  "server": "https://www.app.qbee.io",
  "username": "you@example.com",
  "password": "your-password"
}
```

The file is read once at startup. Its values are secrets: they are never
logged, never returned through MCP, and error messages name only the file path.

## Run

```bash
npm install
npm run build
npm start          # listens on http://127.0.0.1:3333/mcp
```

Development: `npm run dev` (tsx watch) · Tests: `npm test`

### Environment variables

| Variable              | Default | Purpose |
| --------------------- | ------- | ------- |
| `QBEE_MCP_PORT`       | `3333`  | Port to listen on (always 127.0.0.1) |
| `QBEE_CONFIG_PATH`    | `~/.config/qbee-fleet/config.json` | Config file location |
| `QBEE_MCP_ALLOW_WRITES` | enabled | Set `false` to hide every write tool from the catalog |

### Add to an MCP client

Point any streamable-HTTP MCP client at `http://127.0.0.1:3333/mcp`, e.g.:

```bash
claude mcp add --transport http qb http://127.0.0.1:3333/mcp
```

## How auth works

The server exchanges username/password for a short-lived JWT
(`POST /api/v2/login`) on the **first tool call**, caches it **in memory
only**, and re-logs in automatically before it expires or after any `401`.
Nothing is persisted to disk.

`GET /healthz` (outside MCP) reports liveness and auth state:
`{"ok": true, "auth": "pending" | "authenticated" | "failed"}`.

## Tools (52)

### Reads (29)

| Domain | Tools |
| --- | --- |
| Devices | `qb_devices_list`, `qb_device_get`, `qb_device_status`, `qb_device_metrics_latest`, `qb_device_inventory`, `qb_device_docker_report`, `qb_inventory_search` |
| Groups | `qb_groups_list`, `qb_group_tree` |
| Files | `qb_files_list`, `qb_file_get`, `qb_file_metadata`, `qb_file_stats` |
| CVEs | `qb_cves_list`, `qb_cve_get` |
| Reports | `qb_reports_list`, `qb_notifications_get` |
| IAM | `qb_users_list`, `qb_roles_list`, `qb_tags_list`, `qb_profile_get` |
| Remote access | `qb_remote_console_token` |
| Config workflow | `qb_changes_list`, `qb_change_get`, `qb_commits_list`, `qb_commit_get` |
| Membership / keys | `qb_pending_hosts_list`, `qb_bootstrap_keys_list`, `qb_bootstrap_key_get` |

### Writes — Tier 1, no confirmation (6)

Reversible, cosmetic operations: `qb_reports_mark_read`,
`qb_file_create_dir`, `qb_device_attributes_update` (name/description/location/tags),
`qb_notifications_update`, `qb_profile_update`, `qb_cve_update` (enable/disable).

### Writes — Tier 2, confirmation-gated (17)

Fleet- or account-affecting operations. Each requires a `confirm` argument
that must **exactly echo the operation's primary target identifier** — a
deliberate restatement, not a yes/no. Mismatched or missing confirm fails
before any HTTP request.

| Domain | Tools (`confirm` echoes) |
| --- | --- |
| Config workflow | `qb_config_change_stage` (node_id), `qb_config_change_delete` (sha), `qb_config_commit` (message), `qb_device_run_agent` (deviceID) |
| Files | `qb_file_upload` (path), `qb_file_rename` (path), `qb_file_delete` (path) |
| Groups | `qb_group_node_update` (nodeID) |
| Membership | `qb_pending_host_approve` (node_id), `qb_pending_host_reject` (deviceID) |
| IAM | `qb_user_create` (email), `qb_user_update` (userID), `qb_role_create` (name), `qb_role_update` (roleID) |
| Bootstrap keys | `qb_bootstrap_key_create` (description), `qb_bootstrap_key_update` (keyID), `qb_bootstrap_key_delete` (keyID) |

**Configuration workflow:** changes are staged (`qb_config_change_stage`),
reviewed (`qb_changes_list` / `qb_change_get`), then applied fleet-wide by
`qb_config_commit`. The commit tool deliberately sends only a message — it
commits what is already staged and can never smuggle inline changes.

**Deliberately not exposed:** device removal, company settings/deletion,
user/role **deletion**, host un-approval, whole-tree group replacement, bulk
staged-change discard, auth-provider toggles, and the 2FA flow. Use the Qbee
web UI for those.

### Safety rails

- **Confirm-echo gate** on every Tier-2 tool (see above); failures happen
  before any network request.
- **Audit log**: every mutating call emits one stderr JSON line
  (`ts`, `tool`, `target`, `tier`, `outcome`) — never arguments, credentials,
  or tokens.
- **Kill-switch**: `QBEE_MCP_ALLOW_WRITES=false` removes all write tools from
  the catalog; reads stay.
- Large results are truncated at 256 KB with a notice; narrow queries with
  pagination and `search` parameters.

## Troubleshooting

- **`qbee login failed: Invalid credentials.`** — the config file's
  `username`/`password` were rejected (HTTP 400 from Qbee). Check for typos or
  an outdated password.
- **`qbee login requires two-factor authentication …`** — the account has 2FA
  enabled, which this server does not support. Use a non-2FA account or a
  service account.
- **`qbee unreachable: …`** — the `server` URL could not be reached (network,
  DNS, or wrong URL).
- **`qbee API 403: …`** — authenticated, but your Qbee role lacks the
  permission the endpoint needs (e.g. `device:read`, `files:manage`).
- **`refused: this operation is confirmation-gated …`** — the `confirm`
  argument didn't exactly match the named target argument; restate the target.
- **Port already in use** — set `QBEE_MCP_PORT`.

## Disclaimer

This project is an independent open-source tool and is not affiliated,
associated, authorized, endorsed by, or in any way officially connected with
Qbee AS, or any of its subsidiaries or affiliates.

The official Qbee website can be found at [https://qbee.io](https://qbee.io).

The name "Qbee" as well as related names, marks, emblems, and images are
registered trademarks of their respective owners. Any use of trademarked terms
in this repository is strictly for nominative, referential, or identification
purposes only and does not imply any endorsement or sponsorship.

## License

Copyright 2026 BigPod Limited. Licensed under the
[Apache License, Version 2.0](LICENSE).