Tools reference

Phoenix MCP implements all 23 native Home Assistant MCP tools with identical names and responses, then adds 108 of its own, 128 in all. Entity reads and native controls are scoped to the token's permission tree; the ones below note the capability they additionally require. Automation, script, and other authoring/system tools are governed by capability gates instead of the entity tree (see below).

Automations & scripts

These tools write YAML directly and do not consult the permission tree. See automation and script write.

get_automationAutomation write

Read one automation's full current configuration. This is the read half of a safe edit: edit_automation replaces the whole config, so an agent that edits without reading first silently destroys any trigger, condition, or action it did not resend. The response includes a content_hash to pass back as expected_hash on the edit, making the write conditional on the automation not having changed in between. YAML tags appear as display strings (for example !secret my_key); a secret's value is never resolved or returned. Gated on the same capability as editing, and never requires approval (reads are not gated by Confirm).

params
automation_id (required), from list_automations or the entity's id attribute
create_automationedit_automationdelete_automationAutomation write

Create, edit, or delete automations in the YAML file your configuration.yaml routes automations to. Split configurations (!include and !include_dir_* layouts) are followed to the correct leaf file and only the target entry's lines are rewritten: comments, formatting, and !secret references elsewhere in the file are preserved byte-for-byte. New automations are appended for a plain !include, or written as a new file per entry for directory includes. Ambiguous cases (duplicate ids across files, entries in packages or inline config, multiple create targets) are refused rather than guessed at. Edit replaces the entire configuration, so read it with get_automation first and resend everything you want to keep.

create
config (required), an object holding alias, trigger, action, plus optional condition, mode
edit / delete
automation_id (the slug); edit replaces the entire config and accepts an optional expected_hash from get_automation, which refuses the write if the automation changed since that read
get_scriptScript write

Read one script's full current configuration, the read half of a safe edit for the same reason as get_automation: edit_script replaces the whole config. Returns a content_hash for the edit's expected_hash, and renders YAML tags as display strings without resolving secrets.

params
script_id (required), the part after script. in the entity id
create_scriptedit_scriptdelete_scriptScript write

Create, edit, or delete scripts in the YAML file your configuration.yaml routes scripts to. Split configurations (!include and !include_dir_named/merge_named layouts) are followed to the correct leaf file, preserving untouched content byte-for-byte, the same include-graph handling as the automation tools. Edit replaces the entire configuration, so read it with get_script first.

create
script_id and config (both required), where config holds alias, sequence, mode, variables, fields
edit / delete
script_id (the slug); edit replaces the entire config and accepts an optional expected_hash from get_script

Configuration & templates

get_configConfig read

Read a curated subset of Home Assistant's configuration: version, time zone, unit system, location name, and the list of loaded integrations. Precise home coordinates, internal/external URLs, and host filesystem paths are withheld.

list_blueprintsConfig read

List installed automation and script blueprints with their inputs. To instantiate one, create an automation or script with a use_blueprint config (the existing authoring tools accept it); there is no separate create-from-blueprint tool.

params
domain (optional, automation or script)
get_blueprintConfig read

Read one blueprint's YAML source, so an agent can see what it actually does before instantiating it. list_blueprints reports the inputs a blueprint accepts; this returns the body behind them, !input placeholders included. Only a path the blueprint registry itself enumerated can be read, so no path outside blueprints/ is reachable.

params
domain (required; automation or script), path (required; exactly as returned by list_blueprints)
create_blueprintedit_blueprintdelete_blueprintBlueprint write

Author blueprints from YAML you supply. Confirm-eligible. Gated by two capabilities: cap_blueprint_write plus the matching cap_automation_write or cap_script_write, so a token cannot rewire scripts through a script blueprint while script authoring is denied. Paths are jailed to the domain's blueprint folder and validated against Home Assistant's own blueprint schema before an approval is created, so a malformed document is refused immediately rather than failing after you approve it.

Editing has a wider blast radius than it looks. Home Assistant reloads every automation or script built from the blueprint, and those entities' own configs do not change at all, so the effect is invisible in their history. The approval names the entities that will be reloaded, and the tool result repeats them. Deleting is refused by Home Assistant while anything still uses the blueprint.

There is deliberately no import-from-URL. Fetching an operator-unseen URL from inside Home Assistant would reach the LAN and the supervisor API; instead the agent fetches the URL itself and passes the YAML, so the full source lands in the approval where you read it.

params
domain (required; automation or script), path (required; relative .yaml path, jailed to blueprints/<domain>/), content (required for create/edit; the full blueprint YAML)
get_logsLog read

Recent HA system log entries. Phoenix MCP's own entries are excluded and token values, JWTs, and URL credentials are scrubbed from messages and tracebacks. The scrubber is pattern-based and cannot guarantee removal of arbitrary third-party secrets or PII an integration writes into a log line, so treat Log read as an elevated-trust grant.

