File: README.md
Breaking change (v7.3.0):
ha_config_set_yamlhas been moved to beta.
๐ 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):
Quick start:
- 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. - Restart Home Assistant.
- 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.
- 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.
- 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_authto 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 Assistantconfig/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 โ ยท Setup Wizard for client-specific config โ
๐ 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.
Add the repository to your Home Assistant instance:
If that opens the App store without an add-repository dialog (a known Home Assistant issue), add it manually: Settings โ Apps โ Install app โ โฎ โ Repositories, then paste
https://github.com/homeassistant-ai/ha-mcp.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.)
Open the Logs tab to find your unique MCP URL.
Connect your AI client to that URL โ no token or credential setup needed.
โ ๏ธ 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@latestentry withHOMEASSISTANT_URL/HOMEASSISTANT_TOKENalongside 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 generates the exact client-specific config for each.
Docker (HTTP server): run
ghcr.io/homeassistant-ai/ha-mcpin 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 for the full command and per-client config.PyPI / uvx (HTTP server): run the published
ha-mcppackage withuvx ha-mcp@latest(or pip) as a streamable-HTTP server the same way. Details in the Setup Wizard.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 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.
โ ๏ธ stdio has known transport issues. The stdio transport has connection problems that streamable HTTP does not (#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_urloption). 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.
- Install the MCP Server app (see above) and the Webhook Proxy app from the same store
- Start the webhook proxy and restart Home Assistant when prompted
- Copy the webhook URL from the app logs:text
MCP Server URL (remote): https://xxxxx.ui.nabu.casa/api/webhook/mcp_xxxxxxxx - Configure your AI client with that URL
For other remote access methods (Cloudflare Tunnel, custom reverse proxy), see the Setup Wizard.
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 integration by @norpol runs OpenAI's 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 and #1811.
Webhook proxy documentation โ
๐งช 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
- Go to claude.ai and sign in (or create a free account)
- Open Terminal and run:sh
curl -LsSf https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/scripts/install-macos.sh | sh - Download Claude Desktop (or restart: Claude menu โ Quit)
- Ask Claude: "Can you see my Home Assistant?"
You're now connected to the demo environment! Connect your own Home Assistant โ
๐ง Linux
Anthropic doesn't ship Claude Desktop for Linux, so pick one path:
Claude Desktop โ free, via the community build:
- Install the community Claude Desktop for Linux build and sign in with a free claude.ai account
- Open Terminal and run:sh
curl -LsSf https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/scripts/install-linux.sh | sh - Restart Claude Desktop, then ask: "Can you see my Home Assistant?"
Claude Code โ official CLI, requires a paid Claude plan:
- Install Claude Code:
curl -fsSL https://claude.ai/install.sh | bash - Configure ha-mcp, then run
claude:shcurl -LsSf https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/scripts/install.sh | sh -s -- --claude-code - Start
claude, run/mcpto confirm, then ask: "Can you see my Home Assistant?"
๐ช Windows
- Go to claude.ai and sign in (or create a free account)
- Open Windows PowerShell (from Start menu) and run:powershell
irm https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/scripts/install-windows.ps1 | iex - Download Claude Desktop (or restart: File โ Exit)
- Ask Claude: "Can you see my Home Assistant?"
You're now connected to the demo environment! Connect your own Home Assistant โ
๐ง Setup Wizard for 15+ clients
Claude Code, Gemini CLI, ChatGPT, Open WebUI, VSCode, Cursor, and more.
Having issues? Check the FAQ & Troubleshooting
๐ฌ 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. 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 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:
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 โ.
๐ง 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 โ 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 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 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_ctxis 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 returnUnknown toolwhen 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 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 โ Instructions for pip/uvx, Docker, and Home Assistant app.
๐ค Contributing
For development setup, testing instructions, and contribution guidelines, see CONTRIBUTING.md.
For comprehensive testing documentation, see 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.
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Acknowledgments
- Home Assistant: Amazing smart home platform (!)
- FastMCP: Excellent MCP server framework
- Model Context Protocol: Standardized AI-application communication
- Claude Code: AI-powered coding assistant
- PolicyLayer: Argument-path predicate DSL shape (
args.domain in [...]witheq/in/regex/contains/exists/...) inspired the per-tool approval rule schema (#966).
๐ฅ Contributors
Maintainers
- @julienld โ Project creator.
- @sergeykad โ Core maintainer.
- @kingpanther13 โ Core maintainer.
- @Patch76 โ Core maintainer.
Contributors
- @bigeric08 โ Explicit
mcpdependency for protocol version 2025-11-25 support. - @airlabno โ Support for
datafield in schedule time blocks. - @ryphez โ Codex Desktop UI MCP quick setup guide.
- @Danm72 โ Entity registry tools (
ha_set_entity,ha_get_entity) for managing entity properties. - @Raygooo โ SOCKS proxy support.
- @cj-elevate โ Integration & entity management tools (enable/disable/delete); person/zone/tag config store routing.
- @maxperron โ Beta testing.
- @kingbear2 โ Windows UV setup guide.
- @konradwalsh โ Financial support via GitHub Sponsors. Thank you! โ
- @knowald โ Area resolution via device registry in
ha_get_system_overviewfor entities assigned through their parent device. Financial support via GitHub Sponsors. Thank you! โ - @zorrobyte โ Per-client WebSocket credentials in OAuth mode, fixing WebSocket tool failures.
- @deanbenson โ Fixed
ha_deep_searchtimeout on large Home Assistant instances with many automations. - @saphid โ Config entry options flow tools (initial design, #590).
- @adraguidev โ Fix menu-based config entry flows for group helpers (#647).
- @transportrefer โ Integration options inspection (
ha_get_integrationschema support, #689). - @teh-hippo โ Fix blueprint import missing save step.
- @smenzer โ Documentation fix.
- @The-Greg-O โ REST API for config entry deletion.
- @restriction โ Responsible disclosure: python_transform sandbox missing call target validation.
- @lcrostarosa โ Diagnostic and health monitoring tools concept (#675), inspiring system/error logs, repairs, and ZHA radio metrics integration.
- @roysha1 โ Copilot CLI support in the installation wizard; replaced placeholder logo SVGs with real brand icons on the documentation site.
- @teancom โ Fix add-on stats endpoint (
/addons/{slug}/stats). - @TomasDJo โ Category support for automations, scripts, and scenes.
- @bzelch โ
python_transformsupport for automations and scripts. - @gcormier โ Windows installer improvements: removed unused variable and fixed terminal closing after install.
- @ekobres โ Feature flags for
HAMCP_ENABLE_FILESYSTEM_TOOLSand the (since removed)HAMCP_ENABLE_CUSTOM_COMPONENT_INTEGRATIONin the app config, with beta tagging in source and docs. - @w3z315 โ Financial support via GitHub Sponsors. Thank you! โ
- @griffinmartin โ Added OpenCode (by Anomaly) as a selectable AI client in the setup wizard, with both stdio and streamable HTTP support.
- @hhopke โ Fixed app (add-on) API calls to route through HA Core ingress proxy instead of direct container connections, fixing
ha_manage_addon(nowha_manage_app) proxy mode on app installs. - @tomwilkie โ JMESPath middleware exploration (#1147) whose review-time token-measurement data informed the design of #1199 and #1225.
- @SealKan โ
fields=/attribute_keys=projection on six read-heavy tools (#1225),ha_call_eventtool (#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 โ Cached YAML instance to prevent CPU spikes during bulk edits (#1371).
- @corgan2222 โ HA brand assets for custom integration (#1317).
- @drseanwing โ Progress emission via FastMCP
Contextin long-running tools (#1124); tool-discovery / categorized-search docs (#1123). - @fnordpig โ Config subentry support (#1393) and Assist pipeline management tool (#1392).
- @paul43210 โ
array_patchmode inha_manage_appfor atomic GET-modify-POST (#1063). - @L1AD โ Filed #966 proposing tool security policies; pointed to PolicyLayer's MCP-security work as prior art that inspired the predicate DSL shape.
- @nightcityblade โ Updated stale Home Assistant Advanced Mode references after HA 2026.6 made formerly advanced options available by default (#1533).
- @emmelutzer โ Financial support via GitHub Sponsors. Thank you! โ
- @pkkr โ
ha_knx_get_projecttool exposing KNX group addresses from an uploaded ETS project file. - @cbowns โ Fixed inconsistent hyphen in setup.astro Codex CLI docs.
- @Shaan-alpha โ Extended
ha_restartknown-good error patterns to cover 502/503 responses from reverse proxies. - @rebelancap โ Fixed UTC-to-local timezone conversion in
add_timezone_metadata. - @saevras โ Fixed blueprint import E2E test to use local URL instead of host-to-container networking.
- @jasonjhofmann โ Recurring calendar events via
rrulesupport inha_config_set_calendar_event. - @vpciii โ Coerce JSON-encoded strings on dict/list tool params.
- @pburtchaell โ Financial support via GitHub Sponsors. Thank you! โ
- @norpol โ Built the OpenAI Tunnel for HA-MCP companion integration, connecting ChatGPT to a firewalled Home Assistant MCP server (#1811).
๐ฌ Community
- GitHub Discussions โ Ask questions, share ideas
- Issue Tracker โ 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
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_restorealready 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_TOKENset +/dataexists):/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.Lockmap to serialize fetch+write - Per-entity-key
last_snapshot_tsfor throttle - Domain handler registry:
{domain: DomainHandler} - File operations (list, read, diff, write, delete, rotate)
DomainHandler is a frozen dataclass per backup-domain:
@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 envelopeOne 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)
@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):
- Look up manager off
self._client._auto_backup_manager(lazy-built on first use). - If
enable_auto_backupis False โ call wrapped function and return. - Resolve
entity_key = f"{domain}:{entity_id}". If entity ID isNone/empty (a create call with no ID) โ skip backup, call wrapped function. - Inside per-key lock: check throttle; if elapsed, fetch + write + rotate.
- All exceptions from steps 3-4 are logged as WARNING and swallowed.
- 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
# 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):
- Load file, parse YAML, validate schema (version, required keys).
- Resolve
DomainHandlerfromdomainfield; raisesLookupErrorif no handler registered for that domain (surfaced asVALIDATION_INVALID_PARAMETERby the tool / UI route). - 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_backupisNone). - Call
handler.restore(client, entity_id, config)(which under the hood calls the same HA REST/WS endpoint the originalset_*tool uses). - 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).
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:
- Type validation:
scopeandactionareLiteral-typed so Pydantic
rejects unknown values before the tool body runs. - Scope+action matrix validation: invalid combinations
((snapshot, list),(snapshot, view),(snapshot, delete),(edits, create)) raiseVALIDATION_INVALID_PARAMETERwith the valid
combinations listed insuggestions. - Required-param checks per cell:
(snapshot, restore)requiresbackup_id; ifbackup_nameorentity_idis passed instead, structured error explains the param
belongs to the other scope.(edits, restore/view/delete-single)requiresbackup_name(which
follows the<domain>.<entity_id>.<timestamp>.yamlshape โ clearly
not a tarball ID).
- Annotation differentiation:
(snapshot, restore)keeps the existingdestructiveHint: TrueAND
surfaces the "LAST RESORT โ HA will restart" warning in the response.(edits, restore)is also destructive but explicitly safer; response
includesrestart_required: falseand the path of the safety backup
created.
- 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:
{
"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
/settingsHTML 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_FAILEDerror response with suggestions. - Schema-version mismatch on restore: structured
BACKUP_INCOMPATIBLEerror. - 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.Lockserializes; no duplicate snapshots within one window.
Testing strategy
Unit (tests/src/unit/test_backup_manager.py):
- Throttle math (boundary at
auto_backup_throttle_minutes=0always snapshots; >0 enforces window) - Retention rotation (
auto_backup_retain_per_entityenforced; 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:
- Toggle on
- Create/exist entity
- Edit via the wrapped tool โ backup file appears
- Edit again โ another backup file appears (with throttle off) OR no new file (with throttle on, within window)
- Restore first backup via
ha_manage_backup(action="restore", ...)โ entity returns to original state - Verify a safety backup was created
- Delete via
ha_manage_backup(action="delete", ...)โ file removed - Toggle off โ next edit produces no backup
Files
New:
src/ha_mcp/backup_manager.pysrc/ha_mcp/tools/auto_backup.py(decorator)tests/src/unit/test_backup_manager.pytests/src/e2e/workflows/auto_backup/(one test file per domain)
Modified:
src/ha_mcp/config.py(4 settings)src/ha_mcp/tools/backup.pyโ mergeha_backup_create+ha_backup_restoreinto a singleha_manage_backuppolymorphic tool; add theeditsscope handlerssrc/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_*.pymodules (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:
- Open the app's web UI tab.
- Open the Server Settings tab.
- Flip Enable beta features on.
- Enable the desired beta toggle below it (e.g. Enable YAML config
editing (beta)). - 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.yamlvia 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 throughha_config_set_automation/ha_config_set_script).config/scene/config/*is intentionally not blocked because noha_config_set_scenewrapping 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/saveandlovelace/dashboards/{create,delete,update}(forced throughha_config_set_dashboard), andconfig/{area,device,entity}_registry/{delete,disable,update}(forced throughha_set_area_or_floor/ha_set_device/ha_set_entityetc.).- 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 and goes in configuration.yaml (not the app options):
# configuration.yaml
logger:
default: warning
logs:
ha_mcp.tools.tools_code: debugReload 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=Trueconfirmation 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_guidebefore 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 itsaccess_tokenoption to a Home Assistant long-lived access token, and start
it. ha-mcp discovers the running app through the Supervisor (it matches
the*_puppetslug). (The assistant can do this for you end-to-end viaha_manage_app(action="add_repository", repository=...)thenaction="install"/action="start", but you must supply the token.) - Docker / Container: run Puppet's image as a sidecar (build it from
balloob'spuppet/directory, withaccess_tokenset) and point ha-mcp at
it withHAMCP_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 acceptsdashboard_url_path plus the view's configured views[].path; dashboard get
responses expose render_paths for every static view. The legacy rawdashboard_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 standaloneha_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 reportscreenshot_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-compatiblefull_page=true alias only, an HTTP 400 from Puppet versions older than 2.5.0
triggers the legacy 4096 px fixed-height retry and reportslegacy_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.:latestis the default tag pulled when none is given; use:stableto 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:
# Run dev version directly
uvx ha-mcp-devCheck version:
uvx ha-mcp-dev --version
# Output: ha-mcp 6.3.1.dev140Switch back to stable:
uvx ha-mcpConfig changes required: None. Uses the same HOMEASSISTANT_URL and HOMEASSISTANT_TOKEN environment variables.
pip (Alternative)
If you prefer installing the package:
# Install system-wide (requires sudo/admin on some systems)
pip install --user ha-mcp-dev
# Run the installed package
ha-mcp-devCheck version:
pip show ha-mcp-dev | grep VersionSwitch back to stable:
pip uninstall ha-mcp-dev -y
pip install --user ha-mcpuv (Alternative)
Using uv's package installer:
# 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-devCheck version:
uv pip show ha-mcp-dev | grep VersionSwitch back to stable:
uv pip uninstall ha-mcp-dev
uv pip install ha-mcpDocker
Dev images are published to GitHub Container Registry with the dev tag.
Pull the dev image:
docker pull ghcr.io/homeassistant-ai/ha-mcp:devPin 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:
docker pull ghcr.io/homeassistant-ai/ha-mcp:dev-a1b2c3dThese 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):
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:devRun in HTTP mode (web clients):
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-webConfig changes required: Change the image tag from latest to dev:
- ghcr.io/homeassistant-ai/ha-mcp:latest
+ ghcr.io/homeassistant-ai/ha-mcp:devSwitch back to stable:
docker pull ghcr.io/homeassistant-ai/ha-mcp:latest
# Stop your dev container and start a new one with :latest tagHome 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:
- Open Home Assistant
- Go to Settings โ Apps โ Install app (Settings โ Add-ons โ Add-on store before Home Assistant 2026.2)
- Search for "Home Assistant MCP Server (Dev)"
- Click Install
- Configure with your token (if not using auto-discovery)
- 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 for a detection snippet
For uvx (dev channel):
{
"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:
# 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 --versionDev 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:
- Note the exact version number (e.g.,
6.3.1.dev140) - Check if the issue exists in stable - Try the latest stable release to confirm it's dev-specific
- 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 and include:
- The dev version number
- Whether it reproduces in stable
- Steps to reproduce
See Also
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.
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 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. |
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. |
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. |
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_settingsset_policyha_dev_manage_settingsset_toolwithgated=(astate/llm_api-only
call still works, and a combinedstate+gatedcall is refused whole,
before the state change is written)ha_dev_manage_serverapproveanddenyha_dev_manage_settingsset/resetofenable_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 withlocked_reason: policy_access_required while access is off, anddev_tools_security_policy_access and enable_security_policy_tool reporteditable: 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 ofdev_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 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 toha-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, noturl). An SSE / --transport sse client config pointed at ha-mcp-web returns405 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 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:
- Look for a "Configure" button on the connector โ click it
- 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.
"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:
- Log in to Cloudflare
- In the left sidebar, click Domains, then click Overview
- Click on the domain you use for connecting to Home Assistant
- On the right side of the page, find "Control AI Crawlers"
- Under "Block AI training bots", open the dropdown
- Select "do not block (allow crawlers)"
See #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). 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 (Local Network Privacy), #630 (env vars not reaching ha-mcp), and #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:
ssh -N -L 8123:localhost:8123 user@your-ha-server-ipThen 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 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:
brew install [email protected]Then force a refresh:
uvx --refresh ha-mcp@latestIf uvx still uses the old Python after installing 3.13, explicitly pin it by adding --python 3.13 to your config args:
"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:
Create a combined CA bundle:
bashcat $(python3 -m certifi) /path/to/your-ca.crt > combined-ca-bundle.crtMount 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:
- The MCP Python SDK requires
pywin32on Windows even though server-only users don't need it (python-sdk#2233) uvhas a known issue installingpywin32on Windows (uv#17679)
Workaround โ use Docker:
{
"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 for details.
"uvx not found" error
After installing uv, restart your terminal (or Claude Desktop) for the PATH changes to take effect.
Mac:
# Reload shell or restart terminal
source ~/.zshrc
# Or verify with full path
~/.local/bin/uvx --versionWindows:
# Restart PowerShell/cmd after installing uv
# Or use full path
%USERPROFILE%\.local\bin\uvx.exe --versionMCP server not showing in Claude Desktop
- Restart Claude completely - Use Cmd+Q (Mac) or Alt+F4 (Windows), not just close the window
- 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 for a detection snippet
- Mac:
- Verify JSON syntax - No trailing commas, proper quotes
- Check the MCP icon - Bottom left of Claude Desktop shows connected servers
"Token invalid" or authentication errors
- Generate a new token:
- Home Assistant โ Click your username (bottom left)
- Security tab โ Long-lived access tokens
- Create Token โ Copy immediately (shown only once)
- Check token format - Don't wrap the token in quotes in your config
- Token expiration - Tokens don't expire by default, but can be revoked
Claude says it can't see Home Assistant
- Open Claude Desktop Settings (gear icon)
- Go to the Developer tab
- Check Local MCP Servers for any errors
- If "Home Assistant" is not listed, check your config file syntax
- 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), 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.
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.
- Install the MCP Server app and the Webhook Proxy app from Settings > Apps > Install app
- Start the webhook proxy and restart Home Assistant when prompted
- Copy the webhook URL from the app logs
- Use that URL in your MCP client configuration
See #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 integration by @norpol is an outbound-only alternative:
- It downloads, verifies, and supervises OpenAI's
tunnel-clientas a Home Assistant subprocess (installed as a HACS custom repository; Linuxamd64/aarch64on 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 and a runtime API key with Tunnels Read and Tunnels Use permissions on the API keys page, point the integration at your local ha-mcp URL, and add the ChatGPT connector using the same tunnel ID.
See the integration's README for full setup and #1811 for background. This is a third-party project โ report tunnel issues on its tracker, not here.
Server works but responses are slow
- First request is slow -
uvxdownloads packages on first run - Subsequent requests - Should be faster (packages cached)
- 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:
# Clear the uv cache
uv cache clean
# Force refresh to latest version
uvx --refresh ha-mcp@latestVerify the version:
uvx ha-mcp@latest --versionThe version should match the latest release. 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
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-defaultha_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:
{
"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_categoryis inexclude_categories, itsentity_idis indeny_entity_ids, or its area/label
is inexclude_areas/exclude_labels.exclude_categoriesaccepts only Home
Assistant's two entity categories (diagnostic,config); an unknown value is
ignored and surfaced as awarningsentry on the next read rather than silently
doing nothing. Setexclude_hidden: trueto also fold in entities already
marked hidden in Home Assistant. - Allowlist. The moment any of
allow_entity_ids/allow_areas/allow_labelsis 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_idsstill wins over an allow match โ and so does anyexclude_*match: an entity an allowlist would admit but anexclude_categories/exclude_areas/exclude_labelsalso 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: truethe filter
hides entities not effectively exposed to Home Assistant's Assist
(conversation) assistant, mirroringasync_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 awarningsnote
rather than hiding everything. A registry entity's explicit override โ exposed
or un-exposed โ is read directly from the entity-registryoptionsthe
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 throughexpose_entity/listonly
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 isha_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 canonicalENTITY_NOT_FOUND, so the entity's state and
attributes never flow. Concealment of existence is best-effort: per-tool
not-found shapes vary (a bulkha_get_statenormally 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 toha_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 genericENTITY_VISIBILITY_ENFORCEDerror 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: aha_call_servicetargeting
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 โ purelist_saved stays allowed) and screenshot/pixel output
(ha_get_dashboard_screenshot, ha_config_get_dashboard withinclude_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 โ Share how you use it, ask questions, show off your automations
- GitHub Issues โ Report bugs or request features
- Home Assistant Forum โ 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 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:
- Installs the
ha-mcppackage into Home Assistant at runtime (the first start
takes a little longer while pip downloads it โ see
First start below). - Provisions a dedicated Home Assistant admin token the server uses to reach
Home Assistant over loopback. - Runs the server on its own thread so a slow tool call can never stall Home
Assistant's event loop. - 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
- Install the component. Install HA-MCP Custom Component from HACS
(repositoryhomeassistant-ai/ha-mcp-integration, the component's HACS
distribution mirror), or, without
HACS, copy thecustom_components/ha_mcp_toolsdirectory from this repository
into your Home Assistantconfig/custom_components/directory (so you end up
withconfig/custom_components/ha_mcp_tools/). Restart Home Assistant. - 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.) - 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. - 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:
- Add a conversation-agent integration (for a fully local setup: Ollama).
- In that agent's settings, under Control Home Assistant, select
HA-MCP Server (tool search) (alongside or instead of the built-in
Assist API). - 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) andha_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.
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. |
| 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. |
| 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. 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. |
| 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 latestha-mcprelease from PyPI.dev: the latest development build, published to PyPI asha-mcp-devon
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 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
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 thatha_authcan'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 thenoneURL. 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
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, preferha_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 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 theha-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 and create a free account.
Step 2: Run the Installer
Open Terminal and paste:
curl -LsSf https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/scripts/install-macos.sh | shThis installs the required tools and configures Claude Desktop for the demo environment.
Manual Installation (if the installer doesn't work)
Install uv
brew install uvOr without Homebrew:
curl -LsSf https://astral.sh/uv/install.sh | shConfigure Claude Desktop
- Open Claude Desktop
- Menu bar โ Claude โ Settings... โ Developer โ Edit Config
- Paste:
{
"mcpServers": {
"Home Assistant": {
"command": "uvx",
"args": ["ha-mcp@latest"],
"env": {
"HOMEASSISTANT_URL": "https://ha-mcp-demo-server.qc-h.net",
"HOMEASSISTANT_TOKEN": "demo"
}
}
}
}- 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.
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:
open "$HOME/Library/Application Support/Claude/claude_desktop_config.json"Replace the demo values:
{
"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:
- Open Home Assistant in your browser
- Click your username (bottom left)
- Security tab โ Long-lived access tokens
- 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 โ Share your automations, ask questions
- GitHub 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:
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:
bashssh -N -L 8123:localhost:8123 user@your-ha-server-ipThen set
HOMEASSISTANT_URLtohttp://localhost:8123.Firewall software โ Little Snitch, Lulu, or similar tools may block
python/nodeconnections from Claude Desktop. Add firewall rules to allow them.http:// vs https:// โ HA in container mode (Docker, K3s) uses HTTP by default. Use
http://unless you configured SSL/TLS.Python 3.13 required โ ha-mcp requires Python 3.13+. Older versions get outdated ha-mcp with known bugs. Run
brew install [email protected]thenuvx --refresh ha-mcp@latest. If uvx still picks the old Python, add"--python", "3.13"to your config args.
See the FAQ for full details.
Having issues? See the FAQ & Troubleshooting Guide.
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): The URL field let an attacker submit arbitrary URLs to probe internal networks through the ha-mcp server.
- XSS (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:
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-oauthuvx:
HOMEASSISTANT_URL=https://your-ha-instance.example.com \
MCP_BASE_URL=https://your-mcp-server.example.com \
uvx --from=ha-mcp@latest ha-mcp-oauthThe 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
# Quick tunnel for testing
cloudflared tunnel --url http://localhost:8086For production, set up a persistent Cloudflare Tunnel.
2. Start OAuth Server
Docker:
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-oauthuvx:
export HOMEASSISTANT_URL=http://homeassistant.local:8123
export MCP_BASE_URL=https://your-tunnel.trycloudflare.com
uvx --from=ha-mcp@latest ha-mcp-oauth3. 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_TOKENis NOT required in OAuth mode. Each user provides their own Long-Lived Access Token via the consent form.
4. Connect in Claude.ai
- Go to Settings โ Connectors โ Add custom connector
- Enter URL:
https://your-tunnel.com/mcp - Click Add
- In the consent form that opens:
- Enter your Long-Lived Access Token (how to generate)
- 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.comThe /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
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 and create a free account.
Step 2: Run the Installer
Open Windows PowerShell (from Start menu) and paste:
irm https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/scripts/install-windows.ps1 | iexThis 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:
winget install astral-sh.uv -eConfigure Claude Desktop
- Open Claude Desktop
- Settings โ Developer โ Edit Config
- Paste:
{
"mcpServers": {
"Home Assistant": {
"command": "uvx",
"args": ["ha-mcp@latest"],
"env": {
"HOMEASSISTANT_URL": "https://ha-mcp-demo-server.qc-h.net",
"HOMEASSISTANT_TOKEN": "demo"
}
}
}
}- Save and restart Claude: File โ Exit, then reopen.
Step 3: Install or Restart Claude Desktop
Download and install Claude Desktop from 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:
$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):
notepad "$env:APPDATA\Claude\claude_desktop_config.json"Replace the demo values:
{
"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:
- Open Home Assistant in your browser
- Click your username (bottom left)
- Security tab โ Long-lived access tokens
- 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 โ Share your automations, ask questions
- GitHub Issues โ Report bugs or request features
Having issues? See the FAQ & Troubleshooting Guide.