ha-mcp-dev 7.12.3.dev1942__tar.gz → 7.12.3.dev1946__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 (177) hide show
  1. {ha_mcp_dev-7.12.3.dev1942/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1946}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/config.py +31 -0
  4. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/_handlers_backups.py +1 -0
  5. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/settings.js +9 -0
  6. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/backup.py +349 -13
  7. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  8. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/LICENSE +0 -0
  9. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/MANIFEST.in +0 -0
  10. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/README.md +0 -0
  11. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/setup.cfg +0 -0
  12. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/__init__.py +0 -0
  13. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/__main__.py +0 -0
  14. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/_pypi_marker +0 -0
  15. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/_version.py +0 -0
  16. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/auth/__init__.py +0 -0
  17. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/auth/consent_form.py +0 -0
  18. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/auth/provider.py +0 -0
  19. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/backup_manager.py +0 -0
  20. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/browser_landing.py +0 -0
  21. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/client/__init__.py +0 -0
  22. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/client/rest_client.py +0 -0
  23. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/client/supervisor_client.py +0 -0
  24. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/client/websocket_client.py +0 -0
  25. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/client/websocket_listener.py +0 -0
  26. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  27. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  28. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  29. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  30. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  31. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/errors.py +0 -0
  32. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/llm_exposure.py +0 -0
  33. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/policy/__init__.py +0 -0
  34. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/policy/approval_queue.py +0 -0
  35. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/policy/evaluator.py +0 -0
  36. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/policy/handlers.py +0 -0
  37. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/policy/middleware.py +0 -0
  38. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/policy/model.py +0 -0
  39. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/policy/persistence.py +0 -0
  40. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/policy/value_sources.py +0 -0
  41. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/py.typed +0 -0
  42. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/read_only.py +0 -0
  43. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  44. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  45. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  46. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  47. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  48. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  49. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  50. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  51. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  52. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  53. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  54. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  55. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  56. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  57. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  58. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  59. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  60. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  61. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  62. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  63. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  64. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  65. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  66. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  67. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  68. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/server.py +0 -0
  69. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/__init__.py +0 -0
  70. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  71. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  72. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  73. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  74. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  75. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  76. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  77. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/_theme.py +0 -0
  78. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  79. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/settings.css +0 -0
  80. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/settings_ui/settings.html +0 -0
  81. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/smoke_test.py +0 -0
  82. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  83. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/strict_bps.py +0 -0
  84. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/__init__.py +0 -0
  85. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/auto_backup.py +0 -0
  86. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  87. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/component_api.py +0 -0
  88. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  89. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/device_control.py +0 -0
  90. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/enhanced.py +0 -0
  91. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/helpers.py +0 -0
  92. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/radio/__init__.py +0 -0
  93. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/radio/base.py +0 -0
  94. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/radio/matter.py +0 -0
  95. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/radio/thread.py +0 -0
  96. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  97. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/radio/zwave.py +0 -0
  98. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/reference_validator.py +0 -0
  99. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/registry.py +0 -0
  100. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  101. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  102. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  103. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  104. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  105. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  106. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  107. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  108. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  109. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  110. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_addons.py +0 -0
  111. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_areas.py +0 -0
  112. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  113. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  114. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_calendar.py +0 -0
  115. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_camera.py +0 -0
  116. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_categories.py +0 -0
  117. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_code.py +0 -0
  118. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  119. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  120. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  121. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  122. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  123. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  124. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_dev.py +0 -0
  125. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_energy.py +0 -0
  126. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_entities.py +0 -0
  127. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  128. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_groups.py +0 -0
  129. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_hacs.py +0 -0
  130. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_history.py +0 -0
  131. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_integrations.py +0 -0
  132. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_labels.py +0 -0
  133. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_radio.py +0 -0
  134. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_registry.py +0 -0
  135. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_resources.py +0 -0
  136. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_search.py +0 -0
  137. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_service.py +0 -0
  138. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_services.py +0 -0
  139. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_system.py +0 -0
  140. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_themes.py +0 -0
  141. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_todo.py +0 -0
  142. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_traces.py +0 -0
  143. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_updates.py +0 -0
  144. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_utility.py +0 -0
  145. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  146. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  147. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  148. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/tools_zones.py +0 -0
  149. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/util_helpers.py +0 -0
  150. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/tools/validation_middleware.py +0 -0
  151. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/transforms/__init__.py +0 -0
  152. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/transforms/categorized_search.py +0 -0
  153. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  154. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/transport_security.py +0 -0
  155. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/update_check.py +0 -0
  156. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/utils/__init__.py +0 -0
  157. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/utils/config_hash.py +0 -0
  158. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/utils/data_paths.py +0 -0
  159. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/utils/domain_handlers.py +0 -0
  160. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  161. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  162. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/utils/operation_manager.py +0 -0
  163. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/utils/python_sandbox.py +0 -0
  164. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/utils/skill_loader.py +0 -0
  165. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/utils/usage_logger.py +0 -0
  166. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/visibility/__init__.py +0 -0
  167. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/visibility/model.py +0 -0
  168. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/visibility/persistence.py +0 -0
  169. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp/visibility/resolver.py +0 -0
  170. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  171. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  172. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  173. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  174. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  175. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/tests/__init__.py +0 -0
  176. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/tests/test_constants.py +0 -0
  177. {ha_mcp_dev-7.12.3.dev1942 → ha_mcp_dev-7.12.3.dev1946}/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.12.3.dev1942
