ha-mcp-dev 7.14.2.dev2093__tar.gz → 7.14.2.dev2098__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.dev2093/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.14.2.dev2098}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/backup_manager.py +53 -22
  4. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/paths.py +66 -40
  5. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_fs.py +20 -2
  6. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/backup.py +5 -2
  7. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/helpers.py +35 -0
  8. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_bug_report.py +153 -4
  9. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_dashboards.py +165 -36
  10. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_filesystem.py +79 -38
  11. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  12. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/LICENSE +0 -0
  13. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/MANIFEST.in +0 -0
  14. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/README.md +0 -0
  15. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/setup.cfg +0 -0
  16. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/__init__.py +0 -0
  17. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/__main__.py +0 -0
  18. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/_pypi_marker +0 -0
  19. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/_version.py +0 -0
  20. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/auth/__init__.py +0 -0
  21. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/auth/consent_form.py +0 -0
  22. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/auth/provider.py +0 -0
  23. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/browser_landing.py +0 -0
  24. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/__init__.py +0 -0
  25. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/rest_client.py +0 -0
  26. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/supervisor_client.py +0 -0
  27. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/websocket_client.py +0 -0
  28. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/websocket_listener.py +0 -0
  29. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/config.py +0 -0
  30. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  31. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  32. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  33. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  34. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  35. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/errors.py +0 -0
  36. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/llm_exposure.py +0 -0
  37. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/__init__.py +0 -0
  38. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/approval_queue.py +0 -0
  39. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/evaluator.py +0 -0
  40. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/handlers.py +0 -0
  41. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/middleware.py +0 -0
  42. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/model.py +0 -0
  43. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/persistence.py +0 -0
  44. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/value_sources.py +0 -0
  45. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/py.typed +0 -0
  46. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/read_only.py +0 -0
  47. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  48. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  49. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  50. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  51. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  52. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  53. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  54. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  55. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  56. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  57. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  58. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  59. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  60. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  61. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  62. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  63. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  64. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  65. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  66. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  67. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  68. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  69. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  70. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  71. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/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.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/server.py +0 -0
  73. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/__init__.py +0 -0
  74. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  75. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  76. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  77. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  78. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  79. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  80. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  81. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  82. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_theme.py +0 -0
  83. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  84. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  85. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  86. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  87. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  88. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  89. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/settings.css +0 -0
  90. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/settings.html +0 -0
  91. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/settings.js +0 -0
  92. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/smoke_test.py +0 -0
  93. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  94. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/strict_bps.py +0 -0
  95. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/__init__.py +0 -0
  96. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/auto_backup.py +0 -0
  97. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  98. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_api.py +0 -0
  99. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_config_reads.py +0 -0
  100. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_devices.py +0 -0
  101. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_registries.py +0 -0
  102. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  103. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  104. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/device_control.py +0 -0
  105. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/enhanced.py +0 -0
  106. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/__init__.py +0 -0
  107. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/base.py +0 -0
  108. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/matter.py +0 -0
  109. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/thread.py +0 -0
  110. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  111. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/zwave.py +0 -0
  112. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/reference_validator.py +0 -0
  113. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/registry.py +0 -0
  114. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  115. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  116. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  117. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  118. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  119. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  120. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  121. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  122. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  123. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  124. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_addons.py +0 -0
  125. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_areas.py +0 -0
  126. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  127. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_calendar.py +0 -0
  128. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_camera.py +0 -0
  129. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_categories.py +0 -0
  130. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_code.py +0 -0
  131. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  132. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  133. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  134. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  135. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  136. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_dev.py +0 -0
  137. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_energy.py +0 -0
  138. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_entities.py +0 -0
  139. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_groups.py +0 -0
  140. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_hacs.py +0 -0
  141. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_history.py +0 -0
  142. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_integrations.py +0 -0
  143. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_labels.py +0 -0
  144. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_radio.py +0 -0
  145. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_registry.py +0 -0
  146. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_resources.py +0 -0
  147. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_search.py +0 -0
  148. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_service.py +0 -0
  149. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_services.py +0 -0
  150. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_system.py +0 -0
  151. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_themes.py +0 -0
  152. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_todo.py +0 -0
  153. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_traces.py +0 -0
  154. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_updates.py +0 -0
  155. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_utility.py +0 -0
  156. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  157. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  158. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  159. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_zones.py +0 -0
  160. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/util_helpers.py +0 -0
  161. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/validation_middleware.py +0 -0
  162. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transforms/__init__.py +0 -0
  163. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transforms/categorized_search.py +0 -0
  164. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  165. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transport_security.py +0 -0
  166. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/update_check.py +0 -0
  167. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/__init__.py +0 -0
  168. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/config_hash.py +0 -0
  169. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/data_paths.py +0 -0
  170. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/domain_handlers.py +0 -0
  171. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  172. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  173. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/operation_manager.py +0 -0
  174. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/python_sandbox.py +0 -0
  175. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/skill_loader.py +0 -0
  176. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/usage_logger.py +0 -0
  177. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/__init__.py +0 -0
  178. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/model.py +0 -0
  179. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/persistence.py +0 -0
  180. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/resolver.py +0 -0
  181. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  182. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  183. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  184. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  185. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  186. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/tests/__init__.py +0 -0
  187. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/tests/test_constants.py +0 -0
  188. {ha_mcp_dev-7.14.2.dev2093 → ha_mcp_dev-7.14.2.dev2098}/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.dev2093
