Skip to main content
Glama
README.md
# HA Admin MCP

A self-hosted Model Context Protocol (MCP) server for safely previewing,
creating, validating, editing and rolling back AI-managed Home Assistant
automations.

This is an early open-source release. Every user runs their own instance
against their own Home Assistant installation. There is no hosted service,
central MCP endpoint, telemetry or cloud dependency.

## Why it exists

Home Assistant's
[official MCP server](https://www.home-assistant.io/integrations/mcp_server/)
is well suited to discovering entities, understanding state and performing
normal day-to-day control.

HA Admin MCP provides a complementary configuration-administration layer:

```text
AI client
|
+-- Official Home Assistant MCP
|      +-- entities
|      +-- state and context
|      +-- normal control
|
+-- HA Admin MCP
       +-- AI automation lifecycle
       +-- validation
       +-- backups and rollback
       +-- audit logging
```

It is not a replacement for Home Assistant's official MCP server.

## Features

- Streamable HTTP MCP transport
- Separate bearer authentication for MCP clients
- Home Assistant API authentication
- Isolated AI-managed automation file
- Automation preview and structural validation
- Create, update and delete operations
- Enable, disable and manual trigger operations
- Full Home Assistant configuration validation before reload
- Transactional writes with file locking and atomic replacement
- Timestamped backups, diffs and rollback
- JSON Lines mutation audit log with credential redaction
- Home Assistant and file health checks
- Read-only integration tests
- Hardened Docker deployment:
  - non-root UID/GID 10001
  - read-only container root
  - all Linux capabilities dropped
  - no Docker socket
  - no privileged mode
  - only dedicated data mounts

The reference deployment is tested with Home Assistant 2026.7.4. Other recent
Home Assistant versions may work but are not yet part of a compatibility
matrix.

## Tools

The server currently exposes 15 tools:

- `ha_health`
- `list_ai_automations`
- `get_ai_automation`
- `preview_ai_automation`
- `create_ai_automation`
- `update_ai_automation`
- `delete_ai_automation`
- `enable_ai_automation`
- `disable_ai_automation`
- `trigger_ai_automation`
- `validate_home_assistant`
- `reload_ai_automations`
- `diff_ai_automations`
- `list_ai_automation_backups`
- `rollback_ai_automations`

New automations default to disabled unless the caller explicitly requests an
enabled initial state.

## Safety model

HA Admin MCP never needs access to Home Assistant's normal
`automations.yaml`. AI-managed automations live in a separate file and are
loaded under a separate labelled automation configuration.

Every file mutation follows this sequence:

```text
AI requests a change
        |
        v
parse and structurally validate
        |
        v
lock file and create backup
        |
        v
write temporary file and atomically replace
        |
        v
run full Home Assistant configuration validation
        |
   +----+----+
   |         |
 failure    success
   |         |
restore     reload automations
old bytes    |
   |         v
validate    verify Home Assistant health
and reload
```

A validation failure restores the exact previous bytes. Rollback itself creates
a new backup before restoring an older version. Mutations and runtime actions
are written to the rotating audit log.

This design reduces risk; it does not make arbitrary automation changes safe.
Review generated automations before enabling them.

See [ARCHITECTURE.md](ARCHITECTURE.md) for the full trust and failure
boundaries.

## Requirements

- Docker Engine with Docker Compose v2
- A Home Assistant installation reachable from the container
- A Home Assistant long-lived access token from an appropriately privileged
  account
- Write access to one dedicated AI automation directory
- An MCP client that supports Streamable HTTP and bearer authentication

## Home Assistant preparation

Back up the Home Assistant configuration before changing it.

Keep the existing UI automation include and add a separately labelled include
to `configuration.yaml`:

```yaml
automation: !include automations.yaml
automation ai_managed: !include ai_managed/ai_automations.yaml
```

Then create the dedicated file within the Home Assistant configuration
directory:

```bash
mkdir -p /path/to/home-assistant/config/ai_managed
printf '[]\n' > /path/to/home-assistant/config/ai_managed/ai_automations.yaml
```

The directory mounted at `/ha-ai` must be writable by container UID/GID 10001.
Choose permissions that also allow Home Assistant to read the file. Do not
mount the complete Home Assistant configuration directory into HA Admin MCP.

Run Home Assistant's configuration validation before restarting or reloading
after adding the labelled include.

## Installation

1. Clone the repository:

   ```bash
   git clone https://github.com/paulcakeface/ha-admin-mcp.git
   cd ha-admin-mcp
   ```

2. Create the local environment file:

   ```bash
   cp .env.example .env
   chmod 600 .env
   ```

3. Generate a dedicated MCP client token:

   ```bash
   openssl rand -hex 32
   ```

   Put the generated value in `MCP_CLIENT_TOKEN`. Do not reuse the Home
   Assistant token.

4. Edit `.env`:

   - Set `HA_URL` to a Home Assistant address reachable from the container.
   - Set `HA_TOKEN` to a Home Assistant long-lived access token.
   - Set `MCP_CLIENT_TOKEN` to the independently generated random token.
   - Set `HA_AI_DIRECTORY` to the host's dedicated `ai_managed` directory.

5. Make the persistent directories writable by UID/GID 10001:

   ```bash
   mkdir -p data/backups data/logs
   sudo chown -R 10001:10001 data/backups data/logs
   sudo chown -R 10001:10001 /path/to/home-assistant/config/ai_managed
   ```

6. Build and start the service:

   ```bash
   docker compose up -d --build
   docker compose ps
   ```

7. Check the deliberately minimal container health endpoint:

   ```bash
   curl --fail http://127.0.0.1:8765/healthz
   ```

   Detailed Home Assistant and automation-file status is available only
   through the authenticated `ha_health` MCP tool.

The default endpoint is `http://127.0.0.1:8765/mcp`.

## Home Assistant networking

`HA_URL` must work from inside the HA Admin MCP container.

- Home Assistant Container: use a shared Docker network hostname, a LAN
  address, or `http://host.docker.internal:8123`.
- Home Assistant OS or Supervised: use an address routable from the Docker
  host, normally a trusted LAN address or HTTPS hostname.
- Home Assistant Core: use the address on which Home Assistant is listening.

The Compose file includes the Linux `host-gateway` mapping for
`host.docker.internal`; using it is optional. A shared Docker network can be
added in a local Compose override without changing the project.

## Configuration

| Variable | Purpose | Default |
| --- | --- | --- |
| `HA_URL` | Home Assistant API base URL reachable from the container | empty |
| `HA_TOKEN` | Home Assistant long-lived access token | empty |
| `MCP_CLIENT_TOKEN` | Independent MCP client bearer token; minimum 32 bytes | required |
| `HA_AI_DIRECTORY` | Host directory mounted at `/ha-ai` by Compose | `./ha-ai` |
| `HA_AUTOMATIONS_FILE` | AI automation file inside the container | `/ha-ai/ai_automations.yaml` |
| `BACKUP_DIR` | Backup directory inside the container | `/data/backups` |
| `LOG_DIR` | Audit-log directory inside the container | `/data/logs` |
| `LOG_LEVEL` | Application log level | `INFO` |
| `MCP_HOST` | Listen address inside the container | `0.0.0.0` |
| `MCP_PORT` | Listen port inside the container | `8000` |
| `MCP_PUBLISH_HOST` | Host address to which Compose publishes the port | `127.0.0.1` |
| `MCP_PUBLISH_PORT` | Published host port | `8765` |
| `MCP_ISSUER_URL` | Static-auth issuer identifier; use the public base URL remotely | `http://127.0.0.1:8765` |
| `MCP_ALLOWED_HOSTS` | Comma-separated Host header allowlist | `127.0.0.1:*,localhost:*` |
| `MCP_ALLOWED_ORIGINS` | Comma-separated Origin header allowlist | local origins |

For a remote endpoint at `https://ha-admin.example.com/mcp`, add
`ha-admin.example.com` to `MCP_ALLOWED_HOSTS`, add
`https://ha-admin.example.com` to `MCP_ALLOWED_ORIGINS`, and set
`MCP_ISSUER_URL=https://ha-admin.example.com`.

## Authentication

There are two separate trust directions:

```text
MCP client -- MCP_CLIENT_TOKEN --> HA Admin MCP
HA Admin MCP -- HA_TOKEN -------> Home Assistant API
```

Never reuse one token for both purposes.

`MCP_CLIENT_TOKEN` is compared in constant time. Invalid bearer bursts receive
a bounded application-level delay. Credentials are never intentionally
returned in errors and both configured secrets are redacted from mutation audit
records.

Protect `.env` as a secret. It is ignored by Git and excluded from the Docker
build context.

## Connecting Codex

Codex supports Streamable HTTP MCP servers and can load the bearer token from
an environment variable:

```bash
export HA_ADMIN_MCP_TOKEN='your-mcp-client-token'
codex mcp add ha-admin-mcp \
  --url http://127.0.0.1:8765/mcp \
  --bearer-token-env-var HA_ADMIN_MCP_TOKEN
codex mcp list
```

Equivalent `~/.codex/config.toml`:

```toml
[mcp_servers.ha-admin-mcp]
url = "http://127.0.0.1:8765/mcp"
bearer_token_env_var = "HA_ADMIN_MCP_TOKEN"
```

Keep the token in the process environment or a protected launcher/secret
manager, not in `config.toml`. For remote access, replace the URL with the
user's own HTTPS endpoint.

See [client configuration](docs/client-configuration.md) for a generic example
and the complementary official Home Assistant MCP layout.

## Remote and private access

Remote access is optional. The default Compose binding is loopback-only and
requires no domain.

Supported deployment patterns include:

- local host: `http://127.0.0.1:8765/mcp`
- trusted LAN or private DNS: `http://home-server.local:8765/mcp`
- Tailscale or another private VPN
- SSH local forwarding
- an HTTPS reverse proxy
- Cloudflare Tunnel or another secure HTTPS tunnel

Do not directly expose raw TCP port 8765 to the public internet. For remote
clients, use HTTPS, preserve the `Authorization` header, disable caching and
proxy buffering, and allow Streamable HTTP's GET/POST behaviour and streaming
responses.

Cloudflare Tunnel is one optional approach, not a dependency. See
[remote access](docs/remote-access.md).

## Example workflow

A harmless conceptual request might be:

> Create an automation that sends a persistent Home Assistant notification at
> sunset, but leave it disabled until I approve it.

The client should first call `preview_ai_automation`. Creation then backs up the
current AI file, writes and validates the candidate, reloads automations and
verifies health. The client can inspect the result before explicitly enabling
the automation.

## Backups and logs

Backups are stored under `data/backups` by the default Compose deployment. Use
`list_ai_automation_backups`, `diff_ai_automations` and
`rollback_ai_automations` rather than editing backup files manually.

Mutation audit records are JSON Lines under:

```text
data/logs/mutations.jsonl
```

Both locations are ignored by Git.

## Testing

Create a development environment:

```bash
python3 -m venv .venv
.venv/bin/pip install -r requirements-dev.txt
```

Run the standard checks:

```bash
.venv/bin/ruff format --check .
.venv/bin/ruff check .
.venv/bin/mypy app scripts
.venv/bin/pytest -q
docker build -t ha-admin-mcp:test .
```

The real-API integration tests are read-only and require explicit environment
variables:

```bash
export HA_INTEGRATION_URL='http://homeassistant:8123'
export HA_INTEGRATION_TOKEN='your-test-token'
.venv/bin/pytest -q -m integration
unset HA_INTEGRATION_TOKEN
```

`scripts/live_acceptance.py` is not part of the normal suite. It deliberately
creates, enables, triggers and removes a harmless automation in the isolated AI
file. Read the script and use a disposable test instance before running it.

## Troubleshooting

### Home Assistant authentication fails

Confirm `HA_TOKEN` is a valid long-lived access token and that `HA_URL` is
reachable from inside the container:

```bash
docker compose exec ha-admin-mcp python -c \
  "import os, urllib.request; r=urllib.request.Request(os.environ['HA_URL'] + '/api/', headers={'Authorization': 'Bearer ' + os.environ['HA_TOKEN']}); print(urllib.request.urlopen(r).status)"
```

Do not paste the token into an issue or command output.

### MCP returns HTTP 401

The client's bearer value does not exactly match `MCP_CLIENT_TOKEN`, or the
client did not send an `Authorization: Bearer` header. Restart the MCP client
after exporting its token environment variable.

### MCP returns HTTP 421 or 403

Add the exact hostname to `MCP_ALLOWED_HOSTS`. If the client sends an Origin
header, add that exact origin to `MCP_ALLOWED_ORIGINS`. Do not disable DNS
rebinding protection globally.

### Home Assistant validation rejects a change

The service automatically restores the previous bytes. Inspect the MCP error,
Home Assistant logs and the created backup. Correct the candidate and preview
it again.

### Permission denied on the AI file

Confirm that `/ha-ai` maps to the intended dedicated directory and that UID/GID
10001 can create temporary files and atomically rename them there.

### Reverse proxy or handshake problems

Verify that the proxy:

- forwards `Authorization`, `Content-Type`, `Accept` and MCP headers
- permits GET and POST at `/mcp`
- does not cache, buffer or transform responses
- has a suitable streaming/read timeout
- forwards the expected Host header

## Security

MCP administration access can create and trigger Home Assistant automations.
Treat it as privileged access. Read [SECURITY.md](SECURITY.md) before exposing
the service beyond a trusted host.

## Roadmap

Potential future work, not functionality claimed by this release:

- standards-compliant OAuth
- Home Assistant App/add-on packaging
- a companion HACS integration for setup and diagnostics
- richer onboarding and audit visibility
- Home Assistant trace analysis
- a human-approval automation lifecycle interface

Docker remains the primary installation method for this release.

## Contributing

Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).

## Licence

Licensed under the [Apache License 2.0](LICENSE).