3
+ Version: 7.12.3.dev1946
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.12.3.dev1942"
7
+ version = "7.12.3.dev1946"
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
@@ -347,6 +347,26 @@ class Settings(BaseSettings):
347
347
  7, ge=1, le=365, alias="HAMCP_AUTO_BACKUP_CALENDAR_LOOKAHEAD_DAYS"
348
348
  )
349
349
 
350
+ # Snapshot-tarball deletion gate (#1861). Off by default: an agent
351
+ # deleting a full HA snapshot is categorically riskier than the
352
+ # lightweight `edits`-scope auto-backups (which already delete freely),
353
+ # since a snapshot may be the last recovery point after the agent
354
+ # itself broke something. A human must opt in via env var, the web
355
+ # settings UI override file, or (in the add-on) the Supervisor options
356
+ # — never something the agent can flip on itself.
357
+ enable_snapshot_delete: bool = Field(False, alias="ENABLE_SNAPSHOT_DELETE")
358
+
359
+ # Minimum age (days) a snapshot must have before it's deletable. This is
360
+ # the load-bearing guard, not `enable_snapshot_delete`: a count-based
361
+ # "keep the last N" rule is defeatable by an agent flooding new
362
+ # snapshots before deleting old ones, but it cannot forge a backup's
363
+ # HA-stamped creation date. 0 disables the age floor (still gated by
364
+ # enable_snapshot_delete + the newest-snapshot / automatic-backup
365
+ # guards enforced in tools/backup.py).
366
+ snapshot_delete_min_age_days: int = Field(
367
+ 7, ge=0, le=365, alias="SNAPSHOT_DELETE_MIN_AGE_DAYS"
368
+ )
369
+
350
370
  # Mirror the legacy ``os.getenv("FLAG", "").lower() in ("true", ...)``
351
371
  # semantics for the ex-direct-getenv ``enable_filesystem_tools`` flag (and
352
372
  # its sibling toggles listed above): an empty env var value MUST be treated
@@ -1486,6 +1506,10 @@ BACKUP_OVERRIDE_FIELDS: tuple[BackupOverrideField, ...] = (
1486
1506
  "HAMCP_AUTO_BACKUP_CALENDAR_LOOKAHEAD_DAYS",
1487
1507
  int,
1488
1508
  ),
1509
+ BackupOverrideField("enable_snapshot_delete", "ENABLE_SNAPSHOT_DELETE", bool),
1510
+ BackupOverrideField(
1511
+ "snapshot_delete_min_age_days", "SNAPSHOT_DELETE_MIN_AGE_DAYS", int
1512
+ ),
1489
1513
  )
1490
1514
 
1491
1515
  # Override-file location is the same data dir that holds tool_config.json
@@ -1640,6 +1664,13 @@ def _coerce_backup_int_value(field_name: str, raw: object) -> tuple[bool, Any]:
1640
1664
  coerced,
1641
1665
  )
1642
1666
  return False, None
1667
+ if field_name == "snapshot_delete_min_age_days" and not 0 <= coerced <= 365:
1668
+ logger.warning(
1669
+ "backup_settings.json: snapshot_delete_min_age_days=%d out of "
1670
+ "range 0..365; ignoring",
1671
+ coerced,
1672
+ )
1673
+ return False, None
1643
1674
  return True, coerced
1644
1675
 
1645
1676
 
@@ -278,6 +278,7 @@ _BACKUP_INT_BOUNDS: dict[str, tuple[int, int]] = {
278
278
  "auto_backup_throttle_minutes": (0, 1440),
279
279
  "auto_backup_retain_per_entity": (1, 10_000),
280
280
  "auto_backup_calendar_lookahead_days": (1, 365),
281
+ "snapshot_delete_min_age_days": (0, 365),
281
282
  }
282
283
 
283
284
 
@@ -1053,6 +1053,14 @@ const BACKUP_FIELD_LABELS = {
1053
1053
  label: 'Calendar lookahead (days)',
1054
1054
  help: 'How far ahead to query for calendar events when capturing pre-edit snapshots. Range 1–365.',
1055
1055
  },
1056
+ enable_snapshot_delete: {
1057
+ label: 'Allow snapshot deletion',
1058
+ help: 'Lets ha_manage_backup delete full HA snapshot tarballs. Off by default: a snapshot may be the last recovery point after a mistaken change. Even when on, scheduled backups, the newest remaining snapshot, and anything younger than the age floor below stay protected.',
1059
+ },
1060
+ snapshot_delete_min_age_days: {
1061
+ label: 'Minimum snapshot age to delete (days)',
1062
+ help: 'A snapshot must be at least this old before it can be deleted. Range 0–365; 0 disables the floor (the newest-snapshot and scheduled-backup protections still apply).',
1063
+ },
1056
1064
  };