3
+ Version: 7.14.2.dev2098
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.dev2093"
7
+ version = "7.14.2.dev2098"
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
@@ -766,16 +766,18 @@ class BackupManager:
766
766
 
767
767
  # ----- legacy store (pre-#1579 .ha_mcp_tools_backups/) ---------------
768
768
 
769
- async def list_legacy(self) -> list[dict[str, Any]]:
769
+ async def list_legacy(self) -> tuple[list[dict[str, Any]], str | None]:
770
770
  """List pre-#1579 ``.bak`` backups via the component service.
771
771
 
772
772
  Each entry is normalized to a synthetic ``name`` (``legacy:<file>``)
773
773
  plus ``source="legacy"`` and the decode hints (``file_path`` /
774
774
  ``path_ambiguous``) so the caller can route view/diff/restore and warn
775
- on un-restorable (ambiguous) names. Returns ``[]`` when the component
776
- is too old to expose the service, so ``list`` still works.
775
+ on un-restorable (ambiguous) names. Returns ``(entries,
776
+ unavailable_reason)``: entries is ``[]`` when the component is too
777
+ old, unconfigured, or absent — ``list`` still works — and the reason
778
+ (when set) lets the caller flag the omission.
777
779
  """
778
- backups = await _list_legacy_backups(self._client)
780
+ backups, unavailable_reason = await _list_legacy_backups(self._client)
779
781
  out: list[dict[str, Any]] = []
780
782
  for b in backups:
781
783
  filename = b.get("filename")
@@ -792,7 +794,7 @@ class BackupManager:
792
794
  "path_ambiguous": b.get("path_ambiguous", True),
793
795
  }
794
796
  )
795
- return out
797
+ return out, unavailable_reason
796
798
 
797
799
  async def read_legacy(self, filename: str) -> dict[str, Any]:
798
800
  """Read one legacy ``.bak`` (raw content + decode hints).
@@ -815,7 +817,7 @@ class BackupManager:
815
817
  domain: str | None = None,
816
818
  entity_id: str | None = None,
817
819
  limit: int | None = None,
818
- ) -> list[dict[str, Any]]:
820
+ ) -> tuple[list[dict[str, Any]], list[str]]:
819
821
  """Edits-store snapshots plus pre-#1579 legacy ``.bak`` entries (#1579).
820
822
 
821
823
  ``list_snapshots`` is sync (dir glob, run off-thread); the legacy store
@@ -828,9 +830,22 @@ class BackupManager:
828
830
  Legacy entries get reserved room within ``limit`` so a full edits store
829
831
  (>= ``limit`` snapshots) can't truncate the few historical legacy
830
832
  entries out of the listing — surfacing them is the whole point.
