Skip to main content
Glama
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.

Maintenance

ActivityMaintained
ResponsivenessNo issues