1057
1065
 
1058
1066
  const BACKUP_ORIGIN_LABELS = {
@@ -1104,6 +1112,7 @@ function renderBackupConfig() {
1104
1112
  let max = 10000;
1105
1113
  if (f.field === 'auto_backup_throttle_minutes') { min = 0; max = 1440; }
1106
1114
  else if (f.field === 'auto_backup_calendar_lookahead_days') { min = 1; max = 365; }
1115
+ else if (f.field === 'snapshot_delete_min_age_days') { min = 0; max = 365; }
1107
1116
  controlHtml = `<input type="number" name="backup:${escapeHtml(f.field)}" data-field="${escapeHtml(f.field)}" aria-labelledby="label-backup-${escapeHtml(f.field)}" value="${Number(f.value)}" min="${min}" max="${max}" ${f.editable ? '' : 'disabled'}>`;
1108
1117
  }
1109
1118
  let originMsg;
@@ -19,8 +19,9 @@ HA restore path. Each call must explicitly pick its scope.
19
19
  import asyncio
20
20
  import logging
21
21
  from datetime import UTC, datetime, timedelta
22
- from typing import TYPE_CHECKING, Annotated, Any, Literal, cast
22
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, NoReturn, cast
23
23
 
24
+ from fastmcp import Context
24
25
  from fastmcp.exceptions import ToolError
25
26
  from pydantic import Field
26
27
 
@@ -38,6 +39,7 @@ from .helpers import (
38
39
  get_connected_ws_client,
39
40
  log_tool_usage,
40
41
  raise_tool_error,
42
+ safe_progress,
41
43
  )
42
44
 
43
45
  if TYPE_CHECKING:
@@ -45,12 +47,26 @@ if TYPE_CHECKING:
45
47
 
46
48
  logger = logging.getLogger(__name__)
47
49
 
48
- # Default poll window for full HA backups. The 120s prior default underfit
49
- # slow HA instances (#1433: poll loop exited while HA was still in
50
+ # Poll window for full HA backups. HA's own frontend imposes no timeout at
51
+ # all on backup creation — it subscribes to `backup/subscribe_events` and
52
+ # waits however long the job takes. Our poll-based design still needs a
53
+ # finite bound per tool call, so this mirrors _SAFETY_BACKUP_MAX_WAIT_S (the
54
+ # codebase's existing "generous but bounded" ceiling) rather than inventing a
55
+ # third number.
56
+ #
57
+ # Two distinct problems, both fixed in the same PR, by different mechanisms:
58
+ # the client-side abort reported in #1861 (an MCP client that received no
59
+ # progress notifications for 300s concluded the connection was dead) is
60
+ # fixed by the ctx.report_progress heartbeats in _poll_backup_completion,
61
+ # not by this constant. Raising it from 300 to 1800 instead fixes *our own*
62
+ # poll loop giving up on a legitimately >5-minute backup — the job keeps
63
+ # running server-side after our loop times out, so a too-tight ceiling here
64
+ # means _poll_backup_completion raises TIMEOUT_OPERATION on a backup that
65
+ # would otherwise have succeeded. 120s before that underfit slow HA
66
+ # instances too (#1433: poll loop exited while HA was still in
50
67
  # state="create_backup", the wrapper treated that as failure, retried, and
51
- # produced duplicate backups). 300s covers the long tail without making the
52
- # happy-path wait noticeable.
53
- _BACKUP_MAX_WAIT_S = 300
68
+ # produced duplicate backups).
69
+ _BACKUP_MAX_WAIT_S = 1800
54
70
  _BACKUP_POLL_INTERVAL_S = 2
55
71
  # The pre-restore safety backup is a *full* backup (include_database=True, plus
56
72
  # all add-ons on Supervised), unlike the fast create_backup path. A multi-GB
@@ -60,6 +76,19 @@ _BACKUP_POLL_INTERVAL_S = 2
60
76
  _SAFETY_BACKUP_MAX_WAIT_S = 1800
61
77
  # Clock-skew tolerance when filtering backup entries by date vs job-start.
62
78
  _BACKUP_DATE_FILTER_TOLERANCE_S = 5
79
+ # Minimum spacing between in-loop MCP progress heartbeats during a backup
80
+ # poll (#1861: with no progress notifications at all, a client can decide
81
+ # the connection is dead and abort mid-backup — "sent no response or
82
+ # progress for 300s"). Independent of poll_interval so a tight poll loop
83
+ # doesn't flood the client with redundant notifications.
84
+ _BACKUP_PROGRESS_INTERVAL_S = 10
85
+ # backup/delete fans out to every configured agent (including cloud/remote
86
+ # ones — S3, Google Drive, WebDAV, etc.) via asyncio.gather server-side, and
87
+ # the WS response doesn't arrive until all of them finish. The client's
88
+ # default 30s wait (HomeAssistantWebSocketClient.send_command's
89
+ # _wait_timeout) can be too tight for a slow or rate-limited remote agent —
90
+ # same class of false-timeout problem this file already fixes for creation.
91
+ _BACKUP_DELETE_WAIT_S = 60.0
63
92
 
64
93
 
65
94
  def _get_backup_hint_text() -> str:
@@ -288,6 +317,7 @@ async def _poll_backup_completion(
288
317
  max_wait_seconds: int,
289
318
  poll_interval: int,
290
319
  agent_id: str,
320
+ ctx: Context | None = None,
291
321
  ) -> dict[str, Any]:
292
322
  """Poll backup/info until the named backup completes, fails, or times out.
293
323
 
@@ -304,14 +334,34 @@ async def _poll_backup_completion(
304
334
  creation in progress, surfaces ``likely_in_progress=true`` in the error
305
335
  context so callers can back off retries instead of compounding duplicates.
306
336
 
337
+ ``ctx``, when supplied, receives a ``report_progress`` heartbeat before
338
+ the first poll and at least every ``_BACKUP_PROGRESS_INTERVAL_S`` while
339
+ waiting, so an MCP client watching for "no response or progress" doesn't
340
+ abort the connection mid-backup (#1861).
341
+
307
342
  Raises ToolError on backup failure or final timeout with no completion.
308
343
  """
309
344
  job_start_ts = datetime.now(UTC)
310
345
  waited = 0
346
+ next_progress_at = _BACKUP_PROGRESS_INTERVAL_S
347
+ await safe_progress(
348
+ ctx,
349
+ progress=0,
350
+ total=max_wait_seconds,
351
+ message="waiting for backup to complete",
352
+ )
311
353
 
312
354
  while waited < max_wait_seconds:
313
355
  await asyncio.sleep(poll_interval)
314
356
  waited += poll_interval
357
+ if waited >= next_progress_at:
358
+ await safe_progress(
359
+ ctx,
360
+ progress=waited,
361
+ total=max_wait_seconds,
362
+ message=f"waiting for backup to complete ({waited}s elapsed)",
363
+ )
364
+ next_progress_at = waited + _BACKUP_PROGRESS_INTERVAL_S
315
365
 
316
366
  info_result = await ws_client.send_command("backup/info")
317
367
  if not info_result.get("success"):
@@ -448,7 +498,9 @@ async def _poll_backup_completion(
448
498
 
449
499
 
450
500
  async def create_backup(
451
- client: HomeAssistantClient, name: str | None = None
501
+ client: HomeAssistantClient,
502
+ name: str | None = None,
503
+ ctx: Context | None = None,
452
504
  ) -> dict[str, Any]:
453
505
  """
454
506
  Create a fast Home Assistant backup (local only, excludes database).
@@ -456,6 +508,7 @@ async def create_backup(
456
508
  Args:
457
509
  client: Home Assistant REST client
458
510
  name: Optional backup name (auto-generated if not provided)
511
+ ctx: Optional FastMCP context for progress heartbeats during the wait
459
512
 
460
513
  Returns:
461
514
  Dictionary with backup result including backup_id, status, duration, etc.
@@ -528,6 +581,7 @@ async def create_backup(
528
581
  max_wait_seconds=_BACKUP_MAX_WAIT_S,
529
582
  poll_interval=_BACKUP_POLL_INTERVAL_S,
530
583
  agent_id=local_agent,
584
+ ctx=ctx,
531
585
  )
532
586
 
533
587
  except ToolError:
@@ -558,6 +612,7 @@ async def _create_safety_backup(
558
612
  ws_client: HomeAssistantWebSocketClient,
559
613
  password: str | None,
560
614
  agent_id: str,
615
+ ctx: Context | None = None,
561
616
  ) -> tuple[str | None, list[str]]:
562
617
  """Create a pre-restore safety backup and wait for it to complete.
563
618
 
@@ -620,6 +675,7 @@ async def _create_safety_backup(
620
675
  max_wait_seconds=_SAFETY_BACKUP_MAX_WAIT_S,
621
676
  poll_interval=_BACKUP_POLL_INTERVAL_S,
622
677
  agent_id=agent_id,
678
+ ctx=ctx,
623
679
  )
624
680
  logger.info(f"Safety backup completed: {safety_backup_id}")
625
681
  return cast(str, safety_backup_id), poll_result.get("warnings", [])
@@ -645,7 +701,10 @@ def _backup_protected(entry: dict[str, Any]) -> bool | None:
645
701
 
646
702
 
647
703
  async def restore_backup(
648
- client: HomeAssistantClient, backup_id: str, restore_database: bool = False
704
+ client: HomeAssistantClient,
705
+ backup_id: str,
706
+ restore_database: bool = False,
707
+ ctx: Context | None = None,
649
708
  ) -> dict[str, Any]:
650
709
  """
651
710
  Restore Home Assistant from a backup (DESTRUCTIVE - use with caution).
@@ -661,6 +720,8 @@ async def restore_backup(
661
720
  match when restoring Home Assistant; a warning is returned if the
662
721
  value is overridden. On Core installs the caller's value is honoured
663
722
  as-is (Core enforces no such match).
723
+ ctx: Optional FastMCP context for progress heartbeats during the
724
+ pre-restore safety-backup wait
664
725
 
665
726
  Returns:
666
727
  Dictionary with restore result including safety_backup_id, status, etc.
@@ -722,7 +783,7 @@ async def restore_backup(
722
783
  password = None
723
784
 
724
785
  safety_backup_id, safety_warnings = await _create_safety_backup(
725
- ws_client, password, local_agent
786
+ ws_client, password, local_agent, ctx=ctx
726
787
  )
727
788
 
728
789
  # `backup/info` returns ManagerBackup entries: `database_included` is a
@@ -950,12 +1011,263 @@ async def list_backups(client: HomeAssistantClient, limit: int = 200) -> dict[st
950
1011
  return None # unreachable: exception_to_structured_error always raises
951
1012
 
952
1013
 
1014
+ def _refuse_snapshot_delete(message: str, **context: Any) -> NoReturn:
1015
+ """Raise the shared VALIDATION_INVALID_PARAMETER shape for every
1016
+ snapshot-delete guard rejection (gate, confirm, automatic-settings, age
1017
+ floor, newest-snapshot protection)."""
1018
+ raise_tool_error(
1019
+ create_error_response(
1020
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
1021
+ message,
1022
+ context=context,
1023
+ )
1024
+ )
1025
+
1026
+
1027
+ def _newest_backup_id(backups: list[dict[str, Any]]) -> str | None:
1028
+ """Return the ``backup_id`` of the single most-recent entry by date, or
1029
+ None if no entry has a parseable date.
1030
+
1031
+ Entries with a missing/malformed date are excluded from consideration —
1032
+ they can never be "the newest" — but that does not make them safe to
1033
+ delete; ``delete_backup`` fails closed on an unparseable target date
1034
+ separately, so an undated target can never be deleted regardless of
1035
+ this function's result.
1036
+ """
1037
+ dated = [
1038
+ (b.get("backup_id"), parsed)
1039
+ for b in backups
1040
+ if (parsed := _parse_backup_date(b.get("date"))) is not None
1041
+ ]
1042
+ if not dated:
1043
+ return None
1044
+ return max(dated, key=lambda pair: pair[1])[0]
1045
+
1046
+
1047
+ async def delete_backup(
1048
+ client: HomeAssistantClient,
1049
+ backup_id: str,
1050
+ confirm: bool = False,
1051
+ ctx: Context | None = None,
1052
+ ) -> dict[str, Any]:
1053
+ """Delete one full HA snapshot tarball (#1861).
1054
+
1055
+ Off by default (``enable_snapshot_delete``) — a human must opt in via
1056
+ env var, the web settings UI override file, or (in the add-on) the
1057
+ Supervisor options; an agent cannot enable this itself. Layered guards
1058
+ beyond the gate, applied in order:
1059
+
1060
+ 1. ``confirm=True`` is required.
1061
+ 2. ``backup/info`` must not report ``agent_errors`` — evaluated before
1062
+ the "does it exist" check right below, since an agent that failed
1063
+ to enumerate its backups could make that list (and every guard
1064
+ below) incomplete. The backup must then exist (verified against
1065
+ ``backup/info``, mirroring ``restore_backup`` — HA's own
1066
+ ``backup/delete`` silently no-ops on an unknown ID rather than
1067
+ erroring).
1068
+ 3. Scheduled backups (``with_automatic_settings=True``) are never
1069
+ deletable. Fails closed the same way when HA can't confirm the
1070
+ backup is manual (``with_automatic_settings=None`` — not created
1071
+ by this HA instance, or predates this metadata field): only an
1072
+ explicit ``False`` is treated as deletable.
1073
+ 4. The single newest remaining snapshot, of any type, is never
1074
+ deletable — guarantees at least one recovery point always survives.
1075
+ Checked before the age floor below so a backup that is both the
1076
+ newest AND too young reports "newest" (with its own remedy), not
1077
+ a "too young" message whose suggested remedy (lower the floor)
1078
+ would not actually unblock it.
1079
+ 5. The target must be older than ``snapshot_delete_min_age_days`` (0
1080
+ unconditionally disables this floor, even across clock skew between
1081
+ this host and the HA instance that stamped the backup's date). A
1082
+ missing/unparseable date fails closed (treated as too new to delete).
1083
+
1084
+ The actual ``backup/delete`` call uses an elevated WS wait timeout
1085
+ (``_BACKUP_DELETE_WAIT_S``) since it fans out to every configured agent
1086
+ — including slow/rate-limited remote ones — server-side. ``ctx``, when
1087
+ supplied, receives one ``report_progress`` heartbeat immediately before
1088
+ that call.
1089
+
1090
+ Raises ToolError if disabled, unconfirmed, not found, blocked by a
1091
+ guard, or if HA reports a per-agent deletion failure.
1092
+ """
1093
+ settings = get_global_settings()
1094
+ if not settings.enable_snapshot_delete:
1095
+ _refuse_snapshot_delete(
1096
+ "Snapshot deletion is disabled on this server "
1097
+ "(enable_snapshot_delete=false). A human must enable it via the "
1098
+ "ENABLE_SNAPSHOT_DELETE env var, the web settings UI, or (in "
1099
+ "the add-on) the Supervisor options — this cannot be turned on "
1100
+ "from a tool call.",
1101
+ )
1102
+ if not confirm:
1103
+ _refuse_snapshot_delete(
1104
+ "Deletion not confirmed. Set confirm=True to delete a snapshot.",
1105
+ backup_id=backup_id,
1106
+ )
1107
+
1108
+ ws_client = None
1109
+ try:
1110
+ ws_client, error = await get_connected_ws_client(
1111
+ client.base_url, client.token, verify_ssl=client.verify_ssl
1112
+ )
1113
+ if error:
1114
+ raise_tool_error(
1115
+ error
1116
+ or create_error_response(
1117
+ ErrorCode.CONNECTION_FAILED,
1118
+ "Failed to connect to Home Assistant WebSocket for backup deletion",
1119
+ )
1120
+ )
1121
+ ws_client = cast(HomeAssistantWebSocketClient, ws_client)
1122
+
1123
+ info_result = await ws_client.send_command("backup/info")
1124
+ if not info_result.get("success"):
1125
+ raise_tool_error(
1126
+ create_error_response(
1127
+ ErrorCode.SERVICE_CALL_FAILED,
1128
+ info_result.get("error", "Failed to retrieve backup information"),
1129
+ )
1130
+ )
1131
+ info_result_block = info_result.get("result") or {}
1132
+ info_agent_errors = info_result_block.get("agent_errors") or {}
1133
+ if info_agent_errors:
1134
+ raise_tool_error(
1135
+ create_error_response(
1136
+ ErrorCode.SERVICE_CALL_FAILED,
1137
+ "Cannot verify the backup inventory is complete: one or "
1138
+ "more backup agents failed to respond, so the newest-"
1139
+ "snapshot and scheduled-backup guards can't be trusted "
1140
+ "against a possibly-partial list.",
1141
+ context={"backup_id": backup_id, "agent_errors": info_agent_errors},
1142
+ )
1143
+ )
1144
+ backups = info_result_block.get("backups") or []
1145
+ matched = next((b for b in backups if b.get("backup_id") == backup_id), None)
1146
+ if matched is None:
1147
+ raise_tool_error(
1148
+ create_error_response(
1149
+ ErrorCode.RESOURCE_NOT_FOUND,
1150
+ f"Backup '{backup_id}' not found",
1151
+ suggestions=[
1152
+ "Use ha_manage_backup(scope='snapshot', action='list') "
1153
+ "to see available backup IDs",
1154
+ ],
1155
+ )
1156
+ )
1157
+
1158
+ if matched.get("with_automatic_settings") is not False:
1159
+ _refuse_snapshot_delete(
1160
+ "Refusing to delete a scheduled (automatic) backup, or one "
1161
+ "whose automatic/manual origin Home Assistant cannot "
1162
+ "confirm (with_automatic_settings=None — not created by "
1163
+ "this HA instance, or predates this metadata) — treated "
1164
+ "as not provably manual, and therefore not deletable.",
1165
+ backup_id=backup_id,
1166
+ )
1167
+
1168
+ # Checked before the age floor below: a backup that is BOTH the
1169
+ # newest AND too young must report "newest" (create a new one
1170
+ # first), not "too young" — the age-floor message's remedy
1171
+ # ("lower snapshot_delete_min_age_days") would be actively
1172
+ # misleading advice for the newest snapshot, which stays
1173
+ # protected at any age floor including 0.
1174
+ newest_id = _newest_backup_id(backups)
1175
+ if newest_id == backup_id:
1176
+ _refuse_snapshot_delete(
1177
+ "Refusing to delete the newest snapshot — at least one "
1178
+ "recovery point must remain. Create a new snapshot first "
1179
+ "if you specifically need to free this one's space.",
1180
+ backup_id=backup_id,
1181
+ )
1182
+
1183
+ min_age_days = settings.snapshot_delete_min_age_days
1184
+ target_date = _parse_backup_date(matched.get("date"))
1185
+ if target_date is None:
1186
+ _refuse_snapshot_delete(
1187
+ "Refusing to delete a backup with a missing or unparseable "
1188
+ "date — cannot verify it clears the minimum-age floor.",
1189
+ backup_id=backup_id,
1190
+ )
1191
+ cutoff = datetime.now(UTC) - timedelta(days=min_age_days)
1192
+ # min_age_days=0 must unconditionally disable the floor, even if
1193
+ # target_date is slightly ahead of this host's clock (clock skew
1194
+ # vs the HA instance that stamped it) — guard explicitly rather
1195
+ # than relying on the arithmetic reduction, which clock skew breaks.
1196
+ if min_age_days > 0 and target_date > cutoff:
1197
+ _refuse_snapshot_delete(
1198
+ f"Refusing to delete a backup younger than "
1199
+ f"snapshot_delete_min_age_days={min_age_days} days — at "
1200
+ "least one recent recovery point must remain. Delete an "
1201
+ "older backup, or lower snapshot_delete_min_age_days if "
1202
+ "this recurs.",
1203
+ backup_id=backup_id,
1204
+ )
1205
+
1206
+ await safe_progress(
1207
+ ctx,
1208
+ progress=0,
1209
+ total=_BACKUP_DELETE_WAIT_S,
1210
+ message="deleting backup",
1211
+ )
1212
+ delete_result = await ws_client.send_command(
1213
+ "backup/delete",
1214
+ backup_id=backup_id,
1215
+ _wait_timeout=_BACKUP_DELETE_WAIT_S,
1216
+ )
1217
+ if not delete_result.get("success"):
1218
+ raise_tool_error(
1219
+ create_error_response(
1220
+ ErrorCode.SERVICE_CALL_FAILED,
1221
+ delete_result.get("error", "Backup deletion failed"),
1222
+ context={"backup_id": backup_id},
1223
+ )
1224
+ )
1225
+ agent_errors = (delete_result.get("result") or {}).get("agent_errors") or {}
1226
+ if agent_errors:
1227
+ raise_tool_error(
1228
+ create_error_response(
1229
+ ErrorCode.SERVICE_CALL_FAILED,
1230
+ "Backup deletion failed on one or more agents",
1231
+ context={"backup_id": backup_id, "agent_errors": agent_errors},
1232
+ )
1233
+ )
1234
+
1235
+ return {
1236
+ "success": True,
1237
+ "backup_id": backup_id,
1238
+ "name": matched.get("name"),
1239
+ "status": "Backup deleted successfully",
1240
+ }
1241
+
1242
+ except ToolError:
1243
+ raise
1244
+ except Exception as e:
1245
+ logger.error(f"Error deleting backup: {e}")
1246
+ exception_to_structured_error(
1247
+ e,
1248
+ context={"tool": "delete_backup", "backup_id": backup_id},
1249
+ suggestions=["Check Home Assistant connection and backup availability"],
1250
+ )
1251
+ return None # unreachable: exception_to_structured_error always raises
1252
+ finally:
1253
+ if ws_client:
1254
+ try:
1255
+ await ws_client.disconnect()
1256
+ except (TimeoutError, OSError, ConnectionError) as err:
1257
+ logger.debug(
1258
+ "ws disconnect (cleanup) transport error: %s: %s",
1259
+ type(err).__name__,
1260
+ err,
1261
+ )
1262
+
1263
+
953
1264
  # Valid (scope, action) combinations. Anything outside this set is
954
1265
  # rejected with a structured VALIDATION_INVALID_PARAMETER error.
955
1266
  _VALID_COMBOS: set[tuple[str, str]] = {
956
1267
  ("snapshot", "create"),
957
1268
  ("snapshot", "list"),
958
1269
  ("snapshot", "restore"),
1270
+ ("snapshot", "delete"),
959
1271
  ("edits", "create"),
960
1272
  ("edits", "list"),
961
1273
  ("edits", "view"),
@@ -1019,9 +1331,10 @@ def register_backup_tools(
1019
1331
 
1020
1332
  | scope | action | What it does |
1021
1333
  |---|---|---|
1022
- | `snapshot` | `create` | Create a full HA tarball (config + addons, no DB by default). Heavy, seconds-long. |
1334
+ | `snapshot` | `create` | Create a full HA tarball (config + addons, no DB by default). Can take a while on a large instance; progress heartbeats are sent while waiting. |
1023
1335
  | `snapshot` | `list` | List full HA tarball snapshots (id, name, date, size). Read-only — use to discover a `backup_id` or confirm a backup landed. |
1024
1336
  | `snapshot` | `restore` | Restore a full HA tarball. **Restarts HA.** Last-resort recovery. |
1337
+ | `snapshot` | `delete` | Delete one full HA tarball by `backup_id` (`confirm=True` required). **Disabled by default** (`enable_snapshot_delete` setting) and layered with guards even when enabled — see below. |
1025
1338
  | `edits` | `create` | On-demand snapshot of one entity (`domain` + `entity_id` required). Use before the user manually edits in the HA UI. Same handler path the decorator takes on writes; bypasses the `enable_auto_backup` toggle. |
1026
1339
  | `edits` | `list` | List per-entity auto-backups (lightweight). Filter by `domain` and/or `entity_id`. |
1027
1340
  | `edits` | `view` | Read one auto-backup file by name; returns YAML and parsed `config`. |
@@ -1036,12 +1349,21 @@ def register_backup_tools(
1036
1349
  **`scope="snapshot"` backup-hint:**
1037
1350
  {backup_hint_text}
1038
1351
 
1352
+ **`(snapshot, delete)` is off by default and layered even when enabled:** a human must
1353
+ set `enable_snapshot_delete=true` (env var, web settings UI, or add-on Supervisor
1354
+ options) — an agent cannot turn this on itself. When enabled, a delete call is still
1355
+ refused if: the target is a scheduled/automatic backup; it's younger than
1356
+ `snapshot_delete_min_age_days` (default 7, 0 disables the floor); or it's the single
1357
+ newest snapshot remaining. These guarantee at least one recovery point always
1358
+ survives an agent's own mistakes.
1359
+
1039
1360
  **`enable_auto_backup` and `scope="edits"`:** the automatic-on-write capture (every wrapped tool call) is gated by `enable_auto_backup=true` — if the listing is empty, check the toggle (web settings UI or `ENABLE_AUTO_BACKUP=true` env var). The explicit `(edits, create)` action bypasses the toggle since the request is explicit; `list` / `view` / `restore` / `delete` operate on whatever's already on disk regardless of the toggle's current state.
1040
1361
 
1041
1362
  **Examples:**
1042
1363
  - Snapshot before risky op: `ha_manage_backup(scope="snapshot", action="create", name="Before_Big_Change")`
1043
1364
  - List snapshots (to discover a backup_id or confirm one landed): `ha_manage_backup(scope="snapshot", action="list")`
1044
1365
  - Restore full snapshot: `ha_manage_backup(scope="snapshot", action="restore", backup_id="dd7550ed")`
1366
+ - Delete an old snapshot (requires `enable_snapshot_delete=true`): `ha_manage_backup(scope="snapshot", action="delete", backup_id="dd7550ed", confirm=True)`
1045
1367
  - On-demand entity snapshot before a manual UI edit: `ha_manage_backup(scope="edits", action="create", domain="helper_input_boolean", entity_id="kitchen_lights_active")`
1046
1368
  - List recent auto-backups for one automation: `ha_manage_backup(scope="edits", action="list", domain="automation", entity_id="kitchen_lights")`
1047
1369
  - View an auto-backup: `ha_manage_backup(scope="edits", action="view", backup_name="automation.kitchen_lights.20260521_153000.yaml")`
@@ -1086,7 +1408,7 @@ def register_backup_tools(
1086
1408
  str | None,
1087
1409
  Field(
1088
1410
  default=None,
1089
- description="(snapshot.restore) Tarball ID to restore (e.g. 'dd7550ed').",
1411
+ description="(snapshot.restore / snapshot.delete) Tarball ID (e.g. 'dd7550ed').",
1090
1412
  ),
1091
1413
  ] = None,
1092
1414
  restore_database: Annotated[
@@ -1096,6 +1418,16 @@ def register_backup_tools(
1096
1418
  description="(snapshot.restore) Include database in the restore. Default false (config-only).",
1097
1419
  ),
1098
1420
  ] = False,
1421
+ confirm: Annotated[
1422
+ bool,
1423
+ Field(
1424
+ default=False,
1425
+ description=(
1426
+ "(snapshot.delete) Must be True to confirm deletion — a "
1427
+ "safety measure against accidental calls."
1428
+ ),
1429
+ ),
1430
+ ] = False,
1099
1431
  # edits scope params
1100
1432
  domain: Annotated[
1101
1433
  str | None,
@@ -1138,18 +1470,22 @@ def register_backup_tools(
1138
1470
  description="(edits.list / snapshot.list) Maximum number of entries to return.",
1139
1471
  ),
1140
1472
  ] = 200,
1473
+ ctx: Context | None = None,
1141
1474
  ) -> dict[str, Any]:
1142
1475
  """Polymorphic backup tool. See the tool description for the routing matrix."""
1143
1476
  _gate_combo(scope, action)
1144
1477
 
1145
1478
  if scope == "snapshot":
1146
1479
  if action == "create":
1147
- return await create_backup(client, name)
1480
+ return await create_backup(client, name, ctx=ctx)
1148
1481
  if action == "list":
1149
1482
  return await list_backups(client, limit)
1483
+ if action == "delete":
1484
+ bid = _require("backup_id", backup_id, scope, action)
1485
+ return await delete_backup(client, bid, confirm, ctx=ctx)
1150
1486
  # action == "restore"
1151
1487
  bid = _require("backup_id", backup_id, scope, action)
1152
- return await restore_backup(client, bid, restore_database)
1488
+ return await restore_backup(client, bid, restore_database, ctx=ctx)
1153
1489
 
1154
1490
  # scope == "edits"
1155
1491
  settings = get_global_settings()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.12.3.dev1942
3
+ Version: 7.12.3.dev1946
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