833
+
834
+ Returns ``(entries, warnings)``: when the legacy sub-fetch was
835
+ suppressed (tools entry not set up / component too old), ``warnings``
836
+ says so, so the tool response doesn't read as a clean, complete list
837
+ with the ``.bak`` history silently missing (#1996).
831
838
  """
832
839
  want_legacy = domain in (None, "yaml_file") and entity_id is None
833
- legacy = await self.list_legacy() if want_legacy else []
840
+ legacy: list[dict[str, Any]] = []
841
+ warnings: list[str] = []
842
+ if want_legacy:
843
+ legacy, unavailable_reason = await self.list_legacy()
844
+ if unavailable_reason:
845
+ warnings.append(
846
+ "Legacy .bak backups could not be listed and are omitted: "
847
+ f"{unavailable_reason}"
848
+ )
834
849
  edits_limit = max(1, limit - len(legacy)) if limit and legacy else limit
835
850
  entries = await asyncio.to_thread(
836
851
  self.list_snapshots, domain=domain, entity_id=entity_id, limit=edits_limit
@@ -838,7 +853,7 @@ class BackupManager:
838
853
  entries.extend(legacy)
839
854
  if limit:
840
855
  entries = entries[:limit]
841
- return entries
856
+ return entries, warnings
842
857
 
843
858
  async def _diff_legacy(self, filename: str) -> DiffResponseText:
844
859
  info = await self.read_legacy(filename)
@@ -2173,30 +2188,37 @@ async def _restore_yaml_file(client: Any, entity_id: str, config: Any) -> Any:
2173
2188
  return result
2174
2189
 
2175
2190
 
2176
- async def _list_legacy_backups(client: Any) -> list[dict[str, Any]]:
2191
+ async def _list_legacy_backups(client: Any) -> tuple[list[dict[str, Any]], str | None]:
2177
2192
  """Fetch pre-#1579 ``.bak`` backups via the component list_legacy_backups
2178
2193
  service.
2179
2194
 
2180
- Returns ``[]`` (logged at debug) when the component predates the service —
2181
- a service-unavailable rejection surfaces either as a ``success: False``
2182
- response or a ``HomeAssistantError`` — so the edits ``list`` still works
2183
- against an older standalone component. Genuine programming errors propagate.
2195
+ Returns ``(backups, unavailable_reason)``. The legacy store is
2196
+ best-effort: when the component predates the service, isn't configured
2197
+ (tools entry not set up), or is missing entirely, the entry list is ``[]``
2198
+ and ``unavailable_reason`` carries the human-readable cause — a
2199
+ ``HomeAssistantError`` or any ``ToolError`` out of
2200
+ ``call_mcp_tools_service`` (all its caller-token gates included) lands
2201
+ here (also logged at debug), so the edits ``list`` still works and the
2202
+ caller can surface the degradation as a warning. A ``success: False``
2203
+ service response degrades silently as before. Genuine programming errors
2204
+ propagate.
2184
2205
  """
2206
+ from .tools.helpers import extract_structured_error_reason
2185
2207
  from .tools.tools_filesystem import call_mcp_tools_service
2186
2208
  from .tools.util_helpers import unwrap_service_response
2187
2209
 
2188
2210
  try:
2189
2211
  result = await call_mcp_tools_service(client, "list_legacy_backups", {})
2190
- except HomeAssistantError as err:
2212
+ except (HomeAssistantError, ToolError) as err:
2191
2213
  logger.debug("legacy backup list unavailable: %s", err)
2192
- return []
2214
+ return [], extract_structured_error_reason(err) or str(err)
2193
2215
  if not isinstance(result, dict):
2194
- return []
2216
+ return [], None
2195
2217
  result = unwrap_service_response(result)
2196
2218
  if not result.get("success", False):
2197
- return []
2219
+ return [], None
2198
2220
  backups = result.get("backups")
2199
- return backups if isinstance(backups, list) else []
2221
+ return (backups, None) if isinstance(backups, list) else ([], None)
2200
2222
 
2201
2223
 
2202
2224
  async def _read_legacy_backup(client: Any, filename: str) -> dict[str, Any]:
@@ -2204,9 +2226,11 @@ async def _read_legacy_backup(client: Any, filename: str) -> dict[str, Any]:
2204
2226
 
2205
2227
  Returns the unwrapped service response (carries ``success`` / ``content`` /
2206
2228
  ``file_path`` / ``path_ambiguous`` / ``timestamp``). A service-unavailable
2207
- ``HomeAssistantError`` is mapped to a ``success: False`` dict so the caller
2208
- surfaces a not-found rather than crashing.
2229
+ ``HomeAssistantError`` — or a ``ToolError`` from the caller-token gates —
2230
+ is mapped to a ``success: False`` dict so the caller surfaces a not-found
2231
+ rather than crashing.
2209
2232
  """
2233
+ from .tools.helpers import extract_structured_error_reason
2210
2234
  from .tools.tools_filesystem import call_mcp_tools_service
2211
2235
  from .tools.util_helpers import unwrap_service_response
2212
2236
 
@@ -2214,8 +2238,15 @@ async def _read_legacy_backup(client: Any, filename: str) -> dict[str, Any]:
2214
2238
  result = await call_mcp_tools_service(
2215
2239
  client, "read_legacy_backup", {"filename": filename}
2216
2240
  )
2217
- except HomeAssistantError as err:
2218
- return {"success": False, "error": str(err)}
2241
+ except (HomeAssistantError, ToolError) as err:
2242
+ # The human reason, not str(err) verbatim: a ToolError's string is
2243
+ # the whole JSON envelope, which would reach the tool layer as an
2244
+ # unreadable JSON-in-JSON message (#1996's original symptom).
2245
+ logger.debug("legacy backup read unavailable: %s", err)
2246
+ return {
2247
+ "success": False,
2248
+ "error": extract_structured_error_reason(err) or str(err),
2249
+ }
2219
2250
  if not isinstance(result, dict):
2220
2251
  return {"success": False, "error": f"no response for {filename!r}"}
2221
2252
  return unwrap_service_response(result)
@@ -347,42 +347,28 @@ async def fetch_dashboard_render_config(
347
347
  return cast(dict[str, Any], config)
348
348
 
349
349
 
350
- def resolve_dashboard_view(
350
+ def match_dashboard_view(
351
351
  dashboard_url_path: str,
352
352
  config: dict[str, Any],
353
- view_path: str | None,
354
- ) -> DashboardRenderTarget:
355
- """Resolve one named Lovelace view to a canonical frontend route."""
356
- base_path = dashboard_frontend_path(dashboard_url_path)
357
- if view_path is None:
358
- views = config.get("views")
359
- has_static_views = isinstance(views, list) and bool(views)
360
- if "strategy" in config:
361
- warning = (
362
- "No view_path was supplied; this strategy dashboard generates "
363
- "its views at runtime, so only the base route is available."
364
- )
365
- elif has_static_views:
366
- warning = (
367
- "No view_path was supplied; the dashboard base route renders "
368
- "the first view visible to Puppet's Home Assistant user."
369
- )
370
- else:
371
- warning = (
372
- "No view_path was supplied and the dashboard has no static views; "
373
- "only the base route is available."
374
- )
375
- return DashboardRenderTarget(
376
- dashboard_url_path=base_path,
377
- view_path=None,
378
- render_path=base_path,
379
- view_index=None,
380
- stable=False,
381
- warnings=(warning,),
382
- )
383
-
384
- cleaned_view_path = view_path
385
- if not cleaned_view_path.strip():
353
+ view_path: str,
354
+ *,
355
+ strategy_suggestions: tuple[str, ...] = (
356
+ "Use dashboard_path with the frontend route instead",
357
+ ),
358
+ ) -> tuple[int, dict[str, Any]]:
359
+ """Match one named Lovelace view by its configured ``views[].path``.
360
+
361
+ Shared by screenshot view resolution and get-mode view scoping so both
362
+ honour identical semantics: exact match on the configured path, with
363
+ structured errors for an empty path, a strategy dashboard (no static
364
+ views to match), and a missing/ambiguous path (the not-found error lists
365
+ the available paths). Returns ``(view_index, view)``.
366
+
367
+ ``strategy_suggestions`` lets each caller phrase the strategy-dashboard
368
+ remediation in its own vocabulary (the screenshot tools have a
369
+ ``dashboard_path`` route parameter; config scoping does not).
370
+ """
371
+ if not view_path.strip():
386
372
  raise_tool_error(
387
373
  create_error_response(
388
374
  ErrorCode.VALIDATION_INVALID_PARAMETER,
@@ -397,9 +383,9 @@ def resolve_dashboard_view(
397
383
  "Strategy dashboards do not expose static named view paths.",
398
384
  context={
399
385
  "dashboard_url_path": dashboard_url_path,
400
- "view_path": cleaned_view_path,
386
+ "view_path": view_path,
401
387
  },
402
- suggestions=["Use dashboard_path with the frontend route instead"],
388
+ suggestions=list(strategy_suggestions),
403
389
  )
404
390
  )
405
391
  views = config.get("views")
@@ -408,7 +394,7 @@ def resolve_dashboard_view(
408
394
  matches = [
409
395
  (index, view)
410
396
  for index, view in enumerate(views)
411
- if _configured_view_path(view) == cleaned_view_path
397
+ if _configured_view_path(view) == view_path
412
398
  ]
413
399
  if len(matches) != 1:
414
400
  available = [
@@ -420,17 +406,57 @@ def resolve_dashboard_view(
420
406
  raise_tool_error(
421
407
  create_error_response(
422
408
  ErrorCode.RESOURCE_NOT_FOUND,
423
- f"Dashboard view path '{cleaned_view_path}' is {reason}.",
409
+ f"Dashboard view path '{view_path}' is {reason}.",
424
410
  context={
425
411
  "dashboard_url_path": dashboard_url_path,
426
- "view_path": cleaned_view_path,
412
+ "view_path": view_path,
427
413
  "available_view_paths": available,
428
414
  },
429
415
  suggestions=["Use a view_path returned by ha_config_get_dashboard"],
430
416
  )
431
417
  )
418
+ return matches[0]
419
+
432
420
 
433
- view_index, _ = matches[0]
421
+ def resolve_dashboard_view(
422
+ dashboard_url_path: str,
423
+ config: dict[str, Any],
424
+ view_path: str | None,
425
+ ) -> DashboardRenderTarget:
426
+ """Resolve one named Lovelace view to a canonical frontend route."""
427
+ base_path = dashboard_frontend_path(dashboard_url_path)
428
+ if view_path is None:
429
+ views = config.get("views")
430
+ has_static_views = isinstance(views, list) and bool(views)
431
+ if "strategy" in config:
432
+ warning = (
433
+ "No view_path was supplied; this strategy dashboard generates "
434
+ "its views at runtime, so only the base route is available."
435
+ )
436
+ elif has_static_views:
437
+ warning = (
438
+ "No view_path was supplied; the dashboard base route renders "
439
+ "the first view visible to Puppet's Home Assistant user."
440
+ )
441
+ else:
442
+ warning = (
443
+ "No view_path was supplied and the dashboard has no static views; "
444
+ "only the base route is available."
445
+ )
446
+ return DashboardRenderTarget(
447
+ dashboard_url_path=base_path,
448
+ view_path=None,
449
+ render_path=base_path,
450
+ view_index=None,
451
+ stable=False,
452
+ warnings=(warning,),
453
+ )
454
+
455
+ view_index, _ = match_dashboard_view(dashboard_url_path, config, view_path)
456
+ cleaned_view_path = view_path
457
+ views = config.get("views")
458
+ if not isinstance(views, list):
459
+ views = []
434
460
  render_path = _safe_named_render_path(base_path, cleaned_view_path)
435
461
  if render_path is None:
436
462
  return _fallback_view_target(
@@ -25,6 +25,20 @@ if TYPE_CHECKING:
25
25
  logger = logging.getLogger(__name__)
26
26
 
27
27
 
28
+ def _component_error_reason(exc: Exception) -> str | None:
29
+ """Extract the human message from a structured ``ToolError``, else None.
30
+
31
+ ``raise_tool_error`` serializes the whole error envelope into the
32
+ exception string, so rendering ``str(exc)`` verbatim shows the settings
33
+ UI a raw JSON blob behind a misleading "could not reach" prefix — the
34
+ hard-to-read surface from #1996. Thin seam over the shared extractor
35
+ (imported lazily like the other tools imports in this module).
36
+ """
37
+ from ..tools.helpers import extract_structured_error_reason
38
+
39
+ return extract_structured_error_reason(exc)
40
+
41
+
28
42
  async def _fs_custom_paths_call(
29
43
  server: HomeAssistantSmartMCPServer | None, service: str, data: dict[str, Any]
30
44
  ) -> Any:
@@ -90,7 +104,10 @@ async def _get_fs_custom_paths(
90
104
  result = await _fs_custom_paths_call(server, "get_allowed_paths", {})
91
105
  except Exception as exc:
92
106
  logger.warning("fs-custom-paths GET could not reach ha_mcp_tools: %s", exc)
93
- return _unavailable(f"Could not reach the ha_mcp_tools component: {exc}")
107
+ return _unavailable(
108
+ _component_error_reason(exc)
109
+ or f"Could not reach the ha_mcp_tools component: {exc}"
110
+ )
94
111
 
95
112
  data = unwrap_service_response(result) if isinstance(result, dict) else {}
96
113
  if not isinstance(data, dict) or not data.get("success", False):
@@ -163,7 +180,8 @@ async def _save_fs_custom_paths(
163
180
  return JSONResponse(
164
181
  create_error_response(
165
182
  ErrorCode.SERVICE_CALL_FAILED,
166
- f"Could not reach the ha_mcp_tools component: {exc}",
183
+ _component_error_reason(exc)
184
+ or f"Could not reach the ha_mcp_tools component: {exc}",
167
185
  ),
168
186
  status_code=502,
169
187
  )
@@ -1908,12 +1908,12 @@ async def _edits_list(
1908
1908
  # Edits-store snapshots + pre-#1579 legacy .bak entries (#1579).
1909
1909
  # The manager owns the merge (sync dir-glob off-thread + async
1910
1910
  # legacy service call) so this layer stays source-agnostic.
1911
- entries = await mgr.list_edits_and_legacy(
1911
+ entries, warnings = await mgr.list_edits_and_legacy(
1912
1912
  domain=domain,
1913
1913
  entity_id=entity_id,
1914
1914
  limit=limit,
1915
1915
  )
1916
- return {
1916
+ response: dict[str, Any] = {
1917
1917
  "success": True,
1918
1918
  "data": {
1919
1919
  "backups": entries,
@@ -1924,6 +1924,9 @@ async def _edits_list(
1924
1924
  "retain_per_entity": settings.auto_backup_retain_per_entity,
1925
1925
  },
1926
1926
  }
1927
+ if warnings:
1928
+ response["warnings"] = warnings
1929
+ return response
1927
1930
 
1928
1931
 
1929
1932
  async def _edits_view(
@@ -78,6 +78,41 @@ def extract_tool_error_message(te: ToolError) -> str:
78
78
  return str(te)
79
79
 
80
80
 
81
+ def extract_structured_error_reason(exc: BaseException) -> str | None:
82
+ """Extract "message" + first actionable suggestion from a structured error.
83
+
84
+ Like ``extract_tool_error_message`` but for surfaces that show the reason
85
+ to a human (settings UI, degraded-result warnings): it appends the first
86
+ suggestion when one exists, and returns ``None`` — instead of the raw
87
+ ``str(exc)`` JSON envelope — when the payload is not a structured error
88
+ or carries no usable message, so callers pick their own fallback.
89
+ ``create_error_response`` emits only the singular ``suggestion`` key for
90
+ one-suggestion errors (the plural ``suggestions`` list needs two or
91
+ more), so both keys are honored.
92
+ """
93
+ try:
94
+ payload = json.loads(str(exc))
95
+ except (TypeError, ValueError):
96
+ return None
97
+ if not isinstance(payload, dict):
98
+ return None
99
+ error = payload.get("error")
100
+ if not isinstance(error, dict):
101
+ return None
102
+ message = error.get("message")
103
+ if not isinstance(message, str) or not message:
104
+ return None
105
+ suggestions = error.get("suggestions")
106
+ first = (
107
+ suggestions[0]
108
+ if isinstance(suggestions, list) and suggestions
109
+ else error.get("suggestion")
110
+ )
111
+ if isinstance(first, str) and first:
112
+ return f"{message} {first}"
113
+ return message
114
+
115
+
81
116
  def validate_identifier_not_empty(
82
117
  value: str | None,
83
118
  param_name: str,