Skip to main content
Glama
Home Assistant MCP Server

Home Assistant MCP Server

Official

Create or Update Script

ha_config_set_script
Destructive

Create, update, or run Home Assistant scripts; make surgical edits, replace configs, convert blueprints to standalone scripts, and start or stop executions.

Instructions

Create or update a Home Assistant script.

MUST call ha_get_skill_guide OR refer to your locally installed skills first.

Prefer native actions (choose / if, wait_for_trigger, repeat, for:) over templates in logic positions; templates belong only in data.*, notification text, event_data and variables. The best-practice checker reports violations under best_practice_warnings. automation-actions.md, triggers-and-conditions.md and template-guidelines.md ship under skill_content by default.

Scripts use 'sequence', NOT 'trigger' or 'action'; for trigger-based execution use ha_config_set_automation.

MODES (pick one):

  • python_transform: surgical edits to an existing script. Requires config_hash from ha_config_get_script(), e.g. python_transform="config['sequence'].append({'delay': {'seconds': 5}})"

  • config: new scripts or full restructures. Needs 'sequence' (regular) or 'use_blueprint' {path, input} (blueprint-based).

  • take_control_of_blueprint: convert a blueprint-backed script into a standalone one. Takes no config of its own.

  • run (alone with script_id): 'start' runs the script (optional variables), 'stop' stops its running executions. Write config changes in a separate call first: Home Assistant signals no completion of the script reload a write triggers.

EXAMPLES:

  • Create: ha_config_set_script(script_id="blink_light", config={"alias": "Light Blink", "sequence": [{"action": "light.turn_on", "target": {"entity_id": "light.living_room"}}, {"delay": {"seconds": 2}}, {"action": "light.turn_off", "target": {"entity_id": "light.living_room"}}]})

  • From a blueprint: ha_config_set_script(script_id="notification_script", config={"alias": "My Notification Script", "use_blueprint": {"path": "notification_script.yaml", "input": {"message": "Hello World"}}})

  • Take control: ha_config_set_script(script_id="notification_script", take_control_of_blueprint=True)

TAKE CONTROL is one-way: later blueprint edits stop reaching the script; to change an input value, update 'use_blueprint.input' instead. It does NOT free the blueprint — Home Assistant keeps counting the converted script as a user, so deleting that blueprint stays refused until the script is removed. To preview the rendering without writing anything, use ha_manage_blueprints(action="substitute", domain="script", path=..., input=...).

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.

create update modify edit script sequence actions new script write save take control blueprint detach unlink standalone convert run start stop execute

Tool call template

Replace each placeholder with a value matching the input schema. Only required parameters are included; add optional parameters as needed.

{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "arguments": {
      "script_id": "<script_id>"
    },
    "name": "ha_config_set_script"
  }
}

Input Schema

NameRequiredDescriptionDefault
runNoRun control, used alone with script_id (no config, python_transform or take_control_of_blueprint): 'start' runs the script (script.turn_on; returns once it has started, without waiting for it to finish), 'stop' stops its running executions (script.turn_off). Stopping does not disable the script; Home Assistant has no script enable/disable.
waitNoWait for script to be queryable before returning. Set to False for bulk operations.
configNoScript configuration dictionary. Mutually exclusive with python_transform.
categoryNoCategory ID to assign to this script. Use ha_config_get_category(scope='script') to list available categories, or ha_config_set_category() to create one.
script_idYesScript identifier — bare storage key ('morning_routine') or entity_id form ('script.morning_routine'); a leading 'script.' prefix is stripped before lookup.
variablesNoWith run='start': values for the script's fields, passed to the run as its variables.
config_hashNoConfig hash from ha_config_get_script for optimistic locking. Required for python_transform; optional for config updates (validates before full replacement if provided).
MandatoryBPSNo
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.
python_transformNoPython expression to transform existing script config. Mutually exclusive with config. WARNING: Expressions with infinite loops will hang the server. Examples: Simple: python_transform="config['sequence'][0]['data']['message'] = 'Hello'" Pattern: python_transform="for step in config['sequence']: if step.get('alias') == 'My Step': step['data']['value'] = 100" 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, index, count, sort, reverse - Dict methods: update, get, setdefault, keys, values, items, pop, clear - 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, lstrip, rstrip, removeprefix, removesuffix, split, join, replace, count, index - 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
take_control_of_blueprintNoConvert a blueprint-backed script into an editable standalone one -- the UI's "Take control". Renders the blueprint with its current inputs and saves the result over the same script, which keeps its script_id, alias and description and then has its own sequence and no 'use_blueprint'. Mutually exclusive with config and python_transform.

Output Schema

NameRequiredDescriptionDefault

No arguments

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed1 schema field changedv9.1.0
    • changedInput schema / properties / python_transform / description
      Previous value: -"Python expression to transform existing script config. Mutually exclusive with config. WARNING: Expressions with infinite loops will hang the server. Examples: Simple: python_transform=\"config['sequence'][0]['data']['message'] = 'Hello'\" Pattern: python_transform=\"for step in config['sequence']: if step.get('alias') == 'My Step': step['data']['value'] = 100\" \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"New value: +"Python expression to transform existing script config. Mutually exclusive with config. WARNING: Expressions with infinite loops will hang the server. Examples: Simple: python_transform=\"config['sequence'][0]['data']['message'] = 'Hello'\" Pattern: python_transform=\"for step in config['sequence']: if step.get('alias') == 'My Step': step['data']['value'] = 100\" \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, index, count, sort,\n  reverse\n- Dict methods: update, get, setdefault, keys, values, items, pop, clear\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, lstrip, rstrip,\n  removeprefix, removesuffix, split, join, replace, count, index\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"
  2. Changed7 schema fields changedv8.6.0
    • changedInput schema / properties / config / description
      Previous value: -"Script configuration dictionary. Must include EITHER 'sequence' (for regular scripts) OR 'use_blueprint' (for blueprint-based scripts). Optional fields: 'alias', 'description', 'icon', 'mode', 'max', 'fields'. Mutually exclusive with python_transform."New value: +"Script configuration dictionary. Mutually exclusive with python_transform."
    • changedInput schema / properties / config_hash / description
      Previous value: -"Config hash from ha_config_get_script for optimistic locking. REQUIRED for python_transform (validates script unchanged). Optional for config updates (validates before full replacement if provided)."New value: +"Config hash from ha_config_get_script for optimistic locking. Required for python_transform; optional for config updates (validates before full replacement if provided)."
    • changedInput schema / properties / python_transform / description
      Previous value: -"Python expression to transform existing script config. Mutually exclusive with config. Requires config_hash for validation. WARNING: Expressions with infinite loops will hang the server. Examples: Simple: python_transform=\"config['sequence'][0]['data']['message'] = 'Hello'\" Pattern: python_transform=\"for step in config['sequence']: if step.get('alias') == 'My Step': step['data']['value'] = 100\" \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 script config. Mutually exclusive with config. WARNING: Expressions with infinite loops will hang the server. Examples: Simple: python_transform=\"config['sequence'][0]['data']['message'] = 'Hello'\" Pattern: python_transform=\"for step in config['sequence']: if step.get('alias') == 'My Step': step['data']['value'] = 100\" \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"
    • addedInput schema / properties / run
      Added value: +{
      +  "anyOf": [
      +    {
      +      "enum": [
      +        "start",
      +        "stop"
      +      ],
      +      "type": "string"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "description": "Run control, used alone with script_id (no config, python_transform or take_control_of_blueprint): 'start' runs the script (script.turn_on; returns once it has started, without waiting for it to finish), 'stop' stops its running executions (script.turn_off). Stopping does not disable the script; Home Assistant has no script enable/disable."
      +}
    • changedInput schema / properties / take_control_of_blueprint / description
      Previous value: -"Convert a blueprint-backed script into an editable standalone one -- the UI's \"Take control\". Renders the blueprint with its current inputs and saves the result over the same script, which then has its own sequence and no 'use_blueprint'. Mutually exclusive with config and python_transform. Irreversible: the link to the blueprint is gone afterwards, so edit inputs instead if you only want to change a value. Does NOT free the blueprint: Home Assistant keeps counting the converted script as a user, so deleting that blueprint stays refused until the script is removed. To preview the rendering without writing anything, use ha_manage_blueprints(action=\"substitute\", domain=\"script\")."New value: +"Convert a blueprint-backed script into an editable standalone one -- the UI's \"Take control\". Renders the blueprint with its current inputs and saves the result over the same script, which keeps its script_id, alias and description and then has its own sequence and no 'use_blueprint'. Mutually exclusive with config and python_transform."
    • addedInput schema / properties / variables
      Added value: +{
      +  "anyOf": [
      +    {
      +      "additionalProperties": true,
      +      "type": "object"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "description": "With run='start': values for the script's fields, passed to the run as its variables."
      +}
    • changedInput schema / properties / wait / description
      Previous value: -"Wait for script to be queryable before returning. Default: True. Set to False for bulk operations."New value: +"Wait for script to be queryable before returning. Set to False for bulk operations."
  3. Changed1 schema field changedv8.5.0
    • addedInput schema / properties / take_control_of_blueprint
      Added value: +{
      +  "default": false,
      +  "description": "Convert a blueprint-backed script into an editable standalone one -- the UI's \"Take control\". Renders the blueprint with its current inputs and saves the result over the same script, which then has its own sequence and no 'use_blueprint'. Mutually exclusive with config and python_transform. Irreversible: the link to the blueprint is gone afterwards, so edit inputs instead if you only want to change a value. Does NOT free the blueprint: Home Assistant keeps counting the converted script as a user, so deleting that blueprint stays refused until the script is removed. To preview the rendering without writing anything, use ha_manage_blueprints(action=\"substitute\", domain=\"script\").",
      +  "type": "boolean"
      +}
  4. First observedv7.14.2

TDQS

A4.7/5.0
Behavior5/5

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

Goes well beyond the annotations (destructiveHint, non-idempotent): take control is one-way and does not free the blueprint (HA still counts the script as a user, so deletion stays refused), the script reload signals no completion, python_transform expressions with infinite loops hang the server, and config_hash provides optimistic locking. The documented 'wait' semantics and the known Claude Desktop hang workaround add further operational context the schema hints at but does not fully explain.

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

Conciseness3/5

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

Well front-loaded and organized with a MODES block and EXAMPLES, but it carries avoidable weight: the appended keyword dump ('create update modify edit script sequence actions new script write save...') earns nothing, and the Claude Desktop timeout paragraph is verbose relative to its payoff. Substantively dense for an 11-parameter tool, so length is partly justified, but trimming is possible.

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 an 11-parameter, multi-mode, destructive write tool with an output schema, this covers what matters: mode selection, required inputs per mode, irreversibility of take-control, reload timing, and best-practice warning reporting under best_practice_warnings. Nothing an agent needs in order to call it safely appears to be 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 91%, so the baseline is 3; the description still adds real value by explaining cross-parameter constraints the per-property docs cannot — which parameters are mutually exclusive, that python_transform requires config_hash, that run is used alone, and worked examples of config/use_blueprint/take_control payloads. It does not cover wait, category, or BestPracticeKey, leaving those entirely to 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 script') and immediately draws the boundary against the nearest sibling: scripts use 'sequence', and trigger-based execution belongs to ha_config_set_automation. An agent can route between the two script/automation tools without opening either schema.

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?

Enumerates four mutually exclusive modes with the condition that selects each (python_transform for surgical edits with config_hash; config for new/restructured; take_control for blueprint conversion; run alone with script_id), and names alternatives — ha_config_get_script for hashes, ha_manage_blueprints(substitute) for previewing without writing. It also excludes: 'write config changes in a separate call first'.

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