## File: README.md

> **Breaking change (v7.3.0):** `ha_config_set_yaml` has been moved to [beta](docs/beta.md).



---



---

## 🚀 Get Started

The recommended way to run ha-mcp is the **HA-MCP Custom Component**. It installs into Home Assistant through HACS, runs the full server **in-process**, and works on **every** Home Assistant installation type — Home Assistant OS, Supervised, Container, and Core — with full feature parity. It is the easiest setup in every case, with no access token to manage.

**Add it to Home Assistant via HACS (the preferred install):**

[](https://my.home-assistant.io/redirect/hacs_repository/?owner=homeassistant-ai&repository=ha-mcp-integration&category=integration)

**Quick start:**

1. Install the **HA-MCP Custom Component** from HACS — click the badge above, or in HACS open **Integrations → ⋮ → Custom repositories**, add `https://github.com/homeassistant-ai/ha-mcp-integration` (category: **Integration**), then **Download**.
2. **Restart Home Assistant.**
3. Go to **Settings → Devices & Services → Add Integration**, search for **HA-MCP Custom Component**, choose **HA-MCP Server**, and click **Submit**. Creating the entry starts the server.
4. Copy the connect URL from the entry's **Configure** screen (**Settings → Devices & Services → HA-MCP Custom Component → HA-MCP Server → Configure**) — it is also printed in the Home Assistant log. A notification confirms the server started and points you there.
5. Paste that URL into your AI client — done.

**Connect URL.** The Configure screen gives you a Home Assistant webhook URL for remote clients — `https://<your-ha-domain>/api/webhook/<webhook-id>` through Nabu Casa or any reverse proxy already pointed at Home Assistant (locally, `http://<ha-host>:8123/api/webhook/<webhook-id>`). For clients on the same network, the server is also reachable directly at `http://<ha-ip>:9584/private_<random>`.

- **Replaces other install methods:** the in-process server is a complete, standalone ha-mcp install — it takes the place of the app (add-on), Docker, and uvx/PyPI (stdio) methods. Run only one; do not run the in-process server alongside another install.
- **Local only?** Turn off **Remote access via webhook** in the entry options — no webhook is registered at all, while the direct port and sidebar panel keep working.
- **Settings panel:** while the server runs, an admin-only **HA-MCP** panel appears in the Home Assistant sidebar for managing tools, feature flags, backups, and themes.
- **Optional authentication:** set **Webhook authentication** to `ha_auth` to require a Home Assistant account sign-in instead of using the secret URL as the credential.
- **Manual install (no HACS):** copy `custom_components/ha_mcp_tools/` from this repository into your Home Assistant `config/custom_components/` directory, then restart and add the integration as above.

The component's second entry type, the **File & YAML services entry** (**HA-MCP File & YAML Tools**), is only needed if you enable ha-mcp's opt-in file and YAML editing tools (feature flags, off by default) — skip it otherwise; you can add it later at any time. It works with any server type (in-process, app, Docker, or stdio).

[Full in-process server documentation →](docs/in-process-server.md) · [Setup Wizard for client-specific config →](https://homeassistant-ai.github.io/ha-mcp/setup/)

### 🏠 Home Assistant app (add-on)

Prefer to run ha-mcp as a Home Assistant **app (add-on)**? On **Home Assistant OS** and **Supervised** installs it is a close second — no access token to manage, and it works with Claude Desktop, Claude.ai, ChatGPT, and any other MCP client on your local network or configured for remote access.

1. Add the repository to your Home Assistant instance:

   [](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Fhomeassistant-ai%2Fha-mcp)

   If that opens the App store without an add-repository dialog (a [known Home Assistant issue](https://github.com/home-assistant/my.home-assistant.io/issues/698)), add it manually: **Settings → Apps → Install app → ⋮ → Repositories**, then paste `https://github.com/homeassistant-ai/ha-mcp`.

2. Install **"Home Assistant MCP Server"** from **Settings → Apps → Install app** and click **Start**. *(Home Assistant 2026.2 renamed "Add-ons" to "Apps"; on older versions this is the Add-on store.)*
3. Open the **Logs** tab to find your unique MCP URL.
4. Connect your AI client to that URL — **no token or credential setup needed**.

[Full app documentation →](homeassistant-addon/DOCS.md)

> ⚠️ **Configure exactly one install method per client.** The custom component, the app, Docker/PyPI, and local stdio are independent ways to run the same server — pick one and point your AI client at that single URL. Keeping two entries for the same server in one client (for example a local `uvx ha-mcp@latest` entry with `HOMEASSISTANT_URL` / `HOMEASSISTANT_TOKEN` alongside an app or component URL) is a known cause of connection hangs.

### Other install methods

These run the server outside Home Assistant — useful for **Container** / **Core** installs (which can't run apps) or a separate host. The [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/) generates the exact client-specific config for each.

- **Docker (HTTP server):** run `ghcr.io/homeassistant-ai/ha-mcp` in HTTP mode, pointed at your Home Assistant URL and a long-lived token, and connect your client to its secret URL. See the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/) for the full command and per-client config.
- **PyPI / uvx (HTTP server):** run the published `ha-mcp` package with `uvx ha-mcp@latest` (or pip) as a streamable-HTTP server the same way. Details in the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/).
- **Local stdio (not recommended):** runs ha-mcp on your own machine over stdio. The one-command installers in the **Demo server** section below use this path; the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/) covers connecting it to your own Home Assistant.
- **OIDC authentication:** gate remote access behind an external identity provider (Authentik, Keycloak, Auth0, etc.) instead of a secret URL — all authenticated users share the server's Home Assistant credentials. See [OIDC Mode](docs/oidc.md).

  > ⚠️ **stdio has known transport issues.** The stdio transport has connection problems that streamable HTTP does not ([#1713](https://github.com/homeassistant-ai/ha-mcp/issues/1713)). It is recommended only for demo/testing tinkering — for a real setup, use the custom component or an HTTP method above.


**🌐 Remote access (Nabu Casa / Webhook Proxy app / OpenAI Tunnel)**

> **Using the HA-MCP custom component?** You do not need the **Webhook Proxy** — the component has its own built-in webhook for remote access (see the **Get Started** quick start at the top). The proxy is for the **app** (it can also front another external server via its `mcp_server_url` option). The **OpenAI Tunnel** below is different: it applies to **any** install method when Home Assistant isn't publicly reachable at all (no Nabu Casa or reverse proxy).

Already have **Nabu Casa** or another reverse proxy pointing at your Home Assistant? The Webhook Proxy app routes MCP traffic through your existing setup — no separate tunnel or port forwarding needed.

1. Install the **MCP Server app** (see above) and the **Webhook Proxy** app from the same store
2. Start the webhook proxy and **restart Home Assistant** when prompted
3. Copy the webhook URL from the app logs:
   ```
   MCP Server URL (remote): https://xxxxx.ui.nabu.casa/api/webhook/mcp_xxxxxxxx
   ```
4. Configure your AI client with that URL

For other remote access methods (Cloudflare Tunnel, custom reverse proxy), see the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/).

**ChatGPT / Codex behind a firewall (OpenAI Tunnel).** ChatGPT connectors normally require a publicly reachable URL. If you can't (or don't want to) expose one, the community [OpenAI Tunnel for HA-MCP](https://github.com/norpol/hass-codex-tunnel-mcp) integration by [@norpol](https://github.com/norpol) runs OpenAI's [`tunnel-client`](https://github.com/openai/tunnel-client) inside Home Assistant and connects your local MCP server URL to an OpenAI-hosted tunnel over an outbound-only connection — no port forwarding, reverse proxy, or public URL. Point it at your ha-mcp URL, then attach the ChatGPT connector to the same tunnel ID. See the [FAQ entry](https://homeassistant-ai.github.io/ha-mcp/faq/#openai-tunnel) and [#1811](https://github.com/homeassistant-ai/ha-mcp/issues/1811).

[Webhook proxy documentation →](https://github.com/homeassistant-ai/ha-mcp/blob/master/homeassistant-addon-webhook-proxy/DOCS.md)



### 🧪 Demo server (Windows / macOS / Linux)

Want to try ha-mcp before connecting your own Home Assistant? *No paid subscription required.* These one-command scripts set up a local **stdio** connection to a hosted **demo** environment so you can see it working in a few minutes. Each script's **Connect your own Home Assistant** link then shows how to point it at your instance.


**🍎 macOS**

1. Go to [claude.ai](https://claude.ai) and sign in (or create a free account)
2. Open **Terminal** and run:
   ```sh
   curl -LsSf https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/scripts/install-macos.sh | sh
   ```
3. [Download Claude Desktop](https://claude.ai/download) (or restart: Claude menu → Quit)
4. Ask Claude: **"Can you see my Home Assistant?"**

You're now connected to the demo environment! [Connect your own Home Assistant →](https://homeassistant-ai.github.io/ha-mcp/guide-macos/#step-6-connect-your-home-assistant)




**🐧 Linux**

Anthropic doesn't ship Claude Desktop for Linux, so pick one path:

**Claude Desktop** — free, via the community build:

1. Install the community [Claude Desktop for Linux](https://github.com/aaddrick/claude-desktop-debian) build and sign in with a free [claude.ai](https://claude.ai) account
2. Open **Terminal** and run:
   ```sh
   curl -LsSf https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/scripts/install-linux.sh | sh
   ```
3. Restart Claude Desktop, then ask: **"Can you see my Home Assistant?"**

**Claude Code** — official CLI, requires a paid Claude plan:

1. Install Claude Code: `curl -fsSL https://claude.ai/install.sh | bash`
2. Configure ha-mcp, then run `claude`:
   ```sh
   curl -LsSf https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/scripts/install.sh | sh -s -- --claude-code
   ```
3. Start `claude`, run `/mcp` to confirm, then ask: **"Can you see my Home Assistant?"**

[Full Linux guide →](https://homeassistant-ai.github.io/ha-mcp/guide-linux/)




**🪟 Windows**

1. Go to [claude.ai](https://claude.ai) and sign in (or create a free account)
2. Open **Windows PowerShell** (from Start menu) and run:
   ```powershell
   irm https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/scripts/install-windows.ps1 | iex
   ```
3. [Download Claude Desktop](https://claude.ai/download) (or restart: File → Exit)
4. Ask Claude: **"Can you see my Home Assistant?"**

You're now connected to the demo environment! [Connect your own Home Assistant →](https://homeassistant-ai.github.io/ha-mcp/guide-windows/#step-6-connect-your-home-assistant)



### 🧙 Setup Wizard for 15+ clients

**Claude Code, Gemini CLI, ChatGPT, Open WebUI, VSCode, Cursor, and more.**


[](https://homeassistant-ai.github.io/ha-mcp/setup/)


Having issues? Check the **[FAQ & Troubleshooting](https://homeassistant-ai.github.io/ha-mcp/faq/)**

---

## 💬 What Can You Do With It?

Just talk to Claude naturally. Here are some real examples:

| You Say | What Happens |
|---------|--------------|
| *"Create an automation that turns on the porch light at sunset"* | Creates the automation with proper triggers and actions |
| *"Add a weather card to my dashboard"* | Updates your Lovelace dashboard with the new card |
| *"The motion sensor automation isn't working, debug it"* | Analyzes execution traces, identifies the issue, suggests fixes |
| *"Make my morning routine automation also turn on the coffee maker"* | Reads the existing automation, adds the new action, updates it |
| *"Create a script that sets movie mode: dim lights, close blinds, turn on TV"* | Creates a reusable script with the sequence of actions |

Spend less time configuring, more time enjoying your smart home.

---

## ✨ Features

| Category | Capabilities |
|----------|--------------|
| **🔍 Search** | Fuzzy entity search, deep config search, system overview |
| **🏠 Control** | Any service, bulk device control, real-time states |
| **🔧 Manage** | Automations, scripts, helpers, dashboards, areas, zones, groups, calendars, blueprints |
| **📊 Monitor** | History, statistics, camera snapshots, automation traces, ZHA devices |
| **💾 System** | Backup/restore, updates, apps, device registry |
| **🔒 Safety** | Read Only Mode toggle, per-tool enable/disable, tool security policies (user approval), automatic edit backups |




**Complete Tool List (88 tools)**

| Category | Tools |
|----------|-------|
| **Apps (add-ons)** | `ha_get_app`, `ha_manage_app` |
| **Areas & Floors** | `ha_list_floors_areas`, `ha_remove_area_or_floor`, `ha_set_area_or_floor` |
| **Assist** | `ha_manage_pipeline` |
| **Automations** | `ha_config_get_automation`, `ha_config_remove_automation`, `ha_config_set_automation` |
| **Blueprints** | `ha_get_blueprint`, `ha_import_blueprint` |
| **Calendar** | `ha_config_get_calendar_events`, `ha_config_remove_calendar_event`, `ha_config_set_calendar_event` |
| **Camera** | `ha_get_camera_image` |
| **Dashboard** | `ha_get_dashboard_screenshot` *(beta)* |
| **Dashboards** | `ha_config_delete_dashboard_resource`, `ha_config_delete_dashboard`, `ha_config_get_dashboard`, `ha_config_list_dashboard_resources`, `ha_config_set_dashboard_resource`, `ha_config_set_dashboard` |
| **Developer** | `ha_dev_manage_server`, `ha_dev_manage_settings` |
| **Device Registry** | `ha_get_device`, `ha_remove_device`, `ha_set_device` |
| **Energy** | `ha_manage_energy_prefs` |
| **Entity Registry** | `ha_get_entity_exposure`, `ha_get_entity`, `ha_remove_entity`, `ha_set_entity` |
| **Files** | `ha_delete_file` *(beta)*, `ha_list_files` *(beta)*, `ha_read_file` *(beta)*, `ha_write_file` *(beta)* |
| **Groups** | `ha_config_list_groups`, `ha_config_remove_group`, `ha_config_set_group` |
| **HACS** | `ha_get_hacs_info`, `ha_manage_hacs` |
| **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_remove_helpers_integrations` |
| **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs` |
| **Integrations** | `ha_get_integration`, `ha_get_system_health`, `ha_set_integration` |
| **Labels & Categories** | `ha_config_get_category`, `ha_config_get_label`, `ha_config_remove_category`, `ha_config_remove_label`, `ha_config_set_category`, `ha_config_set_label` |
| **Matter** | `ha_manage_radio` |
| **Scenes** | `ha_config_get_scene`, `ha_config_remove_scene`, `ha_config_set_scene` |
| **Scripts** | `ha_config_get_script`, `ha_config_remove_script`, `ha_config_set_script` |
| **Search & Discovery** | `ha_get_overview`, `ha_get_state`, `ha_search` |
| **Service & Device Control** | `ha_bulk_control`, `ha_call_event`, `ha_call_service`, `ha_get_operation_status`, `ha_list_services` |
| **System** | `ha_config_get_yaml` *(beta)*, `ha_config_set_yaml` *(beta)*, `ha_manage_backup`, `ha_manage_custom_tool` *(beta)*, `ha_manage_security_policy`, `ha_manage_theme`, `ha_manage_updates`, `ha_reload_core`, `ha_restart` |
| **Todo Lists** | `ha_get_todo`, `ha_remove_todo_item`, `ha_set_todo_item` |
| **Utilities** | `ha_eval_template`, `ha_report_issue` |
| **Zones** | `ha_get_zone`, `ha_remove_zone`, `ha_set_zone` |




---

## 🆚 ha-mcp vs. Home Assistant's built-in MCP Server

Home Assistant ships its own [MCP Server integration](https://www.home-assistant.io/integrations/mcp_server/). It is built on the **Assist** pipeline, so a connected MCP client can read and control the entities you have exposed to Assist and run the intents Assist understands — handy for voice-style control of already-exposed devices.

ha-mcp is a standalone server built for **configuring, building, and debugging** your smart home, not just controlling it. On top of device control, it adds capabilities the built-in integration does not have:

| Capability | Built-in MCP Server | ha-mcp |
|------------|:-------------------:|:------:|
| Control exposed devices, query states | Yes | Yes |
| Entity scope | Only entities exposed to Assist | Everything in Home Assistant |
| Create / edit automations, scripts, scenes | No | Yes |
| Build & edit dashboards | No | Yes |
| Debug automations from traces, read history & logs | No | Yes |
| Manage helpers, areas, zones, labels, groups | No | Yes |
| Backups, apps, HACS, device & entity registry | No | Yes |

**Rule of thumb:** Use the built-in integration for voice-style control of devices you have already exposed; use ha-mcp when you want an AI assistant that can also build and maintain your Home Assistant setup.

---

## 🔌 Custom Component (ha_mcp_tools) — File & YAML Services

The **HA-MCP Custom Component** also powers a set of privileged tools that standard Home Assistant APIs can't provide: file system access and YAML config editing. (The same component runs the full server in-process — that's the recommended install in the **Get Started** section at the top.) Its **File & YAML services entry** (**HA-MCP File & YAML Tools**) enables the tools below.

**Tools that require the component:**

| Tool | Description |
|------|-------------|
| `ha_config_set_yaml` *(beta)* | Safely add, replace, or remove top-level YAML keys in `configuration.yaml` and package files (automatic backup, validation, and config check) |
| `ha_list_files` *(beta)* | List files in allowed directories |
| `ha_read_file` *(beta)* | Read files from allowed paths (config YAML, logs, and allowed directories) |
| `ha_write_file` *(beta)* | Write files to allowed directories |
| `ha_delete_file` *(beta)* | Delete files from allowed directories |

All other tools work without the component. These five return an error with installation instructions if the component is missing.

These tools also require beta feature flags. See **[Beta Features](docs/beta.md)** for how to enable them — including the `ENABLE_BETA_FEATURES` master flag, which must be on before the filesystem/YAML sub-flags take effect.

### Install

Install the **HA-MCP File & YAML Tools** entry from the same **HA-MCP Custom Component**:

[](https://my.home-assistant.io/redirect/hacs_repository/?owner=homeassistant-ai&repository=ha-mcp-integration&category=integration)

To add manually: open **HACS** > **Integrations** > three-dot menu > **Custom repositories** > add `https://github.com/homeassistant-ai/ha-mcp-integration` (category: Integration) > **Download**. Or copy `custom_components/ha_mcp_tools/` from this repository into your HA `config/custom_components/` directory.

After installing, restart Home Assistant, then open **Settings** > **Devices & Services** > **Add Integration**, search for **HA-MCP Custom Component**, and add the **HA-MCP File & YAML Tools** entry.

To run the full ha-mcp server in-process through this same component, see the **Get Started** section at the top and the [full in-process server documentation →](docs/in-process-server.md).

---

## 🧠 Better Results with Agent Skills

This server gives your AI agent tools to control Home Assistant. For better configurations, pair it with [Home Assistant Agent Skills](https://github.com/homeassistant-ai/skills) — domain knowledge that teaches the agent Home Assistant best practices.

An MCP server can create automations, helpers, and dashboards, but it has no opinion on *how* to structure them. Without domain knowledge, agents tend to over-rely on templates, pick the wrong helper type, or produce automations that are hard to maintain. The skills fill that gap: native constructs over Jinja2 workarounds, correct helper selection, safe refactoring workflows, and proper use of automation modes.

### Bundled Skills (built-in)

Skills from `homeassistant-ai/skills` are bundled and served as [MCP resources](https://modelcontextprotocol.io/docs/concepts/resources) via `skill://` URIs. Any MCP client that supports resources can discover them automatically — no manual installation needed. For tool-only clients (claude.ai, etc.), the same skills are reachable through the polymorphic `ha_get_skill_guide` tool — call it with no args to list bundled skills, with a `skill` arg to list its files, or with `skill` + `file` to read content. Resources are not auto-injected into context — clients must explicitly request them, so idle context cost is just the metadata listing.

`ha_get_skill_guide` is a mandatory tool: the catalog always exposes it (it can't be disabled) so tool-only clients never see a silently missing skill surface.

Skills can still be installed manually for clients that prefer local skill files — see the [skills repo](https://github.com/homeassistant-ai/skills) for instructions.

---

## 🔍 Tool Discovery for AI Agents

By default, the full tool catalog (~84 tools) is listed to the client through the standard MCP `tools/list` response. Clients with deferred / on-demand tool loading (claude.ai, Claude Desktop, Claude Code) handle that fine — tools are pulled into context only when needed, so idle context cost is near-zero.

For setups *without* deferred tool support — models like Claude Haiku, Gemini, OpenAI-compatible local models and smaller open-weights models, or clients that inline all tool schemas regardless of model (e.g. GitHub Copilot CLI) — listing the full tool catalog up front adds a lot of idle context and can overwhelm smaller models. To address that, the server ships with a **search-based discovery mode** built on top of FastMCP's BM25 search transform.

### Smaller or local LLMs (Ollama, etc.)

If your model can't see the tools or your Home Assistant, it may be getting handed the whole tool catalog at once and struggling with it. It's recommended to try the following to see if it helps:

- **Enable tool search** (`ENABLE_TOOL_SEARCH=true`, or the app option below). Instead of listing every tool up front, the server defers the catalog behind a search interface so the model pulls in only the tools it needs, when it needs them.
- **Raise the model's context window above the default.** Local runtimes ship with small defaults (Ollama's `num_ctx` is one example) that can't hold a large tool set plus the conversation — increase it well beyond the default.

### Enable search-based discovery

Set ENABLE_TOOL_SEARCH=true (or toggle the option in the HA app). The full catalog is replaced in the tool list with four entry points plus a small set of always-visible "pinned" tools (ha_search, ha_get_overview, ha_report_issue, etc.). All tools remain callable directly by name once discovered:

| Tool | Purpose |
|------|---------|
| `ha_search_tools` | BM25 keyword search across all tools. Returns name, description, parameters, and annotations (`readOnlyHint` / `destructiveHint`) so the agent can pick the right one. |
| `ha_call_read_tool` | Execute a `readOnlyHint` tool by name. Safe — clients can auto-approve. |
| `ha_call_write_tool` | Execute a write tool that creates or updates data. |
| `ha_call_delete_tool` | Execute a tool that removes / deletes data. |

The proxy split lets MCP clients apply different permission policies per category (e.g. auto-approve reads, prompt for writes, confirm deletes) without parsing tool docstrings.

| Setting | Default | Description |
|---------|---------|-------------|
| `ENABLE_TOOL_SEARCH` | `false` | Replace full tool catalog with search-based discovery (tools deferred behind on-demand search). |
| `TOOL_SEARCH_MAX_RESULTS` | `5` | Max results returned by `ha_search_tools` (range 2–10). |
| `PINNED_TOOLS` | empty | Comma-separated tool names to keep always visible. The web settings UI is the primary way to manage this. |

### When to enable

- **Claude Haiku, OpenAI-compatible local models, Gemini, or any model without native deferred tool support** — large idle-context savings. The same applies to clients that inline all tool schemas regardless of model (e.g. GitHub Copilot CLI, even when running Claude Sonnet/Opus).
- MCP clients that cap total tool count (some cap at 100) — surfaces a minimal set (~10 tools) instead of 84.
- **Cost-sensitive deployments** — fewer idle tokens per turn.

Leave it off in clients with deferred tool loading (claude.ai, Claude Desktop, Claude Code); the full catalog has no idle cost there, direct calls skip the search step, and the client's built-in tool search is the better choice — there is no benefit to running ha-mcp's on top of it. Whether tools are deferred depends on the client and model combination: the same model can behave differently per client — GitHub Copilot CLI running Claude Sonnet/Opus inlines the full catalog and still benefits from tool search here. Some Codex models and ChatGPT include deferred tools too — check your client/model directly to confirm its features so you don't leave this enabled unnecessarily.

> 🔄 **Refresh your client's tool list after changing this (or any) setting.** Toggling `ENABLE_TOOL_SEARCH` (or changing pinned/disabled tools, Read Only Mode, etc.) changes the tools the server exposes, but your AI client keeps serving its **cached** tool list until it re-fetches. Restarting the app or Home Assistant does **not** refresh the client — reconnect or refresh the MCP server in your client (e.g. re-add/refresh the connector in ChatGPT, or close and reopen Claude Desktop). If you skip this, newly enabled tools won't appear in the client at all, and tools the server no longer exposes still show as available but return `Unknown tool` when called. ChatGPT sometimes keeps serving the stale list even after the connector is removed and re-added under the same name — if tools are still missing after re-adding, delete the connector and create a new one with a **different name**.

For the HA app, the same option is documented in [`homeassistant-addon/DOCS.md`](homeassistant-addon/DOCS.md#enable_tool_search) along with the in-app settings UI for fine-grained tool enable/disable/pin.

---

## 🧪 Dev Channel

Want early access to new features and fixes? Dev releases (`.devN`) are published on every push to master.

**[Dev Channel Documentation](docs/dev-channel.md)** — Instructions for pip/uvx, Docker, and Home Assistant app.

---

## 🤝 Contributing

For development setup, testing instructions, and contribution guidelines, see **[CONTRIBUTING.md](CONTRIBUTING.md)**.

For comprehensive testing documentation, see **[tests/README.md](tests/README.md)**.

---

## 🔒 Privacy

Ha-mcp runs **locally** on your machine. Your smart home data stays on your network.

- **No telemetry today** — anonymous usage stats are a planned future feature (as of June 2026); when it lands it will follow your Home Assistant analytics/telemetry setting (which you can override), announced prominently in the release notes and the web Settings UI at least one month beforehand
- **No personal data collection** — we never collect entity names, configs, or device data
- **User-controlled bug reports** — only sent with your explicit approval

For full details, see our [Privacy Policy](PRIVACY.md).

---

## 📄 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

---

## 🙏 Acknowledgments

- **[Home Assistant](https://home-assistant.io/)**: Amazing smart home platform (!)
- **[FastMCP](https://github.com/jlowin/fastmcp)**: Excellent MCP server framework
- **[Model Context Protocol](https://modelcontextprotocol.io/)**: Standardized AI-application communication
- **[Claude Code](https://github.com/anthropics/claude-code)**: AI-powered coding assistant
- **[PolicyLayer](https://policylayer.com/)**: Argument-path predicate DSL shape (`args.domain in [...]` with `eq`/`in`/`regex`/`contains`/`exists`/...) inspired the per-tool approval rule schema (#966).

## 👥 Contributors

### Maintainers

- **[@julienld](https://github.com/julienld)** — Project creator.
- **[@sergeykad](https://github.com/sergeykad)** — Core maintainer.
- **[@kingpanther13](https://github.com/kingpanther13)** — Core maintainer.
- **[@Patch76](https://github.com/Patch76)** — Core maintainer.

### Contributors

- **[@bigeric08](https://github.com/bigeric08)** — Explicit `mcp` dependency for protocol version 2025-11-25 support.
- **[@airlabno](https://github.com/airlabno)** — Support for `data` field in schedule time blocks.
- **[@ryphez](https://github.com/ryphez)** — Codex Desktop UI MCP quick setup guide.
- **[@Danm72](https://github.com/Danm72)** — Entity registry tools (`ha_set_entity`, `ha_get_entity`) for managing entity properties.
- **[@Raygooo](https://github.com/Raygooo)** — SOCKS proxy support.
- **[@cj-elevate](https://github.com/cj-elevate)** — Integration & entity management tools (enable/disable/delete); person/zone/tag config store routing.
- **[@maxperron](https://github.com/maxperron)** — Beta testing.
- **[@kingbear2](https://github.com/kingbear2)** — Windows UV setup guide.
- **[@konradwalsh](https://github.com/konradwalsh)** — Financial support via [GitHub Sponsors](https://github.com/sponsors/julienld). Thank you! ☕
- **[@knowald](https://github.com/knowald)** — Area resolution via device registry in `ha_get_system_overview` for entities assigned through their parent device. Financial support via [GitHub Sponsors](https://github.com/sponsors/julienld). Thank you! ☕
- **[@zorrobyte](https://github.com/zorrobyte)** — Per-client WebSocket credentials in OAuth mode, fixing WebSocket tool failures.
- **[@deanbenson](https://github.com/deanbenson)** — Fixed `ha_deep_search` timeout on large Home Assistant instances with many automations.
- **[@saphid](https://github.com/saphid)** — Config entry options flow tools (initial design, #590).
- **[@adraguidev](https://github.com/adraguidev)** — Fix menu-based config entry flows for group helpers (#647).
- **[@transportrefer](https://github.com/transportrefer)** — Integration options inspection (`ha_get_integration` schema support, #689).
- **[@teh-hippo](https://github.com/teh-hippo)** — Fix blueprint import missing save step.
- **[@smenzer](https://github.com/smenzer)** — Documentation fix.
- **[@The-Greg-O](https://github.com/The-Greg-O)** — REST API for config entry deletion.
- **[@restriction](https://github.com/restriction)** — Responsible disclosure: python_transform sandbox missing call target validation.
- **[@lcrostarosa](https://github.com/lcrostarosa)** — Diagnostic and health monitoring tools concept (#675), inspiring system/error logs, repairs, and ZHA radio metrics integration.
- **[@roysha1](https://github.com/roysha1)** — Copilot CLI support in the installation wizard; replaced placeholder logo SVGs with real brand icons on the documentation site.
- **[@teancom](https://github.com/teancom)** — Fix add-on stats endpoint (`/addons/{slug}/stats`).
- **[@TomasDJo](https://github.com/TomasDJo)** — Category support for automations, scripts, and scenes.
- **[@bzelch](https://github.com/bzelch)** — `python_transform` support for automations and scripts.
- **[@gcormier](https://github.com/gcormier)** — Windows installer improvements: removed unused variable and fixed terminal closing after install.
- **[@ekobres](https://github.com/ekobres)** — Feature flags for `HAMCP_ENABLE_FILESYSTEM_TOOLS` and the (since removed) `HAMCP_ENABLE_CUSTOM_COMPONENT_INTEGRATION` in the app config, with beta tagging in source and docs.
- **[@w3z315](https://github.com/w3z315)** — Financial support via [GitHub Sponsors](https://github.com/sponsors/julienld). Thank you! ☕
- **[@griffinmartin](https://github.com/griffinmartin)** — Added OpenCode (by Anomaly) as a selectable AI client in the setup wizard, with both stdio and streamable HTTP support.
- **[@hhopke](https://github.com/hhopke)** — Fixed app (add-on) API calls to route through HA Core ingress proxy instead of direct container connections, fixing `ha_manage_addon` (now `ha_manage_app`) proxy mode on app installs.
- **[@tomwilkie](https://github.com/tomwilkie)** — JMESPath middleware exploration (#1147) whose review-time token-measurement data informed the design of #1199 and #1225.
- **[@SealKan](https://github.com/SealKan)** — `fields=`/`attribute_keys=` projection on six read-heavy tools (#1225), `ha_call_event` tool (#1239), dashboards-list helper refactor (#1207), `for:`-field duration-math detector in the best-practice checker (#1264), persistent DCR OAuth client registrations across restarts (#1265), and issue-triage prompt token-budgeting (#1522).
- **[@KarelTestSpecial](https://github.com/KarelTestSpecial)** — Cached YAML instance to prevent CPU spikes during bulk edits (#1371).
- **[@corgan2222](https://github.com/corgan2222)** — HA brand assets for custom integration (#1317).
- **[@drseanwing](https://github.com/drseanwing)** — Progress emission via FastMCP `Context` in long-running tools (#1124); tool-discovery / categorized-search docs (#1123).
- **[@fnordpig](https://github.com/fnordpig)** — Config subentry support (#1393) and Assist pipeline management tool (#1392).
- **[@paul43210](https://github.com/paul43210)** — `array_patch` mode in `ha_manage_app` for atomic GET-modify-POST (#1063).
- **[@L1AD](https://github.com/L1AD)** — Filed #966 proposing tool security policies; pointed to PolicyLayer's MCP-security work as prior art that inspired the predicate DSL shape.
- **[@nightcityblade](https://github.com/nightcityblade)** — Updated stale Home Assistant Advanced Mode references after HA 2026.6 made formerly advanced options available by default (#1533).
- **[@emmelutzer](https://github.com/emmelutzer)** — Financial support via [GitHub Sponsors](https://github.com/sponsors/julienld). Thank you! ☕
- **[@pkkr](https://github.com/pkkr)** — `ha_knx_get_project` tool exposing KNX group addresses from an uploaded ETS project file.
- **[@cbowns](https://github.com/cbowns)** — Fixed inconsistent hyphen in setup.astro Codex CLI docs.
- **[@Shaan-alpha](https://github.com/Shaan-alpha)** — Extended `ha_restart` known-good error patterns to cover 502/503 responses from reverse proxies.
- **[@rebelancap](https://github.com/rebelancap)** — Fixed UTC-to-local timezone conversion in `add_timezone_metadata`.
- **[@saevras](https://github.com/saevras)** — Fixed blueprint import E2E test to use local URL instead of host-to-container networking.
- **[@jasonjhofmann](https://github.com/jasonjhofmann)** — Recurring calendar events via `rrule` support in `ha_config_set_calendar_event`.
- **[@vpciii](https://github.com/vpciii)** — Coerce JSON-encoded strings on dict/list tool params.
- **[@pburtchaell](https://github.com/pburtchaell)** — Financial support via [GitHub Sponsors](https://github.com/sponsors/julienld). Thank you! ☕
- **[@norpol](https://github.com/norpol)** — Built the [OpenAI Tunnel for HA-MCP](https://github.com/norpol/hass-codex-tunnel-mcp) companion integration, connecting ChatGPT to a firewalled Home Assistant MCP server (#1811).
---

## 💬 Community

- **[GitHub Discussions](https://github.com/homeassistant-ai/ha-mcp/discussions)** — Ask questions, share ideas
- **[Issue Tracker](https://github.com/homeassistant-ai/ha-mcp/issues)** — Report bugs, request features, or suggest tool behavior improvements

---

## Star History

---

## File: docs/superpowers/specs/2026-05-21-1288-auto-backup-design.md

# Auto-backup edited entities (closes #1288)

**Status:** approved 2026-05-21 (user override on spec-review-subagent and writing-plans gates — direct directive to execute to draft PR)
**Issue:** [#1288](https://github.com/homeassistant-ai/ha-mcp/issues/1288)
**Branch:** `1288-auto-backup` (worktree `worktree/1288-auto-backup/`)

## Goal

Capture per-entity backups before every write/destructive MCP tool call,
expose them for LLM-driven and UI-driven inspection, restore, and delete.
Works in every ha-mcp deployment mode (addon, Docker, uvx) with no custom
component dependency and no addon manifest privilege change.

## Non-goals

- Full HA snapshot rollback — `ha_backup_create` / `ha_backup_restore` already cover that
- Cross-entity transactional restore (each restore is one entity)
- Preserving user comments / whitespace from source YAML files (API-derived format)

## Settings

| Name | Env var | Default | Purpose |
|---|---|---|---|
| `enable_auto_backup` | `ENABLE_AUTO_BACKUP` | `False` | Master toggle |
| `auto_backup_throttle_minutes` | `AUTO_BACKUP_THROTTLE_MINUTES` | `0` | Per-entity throttle window; 0 = no throttle, backup every write |
| `auto_backup_retain_per_entity` | `AUTO_BACKUP_RETAIN_PER_ENTITY` | `20` | Max snapshots kept per entity; oldest rotated out |
| `auto_backup_dir` | `HAMCP_BACKUP_DIR` | `""` (auto) | Override backup directory |

Auto-default for `auto_backup_dir`:
- Addon (`SUPERVISOR_TOKEN` set + `/data` exists): `/data/ha_mcp_backups/`
- Else (Docker/uvx): `${XDG_DATA_HOME:-~/.local/share}/ha_mcp/backups/`

## Architecture

```
write/destructive tool  →  @with_auto_backup  →  BackupManager.maybe_snapshot
                                                       │
                                                       ├── DomainHandler.fetch (HA REST/WS)
                                                       └── write yaml, rotate, return path

ha_manage_backup tool  ─┐
/api/settings/backups        ├──→  BackupManager  (list/read/diff/restore/delete)
Settings UI Backups tab      ─┘
```

**`BackupManager`** (single instance per server, cached on the client object) owns:
- Backup dir resolution and creation
- Per-entity-key `asyncio.Lock` map to serialize fetch+write
- Per-entity-key `last_snapshot_ts` for throttle
- Domain handler registry: `{domain: DomainHandler}`
- File operations (list, read, diff, write, delete, rotate)

**`DomainHandler`** is a frozen dataclass per backup-domain:
```python
@dataclass(frozen=True)
class DomainHandler:
    domain: str                                              # "automation", "helper_input_boolean", "dashboard", ...
    fetch: Callable[[Any, str], Awaitable[Any]]              # (client, entity_id) -> current config (dict | None)
    restore: Callable[[Any, str, Any], Awaitable[Any]]       # (client, entity_id, config) -> result envelope
```

One handler per backed-up domain; helper types each get their own handler keyed `helper_<type>` so each helper type lists/restores independently.

## Write flow (the decorator)

```python
@with_auto_backup(domain="automation", id_param="identifier")
@mcp.tool(...)
@log_tool_usage
async def ha_config_set_automation(self, identifier: str, ...):
    ...
```

`id_param` names the kwarg that carries the entity ID. For helpers, the
decorator takes `domain_fn` instead — a callable that computes the domain
key from kwargs (so `helper_type="timer"` → domain `helper_timer`).

Decorator behavior (best-effort, never raises):
1. Look up manager off `self._client._auto_backup_manager` (lazy-built on first use).
2. If `enable_auto_backup` is False → call wrapped function and return.
3. Resolve `entity_key = f"{domain}:{entity_id}"`. If entity ID is `None`/empty (a *create* call with no ID) → skip backup, call wrapped function.
4. Inside per-key lock: check throttle; if elapsed, fetch + write + rotate.
5. All exceptions from steps 3-4 are logged as WARNING and swallowed.
6. Call wrapped function with original args.

**Why this shape:** the original tool body is fully decoupled from backup;
removing the decorator should not change tool behavior. Backup is never
on the critical path of the write.

## File format

```yaml
# ha_mcp_backup
schema_version: 1
domain: automation
entity_id: kitchen_lights
captured: 2026-05-21T15:30:00+00:00
tool: ha_config_set_automation
config:
  alias: Kitchen lights
  trigger: ...
  action: ...
```

Filename: `<domain>.<safe_entity_id>.<YYYYMMDD_HHMMSS>.yaml`. `safe_entity_id`
replaces `os.sep` and any non-`[A-Za-z0-9._-]` character with `_`. Backup
dir is flat (no per-domain subdirs) so glob retention is simple.

## Restore flow

User-facing entry points (both call `BackupManager.restore`):
- LLM: `ha_manage_backup(action="restore", name=...)`
- UI: `POST /api/settings/backups/<name>/restore`

`BackupManager.restore_snapshot(name)`:
1. Load file, parse YAML, validate schema (version, required keys).
2. Resolve `DomainHandler` from `domain` field; raises `LookupError` if no handler registered for that domain (surfaced as `VALIDATION_INVALID_PARAMETER` by the tool / UI route).
3. Take a fresh *safety backup* of the entity's CURRENT state via `maybe_snapshot` (best-effort — if the toggle is off or the fetch fails, `safety_backup` is `None`).
4. Call `handler.restore(client, entity_id, config)` (which under the hood calls the same HA REST/WS endpoint the original `set_*` tool uses).
5. Return `{restored_from, domain, entity_id, safety_backup, result}`.

Restore is upsert: re-creates entities that were deleted between backup and restore. If HA rejects (validation error, etc.), error propagates with structured suggestions.

## Polymorphic `ha_manage_backup` tool

`ha_backup_create` and `ha_backup_restore` are **merged** into a single
polymorphic tool `ha_manage_backup` that handles BOTH the existing full-HA-
snapshot functionality AND the new per-edit auto-backup operations. Net tool
count change: **-1** (remove 2 tools, add 1).

```python
ha_manage_backup(
    scope: Literal["snapshot", "edits"],
    action: Literal["create", "restore", "list", "view", "delete"],
    # snapshot scope params
    name: str | None = None,          # snapshot.create: tarball name
    backup_id: str | None = None,     # snapshot.restore: tarball ID
    restore_database: bool = False,   # snapshot.restore: include DB
    # edits scope params
    domain: str | None = None,        # edits.list / edits.delete: filter
    entity_id: str | None = None,     # edits.list / edits.delete: filter
    backup_name: str | None = None,   # edits.view / edits.restore / edits.delete
    older_than_days: int | None = None,  # edits.delete: bulk-by-age
)
```

### Routing matrix

| scope | action | Behavior |
|---|---|---|
| `snapshot` | `create` | Existing `ha_backup_create` behavior — full HA tarball via HA's native backup integration |
| `snapshot` | `restore` | Existing `ha_backup_restore` behavior — HA restarts, pre-restore safety tarball created automatically |
| `edits` | `list` | List auto-backup files filterable by `domain` and/or `entity_id` |
| `edits` | `view` | Return one auto-backup's YAML content + parsed `config` |
| `edits` | `restore` | Re-apply one auto-backup (creates fresh safety snapshot first); no HA restart |
| `edits` | `delete` | Delete one auto-backup by name OR bulk-delete by filter (domain/entity_id/older_than_days) |

### Gating against accidental wrong-mode usage

This is the main design risk — without strong gates, the LLM could route
"restore my automation" through `(scope="snapshot", action="restore")`,
which would restart HA. Layered defenses:

1. **Type validation**: `scope` and `action` are `Literal`-typed so Pydantic
   rejects unknown values before the tool body runs.
2. **Scope+action matrix validation**: invalid combinations
   (`(snapshot, list)`, `(snapshot, view)`, `(snapshot, delete)`,
   `(edits, create)`) raise `VALIDATION_INVALID_PARAMETER` with the valid
   combinations listed in `suggestions`.
3. **Required-param checks per cell**:
   - `(snapshot, restore)` requires `backup_id`; if `backup_name` or
     `entity_id` is passed instead, structured error explains the param
     belongs to the other scope.
   - `(edits, restore/view/delete-single)` requires `backup_name` (which
     follows the `<domain>.<entity_id>.<timestamp>.yaml` shape — clearly
     not a tarball ID).
4. **Annotation differentiation**:
   - `(snapshot, restore)` keeps the existing `destructiveHint: True` AND
     surfaces the "LAST RESORT — HA will restart" warning in the response.
   - `(edits, restore)` is also destructive but explicitly safer; response
     includes `restart_required: false` and the path of the safety backup
     created.
5. **Docstring**: the tool's docstring leads with a routing table so the
   LLM picks the right cell. Per-scope sections clearly state what each
   does and how they differ.

### UI mapping

| Tool call | UI endpoint | UI element |
|---|---|---|
| `(edits, list)` | `GET /api/settings/backups?...` | Backups tab list table with filters |
| `(edits, view)` | `GET /api/settings/backups/<name>` | "View" button → modal showing YAML |
| (no tool — UI-only diff) | `GET /api/settings/backups/<name>/diff` | "Diff" button → modal with unified diff vs current state |
| `(edits, restore)` | `POST /api/settings/backups/<name>/restore` | "Restore" button (confirmation modal) |
| `(edits, delete)` | `DELETE /api/settings/backups/<name>` and `DELETE /api/settings/backups?...` | Per-row "Delete" + bulk "Delete matching filters" |

**List item shape:**
```json
{
  "name": "automation.kitchen_lights.20260521_153000.yaml",
  "domain": "automation",
  "entity_id": "kitchen_lights",
  "captured": "2026-05-21T15:30:00+00:00",
  "tool": "ha_config_set_automation",
  "size": 412
}
```

**Diff:** `difflib.unified_diff` against the current entity's config (fetched via the same handler.fetch as backup), yielding text diff.

## Settings UI changes

`settings_ui/__init__.py` gains:
- 5 new routes (list / view / diff / restore / delete + bulk delete)
- A "Backups" tab in the existing `/settings` HTML page — list table with filter inputs (domain, entity, since), each row with View / Diff / Restore / Delete buttons, plus a "Bulk delete matching" action
- A small JS section at the bottom of the existing inline script tag

## Tool surface (28 wrapped)

`ha_config_set_automation`, `ha_config_remove_automation`,
`ha_config_set_script`, `ha_config_remove_script`,
`ha_config_set_scene`, `ha_config_remove_scene`,
`ha_config_set_helper` (one DomainHandler per helper_type, dispatched via `domain_fn`),
`ha_config_set_dashboard`, `ha_config_delete_dashboard`,
`ha_config_set_dashboard_resource`, `ha_config_delete_dashboard_resource`,
`ha_config_set_label`, `ha_config_remove_label`,
`ha_config_set_category`, `ha_config_remove_category`,
`ha_config_set_group`, `ha_config_remove_group`,
`ha_config_set_calendar_event`, `ha_config_remove_calendar_event`,
`ha_set_zone`, `ha_remove_zone`,
`ha_set_area_or_floor`, `ha_remove_area_or_floor`,
`ha_set_todo_item`, `ha_remove_todo_item`,
`ha_set_entity`,
`ha_set_integration_enabled`, `ha_remove_helpers_integrations`.

**Explicitly NOT wrapped:** `ha_call_service`, `ha_call_event`, `ha_restart`, `ha_reload_core`, `ha_check_config`, `ha_eval_template`, `ha_delete_file`, `ha_remove_entity`, `ha_remove_device`, `ha_set_device`, `ha_install_mcp_tools`, `ha_hacs_*`, blueprint/import ops.

## Error handling

- Backup write errors: WARNING log, original write proceeds.
- Restore errors: structured `RESTORE_FAILED` error response with suggestions.
- Schema-version mismatch on restore: structured `BACKUP_INCOMPATIBLE` error.
- Filesystem init errors (backup dir uncreatable at startup): single startup WARNING, manager runs with snapshots silently skipped until restart.
- Concurrent writes to same entity: per-entity `asyncio.Lock` serializes; no duplicate snapshots within one window.

## Testing strategy

**Unit (`tests/src/unit/test_backup_manager.py`):**
- Throttle math (boundary at `auto_backup_throttle_minutes=0` always snapshots; >0 enforces window)
- Retention rotation (`auto_backup_retain_per_entity` enforced; oldest deleted)
- Filename safety (path separators, special chars, unicode)
- Diff output format
- Schema version validation on restore
- Best-effort error handling (mocked fetch raises → wrapped tool still runs)

**E2E (`tests/src/e2e/workflows/auto_backup/`):**
One test file per backed-up domain (`test_automation.py`, `test_script.py`, ..., one per `DomainHandler`). Each test:
1. Toggle on
2. Create/exist entity
3. Edit via the wrapped tool → backup file appears
4. Edit again → another backup file appears (with throttle off) OR no new file (with throttle on, within window)
5. Restore first backup via `ha_manage_backup(action="restore", ...)` — entity returns to original state
6. Verify a safety backup was created
7. Delete via `ha_manage_backup(action="delete", ...)` — file removed
8. Toggle off → next edit produces no backup

## Files

**New:**
- `src/ha_mcp/backup_manager.py`
- `src/ha_mcp/tools/auto_backup.py` (decorator)
- `tests/src/unit/test_backup_manager.py`
- `tests/src/e2e/workflows/auto_backup/` (one test file per domain)

**Modified:**
- `src/ha_mcp/config.py` (4 settings)
- `src/ha_mcp/tools/backup.py` — merge `ha_backup_create`+`ha_backup_restore` into a single `ha_manage_backup` polymorphic tool; add the `edits` scope handlers
- `src/ha_mcp/settings_ui/__init__.py` (5 routes + Backups tab)
- `homeassistant-addon/config.yaml` + `homeassistant-addon-dev/config.yaml` (4 options + schema)
- `homeassistant-addon-dev/translations/en.yaml` (4 translations)
- 28 `tools_*.py` modules (one `@with_auto_backup(...)` line each above existing `@mcp.tool(...)`)

---

## File: docs/beta.md

# Beta Features

Some ha-mcp tools are gated behind feature flags and disabled by default. They can be enabled via the web Settings UI or via environment variables (non-add-on installs). Beta tools are still being evaluated and may change, be promoted to stable, or be removed based on field experience.

## Current beta tools

| Tool | Toggle / env var | Description |
|---|---|---|
| `ha_config_set_yaml` | `enable_yaml_config_editing` (dev app (add-on) Configuration tab); or web Settings UI master + sub-toggle; or `ENABLE_BETA_FEATURES=true` + `ENABLE_YAML_CONFIG_EDITING=true` env vars | Raw YAML editing of `configuration.yaml` and packages/*.yaml for YAML-only integrations. |
| `ha_list_files` | `enable_filesystem_tools` (dev app); or web Settings UI master + sub-toggle; or `ENABLE_BETA_FEATURES=true` + `HAMCP_ENABLE_FILESYSTEM_TOOLS=true` env vars | List files in allowed directories. Requires `ha_mcp_tools` custom component. |
| `ha_read_file` | `enable_filesystem_tools` (dev app); or web Settings UI master + sub-toggle; or `ENABLE_BETA_FEATURES=true` + `HAMCP_ENABLE_FILESYSTEM_TOOLS=true` env vars | Read files from allowed paths. Requires `ha_mcp_tools` custom component. |
| `ha_write_file` | `enable_filesystem_tools` (dev app); or web Settings UI master + sub-toggle; or `ENABLE_BETA_FEATURES=true` + `HAMCP_ENABLE_FILESYSTEM_TOOLS=true` env vars | Write files to allowed directories. Requires `ha_mcp_tools` custom component. |
| `ha_delete_file` | `enable_filesystem_tools` (dev app); or web Settings UI master + sub-toggle; or `ENABLE_BETA_FEATURES=true` + `HAMCP_ENABLE_FILESYSTEM_TOOLS=true` env vars | Delete files from allowed directories. Requires `ha_mcp_tools` custom component. |
| `ha_manage_custom_tool` | `enable_code_mode` (dev app); or web Settings UI master + sub-toggle; or `ENABLE_BETA_FEATURES=true` + `ENABLE_CODE_MODE=true` env vars | Sandboxed Python "escape hatch" that lets AI assistants write, run, save, and delete custom tools when no built-in tool covers the request. Code runs in pydantic-monty (no filesystem, no network); sandbox can call the HA REST API (`api_get`/`api_post`), send WebSocket commands (`ws_send`), call registered MCP tools (`call_tool`), or delete a saved tool (`delete_saved_tool`). Saved tools persist to disk via `CODE_MODE_SAVED_TOOLS_PATH` (defaults to `/data/saved_tools.json` in the dev app). |
| _(behaviour flag, no new tool)_ | `enable_lite_docstrings` (dev app); or web Settings UI master + sub-toggle; or `ENABLE_BETA_FEATURES=true` + `ENABLE_LITE_DOCSTRINGS=true` env vars | Replaces the docstrings on a handful of heavy ha-mcp tools (automations, scripts, scenes, helpers, dashboards, `ha_call_service`, `ha_config_set_yaml`) with shorter variants that defer schema and example detail to `ha_get_skill_guide` (or its `skill://` resource). Reduces idle catalog token usage; relies on the LLM actually calling the skill tool/resource when it needs detail. See "Known limitations" below. |
| `ha_get_dashboard_screenshot` (+ `include_screenshot` on `ha_config_get_dashboard`, `return_screenshot` on `ha_config_set_dashboard`) | `enable_dashboard_screenshot` (dev app); or web Settings UI master + sub-toggle; or `ENABLE_BETA_FEATURES=true` + `HAMCP_ENABLE_DASHBOARD_SCREENSHOT=true` env vars | Render one or more responsive Lovelace images so the AI can see what it reads or creates. Rendering runs in a separate, opt-in engine — balloob's **Puppet** app (headless Chromium, Apache-2.0), which you install yourself — nothing heavy is installed unless you enable this AND install the engine. |

## How to enable

There are three paths depending on how you run ha-mcp:

### Dev channel app (Home Assistant users)

The dev channel app continues to expose its beta sub-flags on its
Configuration page. Toggle them there, restart the app, done — the
master beta toggle is auto-enabled for dev app users at start-up.
No web-UI step is needed.

### Stable channel app (Home Assistant users)

The stable app does NOT expose individual beta toggles. Use the web
Settings UI instead:

1. Open the app's web UI tab.
2. Open the **Server Settings** tab.
3. Flip **Enable beta features** on.
4. Enable the desired beta toggle below it (e.g. **Enable YAML config
   editing (beta)**).
5. Restart the app.

### Non-add-on installs (Docker, uvx, pip)

Either flip the master + sub-toggles in the web Settings UI as above, or
set both env vars: `ENABLE_BETA_FEATURES=true` AND the specific sub-flag
env var (e.g. `ENABLE_YAML_CONFIG_EDITING=true`). Sub-flag env vars are
ignored unless the master is also true.

## Known limitations

### `ha_config_set_yaml`

This tool edits `configuration.yaml` and package files directly, bypassing Home Assistant's config-entry flow. It includes safeguards (backup before every edit, YAML validation, key allowlist, path traversal blocking, post-edit config check), but operators should be aware of the following:

**Two-step confirm flow (on by default).** Because a YAML edit re-serializes the whole file, an edit can change lines the AI never intended to touch (issue #1720). To surface that before it reaches disk, the confirm flow is **on by default**: the first `ha_config_set_yaml` call writes *nothing* and returns `preview: true`, a `confirm_token`, and a unified `diff` of exactly what would change on disk. The AI reviews that diff for collateral changes, then repeats the identical call adding `confirm_token` to apply the edit; every applied write also returns the final `diff`. If the file changed between preview and confirm the token no longer matches — the tool re-previews (`confirm_token_mismatch: true`) with a fresh token instead of writing stale content. Toggle it off (single-call writes, one less round-trip) via the **Require confirmation for YAML edits** sub-toggle under YAML config editing in the web Settings UI, the dev app Configuration tab, or `ENABLE_YAML_EDIT_CONFIRM=false`.

**Config check has blind spots.** HA's config check — run automatically post-edit, and available manually via `ha_get_system_health(include="config_check")` — validates YAML syntax but does not catch all integration-level schema errors. An edit can pass validation, HA boots cleanly, but the target entity silently does not exist. Common LLM mistakes include mixing legacy and modern template sensor syntax, wrong field names (`value_template:` vs `state:`), and bad Jinja expressions.

**`action: remove` removes the entire top-level key.** Asking an LLM to remove a single sensor can result in the entire `template:` key being deleted, not just the intended entry.

**Most keys require a full HA restart.** Only `template`, `mqtt`, and `group` support reload. All other keys require restarting Home Assistant for changes to take effect. The tool response includes `post_action` indicating which is needed.

**`command_line:` entries execute shell commands.** The allowlist includes `command_line:` for legitimate use cases, but an LLM could inadvertently create a sensor with a command that reads sensitive files or modifies the system.

**The key allowlist is extensible per install.** Some integrations are legitimately YAML-first and too install-specific to hardcode globally. **Extra YAML write keys** (the "HA-MCP File & YAML Tools" integration options, the web Settings UI nested under YAML config editing, or `HA_MCP_EXTRA_YAML_KEYS`) takes a comma-separated list of extra top-level keys `ha_config_set_yaml` may write on this install, e.g. `alert2`. The integration-options set and the server setting are unioned, so a key configured in either place takes effect. Everything else is unchanged: the file allowlist, the confirm flow, the backup, and the post-edit config check all still apply, and an extra key with no reload service gets the conservative full-restart path. A small set of keys can never be added to this setting, because they redefine Home Assistant's own trust boundary rather than one integration's config: `homeassistant:` (auth providers, and the `packages:` root that bounds this very surface), `http:` (trusted proxies, CORS, IP-ban), `frontend:` and `lovelace:` (both load JavaScript modules into the authenticated dashboard). Those stay refused with an explicit message. This bounds the per-key edit path; `action="replace_file"` rewrites a whole file and has never been key-validated. Requires custom component 1.2.4 or newer.

**Recovery requires filesystem access.** If an edit causes HA to enter recovery mode (e.g., a bad `!include` reference), `ha_config_set_yaml` cannot fix its own damage since the custom component doesn't load in recovery mode. Recovery requires SSH, the File Editor app, or `docker exec`.

**Per-edit backups are restorable via `ha_manage_backup`.** Per-edit auto-backups are written to `.ha_mcp_tools_backups/` (at the Home Assistant config root) and can be listed, viewed, restored, and deleted with `ha_manage_backup(scope="edits", ...)`. Full HA snapshot tarballs are separate — create, list, and restore them with `scope="snapshot"`.

**Recommended prerequisites:**
- Comfort with editing `configuration.yaml` via SSH or File Editor when things go wrong
- Understanding that dedicated tools (`ha_config_set_helper`, `ha_config_set_automation`, `ha_config_set_script`, etc.) should be preferred for anything they support

### `ha_list_files`, `ha_read_file`, `ha_write_file`, `ha_delete_file`

These tools provide direct file access to your Home Assistant filesystem and require `HAMCP_ENABLE_FILESYSTEM_TOOLS=true` and the `ha_mcp_tools` custom component installed and active. Install the component through HACS (custom repository `homeassistant-ai/ha-mcp-integration`); an AI agent can drive that install with the generic HACS tools.

**Access is restricted but sensitive.** The built-in writable directories are `www/`, `themes/`, `custom_templates/`, and `dashboards/`; `ha_read_file` additionally allows reading config YAML files, logs, and `custom_components/`. Power users can grant further read+write access to extra config-relative directories or the HAOS sibling volumes (`/share`, `/media`, `/ssl`, `/backup`) via the web Settings UI — each is opt-in and enforced by the `ha_mcp_tools` component. An AI assistant with these tools enabled has meaningful read and write access to your HA configuration.

**No undo.** `ha_delete_file` and `ha_write_file` (with `overwrite=True`) are irreversible. There is no recycle bin or automatic backup for file operations.

**Requires the custom component.** If `ha_mcp_tools` is not installed and active, all file tools will return an error with installation instructions.

### `ha_manage_custom_tool`

This tool exposes a sandboxed Python interpreter (`pydantic-monty`) to the AI as an escape hatch for operations no built-in tool covers. It also lets the AI save tools for reuse via `save_as` / `run_saved` / `list_saved`, and delete them from inside the sandbox via `delete_saved_tool(name)`. Sandbox code can hit the HA REST API directly (`api_get`/`api_post`), send HA WebSocket commands (`ws_send`), or call other registered MCP tools (`call_tool`). The sandbox blocks filesystem and arbitrary network I/O, but operators should still be aware of the following:

**The AI gets to write and run code on your HA instance.** Even though the sandbox prevents it from touching the filesystem or the public network, code can still call any tool the MCP server has registered, including write/destructive tools, and can hit any endpoint reachable via the HA REST or WebSocket API. The WebSocket surface in particular covers most registry CRUD (areas, devices, entities, automations) and template rendering — so this is effectively "do whatever HA's own UI can do, in any combination." Treat this like giving the AI a generic "do whatever existing tools allow you to do, in any combination" capability — not a tightly scoped per-feature tool.

**Saved tools persist by default in the dev app.** Tools the AI saves via `save_as` are written to `CODE_MODE_SAVED_TOOLS_PATH` (defaults to `/data/saved_tools.json` in the app) and re-loaded on the next start. The cap is 256 saved tools per instance. Operators who want a clean slate can stop the app and delete the file. Operators migrating between environments can copy that JSON file to the new instance — it survives app updates, but **not** app uninstall/reinstall (the `/data` volume is recreated). Outside the app (pip / uvx / Docker direct), persistence is opt-in: set `CODE_MODE_SAVED_TOOLS_PATH=/path/to/tools.json` to enable.

**Recursive self-call is blocked, but composition is not.** The sandbox refuses to invoke `ha_manage_custom_tool` from inside itself, so it can't directly recurse, but it can chain together every other tool the server registers. A buggy or adversarial prompt can still cause unexpected fan-out across destructive tools.

**Resource limits are best-effort.** 30s wall-clock, 10 MB memory, recursion depth 100, and 100 API/tool calls per execution are enforced by the sandbox runtime; the per-execution call cap is enforced by ha-mcp itself. All four are configurable via the `CODE_MODE_MAX_*` env vars within the bounds defined in `src/ha_mcp/config.py`. They protect against runaway loops, not against intentionally crafted abuse — keep `ENABLE_CODE_MODE=false` in any environment where untrusted prompts can reach the server.

**Outbound HTTP is restricted to your HA instance.** `api_get` / `api_post` reject absolute URLs (`http://...`, `https://...`), protocol-relative URLs (`//host/...`), and userinfo (`user@host/...`). This stops a prompt-injected LLM from redirecting the request elsewhere and exfiltrating the HA bearer token via the still-attached `Authorization` header. Only HA-relative paths reach the underlying httpx client.

**Safer-path enforcement on REST and WebSocket.** Several endpoints have wrapping MCP tools that perform validation, lint, hash-locking, or invariant checks; raw `api_post` / `ws_send` would skip those. The sandbox blocks a small denylist on each surface:

- `api_post`: writes to `/api/states/<entity_id>` (which can conjure ghost entities), `/api/events/<HA-internal-event-name>` (Core internal events that can fan out into user automations), and `/api/config/{automation,script}/config/*` (forced through `ha_config_set_automation` / `ha_config_set_script`). `config/scene/config/*` is intentionally not blocked because no `ha_config_set_scene` wrapping tool exists yet — the block would just remove capability with no validated alternative path.
- `ws_send`: `config/core/update` (rewrites HA's location/timezone/currency in `.storage/core.config`), `lovelace/config/save` and `lovelace/dashboards/{create,delete,update}` (forced through `ha_config_set_dashboard`), and `config/{area,device,entity}_registry/{delete,disable,update}` (forced through `ha_set_area_or_floor` / `ha_set_device` / `ha_set_entity` etc.).
- Service calls (`POST /api/services/<domain>/<service>`), webhook firing (`POST /api/webhook/`), custom event types (`POST /api/events/my_event_name`), and registry **read** queries (e.g. `config/area_registry/list`) all stay allowed.

**Sandbox failures are classified.** When sandboxed code raises, the error response now uses one of three codes — `SANDBOX_LIMIT_EXCEEDED` (memory / time / recursion / invocation cap), `SANDBOX_SYNTAX_UNSUPPORTED` (imports, classes, `with`, `match`, hard syntax errors) or `SANDBOX_RUNTIME_ERROR` (everything else) — with suggestions tailored to the category. Previously every Monty failure surfaced as `INTERNAL_ERROR` with "check the Python code for syntax errors" advice, which actively misled callers when the real cause was a memory cap or a missing module import.

**Sandbox actions are auditable.** Every state-changing sandbox call (`POST /api/...`, every `ws_send`) logs a structured `sandbox.api_post` / `sandbox.ws_send` line at DEBUG level. Blocked attempts (e.g. a refused `POST /api/states/...` or a refused `config/core/update`) log a `sandbox.api_post.blocked` / `sandbox.ws_send.blocked` line at INFO level so they're visible in default operator logs.

To get a full forensic trail of allowed calls, escalate the `ha_mcp.tools.tools_code` logger to DEBUG. This is HA's [`logger:` integration](https://www.home-assistant.io/integrations/logger/) and goes in **`configuration.yaml`** (not the app options):

```yaml
# configuration.yaml
logger:
  default: warning
  logs:
    ha_mcp.tools.tools_code: debug
```

Reload the `Logger` integration (or restart HA) to apply.

**ARM platforms require the async sandbox path.** On systems where `Monty.run_async` is unavailable, the tool fails fast with a clear error rather than falling back silently.

**Recommended prerequisites:**
- You're comfortable with the AI authoring small Python snippets that wrap existing tools or HA REST endpoints
- You have `destructiveHint=True` confirmation enabled on the MCP client and you actually read the prompts

### `enable_lite_docstrings`

Replaces the docstrings on a handful of heavy ha-mcp tools (automations, scripts, scenes, helpers, dashboards, `ha_call_service`, `ha_config_set_yaml`) with shorter variants that defer schema and example detail to `ha_get_skill_guide` (or its `skill://` resource). This is a behaviour flag, not a new tool.

**The trade-off.** This reduces idle tool-catalog token usage but relies on the LLM actually calling the skill tool (or reading the skill resource) when it needs detail. Some models will skip the extra tool call and produce worse output than they would have with the full docstrings in front of them.

**Search discoverability shrinks too.** Clients that BM25-search the tool catalog (claude.ai's native deferred-tool search, ha-mcp's own `enable_tool_search` index) see fewer tokens per lite-mapped tool, so natural-language queries that previously matched on words in the full docstring may rank lower or miss. ha-mcp's explicit `_SEARCH_KEYWORDS` boosts still apply on top of the lite text, but coverage outside those boosts will be thinner.

Pair this toggle with one of the following to mitigate:

- A client that supports MCP resources (the model can read `skill://` directly without an extra tool round-trip).
- `enable_tool_search` — the search transform's description already nudges the LLM toward the skill resources.
- A clear system prompt that instructs the LLM to consult `ha_get_skill_guide` before creating/editing automations, scripts, or helpers.

**Startup warning.** When this flag is enabled via environment variable, a single WARNING line is emitted at startup so non-add-on users see the trade-off in their logs. The app UI surfaces the same warning in the toggle's description.

**What is replaced.** Only the descriptions exposed via the MCP `list_tools` reply are swapped. The Python docstrings in `src/ha_mcp/tools/` are unchanged — the substitution happens via a FastMCP transform installed during server initialisation. Tools not in the lite mapping pass through with their original descriptions.

**Recommended prerequisites:**
- A client that lets you watch tool-call traces, so you can see whether the LLM is actually fetching the skill content before acting
- Willingness to disable the flag if the model regresses on your prompts

### Dashboard screenshot (`ha_get_dashboard_screenshot` / `include_screenshot` / `return_screenshot`)

Rendering does not run inside ha-mcp; it runs in a separate engine — balloob's
**Puppet** app (headless Chromium, https://github.com/balloob/home-assistant-addons).
ha-mcp does not vendor it; you install it yourself. Operators should know:

**Setup by deployment.**
- *HA OS / Supervised:* add balloob's repository
  (`https://github.com/balloob/home-assistant-addons`) under Settings > Apps >
  Install app > ⋮ > Repositories (Settings > Add-ons > Add-on store before
  Home Assistant 2026.2), install the **Puppet** App (add-on), set its
  `access_token` option to a Home Assistant long-lived access token, and start
  it. ha-mcp discovers the running app through the Supervisor (it matches
  the `*_puppet` slug). (The assistant can do this for you end-to-end via
  `ha_manage_app(action="add_repository", repository=...)` then
  `action="install"` / `action="start"`, but you must supply the token.)
- *Docker / Container:* run Puppet's image as a sidecar (build it from
  balloob's `puppet/` directory, with `access_token` set) and point ha-mcp at
  it with `HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL=http://<engine-host>:10000`.
- *stdio / standalone:* not supported (no place to host the engine); the tool
  returns a clear error.

**A long-lived access token is required.** Puppet authenticates by injecting a
Home Assistant long-lived access token (the `access_token` option) into the
browser. There is no token-less mode: the app's Supervisor token is not a
valid HA frontend credential (HA Core rejects it), and a token cannot be minted
programmatically. Create the token under Profile > Security — ideally for a
dedicated, low-privilege user, since the engine holds whatever access that
token grants. If the token is missing or invalid, Puppet lands on the login
page and (by its design) restarts; ha-mcp surfaces this as a clear "set the
engine's access token" error rather than a silent failure.

Puppet's theme and dark-mode renderer controls used to dispatch Home
Assistant's `settheme` event on every cold render, which Home Assistant
persisted on the frontend profile of the user whose token the engine runs with
— and synced to that user's real web and mobile sessions, flipping a dark-mode
user's whole UI to light on every screenshot (#1909). Recent Puppet versions
fixed that cold-render dispatch, so ha-mcp's snapshot/restore bracket around
each capture is now disabled (#1991); the guard code is retained so it can be
switched back on if a future engine regression reintroduces the write. If you
run an older Puppet build, update the app (or your self-hosted sidecar
image) — older engines still persist the theme selection and will keep
flipping it. A dedicated Puppet account remains a sound belt-and-suspenders
setup. Language selection is local to Puppet's browser session.

To change the Puppet engine app's own options (such as `keep_browser_open`)
or to restart it, use `ha_manage_app`; the screenshot tools only render and
never modify the engine app's configuration.

**Puppet's HTTP listener has no inbound auth, and it publishes host port
10000.** Anyone who can reach `http://<ha-host>:10000` can pull
fully-authenticated dashboard renders. Keep it on a trusted network only — do
NOT expose port 10000 to an untrusted LAN or the internet. (This is balloob's
upstream packaging; the app's own info page calls it a prototype with "no
security.")

**Charts are best-effort.** Canvas cards (ApexCharts, mini-graph-card,
history-graph) paint after the dashboard reports "loaded". The default
render-settle is generous, but a heavy chart card may still come back blank;
raise `wait_ms` on the standalone `ha_get_dashboard_screenshot` tool for those.

**Prefer stable view addressing.** The standalone tool accepts
`dashboard_url_path` plus the view's configured `views[].path`; dashboard get
responses expose `render_paths` for every static view. The legacy raw
`dashboard_path` remains supported, but a numeric route such as `lovelace/0`
returns a warning when that view has a stable named path. Strategy dashboards
generate views at runtime and therefore expose only their dashboard base route.

**Responsive and deterministic requests.** The standalone
`ha_get_dashboard_screenshot` tool accepts the full width, height, zoom, wait,
orientation, theme, dark-mode, language, image-format, and render-timeout
controls; the dashboard get/set workflows expose only `include_screenshot` /
`return_screenshot` plus `view_path` and render at defaults. `viewport_presets`
(standalone only) renders an ordered batch using `mobile` (390x844), `tablet`
(768x1024), and `desktop` (1280x800); every image is returned as a native MCP
image block. PNG, JPEG,
WebP, and BMP are supported by Puppet and carry their matching MIME type, but
client/model support for less-common image formats varies.

Raw image responses are streamed under server safety limits of 20 MiB per
image and 40 MiB per batch before MCP base64 encoding. Oversize responses use
the distinct `IMAGE_PAYLOAD_TOO_LARGE` error class. If one viewport in a batch
fails after another succeeded, the successful native image blocks are retained.
The standalone tool reports `partial=true`; dashboard get/set workflows report
`screenshot_partial=true`; both include ordered `screenshot_failures` entries
identifying the failed preset and failure class. The call errors only when every
requested viewport fails.

The structured `screenshots` metadata binds each image to its `content_index`,
render path, viewport, engine request, local capture options, byte length, MIME
type, and SHA-256 digest. Puppet does not report whether Home Assistant accepted
or fell back from a requested theme or language, so the frontend context is
explicitly marked as not confirmed.

**Capturing below the fold.** By default the render is clipped to the viewport.
Pass `height="auto"` or the backwards-compatible `full_page=true` to ask Puppet
to size the capture to the rendered content. Stock Puppet caps auto-height at
4000 px. It does not expose scroll position, total page height, or segment
capture, so dashboards beyond that limit remain clipped; true ordered scroll
segments require an upstream engine capability. For the backwards-compatible
`full_page=true` alias only, an HTTP 400 from Puppet versions older than 2.5.0
triggers the legacy 4096 px fixed-height retry and reports
`legacy_full_page_fallback=true`; explicit `height="auto"` remains strict.

**Current engine limits.** Puppet does not expose a device-pixel-ratio control,
confirmed applied-context metadata, or segmented scrolling. ha-mcp also cannot
observe an MCP client silently dropping an image after successful server-side
serialization. There is no screenshot file/media fallback: inline native MCP
images remain the only transport, avoiding unauthenticated persisted artifacts.

**Graceful by design — with one deliberate exception.** If the feature is off,
both `include_screenshot` and `return_screenshot` return the dashboard config /
write result with a `warnings` entry. `return_screenshot` (set) also degrades a
render failure to a warning so it never breaks a write that already committed.
`include_screenshot` (get) does not commit a dashboard/config write, and the
screenshot *is* the requested payload, so a total render failure surfaces as
an error (matching the standalone `ha_get_dashboard_screenshot` tool) rather
than a warning a caller might miss. Because Puppet can persist theme/dark
preferences (and the theme-restore bracket writes frontend user data to undo
that), screenshot operations are blocked in server Read Only Mode; ordinary
dashboard get/list/search calls remain available.

**Raw rendered paths remain constrained.** `ha_get_dashboard_screenshot`
validates legacy `dashboard_path` values (rejects URLs, query strings,
fragments, `..`, and backslashes) and checks the first route segment against
Home Assistant's registered Lovelace dashboards. A raw path therefore cannot
use Puppet's independent credential to render another frontend panel.

---

## File: docs/dev-channel.md

# Dev Channel

Want to test the latest changes before the biweekly stable release? The dev channel provides early access to features, bug fixes, and improvements as soon as they're merged to master.

**Who should use this:**
- Contributors testing their PRs
- Issue reporters verifying fixes
- Early adopters wanting the latest features

**Important:** Dev releases (`.devN`) may contain bugs. For production use, stick with stable releases.

## Release Schedule

| Channel | When Updated | Package/Tag |
|---------|--------------|-------------|
| **Dev** | Every push to master | `ha-mcp-dev` / `:dev` |
| **Stable** | Biweekly (Wednesday 10:00 UTC) | `ha-mcp` / `:latest` |

**Docker tags at a glance** (image `ghcr.io/homeassistant-ai/ha-mcp`):

- `:latest` / `:stable` — the most recent stable release; both point at identical bits. `:latest` is the default tag pulled when none is given; use `:stable` to make "stable only" explicit in a compose file or config.
- `:dev` — the rolling dev channel, rebuilt on every push to master.
- `:dev-<sha>` — an immutable tag pinned to one specific dev build (see the Docker section below).

`:latest` only ever tracks stable, never dev.

## Installation Methods

### uvx (Recommended)

The simplest method - no installation required, no virtual environments, just run:

```bash
# Run dev version directly
uvx ha-mcp-dev
```

**Check version:**
```bash
uvx ha-mcp-dev --version
# Output: ha-mcp 6.3.1.dev140
```

**Switch back to stable:**
```bash
uvx ha-mcp
```

**Config changes required:** None. Uses the same `HOMEASSISTANT_URL` and `HOMEASSISTANT_TOKEN` environment variables.

### pip (Alternative)

If you prefer installing the package:

```bash
# Install system-wide (requires sudo/admin on some systems)
pip install --user ha-mcp-dev

# Run the installed package
ha-mcp-dev
```

**Check version:**
```bash
pip show ha-mcp-dev | grep Version
```

**Switch back to stable:**
```bash
pip uninstall ha-mcp-dev -y
pip install --user ha-mcp
```

### uv (Alternative)

Using uv's package installer:

```bash
# Create a virtual environment (one-time setup)
uv venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Install dev version
uv pip install ha-mcp-dev

# Run
ha-mcp-dev
```

**Check version:**
```bash
uv pip show ha-mcp-dev | grep Version
```

**Switch back to stable:**
```bash
uv pip uninstall ha-mcp-dev
uv pip install ha-mcp
```

### Docker

Dev images are published to GitHub Container Registry with the `dev` tag.

**Pull the dev image:**
```bash
docker pull ghcr.io/homeassistant-ai/ha-mcp:dev
```

**Pin a specific dev build:**

Every dev build is also published under an immutable `:dev-<sha>` tag (the short commit SHA), so you can pin to one exact build — handy when verifying a fix on a specific commit. Unlike `:dev`, a `:dev-<sha>` tag never moves:
```bash
docker pull ghcr.io/homeassistant-ai/ha-mcp:dev-a1b2c3d
```

These commands mount `ha-mcp-dev-data` rather than the `ha-mcp-data` volume the
stable recipes use, so trying a dev build never writes to the settings your
stable install depends on — a dev build can persist data a released version
doesn't expect to read back.

**Run in stdio mode (Claude Desktop):**
```bash
docker run --rm -i \
  -v ha-mcp-dev-data:/home/mcpuser/.ha-mcp \
  -e HOMEASSISTANT_URL=http://your-ha-instance:8123 \
  -e HOMEASSISTANT_TOKEN=your_token \
  ghcr.io/homeassistant-ai/ha-mcp:dev
```

**Run in HTTP mode (web clients):**
```bash
docker run -d -p 8086:8086 \
  -v ha-mcp-dev-data:/home/mcpuser/.ha-mcp \
  -e HOMEASSISTANT_URL=http://your-ha-instance:8123 \
  -e HOMEASSISTANT_TOKEN=your_token \
  ghcr.io/homeassistant-ai/ha-mcp:dev ha-mcp-web
```

**Config changes required:** Change the image tag from `latest` to `dev`:
```diff
- ghcr.io/homeassistant-ai/ha-mcp:latest
+ ghcr.io/homeassistant-ai/ha-mcp:dev
```

**Switch back to stable:**
```bash
docker pull ghcr.io/homeassistant-ai/ha-mcp:latest
# Stop your dev container and start a new one with :latest tag
```

### Home Assistant app (add-on)

The dev channel is available as a **separate app (add-on)** in the Home Assistant App store.

**To use the dev channel:**

1. Open Home Assistant
2. Go to **Settings** → **Apps** → **Install app** (**Settings** → **Add-ons** → **Add-on store** before Home Assistant 2026.2)
3. Search for **"Home Assistant MCP Server (Dev)"**
4. Click **Install**
5. Configure with your token (if not using auto-discovery)
6. Start the app

**Key differences from stable app:**

| Property | Stable | Dev |
|----------|--------|-----|
| **Name** | Home Assistant MCP Server | Home Assistant MCP Server (Dev) |
| **Slug** | `ha_mcp` | `ha_mcp_dev` |
| **Stage** | Stable | Experimental |
| **Updates** | Biweekly (Wednesday) | Every master push |

**Can I run both?** Yes! Both apps can be installed simultaneously. They use different slugs and configuration.

**Switch back to stable:** Simply stop the dev app and start/install the stable "Home Assistant MCP Server" app instead.

### Claude Desktop Configuration

If you're using Claude Desktop, update your `claude_desktop_config.json`:

**Location:**
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows (traditional installer):** `%APPDATA%\Claude\claude_desktop_config.json`
- **Windows (Microsoft Store):** path varies by package — see the [Windows setup guide](https://homeassistant-ai.github.io/ha-mcp/guide-windows) for a detection snippet

**For uvx (dev channel):**
```json
{
  "mcpServers": {
    "ha-mcp": {
      "command": "uvx",
      "args": ["ha-mcp-dev"],
      "env": {
        "HOMEASSISTANT_URL": "http://your-ha-instance:8123",
        "HOMEASSISTANT_TOKEN": "your_token"
      }
    }
  }
}
```

**Config changes required:** Change `"ha-mcp"` to `"ha-mcp-dev"` in the args array.

**Switch back to stable:** Change `"ha-mcp-dev"` back to `"ha-mcp"` in the args array, then restart Claude Desktop.

## Checking Your Version

The easiest way to check your version:

```bash
# With uvx
uvx ha-mcp-dev --version

# With pip
pip show ha-mcp-dev | grep Version

# With uv
uv pip show ha-mcp-dev | grep Version

# With Docker
docker run --rm ghcr.io/homeassistant-ai/ha-mcp:dev ha-mcp --version
```

Dev versions follow the format: `6.3.1.dev140` where `.dev140` indicates the 140th commit since the last stable release.

## Reporting Issues

If you encounter issues with a dev release:

1. **Note the exact version number** (e.g., `6.3.1.dev140`)
2. **Check if the issue exists in stable** - Try the [latest stable release](https://github.com/homeassistant-ai/ha-mcp/releases/latest) to confirm it's dev-specific
3. **Report the issue:**
   - If it's a regression (worked in stable, broken in dev), comment on the related PR or issue
   - If it's a new bug, [open a bug report](https://github.com/homeassistant-ai/ha-mcp/issues/new?template=bug_report.md) and include:
     - The dev version number
     - Whether it reproduces in stable
     - Steps to reproduce

## See Also

- [Main Documentation](../README.md)
- [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/)
- [FAQ & Troubleshooting](https://homeassistant-ai.github.io/ha-mcp/faq/)
- [Contributing Guide](../CONTRIBUTING.md)

---

## File: docs/dev-mode.md

# Developer Mode

Developer mode registers two hidden MCP tools intended for people developing
or testing ha-mcp itself. It is **off by default**, and while it is off the
tools are never registered — MCP clients cannot see or call them.

> **Warning**: with developer mode on, any connected MCP client (i.e. any AI
> agent using this server) can change server settings and replace the running
> server version. Enable it only on instances used for development/testing.

## Enabling

The toggle lives at the **very bottom of the web settings UI**: Server
Settings tab → **Developer** section (below the beta features). Flip the
switch, confirm the warning, and restart the server for the tools to
register. Alternatively set the `HAMCP_ENABLE_DEV_MODE=true` env var.

The flag is intentionally absent from the app (add-on) Configuration page.

Rewriting tool security policies and deciding queued approvals needs a second
toggle in the same section — see [Security policy access](#security-policy-access).

## Tools

### `ha_dev_manage_server`

| Action | What it does |
| ------ | ------------- |
| `info` | Reports server version, deployment mode (embedded / `addon` / standalone), Python version, data dir, HA version, and — when the [in-process server](in-process-server.md) entry exists — its current channel and pip spec. |
| `update_source` | Points the in-process (custom component) server at a release `channel` (`stable` / `dev`) or an explicit `pip_spec` — a version pin or a GitHub tarball URL such as `https://github.com/homeassistant-ai/ha-mcp/archive/refs/pull//head.tar.gz` — then reinstalls and restarts it via the component's own options flow. |
| `restart` | Restarts this server: config-entry reload in embedded mode, Supervisor self-restart in app (add-on) mode. Standalone processes must be restarted externally. |
| `list_pending` | Lists the tool calls currently blocked on a tool-security-policy approval, with each one's token, arguments, and expiry. Reports an empty list when policies were off at startup (no approval queue exists). |
| `approve` / `deny` | Decides one blocked call by `token`. Requires [security policy access](#security-policy-access). |

`update_source` makes PR testing a one-call operation on an in-process
server install: point the pip spec at the PR tarball, wait for the reinstall,
reconnect, and verify with `info`. No extra repos or app rebuilds needed.
Server-code updates apply on the entry reload itself (component >= 1.0.1
purges the module cache per worker start); a change that needs *newer
third-party dependencies* still wants a Home Assistant core restart, since
shared libraries already loaded by the HA process are not reloaded.

### `ha_dev_manage_settings`

| Action | What it does |
| ------ | ------------- |
| `list` | Returns the full server-settings matrix (the same fields as the web UI's Server Settings tab) with each value's origin: `env` (pinned, read-only), `file` (override file), `addon` (Supervisor-managed), or `default`. |
| `set` | Validates and persists one setting through the same override layer the web UI uses. Env-pinned settings are refused; beta sub-flags still require the beta master to be on. |
| `reset` | Removes one setting's override-file entry, returning it to its default. Refused for env-pinned and add-on-managed settings, like `set`. |
| `list_tools` | Returns the Tools tab payload: every tool with its state (enabled / disabled / pinned), effective LLM-API exposure, per-tool security gate, and the env-pinned / mandatory / best-practice locks that make a row read-only. |
| `set_tool` | Changes one tool's `state`, `llm_api` exposure, and/or its security `gate` (`gated=`). All requested changes are validated before anything is written. `tool='*'` is refused — author wildcard rules through `set_policy`. `gated=` requires [security policy access](#security-policy-access). |
| `get_policy` | Returns the full tool-security policy (`wait_seconds`, `approval_ttl_minutes`, `rules`, `version`, `schema_version`) plus whether the policy engine is enabled and live. |
| `set_policy` | Writes the full policy, schema-validated and guarded by the `version` from your last `get_policy` (optimistic concurrency). Requires [security policy access](#security-policy-access). |
| `get_backup_config` | Returns the auto-backup config fields (the Backups tab's settings), with each value's origin. |
| `set_backup_config` | Changes auto-backup settings, routed through the Supervisor in app (add-on) mode and the override file elsewhere. |

Changes persist immediately but — like the web UI — most settings only take
effect after a restart (`ha_dev_manage_server` `restart`). Security gates,
policy edits, and LLM-API exposure apply live.

## Security policy access

The dev tools can rewrite the very policies that gate the agent calling them:
`set_policy` replaces the policy wholesale, `set_tool` with `gated=` adds or
removes a per-tool approval gate, and `approve` / `deny` decide queued
approvals — and because a gated call's error carries its approval token, an
agent could otherwise accept its own blocked calls.

That access is a **separate toggle, off by default**:
`dev_tools_security_policy_access` (`HAMCP_DEV_SECURITY_POLICY_ACCESS`), in
the same **Developer** section of the web settings UI, directly below the
dev-mode switch. Developer mode can stay on with policy access off.

While it is off, these are refused with `AUTH_INSUFFICIENT_PERMISSIONS`:

- `ha_dev_manage_settings` `set_policy`
- `ha_dev_manage_settings` `set_tool` with `gated=` (a `state` / `llm_api`-only
  call still works, and a combined `state` + `gated` call is refused whole,
  before the state change is written)
- `ha_dev_manage_server` `approve` and `deny`
- `ha_dev_manage_settings` `set` / `reset` of `enable_tool_security_policies`
  — turning the engine off would bypass every gate at once on the next
  restart (and the dev tools can restart the server)

Reads stay available either way: `get_policy`, `list_tools`, `list_pending`,
and the `list` settings matrix are never gated. The matrix marks the guarded
rows honestly: `enable_tool_security_policies` reports `editable: false` with
`locked_reason: policy_access_required` while access is off, and
`dev_tools_security_policy_access` and `enable_security_policy_tool` report
`editable: false` with `locked_reason: web_ui_or_env_only`. An env-pinned row
keeps its plain env story instead — the pin is the lock an operator must lift
first.

`dev_tools_security_policy_access` applies **live** — no restart needed —
while `enable_security_policy_tool` takes effect on the next restart. The dev
tools' settings surfaces never write either: `set` and `reset` of
`dev_tools_security_policy_access` are refused even while access is on, and so
are `set` and `reset` of `enable_security_policy_tool` — flipping that flag
registers `ha_manage_security_policy` (which can rewrite the policies) once
the server restarts, the same end state as unbuckling the leash. Flip either
in the web settings UI or via the env vars.

This is a leash on the dev tools' settings surfaces, **not a sandbox**. Dev
mode's `update_source` / `restart` can still replace the running server
build, and in app (add-on) deployments `ha_manage_app` can reach the app's own
options and ingress. `read_only_mode` likewise stays dev-tool-writable **by
design** — developer mode is expected to be able to lift it. Where that
boundary matters, gate those tools with policy rules — or keep dev mode off;
it remains a trusted-operator feature.

---

## File: docs/FAQ.md

# FAQ & Troubleshooting

Common questions and solutions for ha-mcp setup.

## General Questions

### Do I need a Claude Pro subscription?

**No.** Claude Desktop works with a free Claude account. The MCP integration is available to all users, though free accounts have usage limits.

You can also use ha-mcp with other AI clients. See the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/) for 15+ supported clients.

### Do I need the Home Assistant app (add-on)?

**No.** The HA app is just one installation method. Most users run ha-mcp directly on their computer using `uvx` (recommended for Claude Desktop). The app is only needed if you want to run ha-mcp inside your Home Assistant OS environment.

### What's the difference between ha-mcp and Home Assistant's built-in MCP?

| Feature | Built-in HA MCP | ha-mcp |
|---------|-----------------|--------|
| Tools | ~15 basic tools | 92+ comprehensive tools |
| Focus | Device control | Full system administration |
| Automations | Limited | Create, edit, debug, trace |
| Dashboards | No | Full dashboard management |
| Cameras | No | Screenshot and analysis |

Built-in = operate devices. ha-mcp = administer your system.

---

## Try Without Your Own Home Assistant

Want to test before connecting to your own Home Assistant? Use our public demo:

| Setting | Value |
|---------|-------|
| **URL** | `https://ha-mcp-demo-server.qc-h.net` |
| **Token** | `demo` |
| **Web UI** | Login with `mcp` / `mcp` |

Just set `HOMEASSISTANT_TOKEN` to `demo` and ha-mcp will automatically use the demo credentials.

The demo environment resets weekly. Your changes won't persist.

---

## Troubleshooting

### `ha-mcp-sse` is gone / my SSE config stopped connecting

The legacy `ha-mcp-sse` entry point (deprecated HTTP+SSE transport on port 8087)
was removed — the MCP specification deprecated the HTTP+SSE transport. Switch to
`ha-mcp-web`, which serves Streamable HTTP at `/mcp` on port 8086, and change any
SSE-style client config accordingly (Gemini CLI users: use the `httpUrl` key, not
`url`). An SSE / `--transport sse` client config pointed at `ha-mcp-web` returns
`405` and won't connect.

### OAuth stopped working after upgrading to v7.0.0

v7.0.0 removed the Home Assistant URL field from the OAuth consent form to fix security vulnerabilities (SSRF and XSS). Set `HOMEASSISTANT_URL` as a server-side environment variable before starting ha-mcp. See the [OAuth migration guide](OAUTH.md#migrating-from-v6x) for instructions.

### Claude.ai says "Couldn't reach the MCP server"

**This is normal.** Claude.ai shows this error during its initial connection handshake, but the server connects successfully afterward. To verify you're actually connected:

1. Look for a **"Configure"** button on the connector — click it
2. If you see tools listed, you're connected and ready to go

You can also start a new conversation and ask Claude if it can see your Home Assistant via the MCP connection — this is the easiest way to confirm it's truly connected. Checking your server logs for successful requests (HTTP 200) after the initial error also confirms the connection is working.

This is a known Claude.ai behavior that affects all MCP servers, not just ha-mcp.

**If it genuinely won't connect** (not just the transient handshake error above): Claude.ai connects from Anthropic's servers, so the MCP URL must be reachable from the public internet — not just your LAN. A URL that works in Claude Code or a local browser can still be unreachable for Claude.ai web. Open the URL on your **phone with Wi-Fi off** (cellular): if it doesn't load there, it isn't publicly reachable (DNS / port-forward / TLS / reverse-proxy) and Claude.ai can't reach it either. Also make sure you clicked **Connect** on the connector (and, with OAuth enabled, **Allow** on the consent page) — adding the connector alone does not complete the connection.

**Check for a port in the URL.** Your connector URL is built on your Home Assistant's own public address, which must **not** contain a port such as `:8123` (or any other port). To check, open just that base address (e.g. `https://ha.example.com`, without the `/api/webhook/...` secret path) in a browser — it should bring up your HA login page. Remote clients cannot reach a URL that carries a port, even though it loads fine in your own browser. Home Assistant can still listen on 8123 internally, as long as a reverse proxy, tunnel, or 443 port-forward serves that hostname — just don't put the port in the URL you paste.

**Tailscale Funnel: use port `443`.** Funnel can also serve on the alternate HTTPS ports it offers (`8443`, `10000`), but Claude.ai's connector backend does not reliably reach non-standard ports: the connection fails identically in every auth mode, and no request from Anthropic's range (`160.79.104.0/21`) ever reaches the server — nothing appears in any log. Use standard port `443` instead, where the same setup connects on the first try. The official Tailscale app's built-in **"Share Home Assistant with Serve or Funnel"** option already exposes Home Assistant on `443`, so use that hostname in the connector URL (same webhook path). See [#2080](https://github.com/homeassistant-ai/ha-mcp/issues/2080).

### "Terminating session: None" in server logs

**This is normal.** ha-mcp runs in stateless HTTP mode, which means each request creates and discards a temporary session. The `Terminating session: None` log message is the MCP SDK reporting this routine cleanup — the connection stays active.

### Cloudflare: LLM can't connect ("Block AI training bots")

If you're using Cloudflare and your LLM client can't connect to the MCP server (but visiting the URL in your browser works), Cloudflare's **"Block AI training bots"** setting is almost certainly the cause. This is the most common connection issue for Cloudflare users.

To disable it:

1. Log in to [Cloudflare](https://dash.cloudflare.com)
2. In the left sidebar, click **Domains**, then click **Overview**
3. Click on the domain you use for connecting to Home Assistant
4. On the right side of the page, find **"Control AI Crawlers"**
5. Under **"Block AI training bots"**, open the dropdown
6. Select **"do not block (allow crawlers)"**



See [#783](https://github.com/homeassistant-ai/ha-mcp/issues/783) for more details.

**Also check geo / country blocking.** This applies to Cloudflare (WAF custom rules) and to any other reverse proxy (NGINX, Traefik, Zoraxy, etc.). Most AI/LLM services connect from US-based cloud infrastructure, so if you block US IP addresses (or only allow your own country), your client cannot connect even with AI-bot blocking disabled. Allow your AI provider's IP ranges — Claude.ai connects from Anthropic's network, `160.79.104.0/21` (see [Anthropic's IP ranges](https://platform.claude.com/docs/en/api/ip-addresses)). Your proxy's access logs will show the blocked attempts.

### macOS: "All connection attempts failed" to local Home Assistant

If ha-mcp connects to the demo server but fails to reach your local Home Assistant (`192.168.x.x`, `10.x.x.x`, etc.) on macOS, the most common causes are listed below. See [#867](https://github.com/homeassistant-ai/ha-mcp/issues/867) (Local Network Privacy), [#630](https://github.com/homeassistant-ai/ha-mcp/issues/630) (env vars not reaching ha-mcp), and [#773](https://github.com/homeassistant-ai/ha-mcp/issues/773) (Python version/read-only filesystem) for related reports.

**1. macOS Local Network Privacy (Sequoia 15+)**

macOS Sequoia silently blocks subprocess connections to local network IPs. Claude Desktop spawns `uvx` as a child process, and macOS may block its outbound LAN connections without showing a permission dialog.

- Check **System Settings → Privacy & Security → Local Network** for Claude Desktop
- If Claude Desktop is not listed, try restarting it to trigger the permission prompt

**Workaround — SSH tunnel to localhost:**

Since macOS does not restrict connections to `localhost`, an SSH port forward bypasses the restriction:

```bash
ssh -N -L 8123:localhost:8123 user@your-ha-server-ip
```

Then set `HOMEASSISTANT_URL` to `http://localhost:8123` in your config.

**2. Firewall software (Little Snitch, Lulu, etc.)**

Third-party firewalls may block `python` or `node` processes spawned by Claude Desktop from making network connections. Check your firewall rules and allow connections for these processes. See [#780](https://github.com/homeassistant-ai/ha-mcp/issues/780) for an example resolution.

**3. http:// vs https://**

Home Assistant running in container mode (Docker, K3s) uses HTTP by default. Using `https://` causes a TLS handshake error. Use `http://` unless you have explicitly configured SSL/TLS or a reverse proxy.

**4. Python version too old**

ha-mcp requires Python 3.13+. If you are on Python 3.12 or older, `uvx` installs an outdated version of ha-mcp that may have known bugs (including read-only filesystem errors). Upgrade Python:

```bash
brew install python@3.13
```

Then force a refresh:

```bash
uvx --refresh ha-mcp@latest
```

If `uvx` still uses the old Python after installing 3.13, explicitly pin it by adding `--python 3.13` to your config args:

```json
"args": ["--python", "3.13", "ha-mcp@latest"]
```

### SSL certificate errors (self-signed certificates)

If your Home Assistant uses HTTPS with a self-signed certificate or custom CA, you may see SSL verification errors.

**Docker solution:**

1. Create a combined CA bundle:
   ```bash
   cat $(python3 -m certifi) /path/to/your-ca.crt > combined-ca-bundle.crt
   ```

2. Mount it and set `SSL_CERT_FILE`:
   ```json
   {
     "mcpServers": {
       "home-assistant": {
         "command": "docker",
         "args": [
           "run", "--rm",
           "-e", "HOMEASSISTANT_URL=https://your-ha:8123",
           "-e", "HOMEASSISTANT_TOKEN=your_token",
           "-e", "SSL_CERT_FILE=/certs/ca-bundle.crt",
           "-v", "./combined-ca-bundle.crt:/certs/ca-bundle.crt:ro",
           "ghcr.io/homeassistant-ai/ha-mcp:latest"
         ]
       }
     }
   }
   ```

### Windows: pywin32 installation fails

If you see `Failed to install: pywin32` or `os error 32` ("file is used by another process") when starting ha-mcp on Windows, this is caused by two upstream bugs:

1. The MCP Python SDK requires `pywin32` on Windows even though server-only users don't need it ([python-sdk#2233](https://github.com/modelcontextprotocol/python-sdk/issues/2233))
2. `uv` has a known issue installing `pywin32` on Windows ([uv#17679](https://github.com/astral-sh/uv/issues/17679))

**Workaround — use Docker:**

```json
{
  "mcpServers": {
    "Home Assistant": {
      "command": "docker",
      "args": [
        "run", "--rm", "-i",
        "-v", "ha-mcp-data:/home/mcpuser/.ha-mcp",
        "-e", "HOMEASSISTANT_URL=http://host.docker.internal:8123",
        "-e", "HOMEASSISTANT_TOKEN=your_token",
        "ghcr.io/homeassistant-ai/ha-mcp:latest"
      ]
    }
  }
}
```

See [#672](https://github.com/homeassistant-ai/ha-mcp/issues/672) for details.

### "uvx not found" error

After installing uv, **restart your terminal** (or Claude Desktop) for the PATH changes to take effect.

**Mac:**
```bash
# Reload shell or restart terminal
source ~/.zshrc
# Or verify with full path
~/.local/bin/uvx --version
```

**Windows:**
```powershell
# Restart PowerShell/cmd after installing uv
# Or use full path
%USERPROFILE%\.local\bin\uvx.exe --version
```

### MCP server not showing in Claude Desktop

1. **Restart Claude completely** - Use Cmd+Q (Mac) or Alt+F4 (Windows), not just close the window
2. **Check config file location:**
   - Mac: `~/Library/Application Support/Claude/claude_desktop_config.json`
   - Windows (traditional installer): `%APPDATA%\Claude\claude_desktop_config.json`
   - Windows (Microsoft Store): path varies by package — see the [Windows setup guide](https://homeassistant-ai.github.io/ha-mcp/guide-windows) for a detection snippet
3. **Verify JSON syntax** - No trailing commas, proper quotes
4. **Check the MCP icon** - Bottom left of Claude Desktop shows connected servers

### "Token invalid" or authentication errors

1. **Generate a new token:**
   - Home Assistant → Click your username (bottom left)
   - Security tab → Long-lived access tokens
   - Create Token → Copy immediately (shown only once)
2. **Check token format** - Don't wrap the token in quotes in your config
3. **Token expiration** - Tokens don't expire by default, but can be revoked

### Claude says it can't see Home Assistant

1. Open Claude Desktop **Settings** (gear icon)
2. Go to the **Developer** tab
3. Check **Local MCP Servers** for any errors
4. If "Home Assistant" is not listed, check your config file syntax
5. Try asking Claude: "Can you list your available tools?"

### Claude Desktop shows the server connected but exposes zero tools

**Fingerprint:** The server shows as connected and both `initialize` and `tools/list` complete successfully in `mcp.log` and the per-server log, yet the model sees no tools – and nothing surfaces an error in the UI or the logs.

**Fix:** Check the server's key in `claude_desktop_config.json` for parentheses and remove them. For example, renaming the key from `"Home Assistant (ha-mcp)"` to `"HASS ha-mcp"` (same URL, everything else unchanged) makes the full tool catalog reappear after a restart. Spaces in the key are fine; parentheses are the characters observed to trigger the drop. If in doubt, keep the key to letters, digits, spaces, `_`, and `-`.

**Why:** The Anthropic API requires every tool name to match `^[a-zA-Z0-9_-]{1,64}$` ([tool-definition docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools)), and Claude Desktop appears to derive each exposed tool's namespaced name from the `mcpServers` key. Spaces in a key are fine (the reporter confirmed spaces work), but a key with `(` or `)` leaves the derived names outside that grammar, so the client discards the affected tools before it ever calls the API – which is why the drop leaves no trace in the logs. (Claude Desktop's exact key-to-name handling is not publicly documented; this explanation is inferred from the reporter's bidirectional repro – same URL, only the key changes – together with the published name constraint.)

None of the shipped example configs use parentheses in the key, so a default setup never hits this – it is specifically a hand-authored key like `Home Assistant (ha-mcp)` that trips it. This is a Claude Desktop client behavior, not a ha-mcp problem: ha-mcp's own tool names are all valid `snake_case`. See [#1743](https://github.com/homeassistant-ai/ha-mcp/issues/1743).

### Can't connect remotely? Try the Webhook Proxy app {#webhook-proxy}

If you're having trouble setting up remote access — TLS errors, Cloudflare configuration issues, or port forwarding problems — the **Webhook Proxy app** may be a simpler alternative.

Instead of requiring a dedicated tunnel to port 9583, the Webhook Proxy routes MCP traffic through Home Assistant's main port (8123) via a webhook. If you already have **Nabu Casa** or any reverse proxy pointing at your HA instance, this can be the easiest remote setup.

1. Install the **MCP Server app** and the **Webhook Proxy app** from **Settings > Apps > Install app**
2. Start the webhook proxy and restart Home Assistant when prompted
3. Copy the webhook URL from the app logs
4. Use that URL in your MCP client configuration

See [#784](https://github.com/homeassistant-ai/ha-mcp/issues/784) for an example where this resolved a TLS connection issue.

### ChatGPT behind a firewall? Try the community OpenAI Tunnel integration {#openai-tunnel}

ChatGPT connectors require a URL reachable from the public internet. If your Home Assistant sits behind a firewall or CGNAT and you don't want to expose it, the community-maintained [OpenAI Tunnel for HA-MCP](https://github.com/norpol/hass-codex-tunnel-mcp) integration by [@norpol](https://github.com/norpol) is an outbound-only alternative:

- It downloads, verifies, and supervises OpenAI's [`tunnel-client`](https://github.com/openai/tunnel-client) as a Home Assistant subprocess (installed as a HACS custom repository; Linux `amd64`/`aarch64` on HA OS / Supervised initially).
- The client connects your MCP server URL to an OpenAI-hosted tunnel, so ChatGPT, Codex, and other OpenAI products can reach it — no port forwarding, reverse proxy, or public URL needed.
- Create a tunnel on the [Tunnels page](https://platform.openai.com/settings/organization/tunnels) and a runtime API key with **Tunnels Read** and **Tunnels Use** permissions on the [API keys page](https://platform.openai.com/settings/organization/api-keys), point the integration at your local ha-mcp URL, and add the ChatGPT connector using the same tunnel ID.

See the [integration's README](https://github.com/norpol/hass-codex-tunnel-mcp#readme) for full setup and [#1811](https://github.com/homeassistant-ai/ha-mcp/issues/1811) for background. This is a third-party project — report tunnel issues on its tracker, not here.

### Server works but responses are slow

1. **First request is slow** - `uvx` downloads packages on first run
2. **Subsequent requests** - Should be faster (packages cached)
3. **Alternative** - Use Docker for consistent performance

### Tools are missing or using old version

If you're seeing fewer tools than expected or outdated behavior, `uvx` may be using a cached old version.

**Solution:**

```bash
# Clear the uv cache
uv cache clean

# Force refresh to latest version
uvx --refresh ha-mcp@latest
```

**Verify the version:**
```bash
uvx ha-mcp@latest --version
```

The version should match the [latest release](https://github.com/homeassistant-ai/ha-mcp/releases/latest). If you see a much older version, the cache needs clearing.

### ChatGPT doesn't show new or newly enabled tools (stale tool list)

ChatGPT (web, including Codex Work Mode) caches a connector's tool list and sometimes keeps serving the stale list even after you enable new tools on the server, restart it, and remove and re-add the connector under the same name. Newly enabled tools (for example the beta filesystem/YAML tools) simply never appear in ChatGPT's tool list, even though the server logs show them registered — and other tools on the stale connection may fail with MCP internal errors.

**Solution:** delete the connector and create a new one with a **different name** — ChatGPT then fetches a fresh tool list. Re-adding it under the original name is not enough; the cached list survives the re-add.

---

---

## Configuration Options

### Environment Variables

| Variable | Description | Default | Required |
|----------|-------------|---------|----------|
| `HOMEASSISTANT_URL` | Your Home Assistant URL | - | Yes |
| `HOMEASSISTANT_TOKEN` | Long-lived access token (or `demo` for demo env) | - | Yes |
| `BACKUP_HINT` | Backup recommendation level | `normal` | No |

### Backup Hint Modes

| Mode | Behavior |
|------|----------|
| `strong` | Suggests backup before first modification each day/session |
| `normal` | Suggests backup only before irreversible operations (recommended) |
| `weak` | Rarely suggests backups |
| `auto` | Same as normal (future: auto-detection) |

### Entity visibility filter (opt-in)

By default the agent sees every entity. If auto-generated diagnostic or helper
entities clutter search and overview results, you can hide a chosen set of them
from the *collection* read tools (`ha_search`, `ha_get_overview`). In its default
form this is **noise reduction, not access control** – a hidden entity is still
returned by a direct `ha_get_state` / `ha_get_entity` on its `entity_id`, and
still appears in automation, dashboard, and template content, so do not rely on
the default filter as a security boundary. The opt-in **[Enforce mode](#enforce-mode)**
below turns it into a genuine read barrier: with `"enforce": true`, direct reads
of a hidden entity are concealed and content reads that would surface one are
refused across tool reads, except for the deliberately unrestricted-by-default
`ha_report_issue` diagnostic path described below.

**Default form: reads only – it does not gate control tools.** Without enforce
mode the filter only scopes what the *collection read* tools return. It does
**not** stop an agent from calling a service on a hidden `entity_id`: gating
writes is a separate concern handled by the Tool Security Policies engine (which
matches on a call's arguments), not by visibility. In the default form,
visibility is deliberately read-scoping only, precisely because it is noise
reduction and cannot be a security boundary (content-bearing reads such as
automation and template bodies would leak hidden entities anyway). Enforce mode
changes this by also concealing hidden entities named in a write call's arguments
(see below).

The easiest way to configure it is the **Entity Visibility** tab in the ha-mcp
settings UI (enable toggle, category checkboxes, area/label fields, per-entity
denylist). It reads and writes the same file described below, so either surface
works.

The filter is off until `entity_visibility.json` exists in the ha-mcp data
directory (the same directory as `tool_policy.json`; `/data` in the app) with
`"enabled": true`:

```json
{
  "version": 1,
  "enabled": true,
  "exclude_categories": ["diagnostic", "config"],
  "exclude_hidden": false,
  "deny_entity_ids": [],
  "exclude_areas": [],
  "exclude_labels": [],
  "allow_entity_ids": [],
  "allow_areas": [],
  "allow_labels": [],
  "respect_assist_exposure": false,
  "enforce": false,
  "restrict_report_issue": false
}
```

The filter is a conjunction of independent dimensions: an entity is shown only if
it passes every active one.

- **Excludes / denylist.** An entity is hidden when its `entity_category` is in
  `exclude_categories`, its `entity_id` is in `deny_entity_ids`, or its area/label
  is in `exclude_areas` / `exclude_labels`. `exclude_categories` accepts only Home
  Assistant's two entity categories (`diagnostic`, `config`); an unknown value is
  ignored and surfaced as a `warnings` entry on the next read rather than silently
  doing nothing. Set `exclude_hidden: true` to also fold in entities already
  marked hidden in Home Assistant.
- **Allowlist.** The moment any of `allow_entity_ids` / `allow_areas` /
  `allow_labels` is non-empty, the filter inverts to *restrict* mode: only
  entities matching an allowlist stay visible and everything else – including
  entities added later – is hidden. Leave all three empty to keep the allowlist
  off. `deny_entity_ids` still wins over an allow match — and so does any
  `exclude_*` match: an entity an allowlist would admit but an
  `exclude_categories` / `exclude_areas` / `exclude_labels` also hides stays
  hidden (every dimension can only hide, so any one hide is enough — the allow
  dimensions cannot un-hide what another dimension excluded).
- **Respect Assist exposure.** With `respect_assist_exposure: true` the filter
  hides entities not effectively exposed to Home Assistant's Assist
  (`conversation`) assistant, mirroring `async_should_expose` (an explicit
  per-entity exposure override wins; otherwise, if the instance exposes new
  entities, the entity's domain and device-class defaults decide). Because HA
  offers no single "effective exposure" API, the decision is reconstructed
  client-side from two extra websocket reads per search — the set of entities
  explicitly exposed to the assistant (`expose_entity/list`, which reports only
  the *exposed* ones) and the "expose new entities" flag that drives the default
  branch; if either read fails the dimension is skipped with a `warnings` note
  rather than hiding everything. A registry entity's explicit override — exposed
  *or* un-exposed — is read directly from the entity-registry `options` the
  registry list already carries, so an explicit un-expose is honored. One residual
  limit: for an entity that lives only in the state machine (a YAML/template entity
  with no entity-registry entry), HA surfaces it through `expose_entity/list` only
  when it is *exposed*; an explicit un-expose cannot be observed there, so such an
  entity falls to its domain/device-class default and stays visible (fail-open).

#### Enforce mode

Set `"enforce": true` (or the **Enforce mode** toggle in the Entity Visibility
tab) to turn the same hidden set into a genuine read barrier applied across tool
reads, not just `ha_search` / `ha_get_overview`. The default exception is
`ha_report_issue`, described below. `enforce` is not a hide
dimension — it does not change *which* entities are hidden, only how strongly the
hiding is applied — so it is inert unless the filter is also `enabled` with at
least one active hide dimension. What it covers:

- **Direct reads are concealed.** A call whose arguments name a hidden entity_id
  exactly (`ha_get_state`, `ha_get_history`, …) is refused *before the tool
  runs* with a canonical `ENTITY_NOT_FOUND`, so the entity's state and
  attributes never flow. Concealment of *existence* is best-effort: per-tool
  not-found shapes vary (a bulk `ha_get_state` normally partial-succeeds, and
  details/suggestions differ per tool), so a caller deliberately comparing
  error shapes may infer that an id is hidden rather than absent. Note this
  also means a bulk read that co-lists one hidden entity is refused as a whole
  — retry without the hidden id to read the rest.
- **Collection reads omit** hidden entities, exactly as they do without enforce.
  In enforce mode this extends to `ha_search`'s configuration-body matches: an
  automation, script, scene, helper, or dashboard record that references a
  hidden entity is omitted from the config results (in the default soft mode
  such records still appear — that is the documented soft-filter behavior).
- **Content reads are refused on contact.** An ordinary dashboard config,
  template result, automation/script body, trace, log, or file read whose output
  would surface a hidden entity_id is refused with a generic
  `ENTITY_VISIBILITY_ENFORCED` error that never names the matched id.
- **Writes naming a hidden entity are concealed too.** The inbound argument scan
  applies to *every* tool, including service calls: a `ha_call_service` targeting
  a hidden entity_id is concealed as not-found, so an agent cannot confirm the
  entity by trying to control it.

What it deliberately **refuses** (their output cannot be text-scanned): sandbox
code execution via `ha_manage_custom_tool` (`code` / `run_saved` — pure
`list_saved` stays allowed) and screenshot/pixel output
(`ha_get_dashboard_screenshot`, `ha_config_get_dashboard` with
`include_screenshot`, or `ha_config_set_dashboard` with `return_screenshot`).

One image surface is deliberately **exempt**: `ha_get_camera_image`. A camera
the filter does not hide returns physical-world imagery — a photograph, not a
rendering of Home Assistant entity data — so its frames are not gated (a hidden
camera is concealed like any other entity). The residual case is a visible
camera whose view happens to include a display showing a hidden entity's state;
if a camera can see something sensitive, hide the camera too (denylist or its
area).

A second surface is deliberately **exempt by default**: `ha_report_issue`.
While `"restrict_report_issue": false`, it bypasses both visibility scans on
every call — not only during a registry failure — and can return diagnostic
fields such as core, app/add-on, recent, and startup logs that contain hidden
entity_ids. This keeps the troubleshooting path available when visibility
configuration or Home Assistant registry inputs are the problem. Set
`"restrict_report_issue": true` to scan and refuse it like other tool reads.

Except for that default diagnostic escape hatch, enforce mode **fails closed**:
if the entity registry (or the config file itself) cannot be loaded, the server
falls back to the last good read from this session — and with none available,
tool calls are refused rather than risk leaking a restricted entity. If no
config can be read, `ha_report_issue` follows its safe unrestricted default;
if a last-good config opted it in, it fails closed too. The hidden set is cached
for ~30s, so an area/label
membership change in Home Assistant can take up to that long to take effect for
the area/label dimensions (a config edit in the settings UI applies on the next
call).

Because refuse-on-contact applies to the *whole* hidden set, broad hide
dimensions make refusals frequent: with the default `diagnostic`/`config`
category excludes still active, any log, automation, or dashboard read that
mentions a diagnostic entity is refused wholesale. Enforce mode works best with
a *targeted* deny — the private areas, labels, or entity_ids you actually need
concealed — rather than broad decluttering dimensions.

**Honest residual limits.** This is a strong barrier against *incidental*
exposure, not a cryptographic guarantee. A Jinja template (or code) that *derives*
a hidden entity's state without ever naming its entity_id — e.g.
`{{ states | selectattr('state','eq','on') | list | count }}` — cannot be caught
by a text scan. Treat enforce mode as robust protection against an agent stumbling
onto hidden entities, not as a boundary against an adversarial prompt author who
is deliberately trying to exfiltrate a hidden entity's state.

`version` drives optimistic-concurrency for the settings UI (it bumps
on each save so two tabs can't clobber each other); when hand-editing the file,
leave it as-is. The config is read live per request, so edits apply on the next
call. A missing file leaves the filter off; an *invalid* one leaves the filter
off for search/overview (with a `warnings` note) while enforce-mode safety falls
back to the session's last good config — with none, tool calls are refused until
the file is fixed (see *Enforce mode* above). When the filter is enabled but the
registry read degrades, search results are unfiltered with a `warnings` note
rather than silently wrong.

---

## Feedback & Help

We'd love to hear how you're using ha-mcp!

- **[GitHub Discussions](https://github.com/homeassistant-ai/ha-mcp/discussions)** — Share how you use it, ask questions, show off your automations
- **[GitHub Issues](https://github.com/homeassistant-ai/ha-mcp/issues)** — Report bugs or request features
- **[Home Assistant Forum](https://community.home-assistant.io/t/brand-new-claude-ai-chatgpt-integration-ha-mcp/937847)** — Community discussion thread

---

## File: docs/in-process-server.md

# Run the MCP server inside Home Assistant

The **HA-MCP Custom Component** (`ha_mcp_tools`) can run the **full ha-mcp server
in-process**, inside the Home Assistant application, and expose it remotely
through a Home Assistant webhook. This is one of the ways to run ha-mcp — and the
recommended one. It is a complete, standalone ha-mcp install — a replacement for
the app (add-on), Docker, and uvx/PyPI (stdio) server, not an addition to them. Run
only one ha-mcp server; never run the in-process server alongside another
install.

The in-process server is one of **two config-entry types** the component offers.
The other is the **File & YAML services entry** (**HA-MCP File & YAML Tools**),
the privileged file / YAML services. They are complementary and independent — you
can add either, or both, under the one integration — see [Relationship to the
File & YAML services entry](#relationship-to-the-file--yaml-services-entry) below.

## Who it's for

- **Home Assistant Container and Home Assistant Core users**, who cannot install
  apps (apps require the Supervisor). Instead of running ha-mcp in a
  separate Docker container or over stdio, you run it inside Home Assistant
  itself.
- **Home Assistant OS / Supervised users** who would rather not run a separate
  app. It works on HAOS too — the app remains a supported alternative, but
  you only need one of them; the two are fully independent.

Because it reaches the internet through a Home Assistant webhook, the connect URL
works through **Nabu Casa remote UI** (or any reverse proxy pointing at Home
Assistant) with no separate tunnel or port forwarding — the same mechanism the
Webhook Proxy app uses.

## How it works

Once the in-process server config entry exists, it:

1. Installs the `ha-mcp` package into Home Assistant at runtime (the first start
   takes a little longer while pip downloads it — see
   [First start](#first-start-takes-a-little-longer) below).
2. Provisions a dedicated Home Assistant admin token the server uses to reach
   Home Assistant over loopback.
3. Runs the server on its own thread so a slow tool call can never stall Home
   Assistant's event loop.
4. Registers a Home Assistant webhook that forwards MCP traffic to the server, so
   it is reachable remotely with the webhook URL as the secret.

The bring-up runs in the background, so it never delays Home Assistant startup.

## Requirements

The in-process server requires **Home Assistant 2026.6.0 or newer**. Older Core
releases constrain dependencies to versions that cannot run current `ha-mcp`
servers. On older releases, the component remains available for its File & YAML
services entry with an external app or Docker server, but its config flow blocks
creation of the incompatible in-process server entry.

## Setup

1. **Install the component.** Install **HA-MCP Custom Component** from HACS
   (repository `homeassistant-ai/ha-mcp-integration`, the component's HACS
   distribution mirror), or, without
   HACS, copy the `custom_components/ha_mcp_tools` directory from this repository
   into your Home Assistant `config/custom_components/` directory (so you end up
   with `config/custom_components/ha_mcp_tools/`). Restart Home Assistant.
2. **Add the in-process server entry.** Go to **Settings → Devices & Services →
   Add Integration**, search for **HA-MCP Custom Component**, and — on the menu
   that appears — choose **HA-MCP Server**, then submit the confirmation.
   Creating the entry starts the server with the defaults. (If you already have
   the **HA-MCP File & YAML Tools** entry, use the same **Add Integration** flow;
   the two entries appear together under the one integration tile.)
3. **Copy your connect URL.** As soon as the server starts, a notification titled
   **HA-MCP Server** confirms it is running and points you to the URL. The
   connect URL itself is on the entry's **Configure** screen (**Settings →
   Devices & Services → HA-MCP Custom Component → HA-MCP Server → Configure**)
   and in the Home Assistant log — both admin-only surfaces, because the URL is
   the credential. The notification deliberately carries no URL: notifications
   are visible to every signed-in user.
4. **Connect your MCP client** to that URL.

To pause the server, **disable** its config entry (**Settings → Devices &
Services → HA-MCP Custom Component → HA-MCP Server → ⋮ → Disable**);
re-enable it to start it again. Removing the entry stops the server and revokes
the provisioned token.

## Connect URLs

The server is reached through a Home Assistant webhook whose id is your secret
(it looks like `mcp_` followed by a long random string):

- **Remote (Nabu Casa or any external URL):**
  `https://<your-nabu-casa-domain>/api/webhook/<webhook-id>`
- **Local network:**
  `http://<home-assistant-host>:8123/api/webhook/<webhook-id>`

The server is also reachable directly on its own port (default - same model
as the app), bypassing the webhook, at the secret path (which looks like
`/private_<random>`):

- **Direct LAN access:** `http://<home-assistant-ip>:9584/private_<random>`

Set **Network access** to `127.0.0.1` to turn direct access off and keep only
the webhook and panel paths. All connect URLs — the webhook forms and, whenever
direct access is on, the direct URL — are listed on the entry's Configure
screen and in the Home Assistant log.

## Chat with the toolset from Home Assistant (conversation agents / voice)

While the server is running, its toolset is also registered as a Home
Assistant **LLM API** named after the entry. Any conversation agent — OpenAI,
Google Generative AI, Anthropic, Ollama, or any other integration that
supports LLM APIs — can select it, with the LLM of your choice, cloud or
local:

1. Add a conversation-agent integration (for a fully local setup: **Ollama**).
2. In that agent's settings, under **Control Home Assistant**, select
   **HA-MCP Server (tool search)** (alongside or instead of the built-in
   Assist API).
3. Talk to the agent from the **Assist chat** dialog, the companion apps, or a
   **voice satellite** whose pipeline uses that agent — "create an automation
   that turns off the lights when everyone leaves" now runs through the
   ha-mcp tools.

### Exposure modes

The **Conversation-agent tool exposure** option picks the shape agents get:

- **Tool search** (default): a compact API — the pinned tools directly, plus
  `ha_search_tools` (find tools for a task) and `ha_call_tool` (run one).
  Keeps the agent's context small; works with modest local models.
- **Full catalog**: every exposed tool listed directly with its schema.
  Better tool selection for large-context models, at ~10× the prompt cost.
- **Both**: registers the two APIs side by side — each agent picks its own
  in the selector. One server serves both; nothing runs twice.

### Per-tool exposure

Which tools agents may see is managed per tool from the **HA-MCP settings
panel** (the new **LLM API** toggle next to enabled/pinned/security-gated).
It is deny-by-default for **beta** tools, **developer-mode** tools, and the
**restart / reload / backup** family — a hidden tool is simply invisible to
agents (absent from the catalog and from search results) while staying fully
available to your regular MCP clients. Changes apply on the agent's next
message, no restart. A tool disabled globally is off for everything,
everywhere; the security gate and Read Only Mode apply to agent calls exactly
as to any MCP client.

Notes:

- No MCP client, external URL, or token is involved: the agent reaches the
  server over loopback inside Home Assistant.
- Home Assistant conversation agents cap tool iterations per turn (around
  ten), so a very complex build may need a follow-up prompt to continue.

**Security:** the toolset runs with the server's admin access. Selecting it
on an agent hands that power to everyone who can talk to that agent,
including anyone within earshot of a voice satellite using it. Keep it off
pipelines where that is not intended.

To remove the API from every agent's selector entirely, turn off
**Conversation-agent LLM API** in the entry's [options](#options).

## Settings panel ("HA-MCP" in the sidebar)

While a server entry is running, the integration adds an **HA-MCP** panel
to the Home Assistant sidebar. It opens the server web settings UI (tool
enable/disable/pin, feature flags, backups, themes) without needing the
loopback URL - the same experience as the app "Open Web UI" button.

The panel is admin-only. Opening it establishes a short-lived session for
your Home Assistant login, and every request re-checks that the account is
still an active administrator. No token or secret ever appears in a URL,
and the secret path stays on the loopback side of the proxy.

## Independent from the app

The in-process server and the Home Assistant MCP Server app are completely
independent: neither requires the other, and there is nothing to configure
between them. The in-process server defaults to port **9584** while the app
uses **9583**, so an existing app install does not conflict.

## Options

Open **Settings → Devices & Services → HA-MCP Custom Component → HA-MCP Server → Configure** to change these. Saving the options reloads the server so
the changes take effect. (The **HA-MCP File & YAML Tools** entry has no options — a
Configure there just reports that.)

| Option | Default | What it does |
|--------|---------|--------------|
| **Release channel** | `stable` | `stable` installs the latest stable release; `dev` installs the latest development build. Both channels update automatically (a reload or restart, plus a periodic check, install the newest build of the selected channel). See [Release channels](#release-channels). |
| **Automatic server updates** | on | When on, the selected channel's newest release is installed automatically (on reload/restart and via a periodic check). When off, the server stays on the version currently installed — new releases are still offered on the server's update entity, and its **Install** button installs one without turning automatic updates back on. Governs the ha-mcp **server package** only — component updates still come through HACS. A package override below overrides this. |
| **Server port** | `9584` | Local TCP port the server listens on. `9584` avoids the app's `9583` so an existing app install does not conflict. |
| **Network access** | `0.0.0.0` | The default matches the app: the port is reachable on your LAN with the secret path as the credential. `127.0.0.1` restricts direct access to the Home Assistant machine (the webhook and panel work either way). |
| **Authentication mode** | `none` | `none`: the secret webhook URL is the credential. `ha_auth`: clients sign in with your Home Assistant account. `legacy`: self-hosted OAuth with a static Client ID + Secret, for OAuth-only clients (Gemini Spark, Copilot CLI). See [Security](#security). |
| **ha-mcp package (advanced)** | empty (tracks the selected release channel) | The pip requirement installed at runtime. Leave it empty unless you are testing a pre-release — it accepts any pip requirement string, including a version pin or a GitHub tarball URL. An explicit value overrides the release channel and **disables automatic updates** (a pin stays put until you clear it); changing it forces a reinstall on the next reload. |
| **Home Assistant URL for the server (advanced)** | empty (derived from your HA's http config) | How the in-process server reaches Home Assistant. Empty derives the loopback URL from your instance's real port and SSL setting (an SSL-enabled HA is reached over `https://127.0.0.1` with certificate verification off — the certificate never matches a loopback address). Only set a value when the server must take a different route entirely. |
| **Remote access via webhook** | on | Turn off for local-only mode: the webhook is never registered, so Home Assistant (including Nabu Casa) cannot reach the server at all. Direct port access and the sidebar panel keep working. |
| **Conversation-agent LLM API** | on | Offers the toolset to Home Assistant conversation agents — see [Chat with the toolset](#chat-with-the-toolset-from-home-assistant-conversation-agents--voice). Enabling only makes it selectable per agent; turn off to remove it from every agent's selector. |
| **Conversation-agent tool exposure** | `tool_search` | Shape of the toolset agents get: compact tool-search API (default), the full catalog, or both side by side (choose per agent). See [Exposure modes](#exposure-modes). |
| **External URL (optional)** | empty | Shown as the primary connect URL - for your own domain / reverse proxy (e.g. `https://ha.example.com`). Opening it should reach your HA login page, and must not contain a port like `:8123` (any port breaks remote MCP clients). Empty = Nabu Casa / local automatically. |
| **Custom webhook secret (optional)** | empty | Replaces the random webhook secret in `/api/webhook/<secret>`. The URL is the credential - use a long, hard-to-guess value. |
| **Custom direct-access path (optional)** | empty | Replaces the random `/private_...` path on the server port. Same rule: the path is the credential. |
| **Regenerate connect secrets now** | off | One-time action: mints fresh random values for both secrets, immediately invalidating the old connect URLs (and clearing the two overrides). |

### Release channels

The **Release channel** option selects which build of the server is installed.
Both channels install unpinned and **update automatically**:

- **`stable` (default):** the latest `ha-mcp` release from PyPI.
- **`dev`:** the latest development build, published to PyPI as `ha-mcp-dev` on
  every change to the project's main branch. Use it to try upcoming fixes, and
  expect the occasional rough edge.

While **Automatic server updates** is on (the default), both channels install
unpinned: an entry reload or a Home Assistant restart always reinstalls the
newest build of the selected channel, and on top of that the component checks
PyPI for a newer build every 6 hours and reloads the entry automatically when
one is published — so a long-running instance picks up releases without a
restart. The reload applies the new server code immediately (component >=
1.0.1 reloads the module cache per worker start); only updates that require
newer *third-party dependencies* still need a Home Assistant core restart.
The web settings UI's **Restart HA-MCP Server** button performs the same
entry reload. Each automatic update also raises a notification naming the old and
new version, with a link to the release notes. Turn **Automatic server
updates** off to freeze the server on the version currently installed:
reloads/restarts keep that exact version until you turn it back on or install
a newer build yourself from the update entity (this governs the server package
only — component updates still arrive through HACS). Setting the **ha-mcp
package (advanced)** field overrides the channel entirely (pin a version, or
install from a URL for pre-release testing) and also disables automatic updates
until you clear it.

The server's version is always visible on its **update entity**, under
**Settings → Devices & Services → HA-MCP Custom Component → HA-MCP Server**
(and under **Settings → System → Updates** whenever an update is available).
The entity shows the installed and latest version of the selected channel and
links the release notes — the 6-hour PyPI check keeps it populated even with
automatic updates off, where its **Install** button installs the offered
version on your schedule. The server (`7.x`) and the component (`1.x`) are
versioned independently: this entity and HACS each own one of the two numbers.

Switching channels reinstalls the server from the other channel on the next
reload. `ha-mcp` and `ha-mcp-dev` share the same import package, so the previous
channel's package is uninstalled first — only one is ever installed at a time.

If the installed server needs a newer version of the custom component than the
one you have (HACS can deliver a server build before you update the component),
a repair issue titled **Update the HA-MCP Custom Component via HACS** appears
under **Settings → Repairs**. The server keeps running; update the component via
HACS to clear it — see [Held server updates](#held-server-updates) if HACS does
not show the update yet.

If the component was installed from the legacy location — the main `ha-mcp`
server repository added directly as a HACS custom repository, before the
dedicated [`ha-mcp-integration`](https://github.com/homeassistant-ai/ha-mcp-integration)
mirror existed — a repair issue titled **Component installed from the legacy
repository** appears. Such an install keeps working, but HACS displays the
server's `7.x` version numbers and the server's release notes for the
component. Follow the issue's link to add the mirror in HACS and reinstall the
component from it (your settings and config entries are kept), then restart
Home Assistant; the issue clears itself afterwards.

### Held server updates

When a new server release also ships a **newer custom component** than the one
you are running, the automatic server update is **held** instead of installed,
and a repair issue titled **HA-MCP server update waiting for a component update**
appears under **Settings → Repairs** to explain it. The hold exists to avoid
auto-starting a server build under a component version it was never tested with —
installing a newer server against an out-of-date component is the combination
that has broken server startups. The server keeps running on its current version
while the hold is in effect.

**HACS usually shows no component update yet at this point.** The component
notices the newer server quickly — it checks PyPI every 6 hours and again on
every reload or restart — but HACS refreshes its own repository information far
less often: for a custom repository (how this component is installed in HACS)
that can take up to about two days. So when the hold appears the component
update is typically not yet visible in HACS, and telling Home Assistant to check
for updates does not make HACS re-fetch any sooner. The one manual refresh that
does is in HACS itself: open the **HA-MCP Custom Component** repository, open its
**⋮** (three-dot) menu, and choose **Update information**; the component update
then appears.

The component asks HACS to perform this refresh automatically whenever the hold —
or the component-outdated repair — first appears, so in most cases the component
update shows up in HACS right away with no manual step. The manual **Update
information** refresh above is the fallback for when that automatic request cannot
reach HACS (for example HACS is still starting up, or its internals changed in a
newer HACS release).

To **clear the hold**, update the component in HACS and restart Home Assistant.
The held server update then installs automatically on the next periodic check or
reload.

To **install the update anyway**, press **Install** on the HA-MCP server update
entity — this bypasses the hold and installs the newer server immediately. Be
aware that it runs a server build against a component it has never been tested
with; if that server in turn requires an even newer component, it raises a
separate **Update the HA-MCP Custom Component via HACS** repair until you update
the component.

### Local-only mode

Turn **Remote access via webhook** off to keep the server unreachable through
Home Assistant entirely - no webhook means no Nabu Casa path and no
`/api/webhook/...` endpoint. You keep direct access on the server port (with
the secret path) and the admin-only sidebar panel.

### Rotating your connect URL

If a connect URL may have leaked, open the entry's options and check
**Regenerate connect secrets now**, then save - both the webhook secret and the
direct-access path are re-minted on the spot and every old URL stops working.
Update your MCP clients with the new URL from the Configure screen. (Removing and
re-adding the entry also rotates everything, including the internal token.)

## Security

The in-process server offers three authentication postures, chosen with the
**Authentication mode** option:

- **`none` (default): the secret webhook URL is the credential.** The webhook id
  is a high-entropy random string, and anyone who has the full URL can reach the
  server — exactly like the Webhook Proxy app's default. When exposed through
  Nabu Casa (or another HTTPS reverse proxy) the URL travels over TLS. Treat the
  URL like a password: don't share it or paste it where it could be logged.
- **`ha_auth`: clients sign in with your Home Assistant account.** Home Assistant
  Core acts as the OAuth authorization server. MCP clients that support OAuth
  (for example claude.ai and ChatGPT) discover the sign-in endpoints
  automatically and authenticate the user against Home Assistant; requests
  without a valid Home Assistant token are rejected. Only **administrator**
  accounts are accepted: the server performs its Home Assistant operations with
  its own provisioned admin token, so a non-admin login is refused rather than
  silently granted admin-equivalent control. There is no separate password or
  credential to manage — it is your existing Home Assistant admin login.
- **`legacy`: a self-hosted OAuth server with a static Client ID + Secret.** For
  OAuth-only clients that `ha_auth` can't serve (Google Gemini Spark, GitHub
  Copilot CLI). The component runs its own OAuth 2.1 authorization server at the
  Home Assistant root and issues a static **Client ID / Client Secret** to paste
  into the client — the secret *is* the credential and grants admin-equivalent
  access, so guard it like the `none` URL. Tokens are self-issued (1h access /
  30d refresh) and carry no Home Assistant login; **rotating** the credential
  (regenerate toggle, or a custom Client ID / Secret override) invalidates
  outstanding tokens, but only after the Home Assistant restart the repair
  prompts for. See [SECURITY.md](../SECURITY.md#in-process-server-ha_mcp_tools-in-process-server-entry)
  for the full threat model (unauthenticated consent page, permissive redirect
  URIs, route ownership vs the app).

All three postures ride Home Assistant's own remote access (Nabu Casa / your
reverse proxy) for TLS. If you expose the server to the internet, prefer
`ha_auth`; keep the `none` URL — or a `legacy` Client Secret — strictly private.

The server reaches Home Assistant with a dedicated admin token the component
provisions and stores in the config entry; that token is handed to the server
in-memory (never through the Home Assistant process environment). Removing the
entry revokes it. As with every deployment, that token's Home Assistant
permissions define what the server can do.

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

## Relationship to the File & YAML services entry

The in-process server entry and the **HA-MCP File & YAML Tools** entry are two
config-entry types of the same **HA-MCP Custom Component** (`ha_mcp_tools`). They
are independent: the server works on its own, and most installs never need the
File & YAML entry. Add it only if you enable ha-mcp's opt-in file and YAML editing
tools (feature flags, off by default) — those tools call the privileged services
that entry registers, and that applies to every server type, including an
external app, Docker, or stdio server as well as the in-process server. Add or
remove it at any time from the same **Add Integration** menu (choose **HA-MCP
File & YAML Tools**); it changes nothing about how the in-process server runs.

## First start takes a little longer

The first time the server starts, the component downloads and installs the
`ha-mcp` package with pip. This can take a minute or two — occasionally longer —
depending on your connection and hardware; the server starts automatically once
the install finishes. Later restarts are fast because the package is already
installed.

## Troubleshooting

**The server won't start.** If the server fails to come up — for example because
the port is already in use, or token provisioning fails — a repair issue titled
**The HA-MCP in-process server failed to start** appears under **Settings →
Repairs**, carrying the specific reason. If the `ha-mcp` package itself can't be
installed, the repair issue is titled **The HA-MCP in-process server package
could not be installed** instead. Fix the cause — check the Home Assistant log and
your network connectivity for an install failure, or set a different **Server
port** for a port conflict — then reload the entry (save the options, or use
**⋮ → Reload**) to retry.

**Nothing happens after updating the component.** Home Assistant loads custom
integration code at startup, so after HACS (or a manual copy) delivers a new
version you must **restart Home Assistant** for the update to take effect.

**Skill guidance is empty after installing from a GitHub tarball.** The **ha-mcp
package (advanced)** field can install from a GitHub tarball URL, but a git
archive excludes submodules — and the bundled skill content ships as a submodule.
A tarball install therefore omits it, so the skill-guidance tools report empty
listings. Install from PyPI instead (either release channel includes the skill
content); the tarball override is only meant for quick pre-release testing.

**Where the logs are.** The in-process server logs into the normal Home Assistant
log (**Settings → System → Logs**, or `home-assistant.log`). Its working data
lives in `.ha_mcp/` under your Home Assistant config directory.

**The Configure screen shows only a webhook path.** If Home Assistant cannot
determine an external or internal URL, the Configure screen and log show the
webhook path on its own (`/api/webhook/<webhook-id>`); prefix it with your Home
Assistant URL. Set your internal/external URLs under **Settings → System →
Network** so the full URL is shown.

---

## File: docs/macOS-uv-guide.md

# macOS Setup Guide

Control Home Assistant with Claude Desktop in about 10 minutes.

**Works with free Claude account** - no subscription needed.

## Step 1: Create a Claude Account

Go to [claude.ai](https://claude.ai) and create a free account.

## Step 2: Run the Installer

Open **Terminal** and paste:

```sh
curl -LsSf https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/scripts/install-macos.sh | sh
```

This installs the required tools and configures Claude Desktop for the demo environment.


**Manual Installation** (if the installer doesn't work)

### Install uv

```bash
brew install uv
```

Or without Homebrew:
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

### Configure Claude Desktop

1. Open Claude Desktop
2. Menu bar → **Claude** → **Settings...** → **Developer** → **Edit Config**
3. Paste:

```json
{
  "mcpServers": {
    "Home Assistant": {
      "command": "uvx",
      "args": ["ha-mcp@latest"],
      "env": {
        "HOMEASSISTANT_URL": "https://ha-mcp-demo-server.qc-h.net",
        "HOMEASSISTANT_TOKEN": "demo"
      }
    }
  }
}
```

4. Save and restart Claude: **Claude menu → Quit Claude**, then reopen.



## Step 3: Install or Restart Claude Desktop

Download and install **Claude Desktop** from [claude.ai/download](https://claude.ai/download).

Already have it? Restart it: **Claude menu → Quit Claude**, then reopen.

## Step 4: Test It

Open Claude Desktop and ask:

```
Can you see my Home Assistant?
```

Claude should respond with a list of entities from the demo environment (lights, sensors, switches, etc.).

## Step 5: Explore the Demo

The demo environment is a real Home Assistant you can experiment with:

- **Web UI:** https://ha-mcp-demo-server.qc-h.net
- **Login:** `mcp` / `mcp`
- **Note:** Resets weekly - your changes won't persist

Try asking Claude:
- "Turn on the kitchen lights"
- "What's the temperature in the living room?"
- "Create an automation that turns off all lights at midnight"

## Step 6: Connect Your Home Assistant

Ready to use your own Home Assistant? Edit the config file:

```bash
open "$HOME/Library/Application Support/Claude/claude_desktop_config.json"
```

Replace the demo values:

```json
{
  "mcpServers": {
    "Home Assistant": {
      "command": "uvx",
      "args": ["ha-mcp@latest"],
      "env": {
        "HOMEASSISTANT_URL": "http://homeassistant.local:8123",
        "HOMEASSISTANT_TOKEN": "your_long_lived_token"
      }
    }
  }
}
```

**To get your token:**
1. Open Home Assistant in your browser
2. Click your username (bottom left)
3. **Security** tab → **Long-lived access tokens**
4. Create token → Copy immediately (shown only once)

Then restart Claude: **Claude menu → Quit Claude**, then reopen.

## Step 7: Share Your Feedback

We'd love to hear how you're using ha-mcp!

- **[GitHub Discussions](https://github.com/homeassistant-ai/ha-mcp/discussions)** — Share your automations, ask questions
- **[GitHub Issues](https://github.com/homeassistant-ai/ha-mcp/issues)** — Report bugs or request features

## Troubleshooting

### Can't connect to local Home Assistant?

If the demo server works but your local HA (`192.168.x.x`) doesn't:

1. **macOS Local Network Privacy (Sequoia 15+)** — macOS may silently block Claude Desktop subprocesses from accessing local network IPs. Check **System Settings → Privacy & Security → Local Network**. As a workaround, use an SSH tunnel:
   ```bash
   ssh -N -L 8123:localhost:8123 user@your-ha-server-ip
   ```
   Then set `HOMEASSISTANT_URL` to `http://localhost:8123`.

2. **Firewall software** — Little Snitch, Lulu, or similar tools may block `python`/`node` connections from Claude Desktop. Add firewall rules to allow them.

3. **http:// vs https://** — HA in container mode (Docker, K3s) uses HTTP by default. Use `http://` unless you configured SSL/TLS.

4. **Python 3.13 required** — ha-mcp requires Python 3.13+. Older versions get outdated ha-mcp with known bugs. Run `brew install python@3.13` then `uvx --refresh ha-mcp@latest`. If uvx still picks the old Python, add `"--python", "3.13"` to your config args.

See the [FAQ](FAQ.md) for full details.

---

Having issues? See the **[FAQ & Troubleshooting Guide](FAQ.md)**.

---

## File: docs/OAUTH.md

# OAuth Authentication for ha-mcp (Beta)

> **Status:** Beta — OAuth provides an alternative to the private URL method. Fully functional but still being refined.

OAuth authentication lets multiple users authenticate with their own Home Assistant Long-Lived Access Token via a consent form.

## Migrating from v6.x

v7.0.0 removed the Home Assistant URL field from the OAuth consent form. You must now set `HOMEASSISTANT_URL` as a server-side environment variable.

**Why:** The consent form had two security vulnerabilities:
- **SSRF** ([GHSA-fmfg-9g7c-3vq7](https://github.com/homeassistant-ai/ha-mcp/security/advisories/GHSA-fmfg-9g7c-3vq7)): The URL field let an attacker submit arbitrary URLs to probe internal networks through the ha-mcp server.
- **XSS** ([GHSA-pf93-j98v-25pv](https://github.com/homeassistant-ai/ha-mcp/security/advisories/GHSA-pf93-j98v-25pv)): Unescaped HTML in the consent form allowed cross-site scripting.

Removing the URL field and sanitizing form output eliminates both attack surfaces.

**What to do:** Add `HOMEASSISTANT_URL` to your server environment before starting ha-mcp:

**Docker:**
```bash
docker run -d -p 8086:8086 \
  -v ha-mcp-data:/home/mcpuser/.ha-mcp \
  -e HOMEASSISTANT_URL=https://your-ha-instance.example.com \
  -e MCP_BASE_URL=https://your-mcp-server.example.com \
  ghcr.io/homeassistant-ai/ha-mcp:latest ha-mcp-oauth
```

**uvx:**
```bash
HOMEASSISTANT_URL=https://your-ha-instance.example.com \
MCP_BASE_URL=https://your-mcp-server.example.com \
uvx --from=ha-mcp@latest ha-mcp-oauth
```

The consent form now accepts only the Long-Lived Access Token. Everything else stays the same.

---

## When to Use OAuth

**Use OAuth if you want:**
- Real authentication instead of relying on secret URLs
- Multi-user support with per-user credentials
- Users to authenticate themselves via consent form

**Use private URL method if you want:**
- Simpler setup (recommended for most users)
- Single-user access

> **Note:** Both methods provide identical Home Assistant access. OAuth only changes how users authenticate.

---

## Setup

### 1. Expose with HTTPS

```bash
# Quick tunnel for testing
cloudflared tunnel --url http://localhost:8086
```

For production, set up a [persistent Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/).

### 2. Start OAuth Server

**Docker:**
```bash
docker run -d --name ha-mcp-oauth \
  -p 8086:8086 \
  -v ha-mcp-data:/home/mcpuser/.ha-mcp \
  -e HOMEASSISTANT_URL=http://homeassistant.local:8123 \
  -e MCP_BASE_URL=https://your-tunnel.trycloudflare.com \
  ghcr.io/homeassistant-ai/ha-mcp:latest \
  ha-mcp-oauth
```

**uvx:**
```bash
export HOMEASSISTANT_URL=http://homeassistant.local:8123
export MCP_BASE_URL=https://your-tunnel.trycloudflare.com
uvx --from=ha-mcp@latest ha-mcp-oauth
```

### 3. Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `HOMEASSISTANT_URL` | **Required.** URL of the Home Assistant instance | None |
| `MCP_BASE_URL` | **Required.** Public URL where this server is accessible | None |
| `MCP_PORT` | Server port | `8086` |
| `MCP_SECRET_PATH` | MCP endpoint path (does **not** gate the settings UI in this mode) | `/mcp` |
| `MCP_SETTINGS_SECRET_PATH` | Dedicated secret path for the web settings UI. In OAuth mode the settings UI never shares the MCP path (its custom routes bypass OAuth auth), so it is served under this separate secret. Auto-generated and printed in the startup log when unset. | Auto-generated `/private_<token>` |
| `HA_MCP_DISABLE_SETTINGS_UI` | Set truthy (`1`/`true`/`yes`/`on`) to not serve the web settings UI at all. | unset (UI served) |

> **Note:** `HOMEASSISTANT_TOKEN` is NOT required in OAuth mode. Each user provides their own Long-Lived Access Token via the consent form.

### 4. Connect in Claude.ai

1. Go to **Settings** → **Connectors** → **Add custom connector**
2. Enter URL: `https://your-tunnel.com/mcp`
3. Click **Add**
4. In the consent form that opens:
   - Enter your Long-Lived Access Token ([how to generate](https://www.home-assistant.io/docs/authentication/#your-account-profile))
5. Click **Authorize**

---

## FAQ

### "404 Not Found" when connecting

Make sure you're using the correct URL in Claude.ai:

```
✅ Correct: https://your-tunnel.com/mcp
❌ Wrong:   https://your-tunnel.com
```

The `/mcp` path is required - this is where the MCP server endpoints are mounted.

### "Invalid credentials" after authorizing

Verify your Long-Lived Access Token:
- Generate a fresh token in HA: Profile → Security → Long-lived access tokens
- Copy the complete token

Check that `HOMEASSISTANT_URL` is correct and accessible from the server running ha-mcp.

### Do tokens persist across server restarts?

**Yes!** Access tokens are stateless and self-contained - they work across server restarts and multi-instance deployments without any configuration.

### Can I use OAuth with Home Assistant OS?

No. The ha-mcp app (add-on) doesn't support OAuth mode.

**Alternatives:**
- Run ha-mcp OAuth on another device (Raspberry Pi, NAS, PC)
- Deploy to a cloud server (AWS, DigitalOcean, etc.)
- Use Home Assistant Container instead of HAOS

The OAuth server needs network access to your Home Assistant instance.

---

**Back to:** [Main Documentation](../README.md)

---

## File: docs/Windows-uv-guide.md

# Windows Setup Guide

Control Home Assistant with Claude Desktop in about 10 minutes.

**Works with free Claude account** - no subscription needed.

## Step 1: Create a Claude Account

Go to [claude.ai](https://claude.ai) and create a free account.

## Step 2: Run the Installer

Open **Windows PowerShell** (from Start menu) and paste:

```powershell
irm https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/scripts/install-windows.ps1 | iex
```

This installs the required tools and configures Claude Desktop for the demo environment.


**Manual Installation** (if the installer doesn't work)

### Install uv

Open **PowerShell** or **cmd**:

```powershell
winget install astral-sh.uv -e
```

### Configure Claude Desktop

1. Open Claude Desktop
2. **Settings** → **Developer** → **Edit Config**
3. Paste:

```json
{
  "mcpServers": {
    "Home Assistant": {
      "command": "uvx",
      "args": ["ha-mcp@latest"],
      "env": {
        "HOMEASSISTANT_URL": "https://ha-mcp-demo-server.qc-h.net",
        "HOMEASSISTANT_TOKEN": "demo"
      }
    }
  }
}
```

4. Save and restart Claude: **File → Exit**, then reopen.



## Step 3: Install or Restart Claude Desktop

Download and install **Claude Desktop** from [claude.ai/download](https://claude.ai/download).

Already have it? Restart it: **File → Exit**, then reopen.

## Step 4: Test It

Open Claude Desktop and ask:

```
Can you see my Home Assistant?
```

Claude should respond with a list of entities from the demo environment (lights, sensors, switches, etc.).

## Step 5: Explore the Demo

The demo environment is a real Home Assistant you can experiment with:

- **Web UI:** https://ha-mcp-demo-server.qc-h.net
- **Login:** `mcp` / `mcp`
- **Note:** Resets weekly - your changes won't persist

Try asking Claude:
- "Turn on the kitchen lights"
- "What's the temperature in the living room?"
- "Create an automation that turns off all lights at midnight"

## Step 6: Connect Your Home Assistant

Ready to use your own Home Assistant? Edit the config file.

If you installed Claude Desktop from the **Microsoft Store**, run this in PowerShell to find your config path:

```powershell
$pkg = Get-AppxPackage -Name Claude -ErrorAction SilentlyContinue | Select-Object -First 1; if ($pkg) { "$env:LOCALAPPDATA\Packages\$($pkg.PackageFamilyName)\LocalCache\Roaming\Claude\claude_desktop_config.json" } else { "$env:APPDATA\Claude\claude_desktop_config.json" }
```

Or open it directly (works for the **traditional installer**):

```powershell
notepad "$env:APPDATA\Claude\claude_desktop_config.json"
```

Replace the demo values:

```json
{
  "mcpServers": {
    "Home Assistant": {
      "command": "uvx",
      "args": ["ha-mcp@latest"],
      "env": {
        "HOMEASSISTANT_URL": "http://homeassistant.local:8123",
        "HOMEASSISTANT_TOKEN": "your_long_lived_token"
      }
    }
  }
}
```

**To get your token:**
1. Open Home Assistant in your browser
2. Click your username (bottom left)
3. **Security** tab → **Long-lived access tokens**
4. Create token → Copy immediately (shown only once)

Then restart Claude: **File → Exit**, then reopen.

## Step 7: Share Your Feedback

We'd love to hear how you're using ha-mcp!

- **[GitHub Discussions](https://github.com/homeassistant-ai/ha-mcp/discussions)** — Share your automations, ask questions
- **[GitHub Issues](https://github.com/homeassistant-ai/ha-mcp/issues)** — Report bugs or request features

---

Having issues? See the **[FAQ & Troubleshooting Guide](FAQ.md)**.