HA MCP Server
Provides tools for controlling and managing a Home Assistant instance, including lights, climate, covers, media, alarm, cameras, automations, scripts, scenes, helpers, dashboards, areas, floors, labels, devices, entities, add-ons, HACS, backups, system health, history, logbook, statistics, and energy. Also supports deleting automations, dashboards, helpers, and users.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@HA MCP Serverset the thermostat to 72"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
HA MCP Server
An MCP server that exposes Home Assistant's REST and WebSocket APIs as 189 tools (185 registered by default — four are off until explicitly enabled, see the registration gate below), so an MCP client — Claude Code among others — can drive an instance directly.
Lights, climate, covers, media, alarm and cameras; automations, scripts, scenes, helpers and dashboards; the area, floor, label, device and entity registries; add-ons, HACS, backups and system health; history, logbook, statistics and energy.
A status dashboard shows what the server sees and which tools have been called.
It can also delete things — automations, dashboards, helpers, users. Every tool was exercised against a live instance before release, but treat it as what it is: a broad, powerful interface to your home.
create_user,update_useranddelete_user— the tools that manage Home Assistant login accounts, not entities — are disabled by default; see the registration gate below to turn them on.

The screenshot is generated by
scripts/screenshots.pyagainst a mock of the Home Assistant API, never against a real instance — the dashboard puts the location name in the page title.
Two ways to run it
As a Home Assistant app
The easiest route if you already run Home Assistant: the app reaches it through the Supervisor, so no long-lived token has to be created, and the dashboard is available in the sidebar through ingress.
Repository: driin0/home-assistant-apps. That repository holds only the packaging — the code is here, and it ships the image published from this repository.
As a container
For a Home Assistant instance you do not control, or to run the server elsewhere:
git clone https://github.com/driin0/ha-mcp-server
cd ha-mcp-server
cp .env.sample .env # fill in HA_URL, HA_TOKEN and MCP_SECRET
docker compose up -dThe image (ghcr.io/driin0/ha-mcp-server) is published multi-architecture
(amd64, arm64), so nothing is built locally. To work on the code, replace image: with build: . in
compose.yaml.
Variable | Default | Meaning |
| — | Home Assistant base URL, e.g. |
| — | a long-lived access token |
| — | bearer token clients must present |
|
| start without a secret — only on a trusted network |
| empty | extra hostnames/IPs (comma-separated, no scheme or port) the MCP endpoint may be reached at, beyond |
| — | Basic Auth password for the status dashboard — its own secret, independent of |
|
| serve the dashboard without a password — only on a trusted network, and a decision separate from |
|
| ports on the host |
| empty | language for conversation tools |
|
| media players treated as Alexa devices |
| empty | entity prefixes routed as remotes |
|
| registers |
|
| registers |
|
| registers |
|
| skip the dashboard's Basic Auth — only when reached exclusively through HA Supervisor Ingress (see below) |
UI_SECRET is its own secret, not MCP_SECRET reused
The dashboard sends UI_SECRET as HTTP Basic — base64-encoded, not
encrypted, and this project ships no TLS. Earlier releases defaulted
UI_SECRET to MCP_SECRET when unset, which meant a passive observer on
the same LAN who saw a single dashboard page load recovered a base64 string
that decodes to the full-admin MCP bearer token. UI_SECRET is now
independent: generate its own value (openssl rand -base64 32, same as
MCP_SECRET — just a different one), and set it explicitly. With neither
UI_SECRET set nor HA_INGRESS_MODE=true, the dashboard refuses to start
unless UI_ALLOW_NO_AUTH=true says to serve it without a password anyway
(trusted networks only).
HA_INGRESS_MODE
Set only by the Home Assistant app's own run.sh — not something a
container deployment normally needs. When the dashboard is reached
exclusively through the HA Supervisor's Ingress proxy, HA has already
authenticated the user before the request ever reaches this server, and
Ingress requests carry no Authorization header at all (Basic Auth would
401 every one of them). HA_INGRESS_MODE=true skips the dashboard's Basic
Auth middleware for exactly that reason — it does not skip UI_SECRET
checking as a matter of policy so much as make it moot, since Ingress is
the only path in.
That trust assumption holds specifically because the add-on's
config.yaml maps only port 47821 (the MCP endpoint, still bearer-auth
protected) to the host — port 47822 (the dashboard) is reachable only
from the internal hassio Docker network, not the LAN. The residual: any
other add-on running on the same Home Assistant instance is also on that
internal network, so a compromised co-resident add-on reads the dashboard
unauthenticated, always, regardless of MCP_SECRET/UI_SECRET. This is a
narrower exposure than the LAN (an add-on has to already be installed) but
is not nothing — the dashboard shows tool names, call statistics and recent
error text, no secrets or tokens (see stats.py's redaction).
Do not set this in a container deployment unless something equivalent
to Supervisor Ingress sits in front of this server and nothing else can
reach port 47822 — otherwise it removes the dashboard's authentication
entirely, on a port most reverse-proxy setups do not otherwise protect.
Two separate "no auth" decisions
MCP_ALLOW_NO_AUTH and UI_ALLOW_NO_AUTH control two different things and
must each be opted into on their own:
MCP_ALLOW_NO_AUTHlets the MCP endpoint — where tools are called, with full control over Home Assistant — start withMCP_SECRETempty.UI_ALLOW_NO_AUTHlets the status dashboard — read-only system info and call statistics, no tool-calling surface — start withUI_SECRETempty.
Setting one does not set the other. Earlier releases conflated the two:
MCP_ALLOW_NO_AUTH alone satisfied both startup checks, so setting it to
get the MCP endpoint running silently left the dashboard unauthenticated as
well, with nothing saying so. MCP_SECRET being set, regardless of either
flag, still means the MCP endpoint requires it; HA_INGRESS_MODE=true
still means the dashboard needs neither UI_SECRET nor UI_ALLOW_NO_AUTH,
since Ingress authenticates upstream.
Origin and Host validation
A browser page on the same network can be made, via DNS rebinding, to reach
this server directly: the page's Origin/Host still name the attacker's
domain, but the connection lands here after that domain's DNS record is
re-pointed at the local IP. This matters most when MCP_SECRET is empty —
the configuration MCP_ALLOW_NO_AUTH exists to create — because after the
rebind the attacker's page is same-origin with this server, so no other
check catches it.
The MCP endpoint therefore rejects any request that carries an Origin
header not resolving to localhost or one of the hostnames/IPs listed in
MCP_ALLOWED_HOSTS, and equally rejects one whose Host header does not
resolve to one of the same. This runs unconditionally, whether or not
MCP_SECRET is set. A request with no Origin header at all — the
ordinary shape of a non-browser MCP client — is not affected: only a real
browser can send one, and a browser cannot omit it. If your MCP client runs
inside a browser and reaches this server at a LAN IP or a hostname other
than localhost, add it to MCP_ALLOWED_HOSTS or every request from it
will be rejected with 403 and a line in the server's log explaining why.
The registration gate: three groups, two defaults
Nothing server-side can make an MCP client ask for confirmation before calling a tool — the one guardrail this server can enforce is not registering a tool at all, so it never appears in the client's menu. Three groups of tools go through this gate, each behind its own env var:
Group | Tools | Env var | Default |
|
|
| off |
|
|
| on |
|
|
| off |
user_management, off by default. create_user, update_user and
delete_user manage Home Assistant login accounts, a different risk
tier from turning off a light or even deleting a scene: a request that
sounds routine ("add a account for the cleaner") is a privileged,
account-creating action, and almost nobody needs a language model to have
that capability.
physical_security, on by default. Locks and the alarm panel are, unlike
user accounts, a normal and common reason to run this server at all —
disabling them by default would break the ordinary case, and upgrading from
an earlier release must not make capabilities disappear with no error, only
absence. They are still gated, unlike other actuating tools, because they
are the two tools a prompt-injection payload would most want to reach: a
cast speaker's media_title or an entity's own friendly_name can carry
attacker-controlled text into a model's context before any tool is even
called (see get_live_context()'s docstring). Set
MCP_ENABLE_PHYSICAL_SECURITY=false to remove them if you would rather a
model never see them at all.
⚠️ This particular gate is weaker than the other two. call_service can
invoke lock.unlock and every alarm_control_panel.* service directly, and
is not covered by this or any gate — so disabling physical_security
removes the named, discoverable tools from an MCP client's tools/list,
which is real value against an injection that names a tool by its obvious
name, but it does not remove the underlying capability from a caller
willing to use call_service instead.
addon_api, off by default. call_addon_api is a generic proxy into
whatever HTTP API an installed add-on exposes — few installations need a
language model to have this, and unlike locks or the alarm panel it is not
why most people run this server. Unlike physical_security, this gate is
a full capability removal: neither call_service nor fire_event can reach
the Supervisor's per-add-on API endpoint at all.
Every other tool that existed in earlier releases — every delete_*,
restart_homeassistant, apply_update — stays ungated and registered by
default.
If a tool you expect is missing, call list_disabled_tools() — it is always
registered, and reports which groups are gated, whether each is currently on,
and the env var that controls it.
call_service and fire_event themselves are not covered by this or
any gate: they are generic passthroughs (any Home Assistant service, any
event on the bus) that a named-tool gate cannot cover without also
restricting them specifically, and restricting either would remove far more
than the gated tools above — so treat them as carrying the safety weight of
whatever they are pointed at, not the weight of their own name.
Related MCP server: hass-mcp
Connecting a client
{
"mcpServers": {
"homeassistant": {
"type": "http",
"url": "http://<host>:47821/",
"headers": { "Authorization": "Bearer <MCP_SECRET>" }
}
}
}Development
pip install -r requirements.txt
HA_URL=… HA_TOKEN=… MCP_ALLOW_NO_AUTH=true python3 server.pyThe tools live in tools/, one module per area, and register themselves through
tools/_base.py. web.py serves the dashboard; stats.py keeps the call
counters behind it.
Tests
pip install -r requirements-dev.txt
pytest tests/Runs against an in-process fake Home Assistant, no real instance needed. A normal run prints a warning stating how much of the tool surface the runtime conformance check actually covers (it calls every tool that takes no arguments and confirms it returns the expected shape) — that is a disclosure, not noise, so don't silence warnings to make it go away.
Reference validation
A Home Assistant automation cut mains power to a running NAS mid-write and
corrupted 245 GB. The guard was {{ not is_state("button.nas_shutdown", "unavailable") }}, and the entity had been renamed. In Home Assistant the
state of an entity that does not exist is None, never the string
"unavailable" — so is_state() returned False, not False is True,
and the guard silently started passing instead of blocking. A second,
independent fault stacked on top of it: a wait_for_trigger with a timeout
and no continue_on_timeout: false carried execution past the guard into
switch.turn_off against a machine still writing to disk. Neither fault
raised an error, wrote a log line, or tripped a repair issue.
Four tools exist to catch both shapes before they cause damage:
validate_automation/validate_all_automations— check every entity/device an automation references against this instance's live registries and state machine (dead_reference,restored,disabled,unavailable,unknown), and separately report anywait_for_triggerthat can silently carry a timeout into a destructive action. These are reported as two separate lists (issuesandfail_open_waits) because the incident needed both faults to cause damage — reading only one undercounts.disabledis kept separate fromrestored: both describe a registered id with no current state, butdisabledmeans the entity was deliberately turned off (disabled_byset) — working as configured, not an integration failure.unknownis kept separate fromunavailableand reported at a lower severity: on a vanilla instance with nothing wrong, several entity types (buttons, event entities, scenes, some helper and voice-pipeline entities) sit inunknownas their ordinary resting state, whileunavailablenever happens as a resting state — seetools/validation.py's own docstring for the measurement.find_entity_usages— "if I rename or remove this entity, what breaks?", searched across automations and scripts only (not dashboards, template entities or helpers).list_orphan_entities— registry entries with no current state, exactly what a reconfigured integration leaves behind.
scripts/lint_automations.py is a CLI over the same validator, for CI or
a schedule:
HA_URL=https://your-instance:8123 HA_TOKEN=… python3 scripts/lint_automations.pyIt prints every dead reference, restored reference, disabled reference, unavailable reference, unknown reference and fail-open wait it finds, and exits:
0— no dead references and no fail-open waits found (there may still berestored/disabled/unavailable/unknownfindings printed above — an integration problem, an entity working as configured, or often nothing at all forunknown— not a config defect, so none of the four fail the build).1— at least one dead reference or fail-open wait was found — see the printed report for which automation and which one.2— the sweep could not run at all (missingHA_URL/HA_TOKEN, or a transport/WebSocket failure reading the registry or states).
There is no offline mode: resolving a reference needs the live entity/device registry, which a YAML file on disk cannot answer about itself.
A note on the base image
The image is built on python:3.13-alpine on purpose, and its Alpine and
Python versions must stay aligned with the Home Assistant base image
(ghcr.io/home-assistant/base-python:3.13-alpineX.Y). The Home Assistant app
copies the compiled packages from this image rather than reinstalling them, and
native extensions — pydantic_core, cryptography, rpds, websockets —
only load if musl and the Python minor version match. The Dockerfile imports
them at build time so a future mismatch fails there, not at first start.
Licence
Copyright (C) 2026 Riccardo Riina (driin0)
Distributed under the GNU Affero General Public License v3.0. See LICENSE.
Trademarks
Home Assistant and its logo are trademarks of the Open Home Foundation. The Model Context Protocol and its mark belong to Anthropic. This project is independent and is not affiliated with, nor endorsed by, either.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
A TypeScript MCP server for Home Assistant, enabling programmatic management of entities, automati…
Hosted MCP server connecting claude.ai, ChatGPT and other AI apps to your own computer
Hosted MCP server connecting AI assistants to 9,000+ apps and 40,000+ actions via Zapier.
A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceAn MCP server and Home Assistant add-on that enables AI assistants to manage smart homes by creating automations, designing dashboards, and interacting with entities. It features native access to Home Assistant APIs, built-in Git versioning for safe rollbacks, and full management of HACS integrations.619MIT
- AlicenseAqualityDmaintenanceMCP server for controlling and querying Home Assistant via its REST API, exposing tools to get entity states, list all states, and call services.16237MIT
- AlicenseAqualityCmaintenanceMCP server for full Home Assistant control, enabling AI agents to manage dashboards, automations, files, apps, entities, and more via REST API, WebSocket, and SSH.6692MIT
- AlicenseAqualityBmaintenanceA self-hosted MCP server for Home Assistant that exposes full control over entity states, service calls, history, templates, and areas via local stdio, enabling AI assistants to manage your smart home.991MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/driin0/ha-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server