ha-mcp-dev 7.6.0.dev621__tar.gz → 7.6.0.dev623__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. {ha_mcp_dev-7.6.0.dev621/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.6.0.dev623}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/config.py +73 -8
  4. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/server.py +28 -10
  5. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/settings_ui.py +166 -0
  6. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/best_practice_checker.py +265 -129
  7. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_config_automations.py +84 -30
  8. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_config_dashboards.py +61 -10
  9. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_config_helpers.py +60 -13
  10. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_config_scenes.py +44 -6
  11. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_config_scripts.py +50 -9
  12. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_filesystem.py +1 -1
  13. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_yaml_config.py +119 -7
  14. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/util_helpers.py +285 -0
  15. ha_mcp_dev-7.6.0.dev623/src/ha_mcp/utils/skill_loader.py +271 -0
  16. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  17. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  18. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/LICENSE +0 -0
  19. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/MANIFEST.in +0 -0
  20. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/README.md +0 -0
  21. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/setup.cfg +0 -0
  22. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/__init__.py +0 -0
  23. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/__main__.py +0 -0
  24. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/_pypi_marker +0 -0
  25. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/_version.py +0 -0
  26. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/auth/__init__.py +0 -0
  27. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/auth/consent_form.py +0 -0
  28. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/auth/provider.py +0 -0
  29. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/backup_manager.py +0 -0
  30. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/client/__init__.py +0 -0
  31. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/client/rest_client.py +0 -0
  32. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/client/supervisor_client.py +0 -0
  33. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/client/websocket_client.py +0 -0
  34. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/client/websocket_listener.py +0 -0
  35. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/errors.py +0 -0
  36. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/policy/__init__.py +0 -0
  37. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/policy/approval_queue.py +0 -0
  38. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/policy/evaluator.py +0 -0
  39. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/policy/handlers.py +0 -0
  40. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/policy/middleware.py +0 -0
  41. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/policy/model.py +0 -0
  42. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/policy/persistence.py +0 -0
  43. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/policy/value_sources.py +0 -0
  44. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/py.typed +0 -0
  45. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  46. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  47. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  48. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  49. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  50. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  51. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  52. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  53. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  54. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  55. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  56. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  57. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  58. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  59. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  60. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  61. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  62. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  63. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  64. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  65. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  66. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  67. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/smoke_test.py +0 -0
  68. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  69. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/__init__.py +0 -0
  70. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/auto_backup.py +0 -0
  71. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/backup.py +0 -0
  72. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/device_control.py +0 -0
  73. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/enhanced.py +0 -0
  74. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/helpers.py +0 -0
  75. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/reference_validator.py +0 -0
  76. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/registry.py +0 -0
  77. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/smart_search.py +0 -0
  78. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_addons.py +0 -0
  79. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_areas.py +0 -0
  80. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  81. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  82. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_calendar.py +0 -0
  83. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_camera.py +0 -0
  84. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_categories.py +0 -0
  85. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_code.py +0 -0
  86. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  87. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_energy.py +0 -0
  88. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_entities.py +0 -0
  89. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_groups.py +0 -0
  90. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_hacs.py +0 -0
  91. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_history.py +0 -0
  92. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_integrations.py +0 -0
  93. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_labels.py +0 -0
  94. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  95. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_registry.py +0 -0
  96. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_resources.py +0 -0
  97. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_search.py +0 -0
  98. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_service.py +0 -0
  99. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_services.py +0 -0
  100. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_system.py +0 -0
  101. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_todo.py +0 -0
  102. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_traces.py +0 -0
  103. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_updates.py +0 -0
  104. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_utility.py +0 -0
  105. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  106. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/tools/tools_zones.py +0 -0
  107. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/transforms/__init__.py +0 -0
  108. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/transforms/categorized_search.py +0 -0
  109. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  110. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/utils/__init__.py +0 -0
  111. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/utils/config_hash.py +0 -0
  112. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/utils/data_paths.py +0 -0
  113. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/utils/domain_handlers.py +0 -0
  114. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  115. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  116. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/utils/operation_manager.py +0 -0
  117. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/utils/python_sandbox.py +0 -0
  118. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp/utils/usage_logger.py +0 -0
  119. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  120. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  121. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  122. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  123. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/tests/__init__.py +0 -0
  124. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/tests/test_constants.py +0 -0
  125. {ha_mcp_dev-7.6.0.dev621 → ha_mcp_dev-7.6.0.dev623}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.6.0.dev621
3
+ Version: 7.6.0.dev623
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.6.0.dev621"
7
+ version = "7.6.0.dev623"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -110,7 +110,7 @@ class Settings(BaseSettings):
110
110
 
111
111
  # Master beta-features toggle. UI-only — intentionally not in any
112
112
  # addon config.yaml schema. Consumed by the master gate in
113
- # ``_apply_feature_flag_overrides``, which force-sets the five
113
+ # ``_apply_feature_flag_overrides``, which force-sets the
114
114
  # ``BETA_FEATURE_FIELDS`` sub-flags to False whenever this master is
115
115
  # off. Dev addon ``start.py`` auto-writes ``ENABLE_BETA_FEATURES=true``
116
116
  # whenever any beta sub-flag key is present in ``/data/options.json``
@@ -122,6 +122,27 @@ class Settings(BaseSettings):
122
122
  # files. Disabled by default; only for YAML-only features with no UI/API path.
123
123
  enable_yaml_config_editing: bool = Field(False, alias="ENABLE_YAML_CONFIG_EDITING")
124
124
 
125
+ # Per-key gates for ``automation`` / ``script`` / ``scene`` under
126
+ # ``packages/*.yaml``. The custom component accepts these three
127
+ # PACKAGES_ONLY_YAML_KEYS unconditionally; ha-mcp's UI exposes a
128
+ # toggle per key so an operator who wants YAML-managed
129
+ # automations/scripts/scenes in packages but not the others can
130
+ # narrow the surface. ha_config_set_yaml rejects packages/*.yaml
131
+ # writes for a disabled key client-side, and passes the disabled set
132
+ # to the custom component so the underlying service rejects too
133
+ # (writes of these keys to configuration.yaml are rejected
134
+ # independently of these flags). Each
135
+ # toggle is meaningful only when ``enable_yaml_config_editing`` is
136
+ # on; the UI nests these rows under that parent and dims them when
137
+ # the parent is off.
138
+ enable_yaml_packages_automation: bool = Field(
139
+ False, alias="ENABLE_YAML_PACKAGES_AUTOMATION"
140
+ )
141
+ enable_yaml_packages_script: bool = Field(
142
+ False, alias="ENABLE_YAML_PACKAGES_SCRIPT"
143
+ )
144
+ enable_yaml_packages_scene: bool = Field(False, alias="ENABLE_YAML_PACKAGES_SCENE")
145
+
125
146
  # Seed values for tool visibility (comma-separated tool names).
126
147
  # Used as initial config when no tool_config.json exists.
127
148
  # The web settings UI (/settings) is the primary interface for managing these.
@@ -144,6 +165,18 @@ class Settings(BaseSettings):
144
165
  # env-var users see the trade-off in their logs.
145
166
  enable_lite_docstrings: bool = Field(False, alias="ENABLE_LITE_DOCSTRINGS")
146
167
 
168
+ # Mandatory best-practice skills — server-side master switch for the
169
+ # write-tool skill_content delivery feature (issue #1182). When True
170
+ # (default), the six write tools (automations / scripts / scenes /
171
+ # helpers / dashboards / yaml) attach the canonical best-practice
172
+ # reference files under ``skill_content`` on every successful write,
173
+ # plus auto-embed any sections cited by best-practice warnings. The
174
+ # per-call ``MandatoryBPS`` parameter on each tool controls whether
175
+ # the canonical files ship for that one call. This setting is the
176
+ # master gate above that — when False, NO skill_content goes out
177
+ # regardless of the per-call param or BP warnings. Default on.
178
+ enable_mandatory_bps: bool = Field(True, alias="ENABLE_MANDATORY_BPS")
179
+
147
180
  # Filesystem tools — read/write/delete/list under the HA config dir.
148
181
  # Previously gated by a direct ``os.getenv`` call in
149
182
  # ``tools/tools_filesystem.py`` so callers (and the settings UI)
@@ -412,7 +445,28 @@ FEATURE_FLAG_FIELDS: tuple[FeatureFlagField, ...] = (
412
445
  FeatureFlagField(
413
446
  "enable_tool_security_policies", "ENABLE_TOOL_SECURITY_POLICIES", bool
414
447
  ),
448
+ # Non-beta, default-ON master switch for write-tool skill_content
449
+ # delivery (#1182). Grouped with the non-beta flags above the beta
450
+ # run below; intentionally NOT in BETA_FEATURE_FIELDS (it must not be
451
+ # gated by the beta master) nor in ADVANCED_SETTINGS_FIELDS (registries
452
+ # are name-disjoint per _validate_registries()).
453
+ FeatureFlagField("enable_mandatory_bps", "ENABLE_MANDATORY_BPS", bool),
415
454
  FeatureFlagField("enable_yaml_config_editing", "ENABLE_YAML_CONFIG_EDITING", bool),
455
+ # Per-key sub-gates beneath enable_yaml_config_editing. Nested in
456
+ # the UI, dimmed when the parent is off. Also listed in
457
+ # BETA_FEATURE_FIELDS so they follow the same master-gate +
458
+ # addon-mode override path as the other beta flags — that is what
459
+ # makes the web-UI toggle take effect on the stable add-on (where
460
+ # they are not in config.yaml). See that tuple for the rationale.
461
+ FeatureFlagField(
462
+ "enable_yaml_packages_automation",
463
+ "ENABLE_YAML_PACKAGES_AUTOMATION",
464
+ bool,
465
+ ),
466
+ FeatureFlagField(
467
+ "enable_yaml_packages_script", "ENABLE_YAML_PACKAGES_SCRIPT", bool
468
+ ),
469
+ FeatureFlagField("enable_yaml_packages_scene", "ENABLE_YAML_PACKAGES_SCENE", bool),
416
470
  FeatureFlagField("enable_lite_docstrings", "ENABLE_LITE_DOCSTRINGS", bool),
417
471
  FeatureFlagField("enable_filesystem_tools", "HAMCP_ENABLE_FILESYSTEM_TOOLS", bool),
418
472
  FeatureFlagField(
@@ -449,6 +503,16 @@ _FEATURE_FLAG_INT_BOUNDS: dict[str, tuple[int, int]] = {
449
503
  # gate, never by the per-field iteration.
450
504
  BETA_FEATURE_FIELDS: tuple[str, ...] = (
451
505
  "enable_yaml_config_editing",
506
+ # Per-key sub-gates of enable_yaml_config_editing. Included here so
507
+ # they ride the same master gate + addon-mode override path as the
508
+ # other beta flags. Without this, the addon-mode short-circuit in
509
+ # ``_apply_feature_flag_overrides`` (and the ``get_feature_flag_origin``
510
+ # logic) would leave them dead on the stable add-on — reachable only
511
+ # via the dev add-on's config.yaml options. They still render NESTED
512
+ # under their parent in the web UI (not as separate beta-sub rows).
513
+ "enable_yaml_packages_automation",
514
+ "enable_yaml_packages_script",
515
+ "enable_yaml_packages_scene",
452
516
  "enable_filesystem_tools",
453
517
  "enable_custom_component_integration",
454
518
  "enable_code_mode",
@@ -461,8 +525,9 @@ BETA_FEATURE_FIELDS: tuple[str, ...] = (
461
525
  #
462
526
  # - ``section`` groups fields in the Advanced section of the Server Settings
463
527
  # tab: "connection", "search", "operations", "diagnostics", "tools_surface".
464
- # The 5 beta sub-flags + the master live in a separate "beta" section that
465
- # the UI renders below the Advanced section.
528
+ # The beta sub-flags + the master live in a separate "beta" section that
529
+ # the UI renders below the Advanced section (the per-key yaml-packages
530
+ # sub-flags render nested under enable_yaml_config_editing within it).
466
531
  # - ``editable=False`` marks display-only rows. Connection fields are
467
532
  # non-editable from the running server (chicken-and-egg footgun);
468
533
  # ``MCP_SERVER_VERSION`` is editable (it has an env alias) but the UI
@@ -613,7 +678,7 @@ def get_feature_flag_origin(env_name: str) -> str:
613
678
  schema, ``start.py`` doesn't write the env var, and the master
614
679
  falls through to env / file / default precedence so the
615
680
  standalone web UI master path remains the gate.
616
- - The five ``BETA_FEATURE_FIELDS`` (sub-flags) follow the same
681
+ - The ``BETA_FEATURE_FIELDS`` (sub-flags) follow the same
617
682
  shape — present in dev addon schema, absent from stable. Same
618
683
  env-var-presence signal distinguishes them at runtime.
619
684
  """
@@ -632,8 +697,8 @@ def get_feature_flag_origin(env_name: str) -> str:
632
697
  # Stable addon: env var never written → fall through to
633
698
  # file/default. The master moved from "never schema-bound"
634
699
  # to "schema-bound on dev only"; the same env-var-presence
635
- # signal now distinguishes both for the master and the 5
636
- # sub-flags.
700
+ # signal now distinguishes both for the master and the
701
+ # beta sub-flags.
637
702
  if os.environ.get(env_name) is not None:
638
703
  return "addon"
639
704
  # else: stable / legacy-dev-no-master-key, fall through.
@@ -730,7 +795,7 @@ def _apply_feature_flag_overrides(settings: "Settings") -> None:
730
795
 
731
796
  EXCEPTION: the beta-master + beta-sub-flag fields skip the
732
797
  addon-mode short-circuit. The master isn't in any addon schema;
733
- the five sub-flags are in the dev-addon schema (where ``start.py``
798
+ the sub-flags are in the dev-addon schema (where ``start.py``
734
799
  writes the env var from options.json — env-var-wins skips the
735
800
  file read here, leaving Supervisor authoritative) but NOT in the
736
801
  stable schema (where the env var is never written, so the file
@@ -738,7 +803,7 @@ def _apply_feature_flag_overrides(settings: "Settings") -> None:
738
803
 
739
804
  2. **Beta master gate**: after the per-field pass, if
740
805
  ``enable_beta_features`` is False on the resolved Settings,
741
- force-set the five BETA_FEATURE_FIELDS to False regardless of
806
+ force-set the BETA_FEATURE_FIELDS to False regardless of
742
807
  how they currently look. This is the "master toggle" semantics —
743
808
  even a power user who sets ENABLE_YAML_CONFIG_EDITING=true via
744
809
  env var still needs to flip the master before the flag takes
@@ -200,10 +200,13 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
200
200
 
201
201
  Skills are vendored via a git submodule at resources/skills-vendor/.
202
202
  The actual skill directories live under the skills/ subdirectory
203
- within that repo.
203
+ within that repo. Delegates to
204
+ :func:`ha_mcp.utils.skill_loader.get_skills_dir` so the write-tool
205
+ ``MandatoryBPS`` parameter resolves the same path.
204
206
  """
205
- skills_dir = Path(__file__).parent / "resources" / "skills-vendor" / "skills"
206
- return skills_dir if skills_dir.exists() else None
207
+ from .utils.skill_loader import get_skills_dir
208
+
209
+ return get_skills_dir()
207
210
 
208
211
  def _build_skills_instructions(self) -> str | None:
209
212
  """Build server instructions from bundled skill frontmatter.
@@ -1434,13 +1437,28 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
1434
1437
  )
1435
1438
  )
1436
1439
 
1437
- return {
1438
- "success": True,
1439
- "skill": skill,
1440
- "file": file,
1441
- "uri": f"skill://{skill}/{file}",
1442
- "content": content,
1443
- }
1440
+ # Hint goes at the top of the response so the LLM sees it before
1441
+ # parsing the (potentially large) content body. Scoped to the
1442
+ # best-practice skill because that's the one the write-tool
1443
+ # MandatoryBPS param gates; other skills (if any) are unrelated.
1444
+ from .tools.util_helpers import (
1445
+ _HA_BEST_PRACTICES_SKILL_NAME,
1446
+ _SKILL_GUIDE_MANDATORYBPS_HINT,
1447
+ )
1448
+
1449
+ response: dict[str, Any] = {}
1450
+ if skill == _HA_BEST_PRACTICES_SKILL_NAME:
1451
+ response["skill_content_hint"] = _SKILL_GUIDE_MANDATORYBPS_HINT
1452
+ response.update(
1453
+ {
1454
+ "success": True,
1455
+ "skill": skill,
1456
+ "file": file,
1457
+ "uri": f"skill://{skill}/{file}",
1458
+ "content": content,
1459
+ }
1460
+ )
1461
+ return response
1444
1462
 
1445
1463
  # Helper methods required by EnhancedToolsMixin
1446
1464
 
@@ -814,6 +814,15 @@ _SETTINGS_HTML = (
814
814
  left: 36px; top: 0; bottom: 0; width: 2px; background: var(--border); }
815
815
  .feature-row.codemode-sub.dimmed { opacity: 0.55; }
816
816
  .feature-row.codemode-sub.dimmed input { cursor: not-allowed; }
817
+ /* YAML packages per-key sub-rows — second-level nested under
818
+ enable_yaml_config_editing. Same dimming logic as codemode-sub
819
+ but gated by enable_yaml_config_editing (the parent flag) rather
820
+ than enable_code_mode. */
821
+ .feature-row.yaml-packages-sub { padding-left: 56px; position: relative; }
822
+ .feature-row.yaml-packages-sub::before { content: ""; position: absolute;
823
+ left: 36px; top: 0; bottom: 0; width: 2px; background: var(--border); }
824
+ .feature-row.yaml-packages-sub.dimmed { opacity: 0.55; }
825
+ .feature-row.yaml-packages-sub.dimmed input { cursor: not-allowed; }
817
826
  /* Advanced settings sections — one row per
818
827
  ADVANCED_SETTINGS_FIELDS entry, grouped by section. Visually
819
828
  matches the .feature-row treatment so the Server Settings tab
@@ -2165,6 +2174,10 @@ const FEATURE_META = {
2165
2174
  label: "Enable Tool Security Policies",
2166
2175
  help: "Opt-in middleware that gates high-stakes MCP tool calls behind user approval. When enabled, tools that match a rule in the Tool Security Policies tab require you to click Approve in the web UI before they run. Off by default. Per-tool rules with optional argument conditions are configured in the Tool Security Policies tab. Requires restart to take effect.",
2167
2176
  },
2177
+ enable_mandatory_bps: {
2178
+ label: "Attach best-practice skills on writes",
2179
+ help: "Master switch for the write-tool skill content delivery feature (issue #1182). When enabled (default), the six config write tools (automations, scripts, scenes, helpers, dashboards, raw YAML) attach the canonical Home Assistant best-practice reference files under skill_content on every successful write, plus auto-embed any reference sections cited by best-practice warnings. Each tool also exposes a per-call MandatoryBPS parameter the agent can set to false on subsequent calls once it has the content. When this master switch is off, NO skill_content goes out regardless of the per-call parameter or BP warnings. Leave on if your LLM benefits from inline guidance; turn off to minimise tokens when using an LLM that has the best-practice files indexed via skills or another retrieval path. Requires restart to take effect.",
2180
+ },
2168
2181
  // Master beta toggle — gates the 5 sub-flags below at runtime
2169
2182
  // (see config.py:_apply_feature_flag_overrides master gate). UI
2170
2183
  // dims sub-rows when this is off and re-renders live on flip.
@@ -2176,6 +2189,18 @@ const FEATURE_META = {
2176
2189
  label: "Enable YAML config editing (beta)",
2177
2190
  help: "Beta feature — disabled by default. Allows AI assistants to add, replace, or remove top-level keys in configuration.yaml and packages/*.yaml. Only whitelisted keys are allowed (e.g., template, sensor, command_line, mqtt, knx); core keys like homeassistant, http, and recorder are blocked. Each edit validates YAML syntax, runs a config check, and creates an automatic backup. Changes to most keys require a full HA restart to take effect. See docs/beta.md for known limitations. Dedicated tools (automations, scripts, scenes, helpers, template sensors) should be preferred when available.",
2178
2191
  },
2192
+ enable_yaml_packages_automation: {
2193
+ label: "Allow automation in packages/*.yaml",
2194
+ help: "Sub-toggle of YAML config editing. When on, ha_config_set_yaml accepts yaml_path='automation' inside packages/*.yaml. When off, the wrapper rejects the call client-side AND the custom component rejects it server-side. Storage-mode tools (ha_config_set_automation) cover the UI-managed path and are unaffected. Disabled by default.",
2195
+ },
2196
+ enable_yaml_packages_script: {
2197
+ label: "Allow script in packages/*.yaml",
2198
+ help: "Sub-toggle of YAML config editing. When on, ha_config_set_yaml accepts yaml_path='script' inside packages/*.yaml. When off, the wrapper rejects the call client-side AND the custom component rejects it server-side. Storage-mode tools (ha_config_set_script) cover the UI-managed path and are unaffected. Disabled by default.",
2199
+ },
2200
+ enable_yaml_packages_scene: {
2201
+ label: "Allow scene in packages/*.yaml",
2202
+ help: "Sub-toggle of YAML config editing. When on, ha_config_set_yaml accepts yaml_path='scene' inside packages/*.yaml. When off, the wrapper rejects the call client-side AND the custom component rejects it server-side. Storage-mode tools (ha_config_set_scene) cover the UI-managed path and are unaffected. Disabled by default.",
2203
+ },
2179
2204
  enable_filesystem_tools: {
2180
2205
  label: "Enable filesystem tools (beta)",
2181
2206
  help: "Sets HAMCP_ENABLE_FILESYSTEM_TOOLS=true. Enables direct file read/write access to your Home Assistant filesystem. WARNING: This gives the MCP server sensitive direct file access to your system. Only enable if you trust the AI assistant with file operations. Requires restart to take effect.",
@@ -2200,6 +2225,16 @@ const FEATURE_META = {
2200
2225
  // ``config.BETA_FEATURE_FIELDS`` without duplicating the name list here.
2201
2226
  let BETA_SUB_FLAGS = new Set();
2202
2227
 
2228
+ // Sub-flags of ``enable_yaml_config_editing``. Rendered nested beneath
2229
+ // the parent in renderFeatureFlags so the dependency is visually
2230
+ // obvious. They are NOT in BETA_SUB_FLAGS — the parent is the gate,
2231
+ // and the master-off → parent-off cascade transitively covers them.
2232
+ const YAML_PACKAGES_SUB_FLAGS = [
2233
+ 'enable_yaml_packages_automation',
2234
+ 'enable_yaml_packages_script',
2235
+ 'enable_yaml_packages_scene',
2236
+ ];
2237
+
2203
2238
  // Cached add-on flag. Each settings endpoint (/api/settings/features,
2204
2239
  // /api/settings/advanced, /api/settings/backup-config) returns
2205
2240
  // ``is_addon`` so the env-locked banner copy can adapt — the addon
@@ -2309,6 +2344,10 @@ function renderFeatureFlags(flags) {
2309
2344
  Object.keys(FEATURE_META).forEach(fieldName => {
2310
2345
  const f = flags[fieldName];
2311
2346
  if (!f) return;
2347
+ // Skip yaml-packages sub-rows in the main pass — they're rendered
2348
+ // by renderYamlPackagesSubRows below right after their parent so
2349
+ // the nesting reads in source order.
2350
+ if (YAML_PACKAGES_SUB_FLAGS.includes(fieldName)) return;
2312
2351
  const meta = FEATURE_META[fieldName];
2313
2352
  const isMaster = fieldName === 'enable_beta_features';
2314
2353
  const isBetaSub = BETA_SUB_FLAGS.has(fieldName);
@@ -2377,6 +2416,20 @@ function renderFeatureFlags(flags) {
2377
2416
  renderFeatureFlags(_lastFeatureFlags);
2378
2417
  }
2379
2418
  }
2419
+ // Re-render on enable_yaml_config_editing flip so the 3
2420
+ // packages sub-rows dim/undim immediately. Same pattern as the
2421
+ // master flip above — value is mutated in the live cache and
2422
+ // the panel re-renders synchronously while the save POST runs
2423
+ // in the background.
2424
+ if (fieldName === 'enable_yaml_config_editing') {
2425
+ if (_lastFeatureFlags[fieldName]) {
2426
+ _lastFeatureFlags[fieldName] = {
2427
+ ..._lastFeatureFlags[fieldName],
2428
+ value: input.checked,
2429
+ };
2430
+ renderFeatureFlags(_lastFeatureFlags);
2431
+ }
2432
+ }
2380
2433
  saveFeatureFlag(fieldName, input.checked);
2381
2434
  });
2382
2435
  const slider = document.createElement('span');
@@ -2413,6 +2466,74 @@ function renderFeatureFlags(flags) {
2413
2466
  const codeModeOn = !!f.value;
2414
2467
  renderCodeModeSubRows(targetBody, masterOn, codeModeOn);
2415
2468
  }
2469
+ // After rendering the enable_yaml_config_editing parent, inject
2470
+ // its 3 per-key sub-rows (automation/script/scene). Dimmed when
2471
+ // either the master beta is off (parent forced off) or the parent
2472
+ // itself is off.
2473
+ if (fieldName === 'enable_yaml_config_editing') {
2474
+ const parentOn = !!f.value;
2475
+ renderYamlPackagesSubRows(flags, targetBody, masterOn, parentOn);
2476
+ }
2477
+ });
2478
+ }
2479
+
2480
+ function renderYamlPackagesSubRows(flags, parentEl, masterOn, parentOn) {
2481
+ YAML_PACKAGES_SUB_FLAGS.forEach(fieldName => {
2482
+ const f = flags[fieldName];
2483
+ if (!f) return;
2484
+ const meta = FEATURE_META[fieldName] || { label: fieldName, help: '' };
2485
+ const lockedByGate = !masterOn || !parentOn;
2486
+ const row = document.createElement('div');
2487
+ row.className = 'feature-row yaml-packages-sub' + (lockedByGate ? ' dimmed' : '');
2488
+
2489
+ const info = document.createElement('div');
2490
+ info.className = 'feature-info';
2491
+ const lockedNote = !f.editable
2492
+ ? `<div class="feature-locked-note">` +
2493
+ (f.origin === 'env'
2494
+ ? envLockedNoteHtml(f.env_var, fieldName)
2495
+ : escapeHtml(ORIGIN_LOCKED_NOTE[f.origin] || '')) +
2496
+ `</div>`
2497
+ : '';
2498
+ const infoNote = f.editable && ORIGIN_INFO_NOTE[f.origin]
2499
+ ? `<div class="feature-locked-note">` +
2500
+ `${escapeHtml(ORIGIN_INFO_NOTE[f.origin])}</div>`
2501
+ : '';
2502
+ info.innerHTML =
2503
+ `<div class="feature-name">${escapeHtml(meta.label)}</div>` +
2504
+ `<div class="feature-help">${escapeHtml(meta.help)}</div>` +
2505
+ lockedNote + infoNote;
2506
+
2507
+ const control = document.createElement('div');
2508
+ control.className = 'feature-control';
2509
+ const label = document.createElement('label');
2510
+ label.className = 'switch';
2511
+ const input = document.createElement('input');
2512
+ input.type = 'checkbox';
2513
+ input.checked = !!f.value;
2514
+ input.disabled = !f.editable || lockedByGate;
2515
+ input.addEventListener('change', () => {
2516
+ // Keep the cached flag value in sync (parity with the parent/master
2517
+ // row handlers) so a later parent flip — which re-renders from
2518
+ // _lastFeatureFlags — reflects this sub-row's current state rather
2519
+ // than a stale value.
2520
+ if (_lastFeatureFlags[fieldName]) {
2521
+ _lastFeatureFlags[fieldName] = {
2522
+ ..._lastFeatureFlags[fieldName],
2523
+ value: input.checked,
2524
+ };
2525
+ }
2526
+ saveFeatureFlag(fieldName, input.checked);
2527
+ });
2528
+ const slider = document.createElement('span');
2529
+ slider.className = 'slider';
2530
+ label.appendChild(input);
2531
+ label.appendChild(slider);
2532
+ control.appendChild(label);
2533
+
2534
+ row.appendChild(info);
2535
+ row.appendChild(control);
2536
+ parentEl.appendChild(row);
2416
2537
  });
2417
2538
  }
2418
2539
 
@@ -4186,6 +4307,11 @@ def build_settings_handlers(
4186
4307
  ErrorCode.VALIDATION_INVALID_PARAMETER,
4187
4308
  f"Refusing to flip env-pinned tools: {', '.join(rejected)}. "
4188
4309
  "Unset DISABLED_TOOLS / PINNED_TOOLS first.",
4310
+ suggestions=[
4311
+ "Unset the DISABLED_TOOLS / PINNED_TOOLS environment "
4312
+ "variables (or remove them from your addon/Docker "
4313
+ "config), then restart to edit these tools from the UI.",
4314
+ ],
4189
4315
  context={"rejected": rejected},
4190
4316
  ),
4191
4317
  status_code=409,
@@ -4539,6 +4665,12 @@ def build_settings_handlers(
4539
4665
  "enable_beta_features=true in the same save, or "
4540
4666
  "flip the master on first."
4541
4667
  ),
4668
+ suggestions=[
4669
+ "Include enable_beta_features=true in the same save "
4670
+ "payload as the sub-flag(s).",
4671
+ "Or turn on the master 'Enable beta features' toggle "
4672
+ "first, then enable the sub-flag(s).",
4673
+ ],
4542
4674
  context={"rejected": beta_sub_writes},
4543
4675
  ),
4544
4676
  status_code=409,
@@ -4704,6 +4836,13 @@ def build_settings_handlers(
4704
4836
  create_error_response(
4705
4837
  ErrorCode.INTERNAL_ERROR,
4706
4838
  "Supervisor helper returned ok=False with no error",
4839
+ suggestions=[
4840
+ "Check the Home Assistant Supervisor logs and "
4841
+ "the add-on logs for the underlying failure.",
4842
+ "Report this at "
4843
+ "https://github.com/homeassistant-ai/ha-mcp/issues "
4844
+ "if it persists — this indicates an internal bug.",
4845
+ ],
4707
4846
  ),
4708
4847
  status_code=500,
4709
4848
  )
@@ -5239,6 +5378,11 @@ def build_settings_handlers(
5239
5378
  ErrorCode.VALIDATION_INVALID_PARAMETER,
5240
5379
  f"{fname!r} is set via {env_name} env var — "
5241
5380
  "unset it to edit here.",
5381
+ suggestions=[
5382
+ f"Unset the {env_name} environment variable (or "
5383
+ "remove it from your addon/Docker config), then "
5384
+ "restart to edit this setting from the UI.",
5385
+ ],
5242
5386
  context={"env_var": env_name},
5243
5387
  ),
5244
5388
  status_code=409,
@@ -5279,6 +5423,10 @@ def build_settings_handlers(
5279
5423
  ErrorCode.VALIDATION_INVALID_PARAMETER,
5280
5424
  f"{fname!r} must be between {bounds[0]} and "
5281
5425
  f"{bounds[1]} (got {coerced}).",
5426
+ suggestions=[
5427
+ f"Provide a value for {fname} within the range "
5428
+ f"{bounds[0]}–{bounds[1]}.",
5429
+ ],
5282
5430
  ),
5283
5431
  status_code=400,
5284
5432
  )
@@ -5288,6 +5436,9 @@ def build_settings_handlers(
5288
5436
  create_error_response(
5289
5437
  ErrorCode.VALIDATION_INVALID_PARAMETER,
5290
5438
  f"{fname!r} must be one of {list(choices)} (got {coerced!r}).",
5439
+ suggestions=[
5440
+ f"Set {fname} to one of: {', '.join(map(str, choices))}.",
5441
+ ],
5291
5442
  ),
5292
5443
  status_code=400,
5293
5444
  )
@@ -5338,6 +5489,11 @@ def build_settings_handlers(
5338
5489
  "writes in one batch; the UI should split these "
5339
5490
  f"into separate POSTs ({sorted(file_only)})."
5340
5491
  ),
5492
+ suggestions=[
5493
+ "Submit addon-synced fields (e.g. backup_hint, "
5494
+ "verify_ssl) and override-file fields in separate "
5495
+ "save requests.",
5496
+ ],
5341
5497
  ),
5342
5498
  status_code=500,
5343
5499
  )
@@ -5363,6 +5519,13 @@ def build_settings_handlers(
5363
5519
  create_error_response(
5364
5520
  ErrorCode.INTERNAL_ERROR,
5365
5521
  "Supervisor helper returned ok=False with no error",
5522
+ suggestions=[
5523
+ "Check the Home Assistant Supervisor logs and "
5524
+ "the add-on logs for the underlying failure.",
5525
+ "Report this at "
5526
+ "https://github.com/homeassistant-ai/ha-mcp/issues "
5527
+ "if it persists — this indicates an internal bug.",
5528
+ ],
5366
5529
  ),
5367
5530
  status_code=500,
5368
5531
  )
@@ -5462,6 +5625,9 @@ def build_settings_handlers(
5462
5625
  create_error_response(
5463
5626
  ErrorCode.VALIDATION_INVALID_PARAMETER,
5464
5627
  f"{fname!r} expects {ftype.__name__}, got {type(raw).__name__}.",
5628
+ suggestions=[
5629
+ f"Send {fname} as a {ftype.__name__} value.",
5630
+ ],
5465
5631
  ),
5466
5632
  status_code=400,
5467
5633
  )