Skip to main content
Glama
homeassistant-ai

Home Assistant MCP Server

Official

Create or Update Dashboard

ha_config_set_dashboard
Destructive

Create or update a storage-mode Home Assistant dashboard using JSON patch, Python transforms, or full config replacement, including title, icon, and sidebar settings.

Instructions

Create or update a Home Assistant dashboard.

MUST call ha_get_skill_guide OR refer to your locally installed skills first. dashboard-guide.md ships under skill_content by default. A card's fields: ha_config_get_dashboard(card_type=..., describe=True).

MODES (pick one):

  • patch: edit known paths with literal values using JSON Patch add/remove/replace/test and config_hash, e.g. patch=[{"op": "replace", "path": "/views/0/title", "value": "Home"}]. move/copy are unsupported. Full guide: https://github.com/homeassistant-ai/ha-mcp/blob/master/docs/dashboard-edits.md

  • python_transform: loops or pattern-based changes across cards and views, e.g. 'config["views"][0]["cards"].append({"type": "button", "entity": "light.bedroom"})'. After delete/add operations indices shift, so chain multiple ops in ONE expression where possible; a later call needs a fresh config_hash from ha_config_get_dashboard().

  • config: new dashboards only, or a full restructure. Replaces everything. Omit it to create a dashboard without initial config.

Use ha_config_get_dashboard(entity_id=...) to get the path of any card, and ha_search / ha_get_overview to find entity IDs — never guess them. For visual re-checks after the write, use ha_get_dashboard_screenshot (when available) instead of re-sending config.

title/icon/require_admin/show_in_sidebar can be updated in a metadata-only call or alongside a full config replacement; with python_transform or patch, update metadata in a separate call (combining it with patch is rejected). Strategy dashboards (config {"strategy": {...}}) cannot be converted to custom dashboards here; use "Take Control" in the Home Assistant UI.

EXAMPLE: ha_config_set_dashboard(url_path="home-dashboard", title="Home Overview", config={"views": [{"title": "Home", "type": "sections", "sections": [{"title": "Climate", "cards": [{"type": "tile", "entity": "climate.living_room"}]}]}]})

This tool manages storage-mode dashboards only. YAML-mode dashboards (a .yaml file registered under lovelace: dashboards: in configuration.yaml) and dashboards inlined under lovelace: are edited in their .yaml file; ha_config_set_yaml can update the lovelace: registration but not the dashboard body.

