ha-mcp-dev 7.14.2.dev2095__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.dev2095/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.14.2.dev2098}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/backup_manager.py +53 -22
  4. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_fs.py +20 -2
  5. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/backup.py +5 -2
  6. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/helpers.py +35 -0
  7. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_bug_report.py +117 -0
  8. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_filesystem.py +79 -38
  9. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  10. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/LICENSE +0 -0
  11. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/MANIFEST.in +0 -0
  12. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/README.md +0 -0
  13. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/setup.cfg +0 -0
  14. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/__init__.py +0 -0
  15. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/__main__.py +0 -0
  16. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/_pypi_marker +0 -0
  17. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/_version.py +0 -0
  18. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/auth/__init__.py +0 -0
  19. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/auth/consent_form.py +0 -0
  20. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/auth/provider.py +0 -0
  21. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/browser_landing.py +0 -0
  22. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/__init__.py +0 -0
  23. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/rest_client.py +0 -0
  24. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/supervisor_client.py +0 -0
  25. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/websocket_client.py +0 -0
  26. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/websocket_listener.py +0 -0
  27. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/config.py +0 -0
  28. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  29. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  30. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  31. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  32. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  33. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  34. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/errors.py +0 -0
  35. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/llm_exposure.py +0 -0
  36. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/__init__.py +0 -0
  37. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/approval_queue.py +0 -0
  38. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/evaluator.py +0 -0
  39. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/handlers.py +0 -0
  40. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/middleware.py +0 -0
  41. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/model.py +0 -0
  42. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/persistence.py +0 -0
  43. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/value_sources.py +0 -0
  44. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/py.typed +0 -0
  45. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/read_only.py +0 -0
  46. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  47. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  48. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  49. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  50. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  51. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  52. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  53. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  54. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  55. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  56. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  57. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  58. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  59. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  60. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  61. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  62. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  63. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  64. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  65. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  66. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  67. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  68. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  69. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  70. {ha_mcp_dev-7.14.2.dev2095 → 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
  71. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/server.py +0 -0
  72. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/__init__.py +0 -0
  73. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  74. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  75. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  76. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  77. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  78. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  79. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  80. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  81. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_theme.py +0 -0
  82. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  83. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  84. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  85. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  86. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  87. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  88. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/settings.css +0 -0
  89. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/settings.html +0 -0
  90. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/settings.js +0 -0
  91. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/smoke_test.py +0 -0
  92. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  93. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/strict_bps.py +0 -0
  94. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/__init__.py +0 -0
  95. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/auto_backup.py +0 -0
  96. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  97. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_api.py +0 -0
  98. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_config_reads.py +0 -0
  99. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_devices.py +0 -0
  100. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_registries.py +0 -0
  101. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  102. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  103. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/device_control.py +0 -0
  104. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/enhanced.py +0 -0
  105. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/__init__.py +0 -0
  106. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/base.py +0 -0
  107. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/matter.py +0 -0
  108. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/thread.py +0 -0
  109. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  110. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/zwave.py +0 -0
  111. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/reference_validator.py +0 -0
  112. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/registry.py +0 -0
  113. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  114. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  115. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  116. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  117. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  118. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  119. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  120. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  121. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  122. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  123. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_addons.py +0 -0
  124. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_areas.py +0 -0
  125. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  126. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_calendar.py +0 -0
  127. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_camera.py +0 -0
  128. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_categories.py +0 -0
  129. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_code.py +0 -0
  130. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  131. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  132. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  133. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  134. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  135. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  136. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_dev.py +0 -0
  137. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_energy.py +0 -0
  138. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_entities.py +0 -0
  139. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_groups.py +0 -0
  140. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_hacs.py +0 -0
  141. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_history.py +0 -0
  142. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_integrations.py +0 -0
  143. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_labels.py +0 -0
  144. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_radio.py +0 -0
  145. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_registry.py +0 -0
  146. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_resources.py +0 -0
  147. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_search.py +0 -0
  148. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_service.py +0 -0
  149. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_services.py +0 -0
  150. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_system.py +0 -0
  151. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_themes.py +0 -0
  152. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_todo.py +0 -0
  153. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_traces.py +0 -0
  154. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_updates.py +0 -0
  155. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_utility.py +0 -0
  156. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  157. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  158. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  159. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_zones.py +0 -0
  160. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/util_helpers.py +0 -0
  161. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/validation_middleware.py +0 -0
  162. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transforms/__init__.py +0 -0
  163. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transforms/categorized_search.py +0 -0
  164. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  165. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transport_security.py +0 -0
  166. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/update_check.py +0 -0
  167. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/__init__.py +0 -0
  168. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/config_hash.py +0 -0
  169. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/data_paths.py +0 -0
  170. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/domain_handlers.py +0 -0
  171. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  172. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  173. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/operation_manager.py +0 -0
  174. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/python_sandbox.py +0 -0
  175. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/skill_loader.py +0 -0
  176. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/usage_logger.py +0 -0
  177. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/__init__.py +0 -0
  178. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/model.py +0 -0
  179. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/persistence.py +0 -0
  180. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/resolver.py +0 -0
  181. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  182. {ha_mcp_dev-7.14.2.dev2095 → 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.dev2095 → 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.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  185. {ha_mcp_dev-7.14.2.dev2095 → 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.dev2095 → ha_mcp_dev-7.14.2.dev2098}/tests/__init__.py +0 -0
  187. {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/tests/test_constants.py +0 -0
  188. {ha_mcp_dev-7.14.2.dev2095 → 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.dev2095
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.dev2095"
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)
@@ -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,
@@ -556,6 +556,49 @@ async def _fetch_core_error_log(client: Any) -> str:
556
556
  return sanitized
557
557
 
558
558
 
559
+ def _format_tools_entry_value(diagnostic_info: dict[str, Any]) -> str:
560
+ """Render the File & YAML Tools entry status for the report templates."""
561
+ return diagnostic_info.get("tools_entry_status") or "unknown (probe failed)"
562
+
563
+
564
+ def _format_server_entry_value(diagnostic_info: dict[str, Any]) -> str:
565
+ """Render the in-process server entry status for the report templates."""
566
+ return diagnostic_info.get("server_entry_status") or "unknown (probe failed)"
567
+
568
+
569
+ # Entry titles assigned by the component's config flow (config_flow.py /
570
+ # const.py in custom_components/ha_mcp_tools) — the server code cannot import
571
+ # the separately-shipped component package, so the defaults are mirrored here.
572
+ # "HA MCP Tools" is the pre-#1853 tools-entry title an unloaded legacy entry
573
+ # may still carry. A user-renamed entry lands in the "unrecognized" bucket
574
+ # rather than being misclassified.
575
+ _SERVER_ENTRY_TITLE = "HA-MCP Server"
576
+ _TOOLS_ENTRY_TITLES = frozenset({"HA-MCP File & YAML Tools", "HA MCP Tools"})
577
+
578
+
579
+ def _classify_component_entries(
580
+ entries: list[Any],
581
+ ) -> tuple[list[str], list[str]]:
582
+ """Split ha_mcp_tools config entries into server-entry and unrecognized.
583
+
584
+ Tools-entry titles are dropped: their functional state comes from the
585
+ services probe (``_detect_tools_entry_status``), which sees whether the
586
+ entry actually serves, not just whether it exists.
587
+ """
588
+ server: list[str] = []
589
+ unrecognized: list[str] = []
590
+ for entry in entries:
591
+ if not isinstance(entry, dict):
592
+ continue
593
+ title = str(entry.get("title") or "untitled")
594
+ state = str(entry.get("state") or "unknown state")
595
+ if title == _SERVER_ENTRY_TITLE:
596
+ server.append(f"added ({state})")
597
+ elif title not in _TOOLS_ENTRY_TITLES:
598
+ unrecognized.append(f'"{title}" ({state})')
599
+ return server, unrecognized
600
+
601
+
559
602
  def _build_formatted_report(
560
603
  diagnostic_info: dict[str, Any],
561
604
  mcp_transport: str,
@@ -574,6 +617,8 @@ def _build_formatted_report(
574
617
  "",
575
618
  f"ha-mcp Version: {_format_version_value(diagnostic_info)}",
576
619
  f"Custom Component: {diagnostic_info.get('component_version') or 'not detected (not installed, or probe failed)'}",
620
+ f"File & YAML Tools entry: {_format_tools_entry_value(diagnostic_info)}",
621
+ f"In-process Server entry: {_format_server_entry_value(diagnostic_info)}",
577
622
  f"Installation Method: {diagnostic_info['installation_method']}",
578
623
  f"MCP Transport: {mcp_transport}",
579
624
  f"MCP Client: {_format_client_info_for_template(client_info)}",
@@ -649,6 +694,70 @@ class BugReportTools:
649
694
  logger.info("Component version probe failed: %s", e)
650
695
  return None
651
696
 
697
+ async def _detect_tools_entry_status(self) -> str | None:
698
+ """Describe the File & YAML Tools entry state for the report, best-effort.
699
+
700
+ The ``ha_mcp_tools`` services register only in that entry's setup, so
701
+ the service registry distinguishes "entry set up" from the #1996
702
+ state — integration installed but the second entry never added —
703
+ which the component version alone cannot show. Returns None when the
704
+ probe fails; the report path must never break on it.
705
+ """
706
+ from .tools_filesystem import _bootstrap_service_state
707
+
708
+ try:
709
+ domain_registered, bootstrap_registered = await _bootstrap_service_state(
710
+ self._client
711
+ )
712
+ except Exception as e:
713
+ logger.info("Tools-entry status probe failed: %s", e)
714
+ return None
715
+ if not domain_registered:
716
+ return (
717
+ "not set up — no ha_mcp_tools services registered; add the "
718
+ '"HA-MCP File & YAML Tools" entry via "Add entry" on the '
719
+ "HA-MCP Custom Component integration (or install the "
720
+ "component first)"
721
+ )
722
+ if not bootstrap_registered:
723
+ return "set up, but the component is pre-0.5.0 (update via HACS)"
724
+ return "set up (ha_mcp_tools services registered)"
725
+
726
+ async def _detect_server_entry_status(self) -> str | None:
727
+ """Describe the in-process "HA-MCP Server" entry state, best-effort.
728
+
729
+ Both parts of the custom component ship under the single
730
+ ``ha_mcp_tools`` domain, so "the component is installed" says nothing
731
+ about WHICH part is set up. The File & YAML tools part is covered by
732
+ the services probe; this one classifies the domain's config entries by
733
+ their flow-assigned titles to show whether the in-process server entry
734
+ exists — e.g. an add-on install that erroneously also added it (a
735
+ second, redundant server) becomes visible next to Installation Method.
736
+ Returns None when the probe fails; the report must never break on it.
737
+ """
738
+ try:
739
+ response = await self._client.send_websocket_message(
740
+ {"type": "config_entries/get", "domain": "ha_mcp_tools"}
741
+ )
742
+ if not isinstance(response, dict) or not response.get("success"):
743
+ return None
744
+ result = response.get("result")
745
+ if not isinstance(result, list):
746
+ return None
747
+ except Exception as e:
748
+ logger.info("Server-entry status probe failed: %s", e)
749
+ return None
750
+ server, unrecognized = _classify_component_entries(result)
751
+ if server:
752
+ return ", ".join(server)
753
+ if unrecognized:
754
+ # A renamed entry cannot be classified by title — report it
755
+ # rather than claiming the server entry is absent.
756
+ return "not identified — unrecognized ha_mcp_tools entries: " + ", ".join(
757
+ unrecognized
758
+ )
759
+ return "not added"
760
+
652
761
  @tool(
653
762
  name="ha_report_issue",
654
763
  tags={"Utilities"},
@@ -760,6 +869,8 @@ class BugReportTools:
760
869
  client_info = _extract_client_info(ctx)
761
870
  installed_version = await asyncio.to_thread(_detect_installed_version)
762
871
  component_version = await self._detect_component_version()
872
+ tools_entry_status = await self._detect_tools_entry_status()
873
+ server_entry_status = await self._detect_server_entry_status()
763
874
 
764
875
  diagnostic_info: dict[str, Any] = {
765
876
  "ha_mcp_version": __version__,
@@ -768,6 +879,8 @@ class BugReportTools:
768
879
  installed_version and installed_version != __version__
769
880
  ),
770
881
  "component_version": component_version,
882
+ "tools_entry_status": tools_entry_status,
883
+ "server_entry_status": server_entry_status,
771
884
  "instance": _instance_identity(),
772
885
  "installation_method": install_method,
773
886
  "platform": platform_info,
@@ -1278,6 +1391,8 @@ ha_call_service(domain="light", service="turn_on", entity_id="light.example")
1278
1391
 
1279
1392
  - **ha-mcp Version:** {_format_version_value(diagnostic_info)}
1280
1393
  - **Custom Component:** {diagnostic_info.get("component_version") or "not detected (not installed, or probe failed)"}
1394
+ - **File & YAML Tools entry:** {_format_tools_entry_value(diagnostic_info)}
1395
+ - **In-process Server entry:** {_format_server_entry_value(diagnostic_info)}
1281
1396
  - **Installation Method:** {diagnostic_info.get("installation_method", "Unknown")}
1282
1397
  - **MCP Transport:** {mcp_transport} _(auto-detected — correct if wrong)_
1283
1398
  - **MCP Client:** {_format_client_info_for_template(client_info)} _(auto-detected from the MCP `initialize` handshake)_
@@ -1443,6 +1558,8 @@ def _generate_agent_behavior_template(
1443
1558
 
1444
1559
  - **ha-mcp Version:** {_format_version_value(diagnostic_info)}
1445
1560
  - **Custom Component:** {diagnostic_info.get("component_version") or "not detected (not installed, or probe failed)"}
1561
+ - **File & YAML Tools entry:** {_format_tools_entry_value(diagnostic_info)}
1562
+ - **In-process Server entry:** {_format_server_entry_value(diagnostic_info)}
1446
1563
  - **Installation Method:** {diagnostic_info.get("installation_method", "Unknown")}
1447
1564
  - **MCP Transport:** {mcp_transport} _(auto-detected — correct if wrong)_
1448
1565
  - **MCP Client:** {_format_client_info_for_template(client_info)} _(auto-detected from the MCP `initialize` handshake)_
@@ -122,11 +122,23 @@ def _get_token_lock(client: Any) -> asyncio.Lock:
122
122
  return lock
123
123
 
124
124
 
125
- async def _is_bootstrap_service_registered(client: Any) -> bool:
126
- """Returns True if ha_mcp_tools.get_caller_token is present in HA's
127
- service registry. Old (<0.5.0) versions of the custom component
128
- didn't ship this service; the bootstrap call would otherwise fail
129
- with an opaque 400 from HA."""
125
+ async def _bootstrap_service_state(client: Any) -> tuple[bool, bool]:
126
+ """Return ``(domain_registered, bootstrap_registered)`` from HA's service
127
+ registry.
128
+
129
+ The two flags distinguish the two very different ways the
130
+ ``get_caller_token`` bootstrap can be missing (#1996):
131
+
132
+ - No ``ha_mcp_tools`` services at all — the component's "HA-MCP File &
133
+ YAML Tools" config entry was never added (the services register only in
134
+ that entry's setup), or the component isn't installed. A HACS update
135
+ cannot fix this.
136
+ - Domain services present but no ``get_caller_token`` — a genuinely old
137
+ (<0.5.0) component that pre-dates the bootstrap service; the call would
138
+ otherwise fail with an opaque 400 from HA. This one IS fixed by updating.
139
+ """
140
+ # HA /api/services returns a list of {"domain": str, "services": {...}}
141
+ # objects — stable across all supported HA versions.
130
142
  services = await client.get_services()
131
143
  for entry in services:
132
144
  if not isinstance(entry, dict):
@@ -134,8 +146,42 @@ async def _is_bootstrap_service_registered(client: Any) -> bool:
134
146
  if entry.get("domain") != MCP_TOOLS_DOMAIN:
135
147
  continue
136
148
  domain_services = entry.get("services") or {}
137
- return CALLER_TOKEN_BOOTSTRAP_SERVICE in domain_services
138
- return False
149
+ return True, CALLER_TOKEN_BOOTSTRAP_SERVICE in domain_services
150
+ return False, False
151
+
152
+
153
+ def _raise_tools_entry_not_set_up() -> NoReturn:
154
+ """Actionable error for the no-services state (#1996).
155
+
156
+ Installing the HA-MCP integration is not enough for the file / YAML
157
+ tools: they need the separate "HA-MCP File & YAML Tools" config entry,
158
+ which users routinely miss. Lead with the "Add entry" step; the full
159
+ HACS install is the fallback for a component that isn't there at all.
160
+ """
161
+ raise_tool_error(
162
+ create_error_response(
163
+ ErrorCode.COMPONENT_NOT_INSTALLED,
164
+ 'The optional "HA-MCP File & YAML Tools" entry is not set up in '
165
+ "Home Assistant, so the file and YAML tools are unavailable. "
166
+ "Installing the HA-MCP integration alone is not enough — this "
167
+ "second entry must be added to it.",
168
+ suggestions=[
169
+ "In Home Assistant: Settings → Devices & Services → "
170
+ + 'HA-MCP Custom Component → "Add entry" → choose '
171
+ + '"HA-MCP File & YAML Tools" (NOT '
172
+ + '"HA-MCP Server", which starts a second in-process server '
173
+ + "this ha-mcp server does not need)",
174
+ "If the HA-MCP integration is not listed there, install it "
175
+ + 'first: ha_manage_hacs(action="add_repository", '
176
+ + 'repository="homeassistant-ai/ha-mcp-integration", '
177
+ + 'category="integration"), then ha_manage_hacs('
178
+ + 'action="download", '
179
+ + 'repository_id="homeassistant-ai/ha-mcp-integration"), '
180
+ + "then restart Home Assistant (ha_restart)",
181
+ "Then retry the operation",
182
+ ],
183
+ )
184
+ )
139
185
 
140
186
 
141
187
  def _raise_component_too_old(detail: str) -> NoReturn:
@@ -158,12 +204,17 @@ def _raise_component_too_old(detail: str) -> NoReturn:
158
204
  async def _fetch_caller_token(client: Any) -> str:
159
205
  """Call the bootstrap service and cache the returned token.
160
206
 
161
- Two version gates:
207
+ Three gates, pre-flighted via ``_bootstrap_service_state``:
162
208
 
163
- 1. ``_is_bootstrap_service_registered`` — pre-0.5.0 components don't
164
- ship ``get_caller_token`` at all. Surface an actionable "update"
209
+ 1. No ``ha_mcp_tools`` services registered at all — the "HA-MCP File &
210
+ YAML Tools" config entry was never added (or the component isn't
211
+ installed). Surface the "add the entry" error, NOT an update prompt:
212
+ a current component in this state misled users into fruitless HACS
213
+ updates when the old code lumped it in with "too old" (#1996).
214
+ 2. Domain present but no ``get_caller_token`` — pre-0.5.0 components
215
+ don't ship the bootstrap service. Surface an actionable "update"
165
216
  error instead of letting HA return an opaque 400 to the caller.
166
- 2. ``MIN_COMPONENT_VERSION`` — even when the bootstrap service is
217
+ 3. ``MIN_COMPONENT_VERSION`` — even when the bootstrap service is
167
218
  present, the response now carries the component's manifest
168
219
  version. ha-mcp releases that depend on newer custom-component
169
220
  behavior (e.g. a new accepted yaml_path key, a new schema field)
@@ -176,7 +227,10 @@ async def _fetch_caller_token(client: Any) -> str:
176
227
  reason: the absence of the field IS the signal that the component
177
228
  doesn't yet know how to report its capabilities to ha-mcp.
178
229
  """
179
- if not await _is_bootstrap_service_registered(client):
230
+ domain_registered, bootstrap_registered = await _bootstrap_service_state(client)
231
+ if not domain_registered:
232
+ _raise_tools_entry_not_set_up()
233
+ if not bootstrap_registered:
180
234
  _raise_component_too_old(
181
235
  "the get_caller_token bootstrap service is not registered (pre-0.5.0)"
182
236
  )
@@ -312,17 +366,16 @@ def is_filesystem_tools_enabled() -> bool:
312
366
 
313
367
 
314
368
  async def _is_mcp_tools_available(client: Any) -> bool:
315
- """Return True if the ha_mcp_tools custom component is registered in HA services.
369
+ """Return True if any ha_mcp_tools services are registered in HA.
316
370
 
317
- Raises if the services API call fails — callers handle API errors via
318
- their own exception_to_structured_error blocks.
371
+ No services means the File & YAML Tools entry is not set up (or the
372
+ component is absent entirely — indistinguishable from here, see
373
+ ``_bootstrap_service_state``). Raises if the services API call fails —
374
+ callers handle API errors via their own exception_to_structured_error
375
+ blocks.
319
376
  """
320
- # HA /api/services returns a list of {"domain": str, "services": {...}} objects.
321
- # This format has been stable since before HA 0.7 (the first public release).
322
- services = await client.get_services()
323
- return any(
324
- isinstance(s, dict) and s.get("domain") == MCP_TOOLS_DOMAIN for s in services
325
- )
377
+ domain_registered, _ = await _bootstrap_service_state(client)
378
+ return domain_registered
326
379
 
327
380
 
328
381
  def _assert_caps_version_ok(caps: ComponentCaps) -> None:
@@ -352,7 +405,10 @@ async def _assert_mcp_tools_available(client: Any) -> None:
352
405
  ``caps.component_version`` (info shipped in 1.1.0, already past the floor).
353
406
  Legacy fallback: caps is None for a component in the 0.11.0-1.1.0 band
354
407
  (services, no info command) or an absent one, so fall back to the per-call
355
- ``get_services()`` existence probe.
408
+ ``get_services()`` existence probe. A no-services verdict raises the
409
+ "File & YAML Tools entry not set up" error
410
+ (``_raise_tools_entry_not_set_up``), not an install-the-component prompt —
411
+ the entry-not-added state is the common cause (#1996).
356
412
 
357
413
  Must be called within a try block that handles API errors via
358
414
  exception_to_structured_error, so connection failures are classified
@@ -365,22 +421,7 @@ async def _assert_mcp_tools_available(client: Any) -> None:
365
421
  _assert_caps_version_ok(caps)
366
422
  return
367
423
  if not await _is_mcp_tools_available(client):
368
- raise_tool_error(
369
- create_error_response(
370
- ErrorCode.COMPONENT_NOT_INSTALLED,
371
- f"The {MCP_TOOLS_DOMAIN} custom component is not installed.",
372
- suggestions=[
373
- 'Add the repository to HACS: ha_manage_hacs(action="add_repository",'
374
- + ' repository="homeassistant-ai/ha-mcp-integration", category="integration")',
375
- 'Download the component: ha_manage_hacs(action="download",'
376
- + ' repository_id="homeassistant-ai/ha-mcp-integration")',
377
- "Restart Home Assistant (ha_restart) so the integration loads",
378
- 'In HA, add the "HA-MCP Custom Component" integration and choose the'
379
- + ' "HA-MCP File & YAML Tools" entry — NOT "HA-MCP Server", which starts'
380
- + " a second in-process server this ha-mcp server does not need",
381
- ],
382
- )
383
- )
424
+ _raise_tools_entry_not_set_up()
384
425
 
385
426
 
386
427
  class FilesystemTools:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.14.2.dev2095
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