trmm-mcp
README.md
# trmm-mcp
A **read-only** [MCP](https://modelcontextprotocol.io) server that exposes a
[Tactical RMM](https://github.com/amidaware/tacticalrmm) instance as agent
tools, so an LLM can harvest device, client, and audit data — without any
ability to change anything.
- **Read-only by design.** No run-script, run-command, reboot, install, edit,
create, or delete tools are implemented. Even if the API key were write
capable, the blast radius is zero. Pair it with a **read-only service user**
anyway (defense in depth).
- **Python + [FastMCP](https://github.com/modelcontextprotocol/python-sdk)**.
Two transports from one codebase: **stdio** for local use under your MCP
client, and **Streamable HTTP** (with shared bearer-token auth) for a shared
remote server the whole team connects to — see
[Remote deployment](#remote-deployment-team-access).
- Config is **environment-only** — no secrets in code. Fails fast at startup if
required variables are missing.
## Tools
All tools are `readOnlyHint: true`, `destructiveHint: false`,
`idempotentHint: true`, `openWorldHint: true`, return structured content with an
output schema, and support pagination where applicable.
| Tool | What it does |
|------|--------------|
| `trmm_list_clients` | All clients/tenants (with optional embedded site summaries). |
| `trmm_list_sites` | Sites, filterable by client. |
| `trmm_list_agents` | Agents/devices with summary fields; filter by client/site/type/platform/status/hostname. |
| `trmm_get_agent` | Full detail for one agent (OS, hardware, status, checks tally); `verbose` adds raw services + WMI. |
| `trmm_get_agent_checks` | Check results (cpu/mem/disk/ping/service/script/event) with pass/fail status. |
| `trmm_list_agent_history` | Command/script/task execution history for an agent (newest first). |
| `trmm_search_audit_log` | Query the audit log by agent/client/user/action/object-type/time range. |
| `trmm_list_pending_actions` | Queued reboots/updates/scripts for one or all agents. |
| `trmm_get_agent_software` | Installed software inventory for an agent. |
| `trmm_get_agent_winupdates` | Windows Update entries known for an agent. |
| `trmm_server_status` | Server health/version/counts (requires `TRMM_MON_TOKEN`). |
See [`docs/API_CONTRACT.md`](docs/API_CONTRACT.md) for the exact endpoints and
the audit-log payload.
## Configuration
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `TRMM_API_URL` | ✅ | — | API base URL, the `api.` subdomain, scheme included, no trailing slash (e.g. `https://api.example.com`). |
| `TRMM_API_KEY` | ✅ | — | `X-API-KEY` value, tied to a read-only user. |
| `TRMM_MON_TOKEN` | — | _(unset)_ | Monitoring token for `trmm_server_status` only. Separate from the API key. |
| `TRMM_TIMEOUT` | — | `30` | Per-request timeout (seconds). |
| `TRMM_VERIFY_SSL` | — | `true` | Set `false` only for self-signed certs. |
| `TRMM_MAX_RETRIES` | — | `2` | Retries on transient 5xx / network errors. |
Get an API key in TRMM: **Settings → Global Settings → API Keys** (choose your
read-only service user). The key bypasses 2FA and inherits that user's role
permissions.
## Install
Requires Python 3.10+.
```bash
cd tactical-rmm-mcp
python -m venv .venv
# Windows: .venv\Scripts\activate
# macOS/Linux: source .venv/bin/activate
pip install -r requirements.txt
```
(Or `pip install -e .` to get the `trmm-mcp` console script.)
## Run
The server speaks MCP over stdio, so it's normally launched by your MCP client.
To run it by hand (it will wait for an MCP client on stdin):
```bash
# set env first, then:
python -m trmm_mcp
# or
python server.py
```
If `TRMM_API_URL`/`TRMM_API_KEY` are missing it prints a clear error to stderr
and exits non-zero.
## Register with an MCP client (stdio)
### Claude Code — project `.mcp.json`
Create `.mcp.json` in your project (use the **absolute** path to `server.py`,
and your venv's Python so dependencies resolve):
```json
{
"mcpServers": {
"trmm": {
"command": "C:\\Users\\admin\\your-workspace\\tactical-rmm-mcp\\.venv\\Scripts\\python.exe",
"args": ["C:\\Users\\admin\\your-workspace\\tactical-rmm-mcp\\server.py"],
"env": {
"TRMM_API_URL": "https://api.yourdomain.com",
"TRMM_API_KEY": "your-readonly-key",
"TRMM_MON_TOKEN": "optional-mon-token"
}
}
}
}
```
### Claude Desktop — `claude_desktop_config.json`
Same shape, under the top-level `"mcpServers"` key:
```json
{
"mcpServers": {
"trmm": {
"command": "/abs/path/tactical-rmm-mcp/.venv/bin/python",
"args": ["/abs/path/tactical-rmm-mcp/server.py"],
"env": {
"TRMM_API_URL": "https://api.yourdomain.com",
"TRMM_API_KEY": "your-readonly-key"
}
}
}
}
```
On macOS this file lives at
`~/Library/Application Support/Claude/claude_desktop_config.json`; on Windows at
`%APPDATA%\Claude\claude_desktop_config.json`. Restart the app after editing.
## Remote deployment (team access)
Run trmm-mcp as a shared HTTPS server so engineers/helpdesk use it as a **Claude
connector** instead of each installing it locally. The TRMM API key stays
server-side; clients authenticate with a shared **bearer token**.
### 1. Generate a bearer token and configure
```bash
python -c "import secrets; print(secrets.token_urlsafe(32))"
```
In `.env` (or your Portainer stack env):
```
TRMM_API_URL=https://api.yourdomain.com
TRMM_API_KEY=<read-only service-user key>
TRMM_MCP_TRANSPORT=http
TRMM_MCP_AUTH_TOKEN=<the token you just generated>
```
The server refuses to start in http mode without `TRMM_MCP_AUTH_TOKEN`.
### 2. Deploy with Docker / Portainer
```bash
docker compose up -d --build
curl http://localhost:8080/healthz # -> {"status":"ok",...}
```
In Portainer, deploy a stack from this repo and set the env vars on the stack —
fits your existing build → push → Portainer-webhook redeploy flow. The image runs
as a non-root user and ships a Docker `HEALTHCHECK` against `/healthz`.
### 3. Put it behind HTTPS + a network gate
Terminate TLS and gate access **in front of** the container — a reverse proxy
(Caddy/Nginx/Traefik) serving e.g. `https://trmm-mcp.example.com`, behind
your VPN or Cloudflare Access. The bearer token is defense-in-depth, not the only
layer. **Never expose port 8080 to the internet unauthenticated.**
### 4. Connect Claude (Claude Code / Desktop)
Remote `.mcp.json` — no Python, repo, or TRMM key on the teammate's machine:
```json
{
"mcpServers": {
"trmm": {
"type": "http",
"url": "https://trmm-mcp.example.com/mcp",
"headers": { "Authorization": "Bearer <shared-token>" }
}
}
}
```
### claude.ai web & mobile
The web/mobile custom-connector UI authenticates via **OAuth**, not a static
header — so the bearer setup above covers **Claude Code + Desktop**. For web +
mobile, front the server with **Cloudflare Tunnel + an MCP Server Portal**: it
runs the OAuth/SSO (Entra) flow toward Claude and injects the bearer token
upstream, so **no extra server code** is needed. Full walkthrough:
[docs/REMOTE_OAUTH_CLOUDFLARE.md](docs/REMOTE_OAUTH_CLOUDFLARE.md). Uses
[docker-compose.cloudflare.yml](docker-compose.cloudflare.yml) (tunnel sidecar,
no published ports).
### Endpoints
| Path | Auth | Purpose |
|------|------|---------|
| `POST /mcp` (configurable) | Bearer | The MCP endpoint clients connect to |
| `GET /healthz` | none | Health probe for Docker/orchestrators |
## Test with MCP Inspector
```bash
npx @modelcontextprotocol/inspector \
-e TRMM_API_URL=https://api.yourdomain.com \
-e TRMM_API_KEY=your-readonly-key \
-- python server.py
```
Then in the Inspector UI: list tools, call `trmm_list_clients`, and try
`trmm_search_audit_log` with `within_days: 7` to confirm pagination and the
audit-log filters work against live data.
## Evaluation
[`evaluation.xml`](evaluation.xml) contains 10 read-only MSP questions with
verified answers. Run them with the harness bundled in the `mcp-builder` skill:
```bash
python scripts/evaluation.py \
-t stdio -c python -a server.py \
-e TRMM_API_URL=https://api.yourdomain.com \
-e TRMM_API_KEY=your-readonly-key \
-o evaluation_report.md \
evaluation.xml
```
> Answers in `evaluation.xml` are specific to the instance they were verified
> against; regenerate them for a different dataset.
## Security notes
- Secrets come only from the environment; nothing is logged to stdout (stdio
servers must keep stdout clean — diagnostics go to stderr).
- Errors are translated to actionable messages that distinguish auth (401) vs.
permission (403) vs. not-found (404) vs. network/timeout, without leaking
internals.
- This server never calls a mutating endpoint. A future, explicitly-gated action
server would be a separate project.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues