ha-mcp-dev 7.13.0.dev1977__tar.gz → 7.13.0.dev1983__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 (186) hide show
  1. {ha_mcp_dev-7.13.0.dev1977/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev1983}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/config.py +1 -1
  4. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/dashboard_screenshot/__init__.py +6 -2
  5. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/dashboard_screenshot/capture.py +158 -86
  6. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/dashboard_screenshot/provision.py +62 -7
  7. ha_mcp_dev-7.13.0.dev1983/src/ha_mcp/dashboard_screenshot/theme_guard.py +228 -0
  8. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_config_dashboards.py +25 -9
  9. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_dashboard_screenshot.py +29 -15
  10. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  11. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  12. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/LICENSE +0 -0
  13. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/MANIFEST.in +0 -0
  14. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/README.md +0 -0
  15. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/setup.cfg +0 -0
  16. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/__init__.py +0 -0
  17. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/__main__.py +0 -0
  18. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/_pypi_marker +0 -0
  19. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/_version.py +0 -0
  20. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/auth/__init__.py +0 -0
  21. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/auth/consent_form.py +0 -0
  22. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/auth/provider.py +0 -0
  23. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/backup_manager.py +0 -0
  24. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/browser_landing.py +0 -0
  25. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/client/__init__.py +0 -0
  26. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/client/rest_client.py +0 -0
  27. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/client/supervisor_client.py +0 -0
  28. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/client/websocket_client.py +0 -0
  29. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/client/websocket_listener.py +0 -0
  30. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  31. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  32. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/errors.py +0 -0
  33. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/llm_exposure.py +0 -0
  34. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/__init__.py +0 -0
  35. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/approval_queue.py +0 -0
  36. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/evaluator.py +0 -0
  37. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/handlers.py +0 -0
  38. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/middleware.py +0 -0
  39. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/model.py +0 -0
  40. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/persistence.py +0 -0
  41. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/policy/value_sources.py +0 -0
  42. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/py.typed +0 -0
  43. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/read_only.py +0 -0
  44. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  45. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  46. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  47. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  48. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  49. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  50. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  51. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  52. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  53. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  54. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  55. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  56. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  57. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  58. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  59. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  60. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  61. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  62. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  63. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  64. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  65. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  66. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  67. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  68. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  69. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/server.py +0 -0
  70. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/__init__.py +0 -0
  71. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  72. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  73. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  74. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  75. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  76. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  77. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  78. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  79. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  80. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_theme.py +0 -0
  81. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  82. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  83. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  84. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  85. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/settings.css +0 -0
  86. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/settings.html +0 -0
  87. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/settings_ui/settings.js +0 -0
  88. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/smoke_test.py +0 -0
  89. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  90. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/strict_bps.py +0 -0
  91. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/__init__.py +0 -0
  92. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/auto_backup.py +0 -0
  93. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/backup.py +0 -0
  94. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  95. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/component_api.py +0 -0
  96. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/component_config_reads.py +0 -0
  97. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/component_devices.py +0 -0
  98. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/component_registries.py +0 -0
  99. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  100. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  101. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/device_control.py +0 -0
  102. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/enhanced.py +0 -0
  103. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/helpers.py +0 -0
  104. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/radio/__init__.py +0 -0
  105. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/radio/base.py +0 -0
  106. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/radio/matter.py +0 -0
  107. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/radio/thread.py +0 -0
  108. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  109. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/radio/zwave.py +0 -0
  110. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/reference_validator.py +0 -0
  111. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/registry.py +0 -0
  112. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  113. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  114. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  115. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  116. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  117. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  118. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  119. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  120. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  121. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  122. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_addons.py +0 -0
  123. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_areas.py +0 -0
  124. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  125. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  126. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_calendar.py +0 -0
  127. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_camera.py +0 -0
  128. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_categories.py +0 -0
  129. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_code.py +0 -0
  130. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  131. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  132. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  133. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  134. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_dev.py +0 -0
  135. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_energy.py +0 -0
  136. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_entities.py +0 -0
  137. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  138. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_groups.py +0 -0
  139. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_hacs.py +0 -0
  140. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_history.py +0 -0
  141. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_integrations.py +0 -0
  142. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_labels.py +0 -0
  143. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_radio.py +0 -0
  144. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_registry.py +0 -0
  145. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_resources.py +0 -0
  146. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_search.py +0 -0
  147. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_service.py +0 -0
  148. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_services.py +0 -0
  149. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_system.py +0 -0
  150. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_themes.py +0 -0
  151. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_todo.py +0 -0
  152. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_traces.py +0 -0
  153. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_updates.py +0 -0
  154. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_utility.py +0 -0
  155. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  156. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  157. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  158. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/tools_zones.py +0 -0
  159. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/util_helpers.py +0 -0
  160. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/tools/validation_middleware.py +0 -0
  161. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/transforms/__init__.py +0 -0
  162. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/transforms/categorized_search.py +0 -0
  163. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  164. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/transport_security.py +0 -0
  165. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/update_check.py +0 -0
  166. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/__init__.py +0 -0
  167. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/config_hash.py +0 -0
  168. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/data_paths.py +0 -0
  169. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/domain_handlers.py +0 -0
  170. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  171. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  172. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/operation_manager.py +0 -0
  173. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/python_sandbox.py +0 -0
  174. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/skill_loader.py +0 -0
  175. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/utils/usage_logger.py +0 -0
  176. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/visibility/__init__.py +0 -0
  177. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/visibility/model.py +0 -0
  178. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/visibility/persistence.py +0 -0
  179. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp/visibility/resolver.py +0 -0
  180. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  181. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  182. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  183. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  184. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/tests/__init__.py +0 -0
  185. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/tests/test_constants.py +0 -0
  186. {ha_mcp_dev-7.13.0.dev1977 → ha_mcp_dev-7.13.0.dev1983}/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.13.0.dev1977