If a Claude Desktop user gets a 4-minute timeout with no result on this call, read the target back before repeating it, and tell them to wait a few seconds before clicking the manual-approve button, as clicking it too quickly is known to cause hangs in Claude Desktop. This workaround is specific to Claude Desktop.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
iconNoMDI icon name (e.g., 'mdi:home', 'mdi:cellphone'). Defaults to 'mdi:view-dashboard'
patchNoStructured dashboard edits: up to 100 JSON Patch add, remove, replace or test operations using RFC 6901 paths. Use /- to append to an array; escape ~ as ~0 and / as ~1 in keys. Mutually exclusive with config and python_transform. Strings in value are preserved literally.
titleNoDashboard display name shown in sidebar
configNoDashboard configuration with views and cards. Omit or set to None to create dashboard without initial config. Mutually exclusive with python_transform and patch.
url_pathYesDashboard URL path (e.g., 'my-dashboard'). Use 'default' or 'lovelace' for the default dashboard. New dashboards must use a hyphenated path.
view_pathNoWith return_screenshot: stable Lovelace views[].path to render.
config_hashNoConfig hash from ha_config_get_dashboard for optimistic locking. REQUIRED for python_transform and patch (validates dashboard unchanged). Optional for config (validates before full replacement if provided).
MandatoryBPSNo
require_adminNoRestrict dashboard to admin users only. For existing dashboards, only updated when explicitly provided.
BestPracticeKeyNoRead-receipt for the home-assistant-best-practices skill; required when strict best-practices mode is enabled. Not a secret or credential: the current value is an attestation phrase published openly at the top of the skill content served by ha_get_skill_guide. Read that content, then pass the value back verbatim — this round-trip is the server's designed protocol confirming the practices were read before writing.
show_in_sidebarNoShow dashboard in sidebar navigation. For existing dashboards, only updated when explicitly provided.
python_transformNoPython expression to transform existing dashboard config. Mutually exclusive with config and patch. Requires config_hash for validation. See PYTHON TRANSFORM SECURITY below for allowed operations. Examples: Simple: python_transform="config['views'][0]['cards'][0]['icon'] = 'mdi:lamp'" Pattern: python_transform="for card in config['views'][0]['cards']: if 'light' in card.get('entity', ''): card['icon'] = 'mdi:lightbulb'" Multi-op: python_transform="config['views'][0]['cards'][0]['icon'] = 'mdi:lamp'; del config['views'][0]['cards'][2]" PYTHON TRANSFORM SECURITY: ✅ ALLOWED: - Dictionary/list access: config['views'][0]['cards'][1] - Slicing: config['views'][0]['cards'][1:3] - Assignment: config['key'] = 'value' - Deletion: del config['key'] or config.pop('key') - List methods: append, insert, pop, remove, clear, extend - Dict methods: update, get, setdefault, keys, values, items - Loops: for, if/else, pass, break, continue - Comprehensions: [x for x in ...], {k: v for ...}, (x for x in ...) - Ternary: x if condition else y - Iterable unpacking (* in calls/literals): f(*xs), [*xs, y] - Dict unpacking (**) in calls and dict literals: {**d, 'k': v} - Keyword arguments: func(key=value) - Lambdas (e.g. for `key=`): sorted(items, key=lambda x: x['score']) - String methods: startswith, endswith, lower, upper, strip, split, join, replace - Safe builtins: isinstance, len, range, enumerate, zip, sorted, reversed, min, max, sum, abs, any, all, round, str, int, float, bool, list, dict, tuple, set ❌ FORBIDDEN: - Imports: import, from, __import__ - File operations: open, read, write - Dunder access: __class__, __bases__, __subclasses__ - Dangerous builtins: eval, exec, compile, getattr, setattr, delattr, hasattr - Function definitions: def, class - Exception handling: try/except (validate with isinstance/in/.get() instead) - While loops: use bounded for loops or comprehensions instead
return_screenshotNoAfter writing, also return rendered image(s) of the dashboard. Requires the 'dashboard screenshot' beta feature + engine app (add-on)/sidecar; if unavailable, the write result is returned with a warning.

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed3 schema fields changedv8.6.0
    • changedInput schema / properties / patch / description
      Previous value: -"Structured dashboard edits: up to 100 JSON Patch add, remove, replace or test operations using RFC 6901 paths. Use /- to append to an array; escape ~ as ~0 and / as ~1 in keys. Requires config_hash. Mutually exclusive with config and python_transform. Update title/icon/require_admin/show_in_sidebar in a separate call. Strings in value are preserved literally."New value: +"Structured dashboard edits: up to 100 JSON Patch add, remove, replace or test operations using RFC 6901 paths. Use /- to append to an array; escape ~ as ~0 and / as ~1 in keys. Mutually exclusive with config and python_transform. Strings in value are preserved literally."
    • changedInput schema / properties / python_transform / description
      Previous value: -"Python expression to transform existing dashboard config. Mutually exclusive with config and patch. Requires config_hash for validation. See PYTHON TRANSFORM SECURITY below for allowed operations. Examples: Simple: python_transform=\"config['views'][0]['cards'][0]['icon'] = 'mdi:lamp'\" Pattern: python_transform=\"for card in config['views'][0]['cards']: if 'light' in card.get('entity', ''): card['icon'] = 'mdi:lightbulb'\" Multi-op: python_transform=\"config['views'][0]['cards'][0]['icon'] = 'mdi:lamp'; del config['views'][0]['cards'][2]\" \n\nPYTHON TRANSFORM SECURITY:\n\n✅ ALLOWED:\n- Dictionary/list access: config['views'][0]['cards'][1]\n- Slicing: config['views'][0]['cards'][1:3]\n- Assignment: config['key'] = 'value'\n- Deletion: del config['key'] or config.pop('key')\n- List methods: append, insert, pop, remove, clear, extend\n- Dict methods: update, get, setdefault, keys, values, items\n- Loops: for, if/else, pass, break, continue\n- Comprehensions: [x for x in ...], {k: v for ...}, (x for x in ...)\n- Ternary: x if condition else y\n- Iterable unpacking (* in calls/literals): f(*xs), [*xs, y]\n- Dict unpacking (**) in calls and dict literals: {**d, 'k': v}\n- Keyword arguments: func(key=value)\n- Lambdas (e.g. for `key=`): sorted(items, key=lambda x: x['score'])\n- String methods: startswith, endswith, lower, upper, strip, split, join, replace\n- Safe builtins: isinstance, len, range, enumerate, zip, sorted, reversed,\n  min, max, sum, abs, any, all, round, str, int, float, bool, list, dict,\n  tuple, set\n\n❌ FORBIDDEN:\n- Imports: import, from, __import__\n- File operations: open, read, write\n- Dunder access: __class__, __bases__, __subclasses__\n- Dangerous builtins: eval, exec, compile, getattr, setattr, delattr, hasattr\n- Function definitions: def, class\n- Exception handling: try/except (validate with isinstance/in/.get() instead)\n- While loops: use bounded for loops or comprehensions instead\n\n🎯 PATTERNS:\n- Filter cards: cards = [c for c in cards if keep(c)]\n- Skip in a loop: prefer `continue` over an empty `pass` branch (clearer)\n- Conditionally include: build a new list and `.append(x)` only the\n  cards you want, instead of iterating the original and using if/pass\n  branches to drop entries\n- Modify in place when possible (single pass, fewer surprises) over\n  reconstructing the entire list"New value: +"Python expression to transform existing dashboard config. Mutually exclusive with config and patch. Requires config_hash for validation. See PYTHON TRANSFORM SECURITY below for allowed operations. Examples: Simple: python_transform=\"config['views'][0]['cards'][0]['icon'] = 'mdi:lamp'\" Pattern: python_transform=\"for card in config['views'][0]['cards']: if 'light' in card.get('entity', ''): card['icon'] = 'mdi:lightbulb'\" Multi-op: python_transform=\"config['views'][0]['cards'][0]['icon'] = 'mdi:lamp'; del config['views'][0]['cards'][2]\" \n\nPYTHON TRANSFORM SECURITY:\n\n✅ ALLOWED:\n- Dictionary/list access: config['views'][0]['cards'][1]\n- Slicing: config['views'][0]['cards'][1:3]\n- Assignment: config['key'] = 'value'\n- Deletion: del config['key'] or config.pop('key')\n- List methods: append, insert, pop, remove, clear, extend\n- Dict methods: update, get, setdefault, keys, values, items\n- Loops: for, if/else, pass, break, continue\n- Comprehensions: [x for x in ...], {k: v for ...}, (x for x in ...)\n- Ternary: x if condition else y\n- Iterable unpacking (* in calls/literals): f(*xs), [*xs, y]\n- Dict unpacking (**) in calls and dict literals: {**d, 'k': v}\n- Keyword arguments: func(key=value)\n- Lambdas (e.g. for `key=`): sorted(items, key=lambda x: x['score'])\n- String methods: startswith, endswith, lower, upper, strip, split, join, replace\n- Safe builtins: isinstance, len, range, enumerate, zip, sorted, reversed,\n  min, max, sum, abs, any, all, round, str, int, float, bool, list, dict,\n  tuple, set\n\n❌ FORBIDDEN:\n- Imports: import, from, __import__\n- File operations: open, read, write\n- Dunder access: __class__, __bases__, __subclasses__\n- Dangerous builtins: eval, exec, compile, getattr, setattr, delattr, hasattr\n- Function definitions: def, class\n- Exception handling: try/except (validate with isinstance/in/.get() instead)\n- While loops: use bounded for loops or comprehensions instead"
    • changedInput schema / properties / return_screenshot / description
      Previous value: -"After writing, also return rendered image(s) of the dashboard so you can see what it looks like in a single call (the dashboard creation/iteration loop). Requires the 'dashboard screenshot' beta feature + engine add-on/sidecar; if unavailable, the write result is returned with a warning. For visual re-checks after the write (no config round-trip), use the dedicated ha_get_dashboard_screenshot tool instead."New value: +"After writing, also return rendered image(s) of the dashboard. Requires the 'dashboard screenshot' beta feature + engine app (add-on)/sidecar; if unavailable, the write result is returned with a warning."
  2. Changed4 schema fields changedv8.5.0
    • changedInput schema / properties / config / description
      Previous value: -"Dashboard configuration with views and cards. Omit or set to None to create dashboard without initial config. Mutually exclusive with python_transform."New value: +"Dashboard configuration with views and cards. Omit or set to None to create dashboard without initial config. Mutually exclusive with python_transform and patch."
    • changedInput schema / properties / config_hash / description
      Previous value: -"Config hash from ha_config_get_dashboard for optimistic locking. REQUIRED for python_transform (validates dashboard unchanged). Optional for config (validates before full replacement if provided)."New value: +"Config hash from ha_config_get_dashboard for optimistic locking. REQUIRED for python_transform and patch (validates dashboard unchanged). Optional for config (validates before full replacement if provided)."
    • addedInput schema / properties / patch
      Added value: +{
      +  "anyOf": [
      +    {
      +      "items": {
      +        "additionalProperties": true,
      +        "type": "object"
      +      },
      +      "type": "array"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "description": "Structured dashboard edits: up to 100 JSON Patch add, remove, replace or test operations using RFC 6901 paths. Use /- to append to an array; escape ~ as ~0 and / as ~1 in keys. Requires config_hash. Mutually exclusive with config and python_transform. Update title/icon/require_admin/show_in_sidebar in a separate call. Strings in value are preserved literally."
      +}
    • changedInput schema / properties / python_transform / description
      Previous value: -"Python expression to transform existing dashboard config. Mutually exclusive with config. Requires config_hash for validation. See PYTHON TRANSFORM SECURITY below for allowed operations. Examples: Simple: python_transform=\"config['views'][0]['cards'][0]['icon'] = 'mdi:lamp'\" Pattern: python_transform=\"for card in config['views'][0]['cards']: if 'light' in card.get('entity', ''): card['icon'] = 'mdi:lightbulb'\" Multi-op: python_transform=\"config['views'][0]['cards'][0]['icon'] = 'mdi:lamp'; del config['views'][0]['cards'][2]\" \n\nPYTHON TRANSFORM SECURITY:\n\n✅ ALLOWED:\n- Dictionary/list access: config['views'][0]['cards'][1]\n- Slicing: config['views'][0]['cards'][1:3]\n- Assignment: config['key'] = 'value'\n- Deletion: del config['key'] or config.pop('key')\n- List methods: append, insert, pop, remove, clear, extend\n- Dict methods: update, get, setdefault, keys, values, items\n- Loops: for, if/else, pass, break, continue\n- Comprehensions: [x for x in ...], {k: v for ...}, (x for x in ...)\n- Ternary: x if condition else y\n- Iterable unpacking (* in calls/literals): f(*xs), [*xs, y]\n- Dict unpacking (**) in calls and dict literals: {**d, 'k': v}\n- Keyword arguments: func(key=value)\n- Lambdas (e.g. for `key=`): sorted(items, key=lambda x: x['score'])\n- String methods: startswith, endswith, lower, upper, strip, split, join, replace\n- Safe builtins: isinstance, len, range, enumerate, zip, sorted, reversed,\n  min, max, sum, abs, any, all, round, str, int, float, bool, list, dict,\n  tuple, set\n\n❌ FORBIDDEN:\n- Imports: import, from, __import__\n- File operations: open, read, write\n- Dunder access: __class__, __bases__, __subclasses__\n- Dangerous builtins: eval, exec, compile, getattr, setattr, delattr, hasattr\n- Function definitions: def, class\n- Exception handling: try/except (validate with isinstance/in/.get() instead)\n- While loops: use bounded for loops or comprehensions instead\n\n🎯 PATTERNS:\n- Filter cards: cards = [c for c in cards if keep(c)]\n- Skip in a loop: prefer `continue` over an empty `pass` branch (clearer)\n- Conditionally include: build a new list and `.append(x)` only the\n  cards you want, instead of iterating the original and using if/pass\n  branches to drop entries\n- Modify in place when possible (single pass, fewer surprises) over\n  reconstructing the entire list"New value: +"Python expression to transform existing dashboard config. Mutually exclusive with config and patch. Requires config_hash for validation. See PYTHON TRANSFORM SECURITY below for allowed operations. Examples: Simple: python_transform=\"config['views'][0]['cards'][0]['icon'] = 'mdi:lamp'\" Pattern: python_transform=\"for card in config['views'][0]['cards']: if 'light' in card.get('entity', ''): card['icon'] = 'mdi:lightbulb'\" Multi-op: python_transform=\"config['views'][0]['cards'][0]['icon'] = 'mdi:lamp'; del config['views'][0]['cards'][2]\" \n\nPYTHON TRANSFORM SECURITY:\n\n✅ ALLOWED:\n- Dictionary/list access: config['views'][0]['cards'][1]\n- Slicing: config['views'][0]['cards'][1:3]\n- Assignment: config['key'] = 'value'\n- Deletion: del config['key'] or config.pop('key')\n- List methods: append, insert, pop, remove, clear, extend\n- Dict methods: update, get, setdefault, keys, values, items\n- Loops: for, if/else, pass, break, continue\n- Comprehensions: [x for x in ...], {k: v for ...}, (x for x in ...)\n- Ternary: x if condition else y\n- Iterable unpacking (* in calls/literals): f(*xs), [*xs, y]\n- Dict unpacking (**) in calls and dict literals: {**d, 'k': v}\n- Keyword arguments: func(key=value)\n- Lambdas (e.g. for `key=`): sorted(items, key=lambda x: x['score'])\n- String methods: startswith, endswith, lower, upper, strip, split, join, replace\n- Safe builtins: isinstance, len, range, enumerate, zip, sorted, reversed,\n  min, max, sum, abs, any, all, round, str, int, float, bool, list, dict,\n  tuple, set\n\n❌ FORBIDDEN:\n- Imports: import, from, __import__\n- File operations: open, read, write\n- Dunder access: __class__, __bases__, __subclasses__\n- Dangerous builtins: eval, exec, compile, getattr, setattr, delattr, hasattr\n- Function definitions: def, class\n- Exception handling: try/except (validate with isinstance/in/.get() instead)\n- While loops: use bounded for loops or comprehensions instead\n\n🎯 PATTERNS:\n- Filter cards: cards = [c for c in cards if keep(c)]\n- Skip in a loop: prefer `continue` over an empty `pass` branch (clearer)\n- Conditionally include: build a new list and `.append(x)` only the\n  cards you want, instead of iterating the original and using if/pass\n  branches to drop entries\n- Modify in place when possible (single pass, fewer surprises) over\n  reconstructing the entire list"
  3. Changed1 schema field changedv8.4.1
    • changedInput schema / properties / return_screenshot / description
      Previous value: -"After writing, also return rendered image(s) of the dashboard so you can see what it looks like in a single call (the dashboard creation/iteration loop). Requires the 'dashboard screenshot' beta feature + engine add-on/sidecar; if unavailable, the write result is returned with a warning."New value: +"After writing, also return rendered image(s) of the dashboard so you can see what it looks like in a single call (the dashboard creation/iteration loop). Requires the 'dashboard screenshot' beta feature + engine add-on/sidecar; if unavailable, the write result is returned with a warning. For visual re-checks after the write (no config round-trip), use the dedicated ha_get_dashboard_screenshot tool instead."
  4. First observedv7.14.2

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare destructiveHint=true, idempotentHint=false and openWorldHint=true, and the description adds substantial context beyond them: config 'Replaces everything', strategy dashboards cannot be converted here, config_hash provides optimistic locking, metadata updates are rejected when combined with patch, and the Claude Desktop timeout behavior. This is far richer than the annotation baseline.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Long but front-loaded (purpose, prerequisite, then modes) and nearly every sentence carries operational weight, including the worked EXAMPLE and the mode decision list. Some content, such as the python_transform security pointer and mutual-exclusion notes, overlaps the schema, keeping it from a 5.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 13-parameter, multi-mode, destructive write tool with no output schema, the description covers prerequisites, mode selection, hash/locking requirements, metadata-update constraints, scope limits (storage vs YAML mode), post-write verification, and even a known client hang. Nothing an agent needs to call it safely is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 92%, so the schema already carries most per-parameter meaning (baseline 3). The description adds a layer the schema does not: the mutual-exclusion relationships framed as a mode-selection decision, when config_hash is required vs optional, and that patch values are literal. It stops short of adding new syntax detail beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource ('Create or update a Home Assistant dashboard') and immediately distinguishes the three operating modes (patch, python_transform, config) and the storage-mode-only scope. It also names the sibling tools it is not (ha_config_get_dashboard, ha_config_delete_dashboard, ha_config_set_yaml), so an agent can route correctly without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit decision framework: 'MODES (pick one)' with the condition selecting each, a mandatory prerequisite (call ha_get_skill_guide first), guidance to fetch entity IDs via ha_search/ha_get_overview rather than guessing, and named alternatives for follow-up (ha_get_dashboard_screenshot) and for YAML-mode dashboards (ha_config_set_yaml / UI 'Take Control'). When-not-to-use is covered too.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Deploy Server

Other Tools