trmm-exec-mcp
README.md
# trmm-exec-mcp
**One** MCP server for Tactical RMM: **Entra-gated reads + a read-only PowerShell
runner**. Only members of the assigned Entra security group can connect. It reads
everything on the platform and runs **information-gathering** PowerShell on any
agent — and **refuses anything that changes state**. Built on **FastMCP 3.x** with
**Microsoft Entra** auth (same stack as [`odoo-ticket-mcp`](../OdooMCPServer)).
> **v2 (simplified).** The earlier design added a Roam one-time-token approval
> relay for genuine *write/exec*. That was more machine than the actual need, so
> v2 drops the relay, the proposal store, the approver list, and the agent
> allowlist. Safety now comes from two things: the **Entra group** (who can
> connect) and the **read-only guard** (what a script may do). The old flow lives
> in git history before the `simplify/readonly-entra` branch.
## What keeps it read-only
Arbitrary PowerShell can't be *proven* read-only, so [`guard.py`](trmm_exec_mcp/guard.py)
is a strict, defense-in-depth backstop that errs on refusing. Two layers:
1. **Cmdlet verb allowlist** — PowerShell cmdlets are `Verb-Noun`; only read
verbs (`Get`, `Test`, `Measure`, `Select`, …) pass, so any `Set-*`, `New-*`,
`Remove-*`, `Stop-*`, `Restart-*` is refused by construction.
2. **Dangerous-token denylist** — catches state-changing constructs that aren't
`Verb-Noun` cmdlets (native `del`/`reg delete`/`schtasks`, .NET `::Delete`,
redirection to disk) and obfuscation that would defeat layer 1 (`iex`,
`-EncodedCommand`, Base64 decode, `Invoke-Command`).
A refusal names the offending token. This is **not** a sandbox — still point
`TRMM_API_KEY` at an account whose blast radius you accept.
## Auth: Microsoft Entra (resource-server mode)
FastMCP's `AzureJWTVerifier` + `RemoteAuthProvider`: the server validates
audience-bound Entra tokens and publishes protected-resource metadata; it never
issues tokens. claude.ai does the OAuth against Entra and connects. nginx in front
does only TLS + the Anthropic IP allowlist. Restrict to the senior engineers via
the Entra app's group assignment (**Assignment required = Yes**).
## Tools
| Tool | Kind | Purpose |
|------|------|---------|
| `search_agents` | read | Find machines (filter by hostname/type/platform/status). |
| `get_agent` | read | Full detail for one agent (hardware, OS, IPs, checks, reboot state). |
| `resolve_agent` | read | Hostname → agent_id (errors if 0/>1). |
| `search_audit_log` | read | TRMM's own audit trail (who did what in TRMM). |
| `run_readonly_script` | read | Run information-gathering PowerShell (event logs, services, software, config). Mutating scripts refused. |
## Configuration (env only)
| Variable | Required | Description |
|----------|----------|-------------|
| `TRMM_API_URL` | ✅ | TRMM API base URL. |
| `TRMM_API_KEY` | ✅ | TRMM account key with read + run/send-command. |
| `MCP_AUTH_ENABLED` | — | `true` (default). `false` only for local Inspector testing. |
| `ENTRA_TENANT_ID` / `ENTRA_CLIENT_ID` | ✅† | Tenant + API app-registration client id. |
| `MCP_BASE_URL` | ✅† | Public root (no `/mcp`), e.g. `https://trmm-exec-mcp.example.com`. |
| `ENTRA_IDENTIFIER_URI` | ✅† | **Must equal the App ID URI set in Entra** (e.g. the `https://…/mcp` URL). |
| `ENTRA_REQUIRED_SCOPES` / `ENTRA_SCOPE_NAME` | — | Scope config (`access_as_user` default). |
| `MCP_HOST` / `MCP_PORT` / `MCP_PATH` | — | Container bind (default `0.0.0.0`/`8080`/`/mcp`). |
† required when `MCP_AUTH_ENABLED=true`.
## TRMM account
Uses the **`claude`** TRMM user, provisioned with read + Send Command
(`SendCMDPerms`). Put its key in `TRMM_API_KEY` (via the Portainer stack env,
never in git). The read-only guard limits what a script can do; keep the account
scoped to what the tools need. Treat DCs as extra-sensitive.
## Run / deploy
```bash
python -m venv .venv && .venv\Scripts\activate
pip install -r requirements.txt
# local (auth off): set TRMM_API_URL, TRMM_API_KEY, MCP_AUTH_ENABLED=false, then:
python -m trmm_exec_mcp
```
Production: [PORTAINER_DEPLOY.md](PORTAINER_DEPLOY.md) (Git stack + webhook, Entra
env, host port) + [deploy/nginx-trmm-exec-mcp.conf](deploy/nginx-trmm-exec-mcp.conf).
## Verify
`python -m py_compile trmm_exec_mcp/*.py` — clean. The read-only guard has a case
suite (read scripts allowed; mutating/obfuscated/native-destructive refused).
After deploy: `/healthz` ok, unauthenticated `/mcp` → 401, and the discovery doc
advertises the `https://…/mcp/access_as_user` scope (not `api://…`).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues