ha-mcp Documentation

The Unofficial and Awesome Home Assistant MCP Server

3,162 stars Python Python 1 file ยท ~40,346 tokens
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy ha-mcp on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name ha-mcp --restart always homeassistant-ai/ha-mcp:latest
Configured Services & Ports
โš™๏ธ ha-mcp
image: homeassistant-ai/ha-mcp:latest
โš™๏ธ ha-mcp-screenshot
image: ha-mcp-screenshot:latest
docker-compose.yml
yaml (docker-compose.yml)
# Example: dashboard-screenshot engine sidecar for Docker / Container installs
# (the deployments without a Supervisor, so the HA OS add-on path is N/A).
#
# Pairs with the opt-in "dashboard screenshot" beta feature: enable
# HAMCP_ENABLE_DASHBOARD_SCREENSHOT on the ha-mcp service AND run this engine
# as a sidecar, then point ha-mcp at it via HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL.
# Merge these two services into your existing docker-compose.yml.
#
# The engine performs NO inbound authentication on its HTTP listener, so keep
# it on an internal Docker network only โ€” do NOT publish port 10000 to an
# untrusted LAN/internet (anyone who can reach it could pull authenticated
# dashboard renders). It is reached by service name over the compose network.

services:
  ha-mcp:
    build:
      context: .
      dockerfile: Dockerfile
    image: homeassistant-ai/ha-mcp:latest
    container_name: ha-mcp-server
    # No `volumes:` here on purpose. Compose merges service volumes by their
    # container path, so a mount at /home/mcpuser/.ha-mcp in this overlay would
    # REPLACE whatever the base file mounts there โ€” silently redirecting the
    # settings of anyone using a custom host path. Persistence belongs to the
    # base docker-compose.yml, which mounts ha-mcp-data at that path.
    environment:
      - HOMEASSISTANT_URL=http://homeassistant:8123
      - HOMEASSISTANT_TOKEN=${HA_TOKEN}
      # Opt into the beta feature (the master beta gate must also be on).
      - HAMCP_ENABLE_DASHBOARD_SCREENSHOT=true
      # Reach the engine sidecar by its compose service name (internal only).
      - HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL=http://ha-mcp-screenshot:10000
    restart: unless-stopped

  # Headless-Chromium screenshot engine โ€” balloob's Puppet (ha-puppet).
  # Renders a Lovelace dashboard to a PNG that ha-mcp fetches over the internal
  # network. ha-mcp does not vendor it and balloob publishes no image, so build
  # it from balloob's repository:
  #   git clone https://github.com/balloob/home-assistant-addons
  # then point the build context at its `puppet/` directory (below).
  ha-mcp-screenshot:
    build:
      context: ./home-assistant-addons/puppet
      dockerfile: Dockerfile
    image: ha-mcp-screenshot:latest
    container_name: ha-mcp-screenshot
    environment:
      # Puppet authenticates to Home Assistant with this token. Prefer a
      # long-lived token from a dedicated, low-privilege HA user. ha-puppet
      # reads it from /data/options.json in add-on mode; in plain Docker mount
      # an options file (see balloob's puppet/ha-puppet/options-dev.json.sample).
      - HA_TOKEN=${HA_TOKEN}
    # NOTE: intentionally NO `ports:` mapping โ€” the engine is unauthenticated
    # and must stay reachable only on the internal compose network.
    restart: unless-stopped

# Usage:
# 1. Create .env file: HA_TOKEN=your_long_lived_access_token
# 2. Merge both services into your docker-compose.yml (same network as HA).
# 3. Run: docker-compose -f docker-compose.yml -f docker-compose.screenshot.yml up -d
# 4. In ha-mcp, the master beta toggle must also be enabled.

File: README.md

Breaking change (v7.3.0): ha_config_set_yaml has 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:

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

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

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

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

Full in-process server documentation โ†’ ยท 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.

  1. 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.

  2. Install "Home Assistant MCP Server" from Settings โ†’ Apps โ†’ Install app and click Start. (Home Assistant 2026.2 renamed "Add-ons" to "Apps"; on older versions this is the Add-on store.)

  3. Open the Logs tab to find your unique MCP URL.

  4. Connect your AI client to that URL โ€” no token or credential setup needed.

Full app documentation โ†’

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

Other install methods

These run the server outside Home Assistant โ€” useful for Container / Core installs (which can't run apps) or a separate host. The Setup Wizard generates the exact client-specific config for each.

  • Docker (HTTP server): run ghcr.io/homeassistant-ai/ha-mcp in HTTP mode, pointed at your Home Assistant URL and a long-lived token, and connect your client to its secret URL. See the Setup Wizard for the full command and per-client config.

  • PyPI / uvx (HTTP server): run the published ha-mcp package with uvx ha-mcp@latest (or pip) as a streamable-HTTP server the same way. Details in the Setup Wizard.

  • 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_url option). The OpenAI Tunnel below is different: it applies to any install method when Home Assistant isn't publicly reachable at all (no Nabu Casa or reverse proxy).

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

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

For other remote access methods (Cloudflare Tunnel, custom reverse proxy), see the Setup Wizard.

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

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

You're now connected to the demo environment! Connect your own Home Assistant โ†’

๐Ÿง Linux

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

Claude Desktop โ€” free, via the community build:

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

Claude Code โ€” official CLI, requires a paid Claude plan:

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

Full Linux guide โ†’

๐ŸชŸ Windows

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

You're now connected to the demo environment! Connect your own Home Assistant โ†’

๐Ÿง™ 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_ctx is one example) that can't hold a large tool set plus the conversation โ€” increase it well beyond the default.

Enable search-based discovery

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

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

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

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

When to enable

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

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

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

For the HA app, the same option is documented in homeassistant-addon/DOCS.md 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 [...] with eq/in/regex/contains/exists/...) inspired the per-tool approval rule schema (#966).

๐Ÿ‘ฅ Contributors

Maintainers

Contributors

  • @bigeric08 โ€” Explicit mcp dependency for protocol version 2025-11-25 support.
  • @airlabno โ€” Support for data field 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_overview for 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_search timeout 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_integration schema 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_transform support for automations and scripts.
  • @gcormier โ€” Windows installer improvements: removed unused variable and fixed terminal closing after install.
  • @ekobres โ€” Feature flags for HAMCP_ENABLE_FILESYSTEM_TOOLS and the (since removed) HAMCP_ENABLE_CUSTOM_COMPONENT_INTEGRATION in the app config, with beta tagging in source and docs.
  • @w3z315 โ€” 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 (now ha_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_event tool (#1239), dashboards-list helper refactor (#1207), for:-field duration-math detector in the best-practice checker (#1264), persistent DCR OAuth client registrations across restarts (#1265), and issue-triage prompt token-budgeting (#1522).
  • @KarelTestSpecial โ€” Cached YAML instance to prevent CPU spikes during bulk edits (#1371).
  • @corgan2222 โ€” HA brand assets for custom integration (#1317).
  • @drseanwing โ€” Progress emission via FastMCP Context in long-running tools (#1124); tool-discovery / categorized-search docs (#1123).
  • @fnordpig โ€” Config subentry support (#1393) and Assist pipeline management tool (#1392).
  • @paul43210 โ€” array_patch mode in ha_manage_app for 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_project tool exposing KNX group addresses from an uploaded ETS project file.
  • @cbowns โ€” Fixed inconsistent hyphen in setup.astro Codex CLI docs.
  • @Shaan-alpha โ€” Extended ha_restart known-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 rrule support in ha_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


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_restore already cover that
  • Cross-entity transactional restore (each restore is one entity)
  • Preserving user comments / whitespace from source YAML files (API-derived format)

Settings

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

Auto-default for auto_backup_dir:

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

Architecture

text
write/destructive tool  โ†’  @with_auto_backup  โ†’  BackupManager.maybe_snapshot
                                                       โ”‚
                                                       โ”œโ”€โ”€ DomainHandler.fetch (HA REST/WS)
                                                       โ””โ”€โ”€ write yaml, rotate, return path

ha_manage_backup tool  โ”€โ”
/api/settings/backups        โ”œโ”€โ”€โ†’  BackupManager  (list/read/diff/restore/delete)
Settings UI Backups tab      โ”€โ”˜

BackupManager (single instance per server, cached on the client object) owns:

  • Backup dir resolution and creation
  • Per-entity-key asyncio.Lock map to serialize fetch+write
  • Per-entity-key last_snapshot_ts for throttle
  • Domain handler registry: {domain: DomainHandler}
  • File operations (list, read, diff, write, delete, rotate)

DomainHandler is a frozen dataclass per backup-domain:

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

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

Write flow (the decorator)

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

id_param names the kwarg that carries the entity ID. For helpers, the
decorator takes domain_fn instead โ€” a callable that computes the domain
key from kwargs (so helper_type="timer" โ†’ domain helper_timer).

Decorator behavior (best-effort, never raises):

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

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

File format

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

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

Restore flow

User-facing entry points (both call BackupManager.restore):

  • LLM: ha_manage_backup(action="restore", name=...)
  • UI: POST /api/settings/backups/<name>/restore

BackupManager.restore_snapshot(name):

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

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

Polymorphic `ha_manage_backup` tool

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

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

Routing matrix

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

Gating against accidental wrong-mode usage

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

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

UI mapping

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

List item shape:

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

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

Settings UI changes

settings_ui/__init__.py gains:

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

Tool surface (28 wrapped)

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

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

Error handling

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

Testing strategy

Unit (tests/src/unit/test_backup_manager.py):

  • Throttle math (boundary at auto_backup_throttle_minutes=0 always snapshots; >0 enforces window)
  • Retention rotation (auto_backup_retain_per_entity enforced; oldest deleted)
  • Filename safety (path separators, special chars, unicode)
  • Diff output format
  • Schema version validation on restore
  • Best-effort error handling (mocked fetch raises โ†’ wrapped tool still runs)

E2E (tests/src/e2e/workflows/auto_backup/):
One test file per backed-up domain (test_automation.py, test_script.py, ..., one per DomainHandler). Each test:

  1. Toggle on
  2. Create/exist entity
  3. Edit via the wrapped tool โ†’ backup file appears
  4. Edit again โ†’ another backup file appears (with throttle off) OR no new file (with throttle on, within window)
  5. Restore first backup via ha_manage_backup(action="restore", ...) โ€” entity returns to original state
  6. Verify a safety backup was created
  7. Delete via ha_manage_backup(action="delete", ...) โ€” file removed
  8. Toggle off โ†’ next edit produces no backup

Files

New:

  • src/ha_mcp/backup_manager.py
  • src/ha_mcp/tools/auto_backup.py (decorator)
  • tests/src/unit/test_backup_manager.py
  • tests/src/e2e/workflows/auto_backup/ (one test file per domain)

Modified:

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

File: docs/beta.md

Beta Features

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

Current beta tools

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

How to enable

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

Dev channel app (Home Assistant users)

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

Stable channel app (Home Assistant users)

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

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

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

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

Known limitations

`ha_config_set_yaml`

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

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

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

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

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

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

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

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

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

Recommended prerequisites:

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

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

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

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

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

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

`ha_manage_custom_tool`

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

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

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

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

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

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

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

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

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

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

To get a full forensic trail of allowed calls, escalate the ha_mcp.tools.tools_code logger to DEBUG. This is HA's logger: integration and goes in configuration.yaml (not the app options):

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

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

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

Recommended prerequisites:

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

`enable_lite_docstrings`

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

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

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

Pair this toggle with one of the following to mitigate:

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

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

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

Recommended prerequisites:

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

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

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

Setup by deployment.

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

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

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

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

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

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

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

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

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

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

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

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

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

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


File: docs/dev-channel.md

Dev Channel

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

Who should use this:

  • Contributors testing their PRs
  • Issue reporters verifying fixes
  • Early adopters wanting the latest features

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

Release Schedule

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

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

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

:latest only ever tracks stable, never dev.

Installation Methods

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

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

Check version:

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

Switch back to stable:

bash
uvx ha-mcp

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

pip (Alternative)

If you prefer installing the package:

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

# Run the installed package
ha-mcp-dev

Check version:

bash
pip show ha-mcp-dev | grep Version

Switch back to stable:

bash
pip uninstall ha-mcp-dev -y
pip install --user ha-mcp

uv (Alternative)

Using uv's package installer:

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

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

# Run
ha-mcp-dev

Check version:

bash
uv pip show ha-mcp-dev | grep Version

Switch back to stable:

bash
uv pip uninstall ha-mcp-dev
uv pip install ha-mcp

Docker

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

Pull the dev image:

bash
docker pull ghcr.io/homeassistant-ai/ha-mcp:dev

Pin a specific dev build:

Every dev build is also published under an immutable :dev-<sha> tag (the short commit SHA), so you can pin to one exact build โ€” handy when verifying a fix on a specific commit. Unlike :dev, a :dev-<sha> tag never moves:

bash
docker pull ghcr.io/homeassistant-ai/ha-mcp:dev-a1b2c3d

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

Run in stdio mode (Claude Desktop):

bash
docker run --rm -i \
  -v ha-mcp-dev-data:/home/mcpuser/.ha-mcp \
  -e HOMEASSISTANT_URL=http://your-ha-instance:8123 \
  -e HOMEASSISTANT_TOKEN=your_token \
  ghcr.io/homeassistant-ai/ha-mcp:dev

Run in HTTP mode (web clients):

bash
docker run -d -p 8086:8086 \
  -v ha-mcp-dev-data:/home/mcpuser/.ha-mcp \
  -e HOMEASSISTANT_URL=http://your-ha-instance:8123 \
  -e HOMEASSISTANT_TOKEN=your_token \
  ghcr.io/homeassistant-ai/ha-mcp:dev ha-mcp-web

Config changes required: Change the image tag from latest to dev:

diff
- ghcr.io/homeassistant-ai/ha-mcp:latest
+ ghcr.io/homeassistant-ai/ha-mcp:dev

Switch back to stable:

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

Home Assistant app (add-on)

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

To use the dev channel:

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

Key differences from stable app:

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

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

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

Claude Desktop Configuration

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

Location:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows (traditional installer): %APPDATA%\Claude\claude_desktop_config.json
  • Windows (Microsoft Store): path varies by package โ€” see the Windows setup guide for a detection snippet

For uvx (dev channel):

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

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

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

Checking Your Version

The easiest way to check your version:

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

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

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

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

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

Reporting Issues

If you encounter issues with a dev release:

  1. Note the exact version number (e.g., 6.3.1.dev140)
  2. Check if the issue exists in stable - Try the latest stable release to confirm it's dev-specific
  3. Report the issue:
    • If it's a regression (worked in stable, broken in dev), comment on the related PR or issue
    • If it's a new bug, open a bug report 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_settings set_policy
  • ha_dev_manage_settings set_tool with gated= (a state / llm_api-only
    call still works, and a combined state + gated call is refused whole,
    before the state change is written)
  • ha_dev_manage_server approve and deny
  • ha_dev_manage_settings set / reset of enable_tool_security_policies
    โ€” turning the engine off would bypass every gate at once on the next
    restart (and the dev tools can restart the server)

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

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

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


File: docs/FAQ.md

FAQ & Troubleshooting

Common questions and solutions for ha-mcp setup.

General Questions

Do I need a Claude Pro subscription?

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

You can also use ha-mcp with other AI clients. See the Setup Wizard for 15+ supported clients.

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

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

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

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

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


Try Without Your Own Home Assistant

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

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

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

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


Troubleshooting

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

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

OAuth stopped working after upgrading to v7.0.0

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

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

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

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

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

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

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

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

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

"Terminating session: None" in server logs

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

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

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

To disable it:

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

See #783 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:

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

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

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

Third-party firewalls may block python or node processes spawned by Claude Desktop from making network connections. Check your firewall rules and allow connections for these processes. See #780 for an example resolution.

3. http:// vs https://

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

4. Python version too old

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

bash
brew install [email protected]

Then force a refresh:

bash
uvx --refresh ha-mcp@latest

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

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

SSL certificate errors (self-signed certificates)

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

Docker solution:

  1. Create a combined CA bundle:

    bash
    cat $(python3 -m certifi) /path/to/your-ca.crt > combined-ca-bundle.crt
  2. Mount it and set SSL_CERT_FILE:

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

Windows: pywin32 installation fails

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

  1. The MCP Python SDK requires pywin32 on Windows even though server-only users don't need it (python-sdk#2233)
  2. uv has a known issue installing pywin32 on Windows (uv#17679)

Workaround โ€” use Docker:

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

See #672 for details.

"uvx not found" error

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

Mac:

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

Windows:

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

MCP server not showing in Claude Desktop

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

"Token invalid" or authentication errors

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

Claude says it can't see Home Assistant

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

Claude Desktop shows the server connected but exposes zero tools

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

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

Why: The Anthropic API requires every tool name to match ^[a-zA-Z0-9_-]{1,64}$ (tool-definition docs), 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.

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

See #784 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-client as a Home Assistant subprocess (installed as a HACS custom repository; Linux amd64/aarch64 on HA OS / Supervised initially).
  • The client connects your MCP server URL to an OpenAI-hosted tunnel, so ChatGPT, Codex, and other OpenAI products can reach it โ€” no port forwarding, reverse proxy, or public URL needed.
  • Create a tunnel on the Tunnels page 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

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

Tools are missing or using old version

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

Solution:

bash
# Clear the uv cache
uv cache clean

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

Verify the version:

bash
uvx ha-mcp@latest --version

The version should match the latest release. 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-default
ha_report_issue diagnostic path described below.

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

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

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

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

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

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

Enforce mode

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

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

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

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

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

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

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

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

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


Feedback & Help

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


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:

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

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

Requirements

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

Setup

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

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

Connect URLs

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

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

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

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

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

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

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

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

Exposure modes

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

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

Per-tool exposure

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

Notes:

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

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

To remove the API from every agent's selector entirely, turn off
Conversation-agent LLM API in the entry's options.

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 latest ha-mcp release from PyPI.
  • dev: the latest development build, published to PyPI as ha-mcp-dev on
    every change to the project's main branch. Use it to try upcoming fixes, and
    expect the occasional rough edge.

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

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

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

If the installed server needs a newer version of the custom component than the
one you have (HACS can deliver a server build before you update the component),
a repair issue titled Update the HA-MCP Custom Component via HACS appears
under Settings โ†’ Repairs. The server keeps running; update the component via
HACS to clear it โ€” see Held server updates 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 that ha_auth can't serve (Google Gemini Spark, GitHub
    Copilot CLI). The component runs its own OAuth 2.1 authorization server at the
    Home Assistant root and issues a static Client ID / Client Secret to paste
    into the client โ€” the secret is the credential and grants admin-equivalent
    access, so guard it like the none URL. Tokens are self-issued (1h access /
    30d refresh) and carry no Home Assistant login; rotating the credential
    (regenerate toggle, or a custom Client ID / Secret override) invalidates
    outstanding tokens, but only after the Home Assistant restart the repair
    prompts for. See SECURITY.md
    for the full threat model (unauthenticated consent page, permissive redirect
    URIs, route ownership vs the app).

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

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

See SECURITY.md for the full threat model.

Relationship to the File & YAML services entry

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

First start takes a little longer

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

Troubleshooting

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

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

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

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

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


File: docs/macOS-uv-guide.md

macOS Setup Guide

Control Home Assistant with Claude Desktop in about 10 minutes.

Works with free Claude account - no subscription needed.

Step 1: Create a Claude Account

Go to claude.ai and create a free account.

Step 2: Run the Installer

Open Terminal and paste:

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

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

Manual Installation (if the installer doesn't work)

Install uv

bash
brew install uv

Or without Homebrew:

bash
curl -LsSf https://astral.sh/uv/install.sh | sh

Configure Claude Desktop

  1. Open Claude Desktop
  2. Menu bar โ†’ Claude โ†’ Settings... โ†’ Developer โ†’ Edit Config
  3. Paste:
json
{
  "mcpServers": {
    "Home Assistant": {
      "command": "uvx",
      "args": ["ha-mcp@latest"],
      "env": {
        "HOMEASSISTANT_URL": "https://ha-mcp-demo-server.qc-h.net",
        "HOMEASSISTANT_TOKEN": "demo"
      }
    }
  }
}
  1. 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:

text
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:

Try asking Claude:

  • "Turn on the kitchen lights"
  • "What's the temperature in the living room?"
  • "Create an automation that turns off all lights at midnight"

Step 6: Connect Your Home Assistant

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

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

Replace the demo values:

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

To get your token:

  1. Open Home Assistant in your browser
  2. Click your username (bottom left)
  3. Security tab โ†’ Long-lived access tokens
  4. Create token โ†’ Copy immediately (shown only once)

Then restart Claude: Claude menu โ†’ Quit Claude, then reopen.

Step 7: Share Your Feedback

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

Troubleshooting

Can't connect to local Home Assistant?

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

  1. macOS Local Network Privacy (Sequoia 15+) โ€” macOS may silently block Claude Desktop subprocesses from accessing local network IPs. Check System Settings โ†’ Privacy & Security โ†’ Local Network. As a workaround, use an SSH tunnel:

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

    Then set HOMEASSISTANT_URL to http://localhost:8123.

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

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

  4. Python 3.13 required โ€” ha-mcp requires Python 3.13+. Older versions get outdated ha-mcp with known bugs. Run brew install [email protected] then uvx --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:

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

uvx:

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

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


When to Use OAuth

Use OAuth if you want:

  • Real authentication instead of relying on secret URLs
  • Multi-user support with per-user credentials
  • Users to authenticate themselves via consent form

Use private URL method if you want:

  • Simpler setup (recommended for most users)
  • Single-user access

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


Setup

1. Expose with HTTPS

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

For production, set up a persistent Cloudflare Tunnel.

2. Start OAuth Server

Docker:

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

uvx:

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

3. Environment Variables

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

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

4. Connect in Claude.ai

  1. Go to Settings โ†’ Connectors โ†’ Add custom connector
  2. Enter URL: https://your-tunnel.com/mcp
  3. Click Add
  4. In the consent form that opens:
  5. Click Authorize

FAQ

"404 Not Found" when connecting

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

text
โœ… Correct: https://your-tunnel.com/mcp
โŒ Wrong:   https://your-tunnel.com

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

"Invalid credentials" after authorizing

Verify your Long-Lived Access Token:

  • Generate a fresh token in HA: Profile โ†’ Security โ†’ Long-lived access tokens
  • Copy the complete token

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

Do tokens persist across server restarts?

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

Can I use OAuth with Home Assistant OS?

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

Alternatives:

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

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


Back to: Main Documentation


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:

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

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

Manual Installation (if the installer doesn't work)

Install uv

Open PowerShell or cmd:

powershell
winget install astral-sh.uv -e

Configure Claude Desktop

  1. Open Claude Desktop
  2. Settings โ†’ Developer โ†’ Edit Config
  3. Paste:
json
{
  "mcpServers": {
    "Home Assistant": {
      "command": "uvx",
      "args": ["ha-mcp@latest"],
      "env": {
        "HOMEASSISTANT_URL": "https://ha-mcp-demo-server.qc-h.net",
        "HOMEASSISTANT_TOKEN": "demo"
      }
    }
  }
}
  1. 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:

text
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:

Try asking Claude:

  • "Turn on the kitchen lights"
  • "What's the temperature in the living room?"
  • "Create an automation that turns off all lights at midnight"

Step 6: Connect Your Home Assistant

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

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

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

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

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

Replace the demo values:

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

To get your token:

  1. Open Home Assistant in your browser
  2. Click your username (bottom left)
  3. Security tab โ†’ Long-lived access tokens
  4. Create token โ†’ Copy immediately (shown only once)

Then restart Claude: File โ†’ Exit, then reopen.

Step 7: Share Your Feedback

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


Having issues? See the FAQ & Troubleshooting Guide.