The response reports total (entries matching the level and integration filters) next to count (entries in this page) and a truncated flag. Same on /api/phoenix-mcp/logs.

params
limit (default 50, max 100), level (INFO / WARNING / ERROR), integration (only entries logged by one integration, e.g. zha)
get_logbookLog read

The human-readable logbook (state changes, automations and scripts triggered). Scoped to entities the token can access; entries for out-of-scope entities are dropped. This is the narrative event history, distinct from get_logs (error log) and get_history (raw samples).

params
start_time (default 24h), end_time, entity_id (optional, focus on one entity), limit (default 100, max 1000)
render_templateTemplate render

Render a Jinja2 template in a permission-scoped environment; templates can only reach entities the token can read.

params
template (string, required)

Discovery & diagnostics

Read-only, scoped to accessible entities. Per-object lookups return an identical "not found" for a missing object and one the token cannot see, so they cannot probe for existence.

check_configvalidate_configDiagnostics

check_config validates the HA configuration files. validate_config validates an automation or script config without saving and reports, per referenced entity, whether it is accessible to the token (entities outside the token's scope are reported as inaccessible and not visible).

params
check_config takes none. validate_config: type (required; automation or script), config (required; the config to validate)
compare_stateSearch & discovery

Compare accessible entities' states between two times, backed by the recorder (the tool fails when the Recorder integration is disabled).

params
entity_id (required), t1 (required; ISO or relative like 24h), t2 (default now)
describe_areaSearch & discovery

One area, its floor, and accessible entities grouped by domain.

params
area (required; an area name, alias, or area_id)
describe_entitySearch & discovery

Comprehension summary of one entity: state, area, the services in its domain, what references it, and its MESA control_mode when MESA is active. A configuration or diagnostic entity is flagged with entity_category, so an agent can tell a device's setup and health entities from its primary controls.

params
entity_id (required)
find_available_actionsSearch & discovery

The services in an entity's domain and whether the token can invoke each one now, accounting for write access, physical and dual gates, and the entity's MESA control_mode.

params
entity_id (required)
get_automation_tracesAutomation traces

Execution traces for an accessible automation. Optional run_id and summary (highlights the error and last step).

params
automation_id (required), run_id, summary
get_overviewSearch & discovery

Compact home summary: total accessible entities, counts by domain and area, the unavailable count, and the deployment mesa_mode. When MESA is active and the token has Config Read, it also includes the operator-set prohibited and read-only entities (the “do not operate” list), so the agent learns what is off-limits in one call.

get_relationshipsSearch & discovery

Reverse references (automations, scripts, scenes that reference the entity) and forward references, scoped to what the token can access.

params
entity_id (required)
get_system_healthDiagnostics

Home Assistant version and per-integration system health. Secret-keyed values and URL-embedded credentials in the integration data are scrubbed before they are returned, as are network-topology details (LAN IP addresses, hostnames inside URLs, and filesystem paths) that Phoenix MCP withholds elsewhere. This is an elevated read; treat cap_diagnostics accordingly.

get_esphome_overviewDiagnostics

Status of the ESPHome devices this token can see: firmware version and build time, online state, Bluetooth proxy connection slots (free of limit), and the user-defined API actions each device declares along with the Home Assistant action name each maps to. When the ESPHome Device Builder add-on is set up, each device also reports its configuration filename and whether a firmware update is available. A device appears only if the token can access at least one of its entities; MAC addresses, the API encryption key, and the device's network address are never returned, and Device Builder configurations belonging to devices outside the token's scope are reported as a count only.

get_radio_networkget_radio_deviceDiagnostics

Radio-network health (Zigbee2MQTT or ZHA, auto-detected). get_radio_network reports every radio network present: channel, PAN id, coordinator, and whether the pairing window is open. get_radio_device reports one device's signal quality (LQI/RSSI), availability, power source, interview state, and (ZHA) mesh neighbors, with out-of-scope neighbor identifiers redacted. Network keys never appear in any response.

params
get_radio_network: none; get_radio_device: device_id
list_areaslist_floorslist_zoneslist_devicesget_deviceRegistry read

Registry enumeration restricted to areas, floors, zones, and devices that contain at least one accessible entity. get_device returns one device plus its accessible entities; it takes device_id (required), the list tools take none.

list_automationslist_scriptslist_sceneslist_helpersRegistry read

Enumerate accessible automations, scripts, scenes, and helpers. The automation and script listings carry the id each one's read and edit tools take (get_automation / get_script), alongside the entity id, alias, and current state.

recent_activitySearch & discovery

Accessible entities that changed in the last N minutes, newest first.

search_entitiesSearch & discovery

Filter accessible entities by query, domain, area, device class, state, and staleness. A multi-word query matches entities containing all the words and results are ranked by relevance, so the best matches lead. If that exact matching finds nothing, a typo-tolerant fallback retries against entity id and friendly name, tagging the response fuzzy_fallback: true. Each row carries control_mode when its MESA nature is non-default, so restricted entities are visible without a follow-up call, and entity_category when the entity is a configuration or diagnostic entity rather than a primary control.

params
query, domain, area, device_class, state, unavailable, stale_hours, limit (all optional)
whatifSearch & discovery

Predict which automations would fire if an accessible entity changed to a hypothetical state, without changing anything. State and numeric_state triggers are evaluated best-effort.

params
entity_id (required), hypothetical_state (required)

Files, YAML, integrations & system

get_esphome_yamlset_esphome_yamlESPHome device YAML

Read and edit ESPHome device configuration files in their own jail under esphome/, deliberately separate from cap_filesystem: device YAML embeds C++ lambdas and, in practice, inline credentials, so widening a filesystem grant must never reach it. Call get_esphome_yaml with no argument to list the files this token can see, or with file to read one.

Credentials are masked, not exposed. API encryption keys, wifi/OTA/MQTT passwords, anything under a credential-shaped key, and any value that also appears in the ESPHome secrets.yaml come back as __PHOENIX_REDACTED__<path>__ placeholders. Leave a placeholder where it is and the real value is spliced back on write, so an edit round-trips without the agent, the approval record, or the diff ever holding the secret. Comments and formatting outside those spans survive byte-for-byte.

A credential cannot be changed to a new literal value. Replacing one with a !secret reference is allowed and is the supported migration; the key must already exist in the ESPHome secrets.yaml, which defined_secrets lists. That file is never readable or writable through any tool. The freeze is not only about disclosure: an API encryption key must keep matching what Home Assistant stored, and an OTA password governs the next upload, so a blind change can leave a device unreachable until someone reflashes it over a cable.

Writing never flashes anything. The file lands on disk and the device keeps running its current firmware until a build is installed, which is a separate tool behind a separate capability. content_hash is taken over the real file, so expected_hash still guards against a change during an approval window, and every write is versioned for rollback in the Changes tab.

New credentials are generated, not invented. A device that does not have an API encryption key or OTA password yet is a real problem for an agent: it needs a value, and a language model cannot produce cryptographic randomness, so anything it makes up is guessable. Writing !phoenix_generate as the value solves it from the other side. Phoenix substitutes a genuinely random one as the file is written, so the secret exists only on disk, is masked on every later read, and never reaches the agent, the approval record, or the diff, which shows the tag and therefore the intent. You read the value from the ESPHome dashboard when Home Assistant asks for it. It is refused on a credential that already exists, because rotating a key a device is currently running strands it until Home Assistant is re-keyed, and refused outright on the wifi ssid and password, which belong to the house rather than the device.

params
get_esphome_yaml: file (omit to list); set_esphome_yaml: file, content, expected_hash
validate_esphome_yamlget_esphome_boardget_esphome_componentget_esphome_automationsESPHome device YAML

Four read-only lookups served by the ESPHome Device Builder add-on, which turn ESPHome authoring from recall into reference. They are hidden from a token's tool list entirely when the add-on is not installed, and a call made while it is merely stopped fails with a clear message rather than a silent empty answer. Phoenix MCP speaks a hardcoded command allowlist to the add-on; the add-on has no permission model of its own, so that allowlist is the boundary and is pinned by test.

validate_esphome_yaml runs ESPHome's configuration check over one device file, without compiling or flashing. It validates the file as it is on disk, never caller-supplied content, and that is a security boundary rather than a convenience: ESPHome evaluates configuration at validation time and a config carrying external_components fetches and executes remote code to do it, so accepting an arbitrary payload would be an unreviewed code-execution path. A file on disk got there through set_esphome_yaml, which is capability-gated and can require approval. The validator quotes offending lines back, which can include a credential the file carries inline, so its output is scrubbed of every known secret before it is returned.

get_esphome_board returns a board's full pin map (which GPIOs exist, which are already occupied, and any warnings) plus its hardware and platform details, or searches the board catalog. get_esphome_component returns a component's real configuration schema, searches the catalog, or lists categories. get_esphome_automations lists the triggers, actions and conditions available inside one device's YAML, scoped to the components that device actually loads. The two file-bound tools follow the same jail and entity scoping as get_esphome_yaml, and an out-of-scope file is indistinguishable from a missing one. The automation catalog is projected through a per-section field allowlist, dropping prose, editor-only flags, and display names that merely restate the id: the raw payload measured 64KB for one real device, which is enough to swamp an agent's context. Every entry is kept, only the fields are trimmed.

params
validate_esphome_yaml: file; get_esphome_board: board_id, query, platform, offset, limit; get_esphome_component: component_ids, query, category, exclude_category, platform, board_id, offset, limit; get_esphome_automations: file
compile_esphome_firmwareinstall_esphome_firmwareget_esphome_jobwait_for_esphome_jobcancel_esphome_jobESPHome device YAMLESPHome firmware flashing

Build the firmware an agent just authored, and put it on the device. These close the loop that validation leaves open: validation catches schema and pin errors, but the C++ and lambda mistakes that actually break a model-written ESPHome config only surface in a real build.

Building and flashing are separate capabilities. compile_esphome_firmware rides the same capability as editing the YAML, because compiling produces a binary and touches no device: the capability that authorized writing the file authorizes proving it builds. Flashing is its own grant, ESPHome firmware flashing, which is denied by default in every persona except esphome, where it is set to Confirm. So a token can iterate on firmware all day without being able to put any of it on hardware.

A build takes minutes, so nothing here waits for one. Both tools enqueue a job in the Device Builder and return a job_id straight away; get_esphome_job polls it. That is not only about agent latency: approving a flash runs it inline in the panel's request, and a tool that blocked for eight minutes would hang the Approve button. The jobs live in the add-on and survive its restart, so Phoenix MCP keeps no build state of its own.

A finished compile is not a flashed device. An install enqueues two jobs, a compile and an upload held behind it, and returns only the first. get_esphome_job resolves the second and reports flashed as true only once that upload has actually completed. If the device was offline, there is no upload job at all and the flash is armed for its next wake, which is reported as exactly that rather than as success. Phoenix MCP always installs over the air and ignores any port a caller supplies.

Watching it happen. The build tools return instantly, so wait_for_esphome_job is what holds a call open while the job runs. It waits up to five minutes, returns the moment the job is done, and reports lines like Compiling living-room: 70% as it goes. For an install it follows the whole chain, compile and then upload, so a single call covers the lot and returning means the device really was flashed. In Agent Chat those lines appear live under the running tool, visible even with verbose output switched off. An external MCP client sees them only if it asks for progress on the call and renders progress notifications; where it does not, the percentage is still in every poll for the agent to relay in its own words.

Two smaller safeguards worth knowing. Starting a second build for a file that is already building does not queue behind the first, it cancels it and discards its log, so Phoenix MCP refuses and points at the running job instead. And a job_id is scoped by the file it names, so it is not a way to read another device's build log: an id that does not exist and an id you cannot see return the same answer. Build logs are scrubbed of every known credential and bounded, since a real one runs thousands of lines.

params
compile_esphome_firmware, install_esphome_firmware: file (required). get_esphome_job: job_id, or file for that file's newest job (one of the two is required; a job id only exists in the conversation that started the build, so a voice or Assist follow-up asks by file). cancel_esphome_job: job_id (required). wait_for_esphome_job: job_id (required), timeout.
delete_esphome_yamlclean_esphome_buildrename_esphome_deviceESPHome device YAMLESPHome firmware flashing

The housekeeping that otherwise sends you to the ESPHome panel in the middle of a task: removing a configuration you no longer want, clearing a build that has gone wrong in a way a rebuild cannot fix, and renaming a device.

Deleting is restorable, which is why Phoenix MCP owns it. delete_esphome_yaml snapshots the file into version history before removing it, so a mistaken delete is an ordinary Restore in the Changes tab. The add-on's own delete would bypass that entirely. Deleting a configuration does not touch the device: it keeps running its current firmware, keeps its Home Assistant entities, and is not unadopted. Both the result and the approval card say so, because that is the consequence people expect to be different from what it is.

Cleaning is cheap to authorize, not free to run. clean_esphome_build discards a configuration's cached build artifacts, changes no file and reaches no device, so it needs no approval. It is not fully contained, though: it also clears the shared PlatformIO cache, so the next build of every device is slower, which is why it is worth trying the obvious explanations before reaching for it. If a build is currently running it refuses rather than killing it, and points at cancel_esphome_job. The add-on's fleet-wide equivalent, which cancels every job and wipes the shared build environment, is permanently unreachable.

Renaming compiles and flashes, so it needs the flashing capability, not the YAML one. Entities, entity ids, areas, history, and MESA profiles all survive a rename, because Home Assistant identifies ESPHome entities by the device's MAC address rather than its name. What does not survive is the device's user-defined actions: their service names are built from the device name, so esphome.old_name_action becomes esphome.new_name_action and any automation calling one silently stops working. The approval card names those services. The device must be reachable: the add-on copies the configuration, compiles it, then flashes over the air, and if that flash fails it deletes the new copy so nothing is renamed at all. Both configurations are listed while the build runs, and only one survives it.

params
delete_esphome_yaml, clean_esphome_build: file; rename_esphome_device: file, new_name
get_esphome_device_logsdecode_esphome_backtraceESPHome device YAML

What happened after the flash. get_esphome_device_logs captures a bounded window (15 seconds by default, 60 at most) of a device's live console, which is far more detailed than what reaches Home Assistant. The stream runs until it is killed rather than ending on its own, so the capture window is the request; an empty result means the device logged nothing, not that anything failed.

decode_esphome_backtrace turns the hex panic dump an ESP32 prints when it crashes into a real stack trace with function names and source lines, resolved against that device's last build. It reports stale_build when the firmware has moved on since, so a trace is never silently read against the wrong binary. Both are reads needing only read access to the device, and both are scrubbed.

params
get_esphome_device_logs: file, seconds; decode_esphome_backtrace: file, lines
get_yaml_configset_yaml_configRaw YAML edit

Read and edit configuration.yaml directly. Confirm-eligible. get_yaml_config reads configuration.yaml by default, and with the optional file argument any other .yaml/.yml file in the configuration directory (an !include target, a packages file); secrets.yaml and hidden directories such as .storage are refused, and a symlink cannot launder either past the check. The optional key argument returns just one dotted mapping key instead of the whole file, tag-preserving, naming the target file when the value is an !include. Files are returned verbatim, so any inline secret enters the agent's context; grant this only to fully-trusted clients, and prefer !secret references (which Phoenix MCP preserves) over inline values.

Concurrency guard: the read returns a content_hash fingerprint of the whole file, and set_yaml_config accepts an optional expected_hash; if the file changed since the read (including during an approval window), the write is refused with a re-read-and-reapply error instead of overwriting the other change. Omitting expected_hash skips the check.

Trust-boundary keys are never writable. A write is refused outright if it adds, removes, or changes homeassistant: auth_providers / auth_mfa_modules / packages, http: trusted_proxies / use_x_forwarded_for / cors_allowed_origins / ip_ban_enabled / login_attempts_threshold, frontend: extra_module_url, or lovelace: resources. Copying them through unchanged always passes. These are not merely powerful keys (shell_command stays writable): each one redefines how Home Assistant itself authenticates, whom it trusts, or what JavaScript loads into your authenticated dashboard. The content must also parse as YAML, so a write cannot leave a syntactically broken file behind; that is a parse check only, not configuration validation, so YAML that parses but configures Home Assistant incorrectly is still written. The tool result tells the agent to run check_config afterward, which is the step that catches invalid configuration before a restart applies it.

Deletions have to be declared. A write that removes a top-level key present in the current file is refused unless the optional remove_keys argument names that key. Intentional removals still work, they just have to be stated. This catches the whole-file-replace accident that nothing else would: dropping automation: !include automations.yaml disables every automation while automations.yaml sits untouched and check_config passes. Two cases are exempt from this check (both still go through the trust-boundary refusal above): a file on disk that is missing or does not parse, so a broken configuration stays repairable, and restoring an earlier version from the Changes tab. When a write does remove keys, the approval summary names the keys the write actually drops, not the ones it claimed.

list_backupscreate_backupBackup

List and create backups. Restore is intentionally never exposed. Confirm-eligible. list_backups reports total, returned and a truncated flag, so a clipped page is distinguishable from the whole list.

list_filesread_filewrite_fileFilesystem access

Read and write files under www/, themes/, and custom_templates/. Confirm-eligible. Same concurrency guard as the YAML tools: read_file returns a content_hash, write_file accepts an optional expected_hash and refuses if the file changed since the read (a file that does not exist yet fingerprints as the empty string, so read-then-create is guarded too).

params
path (required for read/write; relative to the allowed directories), content (required for write), expected_hash (write, optional)
list_integrationsset_integration_enabledIntegration management

List integrations and enable or disable them. Confirm-eligible.

params
set_integration_enabled: entry_id (required; from list_integrations), enabled (required; boolean)
set_entitydelete_entityRegistry write

Edit an entity's registry metadata (friendly name, icon, area) or delete a stale/duplicate registry entry. Requires write access to the entity and cap_registry_write; Confirm-eligible and version-captured. Renaming the entity_id is not exposed; a deleted entry cannot be re-created through Phoenix MCP.

params
set_entity: entity_id + one of name / icon / area_id; delete_entity: entity_id
permit_zigbee_joinreconfigure_zigbee_deviceremove_zigbee_deviceRadio management

Manage the Zigbee network (Zigbee2MQTT or ZHA, auto-detected). Open the pairing window for up to 254 seconds or close it with duration 0, re-interview a misbehaving device, or remove one from the network (re-pairing required to rejoin). Requires cap_radio_write; Confirm-eligible. Device-scoped writes need write access to at least one of the device's entities, and the coordinator can never be targeted.

params
permit_zigbee_join: duration (0-254, default 60), optional device_id (router) and backend; others: device_id
restart_haHA restart / stop

Restart Home Assistant. Pass-through-exempt: even pass-through tokens must have this flag enabled.

MESA profiles

Token-scoped reads of the per-entity safety profiles, plus the advisory lease pair. All require Config read, only ever see entities the token can read, and return an identical "not found" for a missing or out-of-scope entity, so they cannot probe for existence. See MESA.

mesa_explain_profileConfig read

Field-by-field explanation of why an accessible entity resolved the way it did, including any conflicts between levels.

params
entity_id (required), show_conflicts
mesa_get_caller_contextConfig read

The MESA context for the current caller.

mesa_get_profileConfig read

The stored and effective profile for one accessible entity. With include_semantic_moments: true, also the purpose-specific triggers and conditions the entity participates in (Home Assistant 2026.7+), read live from Home Assistant; informational only, and omitted on older versions. A profile that declared what would invalidate it (an entity it depends on, or the integration or Home Assistant version it was written against) is checked against the live deployment, so an invalidated one reports staleness_status: stale with a warning naming what changed, rather than continuing to describe itself as current.

params
entity_id (required), include_diagnostic, include_semantic_moments
mesa_query_profilesConfig read

List the profiles in scope, filterable by domain, tag (any or all), area, intent, and origin, with pagination. Tag and intent filters match the effective (resolved) profile. Counts and cursors are relative to the token's scope.

mesa_request_leaseConfig read

Announce a short operation on specific entities (up to 30 seconds) so MESA-aware components can coordinate. A lease is advice, not a lock, and grants no authority: every action stays gated by scope, capabilities, and MESA. Covers only entities the token could already control; entities under protected or critical automation control are denied. Lease endings fire an phoenix_mcp_mesa_lease_expired event.

params
entities (required; list of entity ids), duration_seconds (required; max 30), intent, priority_level, caller_priority, preemption_handling
mesa_release_leaseConfig read

Release a lease early when the work finishes ahead of its expiry. A token can only release its own leases.

params
lease_id (required)

Native control tools

Identical to the native HA MCP server: same names, parameters, and action_done responses. Targeting parameters resolve to an explicit entity list, then filter to WRITE access: area, floor, and name are strings, while domain and device_class are arrays of strings. Each one is published with a description of the shape it expects and a note that at least one targeting parameter is required, which Home Assistant's own schemas do not carry. If nothing matches, they return "No accessible entities matched your request." without revealing why.

HassBroadcastBroadcast

Send an announcement through Assist satellite devices. Satellites are subject to MESA like any other entity, so one marked read-only or prohibited is skipped.

params
message (string, required)
HassCancelAllTimerstree-scoped

Cancel all running timers in an area (or everywhere if area is omitted). Always returns speech_slots: { "canceled": N }, even when N is 0. N counts the timers actually cancelled: a timer MESA holds back is not included, so the number always matches what happened.

HassClimateSetTemperaturetree-scoped

Set a climate device's target temperature.

params
temperature (required), plus area / floor / name
HassFanSetSpeedtree-scoped

Set fan speed by percentage.

params
percentage (0-100, required), plus area / floor / name
HassLightSettree-scoped

Set brightness, color, or color temperature of accessible lights.

params
area, floor, name, brightness (0-100), color, temperature (kelvin)
HassMediaPauseHassMediaUnpauseHassMediaNextHassMediaPreviousHassMediaPlayerMuteHassMediaPlayerUnmutetree-scoped

Control playback on an accessible media player by area, floor, or name. Each gates on the player's current state before calling the service.

HassMediaSearchAndPlaytree-scoped

Search and play media on a player.

params
search_query (required), media_class, plus area / floor / name
HassSetPositionPhysical control

Set the position of covers, blinds, or valves (0-100); each target is routed to its own domain service. Needs the physical-control capability.

HassSetVolumeHassSetVolumeRelativetree-scoped

Set or adjust media player volume. HassSetVolume takes volume_level (0-100); HassSetVolumeRelative takes volume_step ("up", "down", or a signed percentage).

HassVacuumStartHassVacuumReturnToBasetree-scoped

Start a vacuum, or send it back to its dock. Targeted by name, area, or floor; only vacuums that report supporting the operation are targeted, so a vacuum that cannot dock is left out rather than asked and failing.

HassVacuumCleanAreatree-scoped

Send a vacuum to clean a named area. area is required and names the area to clean, which is passed to the service as the cleaning target rather than filtering which vacuum is used; optional name picks a single vacuum, and omitting it uses every accessible vacuum that supports cleaning a named area. An area that matches nothing returns the same response as no accessible match.

HassStopMovingPhysical control

Stop a moving cover or similar device.

HassTurnOnHassTurnOfftree-scoped

Turn entities on or off by area, name, floor, or domain.

params
area, floor, name (strings), domain, device_class (string arrays), all optional

Query & entity access

get_historytree-scoped

Historical state changes for an entity, read from the Recorder integration (enabled by default in Home Assistant; this tool fails when it is disabled). Accepts relative times (24h, 7d, 2w, 1m). Ranges over 7 days are clamped. Returns entity_id, mode, count, and history (each entry's state and when); the REST proxy additionally returns the queried range in X-Phoenix-History-Start/End headers.

params
entity_id (required), start_time (required; ISO or relative), end_time
get_statetree-scoped

Current state of a single entity. Returns a compact, domain-aware view by default (key attributes only); pass detailed=true for the full state, or fields=[...] to select exact fields. Requires READ or WRITE on the entity. Sensitive attributes are always scrubbed.

params
entity_id (string, required), fields (array, optional), detailed (boolean, optional)
returns
by default entity_id, state, and key attributes; the full state with detailed=true
get_statestree-scoped

All entity states the token can read, as a compact domain-aware view per entity by default (detailed=true or fields=[...] to change). Pass-through tokens receive every non-Phoenix entity.

get_statisticstree-scoped

Long-term statistics for numeric entities, aggregated by 5-minute, hour, day, week, or month. Statistics come from the Recorder integration, and only entities that produce Recorder statistics have any; a merely numeric state is not enough.

params
entity_id (required), start_time (required; ISO or relative), end_time, period, statistic_types (subset of the return fields)
returns
min, max, mean, sum, state per period
get_calendar_eventstree-scoped

Events from an accessible calendar.* entity within a time window (defaults to the next 7 days). Requires READ access to the calendar entity.

params
calendar_id (required), start_time (default now), end_time (default start + 7 days)
GetDateTimeno capability

Current date, time, and timezone. Always available, no permission required.

GetLiveContexttree-scoped

YAML snapshot of accessible entity states, optimized for LLM context. Filtered by the token's permissions; pass-through tokens get the same output as the native server.

Realtime subscriptions

watch_entityConfig read

Bounded blocking subscription (max 30 seconds) that waits for an accessible entity to change state, then returns it.

params
entity_id (required), timeout (seconds, max 30)

Scenes, helpers & dashboards

create_helperedit_helperdelete_helperHelper write

Manage helpers (input_*, counter, timer). Confirm-eligible. Created helpers appear in the Helpers UI and persist in HA's storage collection, which HA flushes on a short (~10 second) debounce. A graceful restart (including restart_ha or the homeassistant.restart service) flushes the pending write first, so the helper survives; only an abrupt restart or crash within a few seconds of creation can lose it. If one is lost that way, you can restore it from the Changes tab, since the create was recorded in version history immediately.

params
helper_type (required), config (required for create/edit), helper_id (required for edit/delete)
get_scenecreate_sceneedit_scenedelete_sceneScene write

Read and manage scenes. Confirm-eligible (the read is not: reads are never approval-gated). Split configurations (!include and !include_dir_* layouts in configuration.yaml) are followed to the correct leaf file, the same include-graph handling as the automation tools. get_scene returns the scene's current name and entity map plus a content_hash for edit_scene's optional expected_hash; since edit replaces the whole scene, read it first or you will drop members you did not resend. A scene controlling any entity this token cannot write is refused identically to one that does not exist, so a scene id is never an existence oracle.

params
scene_id (required except for create), config (required for create/edit), expected_hash (edit, optional)
list_dashboardscreate_dashboardedit_dashboarddelete_dashboardget_dashboard_configset_dashboard_configDashboard write

List and manage Lovelace dashboards, and read or replace a dashboard's view/card layout. get_dashboard_config redacts out-of-scope entities; set_dashboard_config writes storage-mode dashboards and is versioned. Confirm-eligible. Concurrency guard: the read returns a content_hash over the stored layout, and the write accepts an optional expected_hash, refusing if the layout changed since the read (checked again at approval time, so a change made while the write sat in the approval queue also fails closed). An auto-generated dashboard has no stored layout, so any expected_hash against it is a conflict. set_dashboard_config walks the whole layout and warns about any custom: card type that is not installed, advisory only. Note that a saved layout is display config, not automatic execution: a card action (for example a call-service tap action) runs under the viewing user's session when they click it, the same indirect control risk as authored automations, so a layout can reference entities outside the token's scope.

params
create_dashboard/edit_dashboard: config (required; title etc.) plus dashboard_id (required for edit); delete_dashboard: dashboard_id (required); the config read/write pair: url_path (omit for the default dashboard), config (required for set), expected_hash (optional)
list_dashboard_cardsDashboard write

List the custom dashboard cards actually installed on this instance (HACS plugins and cards shipped by integrations), so an agent can pick the best-suited card instead of guessing. Each entry carries the card's own name and one-line description, its documentation URL where it publishes one, and, on request, a worked example config taken from the card's own getStubConfig. This matters because a card type that is not installed is not rejected at write time, it renders as an error on the dashboard, and because custom cards publish no config schema, so a working example is the only reliable basis for authoring one. Read-only and never approval-gated, like the other dashboard reads. The catalog is built by the Phoenix MCP panel in a browser rather than read from disk: a card registers itself on window.customCards at runtime, and many card sets build their type names by string concatenation, so those names do not exist anywhere in the shipped files. Until a browser has opened the panel at least once the catalog reports itself as un-harvested, which means the installed cards are unknown, not absent.

params
type (optional; one card type, with or without the custom: prefix, returned in full with its example config), detailed (optional; include every card's example config, verbose on an instance with many plugins)
add_dashboard_cardedit_dashboard_carddelete_dashboard_cardDashboard write

Add, replace, or remove a single card on a dashboard view (or a section, for a sections-type view) by index, without resending the whole layout the way set_dashboard_config does. Confirm-eligible and versioned like a full layout write; the target dashboard must already have a stored layout (create one first with set_dashboard_config; strategy views, strategy dashboards, and auto-generated dashboards are refused up front). The full operation is validated against the current layout before it gates, and re-validated at apply time, so a layout change during the approval window fails closed instead of landing on the wrong index. If the card names a custom: type that is not in the harvested card catalog (or whose element did not load), the result carries a warnings entry saying so. It is advisory and never blocks the write: the catalog is a cache, so refusing would block a legitimate card whenever it is cold or one plugin install stale. Prefer these over set_dashboard_config for a single-card change, both because it is simpler for the agent and because echoing an entire dashboard back in one tool call can exceed some models' output limits.

params
url_path (omit for the default dashboard), view_index, section_index (required when the target view uses sections, rejected otherwise), card (the card object, for add/edit), position (add; omit to append) or card_index (edit/delete), and optional expected_hash from a prior read, strongly recommended since card indexes shift when the layout changes. Unlike the whole-layout writers, each result returns the layout's new content_hash, so several card operations can be chained without re-reading.

Service execution

call_servicetree-scoped

Call any Home Assistant service. device_id and area_id are flattened to an explicit entity list first; denied entities are silently dropped, and if all resolve to denied the call returns 403. Device and area expansion also skips configuration, diagnostic, and hidden entities (matching Home Assistant's own bulk-call behavior), so a "turn off every switch in the bedroom" sweep does not flip a child-lock or LED-indicator switch; naming an entity_id directly is never filtered this way. Targets belong in the entity_id / device_id / area_id parameters, not in service_data: any target selector found there is stripped, since the resolved entity list is what decides the call's reach. Response data is scanned and inaccessible entity IDs are replaced with <redacted>. Physical-control and restart services need their capabilities, and the core config-reload family (automation.reload, script.reload, scene.reload, and the other domain reloads) is gated on cap_yaml_edit and is Confirm-eligible, the same capability that authorizes editing the raw YAML those reloads apply.

params
domain and service (two separate fields, both required, e.g. light and turn_on), entity_id, device_id, area_id, service_data
dry_run_serviceSearch & discovery

Preview a service call without running it. Resolves and flattens targets, reports the MESA verdict per entity, and returns a single predicted_outcome of allowed, pending_approval, or denied, folding the capability gate and MESA together. Same parameter shape as call_service: domain and service required, plus the optional targets and service_data.

Token introspection

Each token can always inspect its own state. No capability required, and you only ever see your own data.

get_approval_statusno capability

Check a Confirm-gated action you submitted. With an approval_id, returns its status and result; without one, lists all of this token's pending approvals, useful after a reconnect.

wait_for_approvalno capability

Block until one of your pending approvals resolves, instead of polling. Returns immediately if already resolved, otherwise waits server-side (up to a capped timeout) for a human to approve or reject, then returns the status and result; on timeout it returns still-pending so you can call again.

params
approval_id (required), timeout (seconds)
get_audit_summaryno capability

This token's own recent audit entries (request_id, time, method, resource, outcome), newest first, with an optional outcome filter.

get_capability_summaryno capability

This token's persona, effective capabilities, Confirm-gated capabilities, write scope, rate limits, and a tool-level gate map (tools.usable / tools.needs_approval / tools.unavailable).

Helpers, dashboards, and backups run over a vetted command set

These tools hold no long-lived token and open no socket. Phoenix MCP dispatches a fixed allowlist of Home Assistant WebSocket commands in-process (helper create / update / delete, Lovelace dashboard CRUD, and backup info / generate), validates each payload against the command's own schema, and runs it as an admin. Commands outside the allowlist are refused, and restore_backup is deliberately not included.

Reconnect when the tool list changes

A token only sees the tools its capabilities unlock, so moving a capability between deny and allow/confirm (or changing write scope or announce_all_tools) changes the list and your MCP client must reconnect to pick it up. Switching a capability between allow and confirm takes effect on the next call with no reconnect needed, though some clients cache tool metadata. In Claude Code, open /mcp and select Reconnect.

The agent is told, too: when a token's capabilities, permissions, or settings preset change after its client last fetched the tool list, Phoenix MCP appends a one-time notice to the next tool response telling the agent its tool list may be stale, to check get_capability_summary, and to ask you to reconnect. Enforcement never depends on this: the per-call gates always reflect the current settings.