Skip to main content
Glama
README.md
# mcp-omv

MCP server for [OpenMediaVault](https://www.openmediavault.org/), delivered as a
hardened container (Docker Compose). It lets an MCP client such as Bob inspect
and manage:

- the **Debian host** (system info, services, logs, apt, reboot/shutdown),
- **Docker Compose** stacks, services, containers, images, volumes and networks
  through the OMV compose plugin,
- **other OMV plugins** (filesystems, network, SMB, NFS, SMART, cron, config apply).

It talks to OMV only through the official `/rpc.php` API. It does not mount the
Docker socket or any host path.

## Safe by default

Everything is read only until the administrator enables more:

| Tier | Env variable | Examples |
| --- | --- | --- |
| read | always on | list stacks, system info, SMART, shares |
| sensitive | `MCP_ALLOW_SENSITIVE=true` | logs, compose files, `inspect`, `config` |
| write | `MCP_ALLOW_WRITE=true` | `up -d`, `restart`, `stop`, `pull`, `apt update`, apply config |
| destructive | `MCP_ALLOW_WRITE=true` and `MCP_ALLOW_DESTRUCTIVE=true` | `down`, prune, `apt install/upgrade`, reboot, shutdown, run cron job |

See [SECURITY.md](SECURITY.md) for the full threat model.

## Tools

| Group | Tools |
| --- | --- |
| Host | `omv_system_info`, `omv_system_top`, `omv_services_status`, `omv_log_read`, `omv_apt_upgradable`, `omv_apt_update`, `omv_apt_upgrade`, `omv_apt_install`, `omv_system_reboot`, `omv_system_shutdown` |
| Tasks | `omv_task_output`, `omv_task_list`, `omv_task_stop` |
| Compose | `omv_compose_stacks`, `omv_compose_services`, `omv_compose_stack_file`, `omv_compose_stack_action`, `omv_compose_service_action` |
| Docker | `omv_docker_containers`, `omv_docker_images`, `omv_docker_volumes`, `omv_docker_networks`, `omv_docker_stats`, `omv_docker_container_action`, `omv_docker_image_action`, `omv_docker_prune` |
| Plugins | `omv_filesystems_list`, `omv_network_interfaces`, `omv_network_devices`, `omv_smb_*`, `omv_nfs_*`, `omv_smart_devices`, `omv_smart_disk`, `omv_cron_jobs`, `omv_cron_run`, `omv_config_dirty`, `omv_config_apply` |

Long commands return a `taskId`. If the command is still running, call
`omv_task_output` with `taskId` and `nextPos`.

## Deploy on OpenMediaVault

1. In OMV create a user for the server, with the administrator role, no MFA and a long password.
2. In the compose plugin shared folder create the stack directory and copy `compose.yaml` and `.env.example` (as `.env`).
3. Create the secrets (files, never committed):

   ```bash
   mkdir -p secrets && chmod 700 secrets
   printf '%s' 'THE_OMV_SERVICE_PASSWORD' > secrets/omv_password
   openssl rand -base64 48 | tr -d '\n=' > secrets/mcp_token
   chmod 600 secrets/*
   ```

4. Edit `.env` (`OMV_URL`, `OMV_USERNAME`). Keep tiers `false` at first.
5. Start the stack from the compose plugin, or `docker compose up -d`.
6. Check: `curl http://127.0.0.1:8787/healthz`

If the OMV web UI uses a private CA, mount the CA file and set `NODE_EXTRA_CA_CERTS`
in the service environment. Do not disable certificate validation.

### Private image

If the GHCR package is private, run once on OMV: `docker login ghcr.io -u <user>`
with a token that has only `read:packages`.

### Verify the image signature

```bash
cosign verify ghcr.io/tommypopescu/mcp_omv:latest \
  --certificate-identity-regexp="https://github.com/tommypopescu/.*" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com"
```

## Configure the MCP client (Bob)

```json
{
  "mcpServers": {
    "omv": {
      "type": "streamable-http",
      "url": "http://127.0.0.1:8787/mcp",
      "headers": { "Authorization": "Bearer ${env:OMV_MCP_TOKEN}" }
    }
  }
}
```

Set `OMV_MCP_TOKEN` in your environment to the content of `secrets/mcp_token`.
If the client is on another machine, publish on a specific LAN IP
(`MCP_PUBLISH_HOST`), add that IP to `MCP_ALLOWED_HOSTS`, and put TLS in front
(for example the OMV nginx or a reverse proxy). The token must not travel over plain HTTP on an untrusted network.

## Configuration

| Variable | Default | Purpose |
| --- | --- | --- |
| `OMV_URL` | required | OMV base URL, HTTPS |
| `OMV_USERNAME` | required | OMV service account |
| `OMV_PASSWORD_FILE` | required | file with the password (or `OMV_PASSWORD`) |
| `MCP_AUTH_TOKEN_FILE` | required | file with the bearer token, 32+ chars (or `MCP_AUTH_TOKEN`) |
| `OMV_ALLOW_HTTP` | `false` | allow plain HTTP to OMV on a trusted network |
| `MCP_BIND_HOST` | `127.0.0.1` | bind address, never `0.0.0.0` |
| `MCP_PORT` | `8787` | listen port |
| `MCP_ALLOWED_HOSTS` | | extra allowed `Host` values |
| `MCP_ALLOWED_ORIGINS` | | allowed `Origin` hostnames |
| `MCP_ALLOW_SENSITIVE` / `MCP_ALLOW_WRITE` / `MCP_ALLOW_DESTRUCTIVE` | `false` | risk tiers |
| `OMV_REQUEST_TIMEOUT_MS` | `20000` | per RPC timeout |
| `OMV_TASK_WAIT_SECONDS` | `120` | how long a tool waits for a background task |
| `MCP_MAX_OUTPUT_BYTES` | `100000` | output cap per tool call |
| `MCP_RATE_LIMIT_PER_MINUTE` | `60` | requests per client per minute |
| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` |

## Develop

```bash
npm ci
npm run typecheck
npm test
npm run build
```

## CI/CD

`build-push.yml` builds the image, blocks on Trivy CRITICAL/HIGH findings,
uploads SARIF to the Security tab, pushes to GHCR and signs the digest with
Cosign (keyless). `ci.yml` runs typecheck, tests and audit. `codeql.yml` runs
CodeQL. Dependabot updates Actions, npm and the base image. All Actions are
pinned to commit SHAs.

## License

MIT