3
+ Version: 7.13.0.dev1983
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.13.0.dev1977"
7
+ version = "7.13.0.dev1983"
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
@@ -836,7 +836,7 @@ ADVANCED_SETTINGS_FIELDS: tuple[AdvancedField, ...] = (
836
836
  AdvancedField("enable_websocket", "ENABLE_WEBSOCKET", bool, "operations", True),
837
837
  # Dashboard-screenshot engine URL (#1538): docker/.env users could set
838
838
  # HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL, but add-on users had no path to
839
- # it. It is resolved live per capture (resolve_engine_url), so unlike the
839
+ # it. It is resolved live per capture (resolve_engine), so unlike the
840
840
  # time budgets it takes effect without a restart. Blank = auto-discover
841
841
  # the Puppet add-on via the Supervisor.
842
842
  AdvancedField(
@@ -19,7 +19,8 @@ from .capture import (
19
19
  DashboardImageCapture,
20
20
  capture_dashboard_images,
21
21
  )
22
- from .provision import resolve_engine_url
22
+ from .provision import EngineTarget, resolve_engine
23
+ from .theme_guard import EngineCredential, ThemeGuard
23
24
 
24
25
  __all__ = [
25
26
  "DEFAULT_HEIGHT",
@@ -27,6 +28,9 @@ __all__ = [
27
28
  "DEFAULT_WAIT_MS",
28
29
  "DEFAULT_WIDTH",
29
30
  "DashboardImageCapture",
31
+ "EngineCredential",
32
+ "EngineTarget",
33
+ "ThemeGuard",
30
34
  "capture_dashboard_images",
31
- "resolve_engine_url",
35
+ "resolve_engine",
32
36
  ]
@@ -2,8 +2,11 @@
2
2
 
3
3
  The engine (balloob's Puppet add-on, or a docker-compose sidecar)
4
4
  authenticates to Home Assistant with its OWN configured long-lived token, so
5
- this client passes only the dashboard path + render parameters — no HA token
6
- ever flows through ha-mcp or the LLM for screenshots.
5
+ this client passes only the dashboard path + render parameters to the engine
6
+ — no HA token is ever sent to the engine or exposed to the LLM. The
7
+ :mod:`theme_guard` bracket around each capture batch may hold the engine's
8
+ token in server memory to restore the engine user's saved theme (issue
9
+ #1909); see that module's docstring for the containment rules.
7
10
  """
8
11
 
9
12
  from __future__ import annotations
@@ -20,7 +23,8 @@ from fastmcp.exceptions import ToolError
20
23
 
21
24
  from ..errors import ErrorCode, create_error_response
22
25
  from ..tools.helpers import raise_tool_error
23
- from .provision import TOKEN_HINT, resolve_engine_url
26
+ from .provision import TOKEN_HINT, resolve_engine
27
+ from .theme_guard import ThemeGuard
24
28
 
25
29
  logger = logging.getLogger(__name__)
26
30
 
@@ -757,9 +761,30 @@ async def _request_or_collect_failure(
757
761
  return None
758
762
 
759
763
 
764
+ def _viewport_params(
765
+ options: _CaptureOptions, viewport: _ViewportRequest
766
+ ) -> dict[str, str]:
767
+ """Build one viewport's engine query parameters."""
768
+ params: dict[str, str] = {
769
+ "viewport": f"{viewport.width}x{viewport.height}",
770
+ "zoom": str(options.zoom),
771
+ "wait": str(options.wait_ms),
772
+ "format": options.image_format,
773
+ }
774
+ if options.theme is not None:
775
+ params["theme"] = options.theme
776
+ if options.dark_mode:
777
+ # Puppet checks only for query-key presence.
778
+ params["dark"] = ""
779
+ if options.language is not None:
780
+ params["lang"] = options.language
781
+ return params
782
+
783
+
760
784
  def _complete_capture_batch(
761
785
  captures: list[DashboardImageCapture],
762
786
  partial_failures: list[dict[str, Any]] | None,
787
+ guard_warnings: list[str],
763
788
  ) -> list[DashboardImageCapture]:
764
789
  """Return partial successes, but raise when every requested capture failed."""
765
790
  if not captures and partial_failures:
@@ -769,10 +794,41 @@ def _complete_capture_batch(
769
794
  "failure_count": len(partial_failures),
770
795
  "screenshot_failures": partial_failures,
771
796
  }
797
+ if guard_warnings:
798
+ aggregate_failure["warnings"] = [
799
+ *aggregate_failure.get("warnings", []),
800
+ *guard_warnings,
801
+ ]
772
802
  raise_tool_error(aggregate_failure)
773
803
  return captures
774
804
 
775
805
 
806
+ def _reraise_with_guard_warnings(error: ToolError, warnings: list[str]) -> NoReturn:
807
+ """Re-raise a capture ToolError with the theme guard's warnings attached.
808
+
809
+ A batch that raises may already have rendered (and clobbered the engine
810
+ user's theme) before failing, so a failed restore must stay visible on
811
+ the error path too — otherwise the tool contract ("an attempted but
812
+ failed restore surfaces as a warning") silently breaks exactly when the
813
+ theme is most likely left flipped.
814
+ """
815
+ if not warnings:
816
+ raise error
817
+ try:
818
+ payload = json.loads(str(error))
819
+ except (json.JSONDecodeError, TypeError):
820
+ payload = None
821
+ if not isinstance(payload, dict):
822
+ raise error
823
+ existing = payload.get("warnings")
824
+ payload["warnings"] = [
825
+ *(existing if isinstance(existing, list) else []),
826
+ *warnings,
827
+ ]
828
+ raise_tool_error(payload)
829
+ raise AssertionError("unreachable: raise_tool_error always raises")
830
+
831
+
776
832
  async def capture_dashboard_images(
777
833
  dashboard_path: str,
778
834
  *,
@@ -789,6 +845,8 @@ async def capture_dashboard_images(
789
845
  image_format: ScreenshotFormat = "png",
790
846
  render_timeout_seconds: float = DEFAULT_RENDER_TIMEOUT_SECONDS,
791
847
  partial_failures: list[dict[str, Any]] | None = None,
848
+ client: Any | None = None,
849
+ capture_warnings: list[str] | None = None,
792
850
  ) -> list[DashboardImageCapture]:
793
851
  """Render one or more ordered dashboard images via the screenshot engine.
794
852
 
@@ -796,6 +854,12 @@ async def capture_dashboard_images(
796
854
  preset or custom dimensions when needed; omitting it preserves each
797
855
  preset's native orientation. ``full_page`` is a compatibility alias for
798
856
  requesting the engine's native ``WIDTHxauto`` viewport.
857
+
858
+ The batch is bracketed by a :class:`ThemeGuard` that restores the engine
859
+ user's saved frontend theme if rendering changed it (issue #1909).
860
+ ``client`` supplies the guard's non-add-on credential fallback and
861
+ ``capture_warnings`` collects the guard's non-fatal warnings; both are
862
+ optional and never affect the captures themselves.
799
863
  """
800
864
  path = _validate_dashboard_path(dashboard_path)
801
865
  options = validate_capture_parameters(
@@ -814,94 +878,102 @@ async def capture_dashboard_images(
814
878
  )
815
879
  viewports = _capture_viewports(options)
816
880
 
817
- engine = await resolve_engine_url()
881
+ engine_target = await resolve_engine()
882
+ engine = engine_target.url
818
883
  url = f"{engine}/{path}"
819
884
  mime_type = _MIME_TYPES[options.image_format]
820
885
  captures: list[DashboardImageCapture] = []
821
886
 
822
- async with httpx.AsyncClient(
823
- timeout=httpx.Timeout(options.render_timeout_seconds)
824
- ) as http_client:
825
- for capture_index, viewport in enumerate(viewports):
826
- params: dict[str, str] = {
827
- "viewport": f"{viewport.width}x{viewport.height}",
828
- "zoom": str(options.zoom),
829
- "wait": str(options.wait_ms),
830
- "format": options.image_format,
831
- }
832
- if options.theme is not None:
833
- params["theme"] = options.theme
834
- if options.dark_mode:
835
- # Puppet checks only for query-key presence.
836
- params["dark"] = ""
837
- if options.language is not None:
838
- params["lang"] = options.language
839
-
840
- request_context = {
841
- "path": path,
842
- "preset": viewport.preset,
843
- "width": viewport.width,
844
- "height": viewport.height,
845
- "requested_format": options.image_format,
846
- "capture_index": capture_index,
847
- "capture_count": len(viewports),
848
- "completed_count": len(captures),
849
- }
850
- aggregate_bytes = sum(capture.size_bytes for capture in captures)
851
- remaining_batch_bytes = MAX_BATCH_PAYLOAD_BYTES - aggregate_bytes
852
- if remaining_batch_bytes <= 0:
853
- assert partial_failures is not None
854
- partial_failures.append(
855
- create_error_response(
856
- ErrorCode.IMAGE_PAYLOAD_TOO_LARGE,
857
- "Screenshot image batch reached the server's safe "
858
- "inline-image limit.",
859
- context={
860
- **request_context,
861
- "aggregate_bytes_before_capture": aggregate_bytes,
862
- "limit_kind": "batch",
863
- "limit_bytes": MAX_BATCH_PAYLOAD_BYTES,
864
- },
887
+ # The engine dispatches a theme write into the authenticated frontend on
888
+ # cold renders, which Home Assistant persists to the engine user's real
889
+ # profile (issue #1909). Snapshot before, restore after — including when
890
+ # the batch fails, since the engine may already have rendered (and
891
+ # written) before the failure.
892
+ guard = ThemeGuard.for_capture(engine_target.addon_credential, client)
893
+ await guard.take_snapshot()
894
+ batch_error: ToolError | None = None
895
+ try:
896
+ async with httpx.AsyncClient(
897
+ timeout=httpx.Timeout(options.render_timeout_seconds)
898
+ ) as http_client:
899
+ for capture_index, viewport in enumerate(viewports):
900
+ params = _viewport_params(options, viewport)
901
+
902
+ request_context = {
903
+ "path": path,
904
+ "preset": viewport.preset,
905
+ "width": viewport.width,
906
+ "height": viewport.height,
907
+ "requested_format": options.image_format,
908
+ "capture_index": capture_index,
909
+ "capture_count": len(viewports),
910
+ "completed_count": len(captures),
911
+ }
912
+ aggregate_bytes = sum(capture.size_bytes for capture in captures)
913
+ remaining_batch_bytes = MAX_BATCH_PAYLOAD_BYTES - aggregate_bytes
914
+ if remaining_batch_bytes <= 0:
915
+ assert partial_failures is not None
916
+ partial_failures.append(
917
+ create_error_response(
918
+ ErrorCode.IMAGE_PAYLOAD_TOO_LARGE,
919
+ "Screenshot image batch reached the server's safe "
920
+ "inline-image limit.",
921
+ context={
922
+ **request_context,
923
+ "aggregate_bytes_before_capture": aggregate_bytes,
924
+ "limit_kind": "batch",
925
+ "limit_bytes": MAX_BATCH_PAYLOAD_BYTES,
926
+ },
927
+ )
865
928
  )
866
- )
867
- break
868
-
869
- outcome = await _request_or_collect_failure(
870
- http_client,
871
- url=url,
872
- path=path,
873
- engine=engine,
874
- params=params,
875
- options=options,
876
- viewport=viewport,
877
- mime_type=mime_type,
878
- request_context=request_context,
879
- aggregate_bytes=aggregate_bytes,
880
- remaining_batch_bytes=remaining_batch_bytes,
881
- partial_failures=partial_failures,
882
- )
883
- if outcome is None:
884
- if (
885
- partial_failures
886
- and partial_failures[-1].get("limit_kind") == "batch"
887
- ):
888
929
  break
889
- continue
890
- image_data, capture_height, fallback_used = outcome
891
-
892
- captures.append(
893
- DashboardImageCapture(
894
- data=image_data,
895
- width=viewport.width,
896
- height=capture_height,
897
- preset=viewport.preset,
898
- orientation=viewport.orientation,
899
- image_format=options.image_format,
900
- mime_type=mime_type,
901
- size_bytes=len(image_data),
930
+
931
+ outcome = await _request_or_collect_failure(
932
+ http_client,
933
+ url=url,
934
+ path=path,
935
+ engine=engine,
936
+ params=params,
902
937
  options=options,
903
- legacy_full_page_fallback=fallback_used,
938
+ viewport=viewport,
939
+ mime_type=mime_type,
940
+ request_context=request_context,
941
+ aggregate_bytes=aggregate_bytes,
942
+ remaining_batch_bytes=remaining_batch_bytes,
943
+ partial_failures=partial_failures,
904
944
  )
905
- )
906
-
907
- return _complete_capture_batch(captures, partial_failures)
945
+ if outcome is None:
946
+ if (
947
+ partial_failures
948
+ and partial_failures[-1].get("limit_kind") == "batch"
949
+ ):
950
+ break
951
+ continue
952
+ image_data, capture_height, fallback_used = outcome
953
+
954
+ captures.append(
955
+ DashboardImageCapture(
956
+ data=image_data,
957
+ width=viewport.width,
958
+ height=capture_height,
959
+ preset=viewport.preset,
960
+ orientation=viewport.orientation,
961
+ image_format=options.image_format,
962
+ mime_type=mime_type,
963
+ size_bytes=len(image_data),
964
+ options=options,
965
+ legacy_full_page_fallback=fallback_used,
966
+ )
967
+ )
968
+ except ToolError as exc:
969
+ # Held (not re-raised here) so the restore in ``finally`` runs first
970
+ # and its outcome can be attached to the error payload below.
971
+ batch_error = exc
972
+ finally:
973
+ await guard.restore()
974
+ if capture_warnings is not None:
975
+ capture_warnings.extend(guard.warnings)
976
+
977
+ if batch_error is not None:
978
+ _reraise_with_guard_warnings(batch_error, guard.warnings)
979
+ return _complete_capture_batch(captures, partial_failures, guard.warnings)
@@ -23,6 +23,7 @@ from __future__ import annotations
23
23
 
24
24
  import logging
25
25
  import os
26
+ from dataclasses import dataclass
26
27
  from typing import Any
27
28
 
28
29
  import httpx
@@ -30,9 +31,27 @@ from fastmcp.exceptions import ToolError
30
31
 
31
32
  from ..errors import ErrorCode, create_error_response
32
33
  from ..tools.helpers import raise_tool_error
34
+ from .theme_guard import EngineCredential, addon_credential_from_options
33
35
 
34
36
  logger = logging.getLogger(__name__)
35
37
 
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class EngineTarget:
41
+ """A resolved screenshot engine endpoint.
42
+
43
+ ``addon_credential`` authenticates as the engine's user (extracted from
44
+ the Puppet add-on's Supervisor options during discovery, so the theme
45
+ guard needs no second round-trip and the raw secret-bearing options
46
+ dict never leaves this module). It is ``None`` for an explicitly
47
+ configured engine URL. The token inside never leaves the server process
48
+ — it must not be logged or surfaced in responses.
49
+ """
50
+
51
+ url: str
52
+ addon_credential: EngineCredential | None = None
53
+
54
+
36
55
  ENGINE_PORT = 10000
37
56
  # The Supervisor slug is ``<repo-hash>_puppet`` for balloob's Puppet add-on.
38
57
  # ``str.endswith`` accepts a tuple, kept as one for easy future extension.
@@ -146,8 +165,8 @@ def _supervisor_addon_listing(payload: Any) -> list[dict[str, Any]]:
146
165
  return addons
147
166
 
148
167
 
149
- async def resolve_engine_url() -> str:
150
- """Return the base URL of the screenshot engine, or raise ToolError.
168
+ async def resolve_engine() -> EngineTarget:
169
+ """Resolve the screenshot engine endpoint, or raise ToolError.
151
170
 
152
171
  See module docstring for the three-mode resolution order.
153
172
  """
@@ -155,10 +174,13 @@ async def resolve_engine_url() -> str:
155
174
 
156
175
  explicit = (get_global_settings().dashboard_screenshot_engine_url or "").strip()
157
176
  if explicit:
158
- return explicit.rstrip("/")
177
+ return EngineTarget(
178
+ url=explicit.rstrip("/"),
179
+ addon_credential=await _addon_credential_best_effort(),
180
+ )
159
181
 
160
182
  if os.environ.get("SUPERVISOR_TOKEN"):
161
- return await _discover_engine_url_via_supervisor()
183
+ return await _discover_engine_via_supervisor()
162
184
 
163
185
  raise_tool_error(
164
186
  create_error_response(
@@ -178,8 +200,35 @@ async def resolve_engine_url() -> str:
178
200
  raise AssertionError("unreachable: raise_tool_error always raises")
179
201
 
180
202
 
181
- async def _discover_engine_url_via_supervisor() -> str:
182
- """Find the Puppet add-on via the Supervisor and return its internal URL.
203
+ async def _addon_credential_best_effort() -> EngineCredential | None:
204
+ """Best-effort theme-guard credential for an explicitly configured URL.
205
+
206
+ HA OS / Supervised users may set ``HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL``
207
+ to override auto-discovery while still running the Puppet add-on; without
208
+ its token the theme guard would go inactive exactly where the add-on
209
+ credential exists (``_client_credential`` refuses Supervisor-proxy auth).
210
+ Any failure — no Supervisor, no verified add-on, not started — simply
211
+ means "no credential": the explicit URL may point at a non-add-on
212
+ engine, so this must never raise. If the explicit URL targets a
213
+ different engine than the discovered add-on, the guard still only ever
214
+ writes back a value that the discovered add-on's own user changed
215
+ mid-capture, which only that add-on's renders would do — a safe no-op.
216
+ """
217
+ if not os.environ.get("SUPERVISOR_TOKEN"):
218
+ return None
219
+ try:
220
+ return (await _discover_engine_via_supervisor()).addon_credential
221
+ except Exception:
222
+ logger.debug(
223
+ "No Supervisor-discoverable engine credential for the explicit "
224
+ "engine URL; the theme guard stays inactive.",
225
+ exc_info=True,
226
+ )
227
+ return None
228
+
229
+
230
+ async def _discover_engine_via_supervisor() -> EngineTarget:
231
+ """Find the Puppet add-on via the Supervisor and return its endpoint.
183
232
 
184
233
  Requires the ha-mcp add-on's ``manager`` role (already declared) for the
185
234
  read-only ``/addons`` + ``/addons/<slug>/info`` endpoints. Raises a
@@ -229,7 +278,13 @@ async def _discover_engine_url_via_supervisor() -> str:
229
278
  context={"slug": slug},
230
279
  )
231
280
  )
232
- return f"http://{hostname}:{ENGINE_PORT}"
281
+ options = data.get("options")
282
+ return EngineTarget(
283
+ url=f"http://{hostname}:{ENGINE_PORT}",
284
+ addon_credential=addon_credential_from_options(
285
+ options if isinstance(options, dict) else None
286
+ ),
287
+ )
233
288
  except ToolError:
234
289
  raise
235
290
  except (httpx.HTTPError, KeyError, ValueError) as e: