ha-mcp-dev 7.14.2.dev2103__tar.gz → 7.14.2.dev2107__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 (188) hide show
  1. {ha_mcp_dev-7.14.2.dev2103/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.14.2.dev2107}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/backup_manager.py +26 -7
  4. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/config.py +56 -0
  5. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/locales/de.json +2 -0
  6. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/locales/en.json +2 -0
  7. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/locales/ru.json +2 -0
  8. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/settings.js +22 -11
  9. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_filesystem.py +142 -0
  10. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_yaml_config.py +11 -1
  11. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  12. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/LICENSE +0 -0
  13. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/MANIFEST.in +0 -0
  14. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/README.md +0 -0
  15. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/setup.cfg +0 -0
  16. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/__init__.py +0 -0
  17. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/__main__.py +0 -0
  18. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/_pypi_marker +0 -0
  19. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/_version.py +0 -0
  20. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/auth/__init__.py +0 -0
  21. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/auth/consent_form.py +0 -0
  22. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/auth/provider.py +0 -0
  23. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/browser_landing.py +0 -0
  24. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/client/__init__.py +0 -0
  25. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/client/rest_client.py +0 -0
  26. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/client/supervisor_client.py +0 -0
  27. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/client/websocket_client.py +0 -0
  28. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/client/websocket_listener.py +0 -0
  29. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  30. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  31. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  32. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  33. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  34. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  35. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/errors.py +0 -0
  36. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/llm_exposure.py +0 -0
  37. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/policy/__init__.py +0 -0
  38. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/policy/approval_queue.py +0 -0
  39. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/policy/evaluator.py +0 -0
  40. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/policy/handlers.py +0 -0
  41. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/policy/middleware.py +0 -0
  42. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/policy/model.py +0 -0
  43. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/policy/persistence.py +0 -0
  44. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/policy/value_sources.py +0 -0
  45. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/py.typed +0 -0
  46. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/read_only.py +0 -0
  47. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  48. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  49. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  50. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  51. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  52. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  53. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  54. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  55. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  56. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  57. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  58. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  59. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  60. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  61. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  62. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  63. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  64. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  65. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  66. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  67. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  68. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  69. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  70. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  71. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  72. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/server.py +0 -0
  73. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/__init__.py +0 -0
  74. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  75. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  76. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  77. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  78. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  79. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  80. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  81. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  82. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  83. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/_theme.py +0 -0
  84. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  85. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  86. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  87. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/settings.css +0 -0
  88. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/settings_ui/settings.html +0 -0
  89. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/smoke_test.py +0 -0
  90. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  91. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/strict_bps.py +0 -0
  92. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/__init__.py +0 -0
  93. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/auto_backup.py +0 -0
  94. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/backup.py +0 -0
  95. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  96. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/component_api.py +0 -0
  97. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/component_config_reads.py +0 -0
  98. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/component_devices.py +0 -0
  99. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/component_registries.py +0 -0
  100. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  101. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  102. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/device_control.py +0 -0
  103. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/enhanced.py +0 -0
  104. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/helpers.py +0 -0
  105. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/radio/__init__.py +0 -0
  106. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/radio/base.py +0 -0
  107. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/radio/matter.py +0 -0
  108. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/radio/thread.py +0 -0
  109. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  110. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/radio/zwave.py +0 -0
  111. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/reference_validator.py +0 -0
  112. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/registry.py +0 -0
  113. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  114. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  115. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  116. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  117. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  118. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  119. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  120. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  121. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  122. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  123. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_addons.py +0 -0
  124. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_areas.py +0 -0
  125. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  126. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  127. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_calendar.py +0 -0
  128. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_camera.py +0 -0
  129. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_categories.py +0 -0
  130. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_code.py +0 -0
  131. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  132. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  133. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  134. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  135. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  136. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  137. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_dev.py +0 -0
  138. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_energy.py +0 -0
  139. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_entities.py +0 -0
  140. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_groups.py +0 -0
  141. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_hacs.py +0 -0
  142. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_history.py +0 -0
  143. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_integrations.py +0 -0
  144. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_labels.py +0 -0
  145. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_radio.py +0 -0
  146. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_registry.py +0 -0
  147. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_resources.py +0 -0
  148. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_search.py +0 -0
  149. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_service.py +0 -0
  150. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_services.py +0 -0
  151. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_system.py +0 -0
  152. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_themes.py +0 -0
  153. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_todo.py +0 -0
  154. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_traces.py +0 -0
  155. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_updates.py +0 -0
  156. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_utility.py +0 -0
  157. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  158. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  159. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/tools_zones.py +0 -0
  160. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/util_helpers.py +0 -0
  161. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/tools/validation_middleware.py +0 -0
  162. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/transforms/__init__.py +0 -0
  163. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/transforms/categorized_search.py +0 -0
  164. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  165. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/transport_security.py +0 -0
  166. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/update_check.py +0 -0
  167. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/utils/__init__.py +0 -0
  168. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/utils/config_hash.py +0 -0
  169. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/utils/data_paths.py +0 -0
  170. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/utils/domain_handlers.py +0 -0
  171. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  172. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  173. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/utils/operation_manager.py +0 -0
  174. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/utils/python_sandbox.py +0 -0
  175. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/utils/skill_loader.py +0 -0
  176. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/utils/usage_logger.py +0 -0
  177. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/visibility/__init__.py +0 -0
  178. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/visibility/model.py +0 -0
  179. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/visibility/persistence.py +0 -0
  180. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp/visibility/resolver.py +0 -0
  181. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  182. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  183. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  184. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  185. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  186. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/tests/__init__.py +0 -0
  187. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/tests/test_constants.py +0 -0
  188. {ha_mcp_dev-7.14.2.dev2103 → ha_mcp_dev-7.14.2.dev2107}/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.14.2.dev2103
3
+ Version: 7.14.2.dev2107
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.14.2.dev2103"
7
+ version = "7.14.2.dev2107"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  # Upper bound widened to <3.15 so the package installs on Home Assistant's
@@ -2128,8 +2128,19 @@ async def _restore_yaml(client: Any, entity_id: str, config: Any) -> Any:
2128
2128
  ``edit_yaml_config`` is the only write path that reaches HA config
2129
2129
  files (``write_file`` rejects them), so YAML restore goes through it
2130
2130
  with ``action="replace"``.
2131
+
2132
+ The operator's extra write keys (#1887) must ride along: the write that
2133
+ produced this snapshot was allowed only because of them, so omitting
2134
+ them here would make an auto-captured backup unrestorable. Note the
2135
+ asymmetry with ``disabled_packages_keys``, whose default is permissive
2136
+ and can therefore be left off.
2131
2137
  """
2132
- from .tools.tools_filesystem import call_mcp_tools_service
2138
+ from .config import get_global_settings
2139
+ from .tools.tools_filesystem import (
2140
+ assert_extra_yaml_keys_supported,
2141
+ call_mcp_tools_service,
2142
+ effective_extra_yaml_write_keys,
2143
+ )
2133
2144
  from .tools.util_helpers import unwrap_service_response
2134
2145
 
2135
2146
  # Split on the LAST "::" (see _fetch_yaml) so an exotic file path
@@ -2137,15 +2148,23 @@ async def _restore_yaml(client: Any, entity_id: str, config: Any) -> Any:
2137
2148
  file, sep, yaml_path = entity_id.rpartition("::")
2138
2149
  if not sep or not file or not yaml_path:
2139
2150
  raise ValueError(f"Invalid yaml snapshot target: {entity_id!r}")
2151
+ service_data: dict[str, Any] = {
2152
+ "file": file,
2153
+ "action": "replace",
2154
+ "yaml_path": yaml_path,
2155
+ "content": str(config),
2156
+ }
2157
+ extra_keys = await effective_extra_yaml_write_keys(client, get_global_settings())
2158
+ if extra_keys:
2159
+ # Same version gate as the write path: without it a component that
2160
+ # predates the field rejects the whole restore call over an option
2161
+ # the snapshot being restored may not even use.
2162
+ await assert_extra_yaml_keys_supported(client, extra_keys)
2163
+ service_data["extra_allowed_keys"] = extra_keys
2140
2164
  result = await call_mcp_tools_service(
2141
2165
  client,
2142
2166
  "edit_yaml_config",
2143
- {
2144
- "file": file,
2145
- "action": "replace",
2146
- "yaml_path": yaml_path,
2147
- "content": str(config),
2148
- },
2167
+ service_data,
2149
2168
  )
2150
2169
  if isinstance(result, dict):
2151
2170
  result = unwrap_service_response(result)
@@ -203,6 +203,25 @@ class Settings(BaseSettings):
203
203
  )
204
204
  enable_yaml_packages_scene: bool = Field(False, alias="ENABLE_YAML_PACKAGES_SCENE")
205
205
 
206
+ # Operator-configured extra top-level keys ha_config_set_yaml may write,
207
+ # comma-separated, on top of the custom component's built-in allowlist
208
+ # (#1887). For YAML-first integrations that are valid on one install but
209
+ # not worth hardcoding globally. Additive only, and never a way past the
210
+ # component's YAML_KEY_DENYLIST: that floor is enforced component-side
211
+ # (the authoritative layer) and is deliberately not mirrored here, so
212
+ # there is one copy to keep correct. A denied key typed into this setting
213
+ # is simply ignored, with the component's explanation on first use.
214
+ # Nor does it lift the packages-only restriction on automation/script/
215
+ # scene: those keep reaching packages/*.yaml through their own per-key
216
+ # toggles and stay rejected in configuration.yaml.
217
+ # Empty (the default) keeps today's behaviour exactly.
218
+ # Registered in ADVANCED_SETTINGS_FIELDS (section ``beta_yamlkeys``), not
219
+ # FEATURE_FLAG_FIELDS, because it is a value rather than a toggle – the
220
+ # same placement the code-mode sub-settings use. Meaningful only when
221
+ # ``enable_yaml_config_editing`` is on; the UI nests it under that parent
222
+ # like the per-key toggles above.
223
+ extra_yaml_write_keys: str = Field("", alias="HA_MCP_EXTRA_YAML_KEYS")
224
+
206
225
  # Seed values for tool visibility (comma-separated tool names).
207
226
  # Used as initial config when no tool_config.json exists.
208
227
  # The web settings UI (/settings) is the primary interface for managing these.
@@ -621,6 +640,7 @@ AdvancedSection = Literal[
621
640
  "tools_surface",
622
641
  "sidecar",
623
642
  "beta_codemode",
643
+ "beta_yamlkeys",
624
644
  "developer",
625
645
  ]
626
646
 
@@ -896,6 +916,18 @@ ADVANCED_SETTINGS_FIELDS: tuple[AdvancedField, ...] = (
896
916
  "beta_codemode",
897
917
  True,
898
918
  ),
919
+ # Extra YAML write keys (issue #1887). Same shape as the code-mode
920
+ # sub-settings above: a non-bool value that belongs visually under a
921
+ # feature toggle rather than in the advanced panel, so it lives here
922
+ # with its own section and the features renderer nests it beneath
923
+ # "Enable YAML config editing".
924
+ AdvancedField(
925
+ "extra_yaml_write_keys",
926
+ "HA_MCP_EXTRA_YAML_KEYS",
927
+ str,
928
+ "beta_yamlkeys",
929
+ True,
930
+ ),
899
931
  # Developer mode (issue #1775). Lives in ADVANCED_SETTINGS_FIELDS —
900
932
  # not FEATURE_FLAG_FIELDS — so it renders in its own "Developer"
901
933
  # section at the very bottom of the Server Settings tab instead of
@@ -1185,6 +1217,30 @@ def _apply_one_feature_flag_override(
1185
1217
  )
1186
1218
 
1187
1219
 
1220
+ def parse_extra_yaml_write_keys(settings: "Settings") -> list[str]:
1221
+ """Parse ``extra_yaml_write_keys`` into a clean key list (#1887).
1222
+
1223
+ Whitespace and empty entries are dropped and the result is deduplicated
1224
+ and sorted, so the service payload is deterministic and unaffected by how
1225
+ the operator spaced the setting.
1226
+
1227
+ The component's ``YAML_KEY_DENYLIST`` is NOT applied here: that floor
1228
+ lives component-side, in the layer that authorizes the write. Mirroring it
1229
+ would mean a second copy to keep in lockstep for no gain: a denied key sent
1230
+ on the wire is dropped there anyway.
1231
+
1232
+ Lives in this module rather than next to the YAML tool because the backup
1233
+ restore path needs it too, and ``backup_manager`` importing a ``tools_*``
1234
+ module would add an import edge that binds ``tools_yaml_config``'s
1235
+ module-level names at restore time.
1236
+ """
1237
+ # Direct attribute access, matching ``_disabled_packages_keys``: a future
1238
+ # rename must raise loudly rather than silently return an empty list,
1239
+ # which would read as "the operator configured nothing".
1240
+ raw = settings.extra_yaml_write_keys or ""
1241
+ return sorted({segment.strip() for segment in raw.split(",") if segment.strip()})
1242
+
1243
+
1188
1244
  def _apply_beta_master_gate(settings: "Settings") -> None:
1189
1245
  """Force BETA_FEATURE_FIELDS to False when ``enable_beta_features`` is off.
1190
1246
 
@@ -412,6 +412,8 @@
412
412
  "advanced.code_mode_max_invocations.help": "API-/Tool-Aufruf-Grenze pro Sandbox-Lauf. Neustart erforderlich.",
413
413
  "advanced.code_mode_saved_tools_path.label": "Pfad für gespeicherte Tools",
414
414
  "advanced.code_mode_saved_tools_path.help": "JSON-Datei, die verwendet wird, um benutzerdefinierte Tools über Neustarts hinweg beizubehalten. Neustart erforderlich.",
415
+ "advanced.extra_yaml_write_keys.label": "Zusätzliche YAML-Schreib-Keys",
416
+ "advanced.extra_yaml_write_keys.help": "Kommagetrennte Top-Level-Keys, die ha_config_set_yaml zusätzlich zu den eingebauten schreiben darf – für YAML-first-Integrationen auf dieser Installation (z. B. alert2). Keys, die die Vertrauensgrenze von Home Assistant selbst verändern, lassen sich nie hinzufügen und werden ignoriert. Erfordert Custom Component 1.2.4 oder neuer.",
415
417
  "advanced.sidecar_pin_port.label": "Einstellungs-UI-Sidecar-Port",
416
418
  "advanced.sidecar_pin_port.help": "0 wählt bei jedem Neustart einen freien Port; 1024-65535 heftet einen bevorzugten Port an. Neustart erforderlich.",
417
419
  "advanced.enable_dev_mode.label": "Entwicklermodus",
@@ -412,6 +412,8 @@
412
412
  "advanced.code_mode_max_invocations.help": "API/tool-call cap per sandbox run. Restart required.",
413
413
  "advanced.code_mode_saved_tools_path.label": "Saved-tools path",
414
414
  "advanced.code_mode_saved_tools_path.help": "JSON file used to persist custom tools across restarts. Restart required.",
415
+ "advanced.extra_yaml_write_keys.label": "Extra YAML write keys",
416
+ "advanced.extra_yaml_write_keys.help": "Comma-separated top-level keys ha_config_set_yaml may write in addition to the built-in ones, for YAML-first integrations on this install (e.g. alert2). Keys that redefine Home Assistant's own trust boundary can never be added and are ignored. Requires custom component 1.2.4 or newer.",
415
417
  "advanced.sidecar_pin_port.label": "Settings UI sidecar port",
416
418
  "advanced.sidecar_pin_port.help": "0 chooses a free port each restart; 1024–65535 pins a preferred port. Restart required.",
417
419
  "advanced.enable_dev_mode.label": "Developer mode",
@@ -412,6 +412,8 @@
412
412
  "advanced.code_mode_max_invocations.help": "Лимит вызовов API и инструментов за один запуск. Нужен перезапуск.",
413
413
  "advanced.code_mode_saved_tools_path.label": "Путь к сохранённым инструментам",
414
414
  "advanced.code_mode_saved_tools_path.help": "JSON-файл для хранения пользовательских инструментов между перезапусками. Нужен перезапуск.",
415
+ "advanced.extra_yaml_write_keys.label": "Дополнительные YAML-ключи для записи",
416
+ "advanced.extra_yaml_write_keys.help": "Список верхнеуровневых ключей через запятую, которые ha_config_set_yaml может записывать вдобавок к встроенным – для YAML-ориентированных интеграций на этой установке (например, alert2). Ключи, меняющие саму границу доверия Home Assistant, добавить нельзя, они игнорируются. Требуется пользовательский компонент 1.2.4 или новее.",
415
417
  "advanced.sidecar_pin_port.label": "Порт sidecar интерфейса настроек",
416
418
  "advanced.sidecar_pin_port.help": "0 выбирает свободный порт при каждом запуске; 1024–65535 закрепляет желаемый порт. Нужен перезапуск.",
417
419
  "advanced.enable_dev_mode.label": "Режим разработчика",
@@ -2167,7 +2167,9 @@ function renderFeatureFlags(flags) {
2167
2167
  // bottom.
2168
2168
  if (fieldName === 'enable_code_mode') {
2169
2169
  const codeModeOn = !!f.value;
2170
- renderCodeModeSubRows(targetBody, masterOn, codeModeOn);
2170
+ renderAdvancedSubRows(
2171
+ targetBody, 'beta_codemode', 'codemode-sub', !masterOn || !codeModeOn
2172
+ );
2171
2173
  }
2172
2174
  // After rendering the enable_yaml_config_editing parent, inject
2173
2175
  // its 3 per-key sub-rows (automation/script/scene). Dimmed when
@@ -2179,6 +2181,11 @@ function renderFeatureFlags(flags) {
2179
2181
  cssClass: 'yaml-packages-sub',
2180
2182
  lockedByGate: !masterOn || !parentOn,
2181
2183
  });
2184
+ // Extra write keys (#1887) – same nesting depth as the per-key
2185
+ // toggles above, but a text value from the advanced cache.
2186
+ renderAdvancedSubRows(
2187
+ targetBody, 'beta_yamlkeys', 'yaml-packages-sub', !masterOn || !parentOn
2188
+ );
2182
2189
  }
2183
2190
  // After the enable_mandatory_bps parent row, inject its strict-mode
2184
2191
  // sub-row. This whole group is non-beta, so the only gate is the
@@ -2210,7 +2217,7 @@ function renderFeatureFlags(flags) {
2210
2217
  // master beta AND parent; mandatory-bps: parent only) and pass the CSS
2211
2218
  // class that carries the group's indent/guide-bar depth. The number/text
2212
2219
  // code-mode sub-numerics are NOT rendered here — they save through
2213
- // commitAdvancedEdit and live in renderCodeModeSubRows.
2220
+ // commitAdvancedEdit and live in renderAdvancedSubRows.
2214
2221
  function renderSubFlagRows(flags, parentEl, subFieldNames, { cssClass, lockedByGate }) {
2215
2222
  subFieldNames.forEach(fieldName => {
2216
2223
  const f = flags[fieldName];
@@ -2276,8 +2283,13 @@ function renderSubFlagRows(flags, parentEl, subFieldNames, { cssClass, lockedByG
2276
2283
  });
2277
2284
  }
2278
2285
 
2279
- function renderCodeModeSubRows(parentEl, masterOn, codeModeOn) {
2280
- const cmRows = (_advancedFields || []).filter(x => x.section === 'beta_codemode');
2286
+ // Shared renderer for non-bool sub-rows that live in ADVANCED_SETTINGS_FIELDS
2287
+ // but render nested under a feature toggle (code-mode numerics under
2288
+ // enable_code_mode, extra YAML write keys under enable_yaml_config_editing).
2289
+ // They save through commitAdvancedEdit rather than saveFeatureFlag, which is
2290
+ // why renderSubFlagRows (checkbox-only) cannot serve them.
2291
+ function renderAdvancedSubRows(parentEl, section, cssClass, lockedByGate) {
2292
+ const cmRows = (_advancedFields || []).filter(x => x.section === section);
2281
2293
  cmRows.forEach(f => {
2282
2294
  const meta = localizeMeta(
2283
2295
  'advanced',
@@ -2285,9 +2297,7 @@ function renderCodeModeSubRows(parentEl, masterOn, codeModeOn) {
2285
2297
  ADVANCED_FIELD_META[f.field] || { label: f.field, help: '' }
2286
2298
  );
2287
2299
  const row = document.createElement('div');
2288
- const lockedByGate = !masterOn || !codeModeOn;
2289
- const dimmed = lockedByGate;
2290
- row.className = 'feature-row codemode-sub' + (dimmed ? ' dimmed' : '');
2300
+ row.className = 'feature-row ' + cssClass + (lockedByGate ? ' dimmed' : '');
2291
2301
 
2292
2302
  const info = document.createElement('div');
2293
2303
  info.className = 'feature-info';
@@ -3390,6 +3400,7 @@ const ADVANCED_FIELD_META = {
3390
3400
  code_mode_max_recursion: { label: "Code-mode max recursion", help: "Recursion-depth cap per sandbox run. Restart required." },
3391
3401
  code_mode_max_invocations: { label: "Code-mode max invocations", help: "API/tool-call cap per sandbox run. Restart required." },
3392
3402
  code_mode_saved_tools_path:{ label: "Saved-tools path", help: "JSON file where ha_manage_custom_tool persists saved tools across restarts. Restart required." },
3403
+ extra_yaml_write_keys: { label: "Extra YAML write keys", help: "Comma-separated top-level keys ha_config_set_yaml may write in addition to the built-in ones, for YAML-first integrations on this install (e.g. alert2). Keys that redefine Home Assistant's own trust boundary can never be added and are ignored. Requires custom component 1.2.4 or newer." },
3393
3404
  sidecar_pin_port: { label: "Settings UI sidecar port", help: "0 = a new free port each restart (default); set 1024–65535 to pin a fixed port so the settings URL stays stable across restarts. Falls back to a free port if the pinned one is busy. Restart required." },
3394
3405
  enable_dev_mode: { label: "Developer mode", help: "⚠ DANGER: registers hidden developer tools (ha_dev_manage_server, ha_dev_manage_settings) that let AI agents change server settings and replace the running server version (e.g. install a PR build). For development and testing only. Restart required." },
3395
3406
  };
@@ -3493,10 +3504,10 @@ async function loadAdvancedSettings() {
3493
3504
  renderAdvancedSection('advSidecar', bySection.sidecar || []);
3494
3505
  renderAdvancedSection('advDeveloper', bySection.developer || []);
3495
3506
  applySidecarAvailability(data.is_stdio !== false);
3496
- // Re-render feature flags so the code_mode sub-numerics show up
3497
- // beneath enable_code_mode (race: loadFeatureFlags may have run
3498
- // before _advancedFields was populated). Cheap no-op if feature
3499
- // flags haven't loaded yet.
3507
+ // Re-render feature flags so the advanced-backed sub-rows (code_mode
3508
+ // numerics, extra YAML write keys) show up beneath their parent toggles
3509
+ // (race: loadFeatureFlags may have run before _advancedFields was
3510
+ // populated). Cheap no-op if feature flags haven't loaded yet.
3500
3511
  if (Object.keys(_lastFeatureFlags).length > 0) {
3501
3512
  renderFeatureFlags(_lastFeatureFlags);
3502
3513
  }
@@ -51,6 +51,11 @@ MCP_TOOLS_DOMAIN = "ha_mcp_tools"
51
51
  # subsequent call comes back unauthorized (covers token rotation).
52
52
  CALLER_TOKEN_FIELD = "_ha_mcp_token"
53
53
  CALLER_TOKEN_BOOTSTRAP_SERVICE = "get_caller_token"
54
+ # Component service that returns the operator's component-side extra YAML write
55
+ # keys (#1887). Ships in the same component version as the ``extra_allowed_keys``
56
+ # field, so it is read only when the component is new enough (see
57
+ # ``effective_extra_yaml_write_keys``).
58
+ GET_EXTRA_YAML_KEYS_SERVICE = "get_extra_yaml_keys"
54
59
 
55
60
  # Minimum version of the ha_mcp_tools custom component that this ha-mcp
56
61
  # release expects. Bumps in lockstep with ``manifest.json`` whenever a
@@ -108,6 +113,15 @@ def _version_tuple(version: str) -> tuple[int, ...]:
108
113
  # when a client is garbage-collected (avoids id() reuse if a freed client's
109
114
  # address gets recycled before the unauthorized-retry fires).
110
115
  _CALLER_TOKEN_CACHE: weakref.WeakKeyDictionary[Any, str] = weakref.WeakKeyDictionary()
116
+ # Component version as reported by the REST bootstrap, keyed like the token
117
+ # cache. Populated in ``_fetch_caller_token``, which already parses and
118
+ # validates it. Feature-scoped version gates read this rather than the
119
+ # WebSocket capability handshake: caps come back ``None`` for a transport
120
+ # blip just as they do for an old component, so gating on them would report
121
+ # a momentary socket drop as "your component is too old".
122
+ _COMPONENT_VERSION_CACHE: weakref.WeakKeyDictionary[Any, str] = (
123
+ weakref.WeakKeyDictionary()
124
+ )
111
125
  _CALLER_TOKEN_LOCKS: weakref.WeakKeyDictionary[Any, asyncio.Lock] = (
112
126
  weakref.WeakKeyDictionary()
113
127
  )
@@ -286,6 +300,7 @@ async def _fetch_caller_token(client: Any) -> str:
286
300
  if parsed < _version_tuple(MIN_COMPONENT_VERSION):
287
301
  _raise_component_too_old(f"reported version is {version}")
288
302
  _CALLER_TOKEN_CACHE[client] = token
303
+ _COMPONENT_VERSION_CACHE[client] = version
289
304
  return token
290
305
 
291
306
 
@@ -961,3 +976,130 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
961
976
 
962
977
  logger.info("Filesystem tools enabled via feature flag")
963
978
  register_tool_methods(mcp, FilesystemTools(client))
979
+
980
+
981
+ # First custom-component version whose ``edit_yaml_config`` schema accepts
982
+ # ``extra_allowed_keys`` (#1887).
983
+ MIN_COMPONENT_VERSION_EXTRA_YAML_KEYS = "1.2.4"
984
+
985
+
986
+ async def assert_extra_yaml_keys_supported(client: Any, extra_keys: list[str]) -> None:
987
+ """Block with an actionable prompt if the component predates #1887.
988
+
989
+ ``edit_yaml_config``'s service schema is strict, so sending
990
+ ``extra_allowed_keys`` to a component that does not declare the field makes
991
+ Home Assistant reject the whole call with an opaque "extra keys not
992
+ allowed". Callers only send the field when the operator configured keys,
993
+ and this turns the remaining server-ahead-of-component window into a clear
994
+ message. Both the write path and the backup restore path use it, so a
995
+ snapshot taken under the setting fails the same recognisable way.
996
+
997
+ Deliberately NOT a bump of ``MIN_COMPONENT_VERSION``: an operator who never
998
+ sets extra keys must not be forced to update the component to keep using
999
+ the filesystem tools.
1000
+
1001
+ The version comes from the REST bootstrap cache, which every call to this
1002
+ component populates and validates, so a WebSocket hiccup cannot be
1003
+ mistaken for an outdated component. An absent entry means the bootstrap
1004
+ has not run yet; the caller below performs it first, and a component too
1005
+ old to report a version at all is already rejected there.
1006
+ """
1007
+ if not extra_keys:
1008
+ return
1009
+
1010
+ def _current() -> tuple[int, ...] | None:
1011
+ reported = _COMPONENT_VERSION_CACHE.get(client, "")
1012
+ if not reported:
1013
+ return None
1014
+ try:
1015
+ return _version_tuple(reported)
1016
+ except ValueError:
1017
+ return None
1018
+
1019
+ floor = _version_tuple(MIN_COMPONENT_VERSION_EXTRA_YAML_KEYS)
1020
+ await _ensure_caller_token(client)
1021
+ parsed = _current()
1022
+ if parsed is None or parsed < floor:
1023
+ # The token cache is keyed by the long-lived REST client and survives
1024
+ # a Home Assistant restart, so a component updated after this process
1025
+ # first bootstrapped would keep reporting its old version forever and
1026
+ # the remediation this error prints ("update, then restart HA") would
1027
+ # never take effect. Re-bootstrap once before blocking, so the update
1028
+ # heals the gate on the next call rather than needing an ha-mcp
1029
+ # restart. Only on the failure path: a satisfied gate costs nothing.
1030
+ await _ensure_caller_token(client, force_refresh=True)
1031
+ parsed = _current()
1032
+ reported = _COMPONENT_VERSION_CACHE.get(client, "")
1033
+ if parsed is not None and parsed >= floor:
1034
+ return
1035
+ raise_tool_error(
1036
+ create_error_response(
1037
+ ErrorCode.COMPONENT_NOT_INSTALLED,
1038
+ "Extra YAML write keys are configured, but the installed "
1039
+ "ha_mcp_tools custom component does not support them "
1040
+ f"(reported version: {reported or 'unknown'}; requires >= "
1041
+ f"{MIN_COMPONENT_VERSION_EXTRA_YAML_KEYS}).",
1042
+ suggestions=[
1043
+ "HACS → Integrations → HA-MCP Custom Component → Update",
1044
+ "Restart Home Assistant after the update completes",
1045
+ # Parenthesised so this reads as one suggestion rather than
1046
+ # a list entry with a missing comma (py/implicit-string-
1047
+ # concatenation-in-list).
1048
+ (
1049
+ "Or clear the extra YAML write keys setting to write "
1050
+ "only the built-in allowed keys"
1051
+ ),
1052
+ ],
1053
+ )
1054
+ )
1055
+
1056
+
1057
+ async def effective_extra_yaml_write_keys(client: Any, settings: Any) -> list[str]:
1058
+ """Return the write allowlist extension in force: server setting + component.
1059
+
1060
+ The operator can set extra YAML write keys in two places (#1887): the ha-mcp
1061
+ server's own ``HA_MCP_EXTRA_YAML_KEYS`` and, via the integration UI, a store
1062
+ the component owns. A key set only on the component would otherwise be
1063
+ rejected by the server's own pre-dispatch allowlist before the write ever
1064
+ reaches the component, so the server reads that store here (via
1065
+ ``get_extra_yaml_keys``) and unions it with its own setting.
1066
+
1067
+ That service ships in the same component version as the ``extra_allowed_keys``
1068
+ field (``MIN_COMPONENT_VERSION_EXTRA_YAML_KEYS``), so it is read only when the
1069
+ bootstrap reports a new-enough component; on an older component, or any read
1070
+ failure, fall back to the server setting alone and let
1071
+ ``assert_extra_yaml_keys_supported`` turn a real mismatch into an actionable
1072
+ prompt. The store is read per write rather than cached: it changes at runtime
1073
+ from the options flow, and a stale cache is the failure mode #1887's own
1074
+ version-cache heal exists to avoid. YAML writes are rare and human-driven, so
1075
+ the extra round-trip is negligible.
1076
+ """
1077
+ from ..config import parse_extra_yaml_write_keys
1078
+
1079
+ keys: set[str] = set(parse_extra_yaml_write_keys(settings))
1080
+ try:
1081
+ await _ensure_caller_token(client)
1082
+ reported = _COMPONENT_VERSION_CACHE.get(client, "")
1083
+ floor = _version_tuple(MIN_COMPONENT_VERSION_EXTRA_YAML_KEYS)
1084
+ try:
1085
+ current = _version_tuple(reported) if reported else None
1086
+ except ValueError:
1087
+ current = None
1088
+ if current is not None and current >= floor:
1089
+ result = await call_mcp_tools_service(
1090
+ client, GET_EXTRA_YAML_KEYS_SERVICE, {}
1091
+ )
1092
+ inner = (
1093
+ unwrap_service_response(result) if isinstance(result, dict) else None
1094
+ )
1095
+ if isinstance(inner, dict) and inner.get("success"):
1096
+ stored = inner.get("keys", [])
1097
+ if isinstance(stored, list):
1098
+ keys.update(k for k in stored if isinstance(k, str) and k)
1099
+ except Exception:
1100
+ logger.debug(
1101
+ "Could not read the component extra-YAML-keys store; "
1102
+ "using the server setting alone.",
1103
+ exc_info=True,
1104
+ )
1105
+ return sorted(keys)
@@ -33,7 +33,9 @@ from .helpers import (
33
33
  from .tools_config_dashboards import fetch_dashboards_list
34
34
  from .tools_filesystem import (
35
35
  _assert_mcp_tools_available,
36
+ assert_extra_yaml_keys_supported,
36
37
  call_mcp_tools_service,
38
+ effective_extra_yaml_write_keys,
37
39
  )
38
40
  from .util_helpers import (
39
41
  attach_skill_content,
@@ -57,7 +59,7 @@ _LOVELACE_DASHBOARD_PREFIX = "lovelace.dashboards."
57
59
  # here, otherwise a key would silently be unreachable through the
58
60
  # wrapper. The parity invariant (keys == PACKAGES_ONLY_YAML_KEYS, and
59
61
  # every value is a real Settings field) is enforced by
60
- # test_yaml_config_tool.py::test_flag_map_matches_packages_only_keys.
62
+ # test_yaml_dashboards.py::test_flag_map_matches_packages_only_keys.
61
63
  _YAML_PACKAGES_FLAG_BY_KEY = {
62
64
  "automation": "enable_yaml_packages_automation",
63
65
  "script": "enable_yaml_packages_script",
@@ -464,6 +466,9 @@ class YamlConfigTools:
464
466
  # Check if custom component is available
465
467
  await _assert_mcp_tools_available(self._client)
466
468
 
469
+ extra_keys = await effective_extra_yaml_write_keys(self._client, settings)
470
+ await assert_extra_yaml_keys_supported(self._client, extra_keys)
471
+
467
472
  # Build service data
468
473
  service_data: dict[str, Any] = {
469
474
  "file": file,
@@ -472,6 +477,11 @@ class YamlConfigTools:
472
477
  "disabled_packages_keys": disabled_keys,
473
478
  "require_confirm": settings.enable_yaml_edit_confirm,
474
479
  }
480
+ # Only sent when the operator actually configured extra keys, so a
481
+ # component that predates the field keeps working unchanged for
482
+ # everyone else (its schema is strict and would reject the call).
483
+ if extra_keys:
484
+ service_data["extra_allowed_keys"] = extra_keys
475
485
  if content is not None:
476
486
  service_data["content"] = content
477
487
  if confirm_token is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.14.2.dev2103
3
+ Version: 7.14.2.dev2107
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