ha-mcp-dev 7.12.3.dev1922__tar.gz → 7.12.3.dev1924__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 (178) hide show
  1. {ha_mcp_dev-7.12.3.dev1922/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1924}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/pyproject.toml +1 -2
  3. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/client/supervisor_client.py +2 -2
  4. ha_mcp_dev-7.12.3.dev1924/src/ha_mcp/settings_ui/__init__.py +599 -0
  5. ha_mcp_dev-7.12.3.dev1924/src/ha_mcp/settings_ui/_handlers_advanced.py +599 -0
  6. ha_mcp_dev-7.12.3.dev1924/src/ha_mcp/settings_ui/_handlers_backups.py +523 -0
  7. ha_mcp_dev-7.12.3.dev1924/src/ha_mcp/settings_ui/_handlers_fs.py +204 -0
  8. ha_mcp_dev-7.12.3.dev1924/src/ha_mcp/settings_ui/_handlers_server.py +774 -0
  9. ha_mcp_dev-7.12.3.dev1924/src/ha_mcp/settings_ui/_handlers_theme.py +89 -0
  10. ha_mcp_dev-7.12.3.dev1924/src/ha_mcp/settings_ui/_handlers_tools.py +299 -0
  11. ha_mcp_dev-7.12.3.dev1924/src/ha_mcp/settings_ui/_persistence.py +333 -0
  12. ha_mcp_dev-7.12.3.dev1924/src/ha_mcp/settings_ui/_supervisor.py +294 -0
  13. ha_mcp_dev-7.12.3.dev1924/src/ha_mcp/settings_ui/_theme.py +133 -0
  14. ha_mcp_dev-7.12.3.dev1924/src/ha_mcp/settings_ui/_tools_meta.py +332 -0
  15. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_dev.py +8 -5
  16. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  17. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp_dev.egg-info/SOURCES.txt +10 -0
  18. ha_mcp_dev-7.12.3.dev1922/src/ha_mcp/settings_ui/__init__.py +0 -3716
  19. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/LICENSE +0 -0
  20. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/MANIFEST.in +0 -0
  21. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/README.md +0 -0
  22. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/setup.cfg +0 -0
  23. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/__init__.py +0 -0
  24. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/__main__.py +0 -0
  25. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/_pypi_marker +0 -0
  26. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/_version.py +0 -0
  27. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/auth/__init__.py +0 -0
  28. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/auth/consent_form.py +0 -0
  29. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/auth/provider.py +0 -0
  30. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/backup_manager.py +0 -0
  31. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/browser_landing.py +0 -0
  32. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/client/__init__.py +0 -0
  33. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/client/rest_client.py +0 -0
  34. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/client/websocket_client.py +0 -0
  35. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/client/websocket_listener.py +0 -0
  36. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/config.py +0 -0
  37. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  38. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  39. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  40. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  41. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  42. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/errors.py +0 -0
  43. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/llm_exposure.py +0 -0
  44. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/policy/__init__.py +0 -0
  45. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/policy/approval_queue.py +0 -0
  46. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/policy/evaluator.py +0 -0
  47. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/policy/handlers.py +0 -0
  48. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/policy/middleware.py +0 -0
  49. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/policy/model.py +0 -0
  50. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/policy/persistence.py +0 -0
  51. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/policy/value_sources.py +0 -0
  52. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/py.typed +0 -0
  53. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/read_only.py +0 -0
  54. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  55. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  56. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  57. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  58. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  59. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  60. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  61. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  62. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  63. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  64. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  65. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  66. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  67. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  68. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  69. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  70. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  71. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  72. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  73. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  74. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  75. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  76. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  77. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  78. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  79. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/server.py +0 -0
  80. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/settings_ui/settings.css +0 -0
  81. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/settings_ui/settings.html +0 -0
  82. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/settings_ui/settings.js +0 -0
  83. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/smoke_test.py +0 -0
  84. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  85. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/strict_bps.py +0 -0
  86. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/__init__.py +0 -0
  87. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/auto_backup.py +0 -0
  88. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/backup.py +0 -0
  89. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  90. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/component_api.py +0 -0
  91. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  92. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/device_control.py +0 -0
  93. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/enhanced.py +0 -0
  94. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/helpers.py +0 -0
  95. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/radio/__init__.py +0 -0
  96. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/radio/base.py +0 -0
  97. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/radio/matter.py +0 -0
  98. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/radio/thread.py +0 -0
  99. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  100. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/radio/zwave.py +0 -0
  101. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/reference_validator.py +0 -0
  102. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/registry.py +0 -0
  103. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  104. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  105. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  106. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  107. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  108. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  109. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  110. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  111. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  112. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  113. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_addons.py +0 -0
  114. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_areas.py +0 -0
  115. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  116. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  117. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_calendar.py +0 -0
  118. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_camera.py +0 -0
  119. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_categories.py +0 -0
  120. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_code.py +0 -0
  121. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  122. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  123. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  124. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  125. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  126. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  127. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_energy.py +0 -0
  128. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_entities.py +0 -0
  129. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  130. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_groups.py +0 -0
  131. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_hacs.py +0 -0
  132. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_history.py +0 -0
  133. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_integrations.py +0 -0
  134. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_labels.py +0 -0
  135. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  136. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_radio.py +0 -0
  137. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_registry.py +0 -0
  138. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_resources.py +0 -0
  139. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_search.py +0 -0
  140. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_service.py +0 -0
  141. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_services.py +0 -0
  142. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_system.py +0 -0
  143. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_themes.py +0 -0
  144. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_todo.py +0 -0
  145. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_traces.py +0 -0
  146. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_updates.py +0 -0
  147. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_utility.py +0 -0
  148. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  149. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  150. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/tools_zones.py +0 -0
  151. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/util_helpers.py +0 -0
  152. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/tools/validation_middleware.py +0 -0
  153. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/transforms/__init__.py +0 -0
  154. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/transforms/categorized_search.py +0 -0
  155. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  156. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/transport_security.py +0 -0
  157. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/update_check.py +0 -0
  158. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/utils/__init__.py +0 -0
  159. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/utils/config_hash.py +0 -0
  160. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/utils/data_paths.py +0 -0
  161. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/utils/domain_handlers.py +0 -0
  162. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  163. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  164. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/utils/operation_manager.py +0 -0
  165. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/utils/python_sandbox.py +0 -0
  166. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/utils/skill_loader.py +0 -0
  167. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/utils/usage_logger.py +0 -0
  168. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/visibility/__init__.py +0 -0
  169. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/visibility/model.py +0 -0
  170. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/visibility/persistence.py +0 -0
  171. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp/visibility/resolver.py +0 -0
  172. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  173. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  174. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  175. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  176. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/tests/__init__.py +0 -0
  177. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/tests/test_constants.py +0 -0
  178. {ha_mcp_dev-7.12.3.dev1922 → ha_mcp_dev-7.12.3.dev1924}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.12.3.dev1922
3
+ Version: 7.12.3.dev1924
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.12.3.dev1922"
7
+ version = "7.12.3.dev1924"
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
@@ -175,7 +175,6 @@ ignore = [
175
175
  "homeassistant-addon-webhook-proxy/start.py" = ["C901"]
176
176
  "scripts/extract_tools.py" = ["C901"]
177
177
  "src/ha_mcp/backup_manager.py" = ["C901"]
178
- "src/ha_mcp/settings_ui/__init__.py" = ["C901"]
179
178
  "src/ha_mcp/tools/auto_backup.py" = ["C901"]
180
179
  "src/ha_mcp/tools/backup.py" = ["C901"]
181
180
  "src/ha_mcp/tools/radio/matter.py" = ["C901"]
@@ -9,8 +9,8 @@ Supervisor — different base URL, different token, different role gate):
9
9
  — fetches addon and system-service logs
10
10
  - :func:`ha_mcp.tools.tools_bug_report._fetch_addon_logs` — bundles ha-mcp's
11
11
  own addon logs into a bug-report payload
12
- - :func:`ha_mcp.settings_ui._restart_addon` — POSTs ``/addons/self/restart``
13
- from the settings UI
12
+ - :func:`ha_mcp.settings_ui._handlers_server._restart_addon` — POSTs
13
+ ``/addons/self/restart`` from the settings UI
14
14
 
15
15
  All three share the same boilerplate (base URL, ``Authorization: Bearer
16
16
  ${SUPERVISOR_TOKEN}`` header), so this module supplies a single factory and
@@ -0,0 +1,599 @@
1
+ """Web-based settings UI for tool visibility configuration.
2
+
3
+ Serves a self-contained HTML page at /settings that lets users enable,
4
+ disable, and pin MCP tools. Changes apply immediately without server
5
+ restart. Persists to a JSON config file alongside the MCP server data.
6
+
7
+ Works across all installation methods (add-on, Docker, standalone).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import functools
13
+ import html
14
+ import json
15
+ import logging
16
+ from collections.abc import Awaitable, Callable
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ from starlette.requests import Request
21
+ from starlette.responses import HTMLResponse, JSONResponse, Response
22
+
23
+ from .._version import is_running_in_addon
24
+ from ..transforms import DEFAULT_PINNED_TOOLS
25
+ from ..utils.data_paths import get_data_dir
26
+ from ._handlers_advanced import build_advanced_handlers
27
+ from ._handlers_backups import build_backups_handlers
28
+ from ._handlers_fs import build_fs_handlers
29
+ from ._handlers_server import (
30
+ _PROCESS_INSTANCE_ID,
31
+ _PROCESS_STARTED_AT,
32
+ build_server_handlers,
33
+ )
34
+ from ._handlers_theme import build_theme_handlers
35
+ from ._handlers_tools import build_tools_handlers
36
+ from ._persistence import (
37
+ _atomic_write_json,
38
+ _get_backup_settings_override_path,
39
+ _get_config_path,
40
+ _get_override_file_lock,
41
+ _load_backup_settings_override,
42
+ _save_backup_settings_override,
43
+ dump_tool_metadata_cache,
44
+ effective_tool_config,
45
+ env_pinned_tools,
46
+ load_tool_config,
47
+ load_tool_metadata_cache,
48
+ save_tool_config,
49
+ )
50
+ from ._supervisor import (
51
+ _BACKGROUND_RESTART_TASKS,
52
+ _SUPERVISOR_SELF_RESTART_FLUSH_DELAY_S,
53
+ _schedule_supervisor_self_restart,
54
+ _supervisor_fetch_current_options,
55
+ _supervisor_merge_and_post_options,
56
+ _SupervisorOptionsError,
57
+ )
58
+ from ._theme import _load_theme_prefs, _sanitize_theme_prefs
59
+ from ._tools_meta import (
60
+ FEATURE_GATED_TOOLS,
61
+ MANDATORY_TOOLS,
62
+ TRANSFORM_GENERATED_TOOLS,
63
+ ToolStub,
64
+ UserToolStateOverrides,
65
+ _get_tool_metadata,
66
+ apply_tool_visibility,
67
+ )
68
+
69
+ if TYPE_CHECKING:
70
+ from fastmcp import FastMCP
71
+
72
+ from ..config import Settings
73
+ from ..server import HomeAssistantSmartMCPServer
74
+
75
+
76
+ logger = logging.getLogger(__name__)
77
+
78
+ # The settings-UI client script lives in settings.js (a real file for
79
+ # editor/JS tooling). It is a template with two sentinel tokens for the
80
+ # Python-injected constant lists; substitute them with the same values the
81
+ # inline literal used so the rendered HTML is byte-identical. Injected
82
+ # inline (not served) -- the serving model is unchanged.
83
+ #
84
+ # This is a module-import-time file read: importing settings_ui (done by
85
+ # server.py / __main__.py / the sidecar) now depends on settings.js being
86
+ # present. If packaging drops it, fail with a packaging-specific ImportError
87
+ # rather than a bare FileNotFoundError so the cause is obvious.
88
+ _SETTINGS_JS_PATH = Path(__file__).parent / "settings.js"
89
+ try:
90
+ _settings_js_template = _SETTINGS_JS_PATH.read_text(encoding="utf-8")
91
+ except OSError as exc: # pragma: no cover - packaging guard
92
+ raise ImportError(
93
+ f"settings.js missing at {_SETTINGS_JS_PATH}. It must ship in "
94
+ "package-data (wheel), MANIFEST.in (sdist), and the PyInstaller datas "
95
+ "(binary) -- this is a packaging bug, not a usage error."
96
+ ) from exc
97
+ # str.replace() silently no-ops on an absent token, and a *renamed* sentinel
98
+ # (e.g. PINNED_DEFAULTS) slips past both the "__HA_MCP_" not-in test and the
99
+ # esbuild/jsdom JS harness (tests/js/harness.mjs) -- `const DEFAULT_PINNED =
100
+ # PINNED_DEFAULTS;` is valid JS (only a runtime ReferenceError), so a drifted
101
+ # settings.js would ship a broken page green. Assert both sentinels are present
102
+ # before substituting.
103
+ for _sentinel in ("__HA_MCP_DEFAULT_PINNED__", "__HA_MCP_MANDATORY__"):
104
+ if _sentinel not in _settings_js_template:
105
+ raise ImportError(
106
+ f"settings.js is out of sync: sentinel {_sentinel} not found. "
107
+ "The Python injection and settings.js have drifted."
108
+ )
109
+ # sorted(), not list(): DEFAULT_PINNED_TOOLS / MANDATORY_TOOLS are sets, so
110
+ # json.dumps(list(...)) is per-process-ordered -- the only reason proving the
111
+ # original extraction byte-identical needed PYTHONHASHSEED pinned. sorted()
112
+ # makes the two injected arrays deterministic across processes.
113
+ _SETTINGS_JS = _settings_js_template.replace(
114
+ "__HA_MCP_DEFAULT_PINNED__", json.dumps(sorted(DEFAULT_PINNED_TOOLS))
115
+ ).replace("__HA_MCP_MANDATORY__", json.dumps(sorted(MANDATORY_TOOLS)))
116
+
117
+
118
+ # The settings-UI CSS lives in settings.css, extracted the same way as
119
+ # settings.js. Unlike the JS it has no Python injection points -- a plain
120
+ # read, no token substitution -- and is injected inline between the same
121
+ # <style>/</style> tags so the served page stays byte-identical. It carries
122
+ # the same import-time packaging dependency as settings.js, so the same
123
+ # OSError -> ImportError packaging guard applies.
124
+ _SETTINGS_CSS_PATH = Path(__file__).parent / "settings.css"
125
+ try:
126
+ _SETTINGS_CSS = _SETTINGS_CSS_PATH.read_text(encoding="utf-8")
127
+ except OSError as exc: # pragma: no cover - packaging guard
128
+ raise ImportError(
129
+ f"settings.css missing at {_SETTINGS_CSS_PATH}. It must ship in "
130
+ "package-data (wheel), MANIFEST.in (sdist), and the PyInstaller datas "
131
+ "(binary) -- this is a packaging bug, not a usage error."
132
+ ) from exc
133
+
134
+
135
+ # The settings page HTML lives in settings.html, extracted the same way as
136
+ # settings.js / settings.css (a real file for editor/HTML tooling). It carries
137
+ # three substitution markers — two filled once at import, one per request:
138
+ # __HA_MCP_CSS__ -> settings.css contents (inside <style>)
139
+ # __HA_MCP_JS__ -> settings.js contents (inside <script>)
140
+ # __HA_MCP_THEME_PREFS__ -> per-request server-seeded theme prefs JSON,
141
+ # substituted in _render_settings_html()
142
+ # Same import-time packaging dependency as settings.js/css (wheel package-data,
143
+ # MANIFEST.in, PyInstaller datas) and the same OSError guard -- but this loader
144
+ # raises RuntimeError, not the ImportError that settings.js/css raise.
145
+ _SETTINGS_HTML_PATH = Path(__file__).parent / "settings.html"
146
+ try:
147
+ _settings_html_template = _SETTINGS_HTML_PATH.read_text(encoding="utf-8")
148
+ except OSError as exc: # pragma: no cover - packaging guard
149
+ raise RuntimeError(
150
+ f"settings.html missing at {_SETTINGS_HTML_PATH}. It must ship in "
151
+ "package-data (wheel), MANIFEST.in (sdist), and the PyInstaller datas "
152
+ "(binary) -- this is a packaging bug, not a usage error."
153
+ ) from exc
154
+
155
+ # Fail fast if a marker was renamed in settings.html but not here (or vice
156
+ # versa) — str.replace() silently no-ops on an absent token, which would ship a
157
+ # page missing its CSS, JS, or server-seeded theme prefs. Assert all three
158
+ # markers are present.
159
+ for _sentinel in ("__HA_MCP_CSS__", "__HA_MCP_JS__", "__HA_MCP_THEME_PREFS__"):
160
+ if _sentinel not in _settings_html_template:
161
+ raise RuntimeError(
162
+ f"settings.html is out of sync: sentinel {_sentinel} not found. "
163
+ "The Python injection and settings.html have drifted."
164
+ )
165
+
166
+ # CSS and JS are injected once at import; __HA_MCP_THEME_PREFS__ remains in the
167
+ # string and is substituted per-request in _render_settings_html().
168
+ _SETTINGS_HTML = _settings_html_template.replace(
169
+ "__HA_MCP_CSS__", _SETTINGS_CSS
170
+ ).replace("__HA_MCP_JS__", _SETTINGS_JS)
171
+
172
+
173
+ def _build_stub_policy_handlers(*, data_dir: Path) -> dict[str, Any]:
174
+ """Sidecar variant of the tool security policies handlers.
175
+
176
+ Serves policy config GET/PUT (the on-disk policy file is shared with
177
+ the main server), but returns 503 for pending/approve/deny — those
178
+ routes touch the in-memory ``ApprovalQueue`` which only exists in
179
+ the main server process.
180
+ """
181
+ from pydantic import ValidationError
182
+
183
+ from ..policy.model import Policy
184
+ from ..policy.persistence import load_policy, save_policy
185
+
186
+ async def get_config(_: Request) -> JSONResponse:
187
+ try:
188
+ return JSONResponse(load_policy(data_dir).model_dump(mode="json"))
189
+ except ValueError as e:
190
+ # Mirror the main-server handler: surface corruption rather
191
+ # than crash the sidecar tab on a 500.
192
+ return JSONResponse(
193
+ {"error": str(e), "policy_file_corrupt": True},
194
+ status_code=500,
195
+ )
196
+
197
+ async def put_config(request: Request) -> JSONResponse:
198
+ try:
199
+ new_policy = Policy.model_validate(await request.json())
200
+ except (ValidationError, ValueError) as e:
201
+ return JSONResponse({"error": str(e)}, status_code=400)
202
+ # Mirror main-server optimistic concurrency: reject if on-disk
203
+ # version moved between this caller's GET and PUT.
204
+ current = load_policy(data_dir)
205
+ if new_policy.version != current.version:
206
+ return JSONResponse(
207
+ {
208
+ "error": "Policy version mismatch. Reload before saving.",
209
+ "current_version": current.version,
210
+ "current_policy": current.model_dump(mode="json"),
211
+ },
212
+ status_code=409,
213
+ )
214
+ save_policy(data_dir, new_policy)
215
+ return JSONResponse({"saved": True, "version": new_policy.version + 1})
216
+
217
+ async def unavailable(_: Request) -> JSONResponse:
218
+ # 503 fires in two distinct situations:
219
+ # 1. The Tool Security Policies feature is turned off in
220
+ # addon config — the middleware never registered, so no
221
+ # approval queue exists.
222
+ # 2. The settings UI is running via the stdio sidecar — the
223
+ # in-memory queue lives in the main server process which
224
+ # isn't reachable from the sidecar.
225
+ # Either way, point users at the addon log for the real reason
226
+ # (a startup ImportError on the policy package surfaces here as
227
+ # the same 503 with a "ModuleNotFoundError" in the log).
228
+ return JSONResponse(
229
+ {
230
+ "error": (
231
+ "Tool security policies live approvals are not active. "
232
+ "Either the feature is turned off in App (add-on) config, the "
233
+ "settings UI is running in stdio-sidecar mode, or the "
234
+ "policy package failed to import at startup. Check the "
235
+ "App (add-on) log for ImportError / RuntimeError details if you "
236
+ "expected gating to be on."
237
+ )
238
+ },
239
+ status_code=503,
240
+ )
241
+
242
+ return {
243
+ "policy_get_config": get_config,
244
+ "policy_put_config": put_config,
245
+ "policy_get_pending": unavailable,
246
+ "policy_post_approve": unavailable,
247
+ "policy_post_deny": unavailable,
248
+ "policy_get_tool_schema": unavailable,
249
+ "policy_get_value_source": unavailable,
250
+ }
251
+
252
+
253
+ def _build_visibility_handlers(*, data_dir: Path) -> dict[str, Any]:
254
+ """Settings-UI handlers for the entity visibility filter config.
255
+
256
+ Serves the on-disk ``entity_visibility.json`` GET/PUT with the same
257
+ optimistic-concurrency (version) guard as the tool policy handlers. Pure
258
+ file I/O, so it is always available (no approval-queue dependency).
259
+ """
260
+ from pydantic import ValidationError
261
+
262
+ from ..visibility.model import VisibilityConfig
263
+ from ..visibility.persistence import (
264
+ load_visibility_config,
265
+ save_visibility_config,
266
+ )
267
+
268
+ async def get_config(_: Request) -> JSONResponse:
269
+ try:
270
+ return JSONResponse(
271
+ load_visibility_config(data_dir).model_dump(mode="json")
272
+ )
273
+ except ValueError as e:
274
+ # Surface corruption rather than crash the tab on a 500.
275
+ return JSONResponse(
276
+ {"error": str(e), "visibility_file_corrupt": True},
277
+ status_code=500,
278
+ )
279
+
280
+ async def put_config(request: Request) -> JSONResponse:
281
+ try:
282
+ new_config = VisibilityConfig.model_validate(await request.json())
283
+ except (ValidationError, ValueError) as e:
284
+ return JSONResponse({"error": str(e)}, status_code=400)
285
+ # Optimistic concurrency: reject if the on-disk version moved between
286
+ # this caller's GET and PUT.
287
+ current = load_visibility_config(data_dir)
288
+ if new_config.version != current.version:
289
+ return JSONResponse(
290
+ {
291
+ "error": (
292
+ "Visibility config version mismatch. Reload before saving."
293
+ ),
294
+ "current_version": current.version,
295
+ "current_config": current.model_dump(mode="json"),
296
+ },
297
+ status_code=409,
298
+ )
299
+ save_visibility_config(data_dir, new_config)
300
+ return JSONResponse({"saved": True, "version": new_config.version + 1})
301
+
302
+ return {
303
+ "visibility_get_config": get_config,
304
+ "visibility_put_config": put_config,
305
+ }
306
+
307
+
308
+ def _render_settings_html() -> str:
309
+ """Substitute the persisted theme prefs into the served settings page.
310
+
311
+ The ``server-prefs`` head script carries a ``data-prefs`` attribute
312
+ with a placeholder token; request-time substitution keeps
313
+ ``_SETTINGS_HTML`` a static import-time constant (and keeps the script
314
+ body itself parseable at rest for the script-surface tests). Values
315
+ are sanitized enums / vetted hex colors and the JSON is HTML-escaped,
316
+ so the attribute cannot break out of its quoting context.
317
+ """
318
+ payload = html.escape(
319
+ json.dumps(_load_theme_prefs(), separators=(",", ":")), quote=True
320
+ )
321
+ rendered = _SETTINGS_HTML.replace("__HA_MCP_THEME_PREFS__", payload, 1)
322
+ if "__HA_MCP_THEME_PREFS__" in rendered:
323
+ # str.replace silently no-ops when the placeholder vanishes in a
324
+ # refactor; the unit contract test catches that in CI, this line
325
+ # catches it loudly in a live deployment instead of quietly
326
+ # serving a page without server-side prefs.
327
+ logger.error(
328
+ "server-prefs placeholder was not substituted; theme prefs "
329
+ "will not survive fresh origins"
330
+ )
331
+ return rendered
332
+
333
+
334
+ def build_settings_handlers(
335
+ server: HomeAssistantSmartMCPServer | None,
336
+ *,
337
+ is_sidecar: bool = False,
338
+ ) -> dict[str, Any]:
339
+ """Construct the settings UI route handlers.
340
+
341
+ When ``server`` is provided (HTTP modes), the tools list and restart
342
+ handler use the live FastMCP server / Supervisor client. When
343
+ ``server`` is ``None`` (stdio sidecar process, which has no live MCP
344
+ server), the tools list is read from the on-disk metadata cache and
345
+ the restart handler returns 400 (the sidecar is not an add-on).
346
+
347
+ ``is_sidecar`` forces the ``settings_info`` handler to report
348
+ ``is_addon=False`` regardless of the inherited ``SUPERVISOR_TOKEN``
349
+ env var. The sidecar process inherits parent env unchanged
350
+ (``subprocess.Popen`` with default ``env=None``), so if the parent
351
+ stdio process happens to run under Supervisor (e.g. an interactive
352
+ debug shell inside the add-on container) the served HTML would
353
+ otherwise show the "Restart Add-on" button that POSTs to a route
354
+ the sidecar doesn't expose, surfacing as a broken UI. The sidecar
355
+ is *by construction* not the add-on entrypoint — pin the flag
356
+ accordingly.
357
+
358
+ Returns a dict mapping handler names to async Starlette handlers.
359
+ Both ``register_settings_routes`` (FastMCP mounting) and the stdio
360
+ sidecar's standalone Starlette app consume the same set of handlers
361
+ so the served page is identical regardless of transport.
362
+ """
363
+
364
+ async def _root_page(_: Request) -> HTMLResponse:
365
+ return HTMLResponse(_render_settings_html())
366
+
367
+ async def _settings_page(_: Request) -> HTMLResponse:
368
+ return HTMLResponse(_render_settings_html())
369
+
370
+ handlers: dict[str, Any] = {
371
+ "root_page": _root_page,
372
+ "settings_page": _settings_page,
373
+ }
374
+
375
+ # Tool security policies. The main server attaches an
376
+ # ApprovalQueue to the server object once PolicyMiddleware is wired
377
+ # in. Only the main server can serve the live pending/approve/deny
378
+ # endpoints because the queue is in-memory; the sidecar (or a main
379
+ # server without the queue attribute yet) falls back to stub handlers
380
+ # that serve config GET/PUT and return 503 for live approval routes.
381
+ approval_queue = (
382
+ getattr(server, "approval_queue", None) if server is not None else None
383
+ )
384
+ if not is_sidecar and approval_queue is not None:
385
+ from ..policy.handlers import build_policy_handlers
386
+
387
+ handlers.update(
388
+ build_policy_handlers(
389
+ data_dir=get_data_dir(),
390
+ queue=approval_queue,
391
+ server=server,
392
+ )
393
+ )
394
+ else:
395
+ handlers.update(_build_stub_policy_handlers(data_dir=get_data_dir()))
396
+
397
+ handlers.update(_build_visibility_handlers(data_dir=get_data_dir()))
398
+ handlers.update(build_theme_handlers())
399
+ handlers.update(build_fs_handlers(server))
400
+ handlers.update(build_tools_handlers(server))
401
+ handlers.update(build_backups_handlers(server))
402
+ handlers.update(build_server_handlers(server, is_sidecar=is_sidecar))
403
+ handlers.update(build_advanced_handlers(server))
404
+
405
+ return handlers
406
+
407
+
408
+ # Home Assistant proxies every ingress request ("Open Web UI") from the
409
+ # Supervisor's fixed network address. Per the add-on ingress contract
410
+ # (https://developers.home-assistant.io/docs/add-ons/presentation/#ingress —
411
+ # "Only connections from 172.30.32.2 must be allowed") the app must reject
412
+ # every other source. This holds under host_network too: ingress proxies to
413
+ # http://{app.ip_address}:{ingress_port}/, and for a host-network add-on
414
+ # app.ip_address is the hassio bridge gateway 172.30.32.1 — the DESTINATION the
415
+ # Supervisor dials (supervisor/docker/app.py ip_address(): host_network ->
416
+ # network.gateway). The Supervisor opens that connection from its own container
417
+ # address 172.30.32.2, so the transport peer the add-on sees is 172.30.32.2 for
418
+ # genuine ingress and some other address (a LAN host, the cloudflared tunnel at
419
+ # 172.30.33.x, another add-on) for a direct port-9583 hit. Verified live via
420
+ # netstat during an "Open Web UI" click.
421
+ SUPERVISOR_INGRESS_IP = "172.30.32.2"
422
+
423
+ # A settings-UI route handler: async (Request) -> Response.
424
+ _SettingsRoute = Callable[[Request], Awaitable[Response]]
425
+
426
+
427
+ def _ingress_only(handler: _SettingsRoute) -> _SettingsRoute:
428
+ """Wrap a root-mounted add-on route so only HA ingress can reach it.
429
+
430
+ Add-on root routes carry no MCP secret, so without this guard a direct
431
+ caller on the published port — a LAN peer, a reverse proxy / tunnel
432
+ forwarding the bare root, or a CSRF POST from a LAN browser — could
433
+ rewrite tool config, flip the tool-security-policy, or restart the
434
+ add-on with no authentication. We gate on the *transport* peer
435
+ (``request.client.host``), never ``X-Forwarded-For`` (which a caller can
436
+ forge). The same handlers stay reachable under ``secret_prefix``, where
437
+ the MCP secret path is the auth for direct/remote access.
438
+ """
439
+
440
+ @functools.wraps(handler)
441
+ async def _guarded(request: Request) -> Response:
442
+ peer = request.client.host if request.client else None
443
+ if peer != SUPERVISOR_INGRESS_IP:
444
+ logger.warning(
445
+ "Blocked non-ingress request to add-on root route %s from "
446
+ "peer %r (only the Supervisor at %s may reach root routes; "
447
+ "use the MCP secret path for direct/remote access).",
448
+ request.url.path,
449
+ peer,
450
+ SUPERVISOR_INGRESS_IP,
451
+ )
452
+ return JSONResponse(
453
+ {
454
+ "error": (
455
+ "This endpoint is only reachable through Home "
456
+ "Assistant ingress. For direct or remote access, use "
457
+ "the settings UI under your MCP secret path."
458
+ )
459
+ },
460
+ status_code=403,
461
+ )
462
+ return await handler(request)
463
+
464
+ return _guarded
465
+
466
+
467
+ # Mount prefix the settings UI is served under in long-lived HTTP transports
468
+ # (Docker / standalone ha-mcp-web / OAuth / the add-on's secret-path mount).
469
+ # Recorded by register_settings_routes so ha_get_overview can point users at
470
+ # the settings page in modes that have no stdio sidecar URL file to surface
471
+ # (issue #1458). Stays None in pure stdio mode, where the sidecar writes
472
+ # ~/.ha-mcp/ui.url instead.
473
+ _http_settings_prefix: str | None = None
474
+
475
+
476
+ def get_http_settings_prefix() -> str | None:
477
+ """Return the settings-UI mount prefix for HTTP transports, or None.
478
+
479
+ Set by :func:`register_settings_routes` when the page is mounted on a
480
+ long-lived HTTP server. ``ha_get_overview`` reads it to hint at the
481
+ settings page when there is no stdio sidecar URL to hand the user.
482
+ """
483
+ return _http_settings_prefix
484
+
485
+
486
+ def register_settings_routes(
487
+ mcp: FastMCP,
488
+ server: HomeAssistantSmartMCPServer,
489
+ secret_path: str = "",
490
+ ) -> None:
491
+ """Register the settings UI HTTP routes on the FastMCP Starlette app.
492
+
493
+ The routes are mounted under ``secret_path`` so HTTP clients (Docker
494
+ / standalone) need the same secret to reach the UI as they do to
495
+ reach the MCP endpoint itself — there's no native auth on FastMCP
496
+ custom routes (they bypass ``RequireAuthMiddleware``), so this
497
+ matches the auth-by-obscurity model the rest of the server uses for
498
+ those modes. In add-on mode (``SUPERVISOR_TOKEN`` set) the routes
499
+ are *also* mounted at root so HA ingress can proxy to ``localhost:9583/``
500
+ and serve the "Open Web UI" button. Stdio transports use a separate
501
+ side-process sidecar instead — see :mod:`ha_mcp.stdio_settings_sidecar`.
502
+
503
+ Args:
504
+ mcp: The FastMCP instance to register routes on.
505
+ server: The HomeAssistantSmartMCPServer wrapping ``mcp``.
506
+ secret_path: The MCP secret path (e.g. ``/private_xxx`` or
507
+ ``/mcp``). Required for non-add-on HTTP modes; if empty in
508
+ non-add-on mode, the function logs a warning and registers
509
+ nothing rather than expose the routes publicly.
510
+ """
511
+ handlers = build_settings_handlers(server)
512
+ secret_prefix = secret_path.rstrip("/") if secret_path else ""
513
+ is_addon = is_running_in_addon()
514
+
515
+ if not is_addon and not secret_prefix:
516
+ logger.warning(
517
+ "register_settings_routes: not in add-on mode and no secret_path "
518
+ "provided — settings UI HTTP routes not registered (would otherwise "
519
+ "be publicly reachable). Pass MCP_SECRET_PATH or run as add-on."
520
+ )
521
+ return
522
+
523
+ # Every route this function mounts except the add-on-only root mount is defined
524
+ # once in this table and mounted under each active prefix below: at root
525
+ # in add-on mode (so HA ingress can proxy localhost:9583/), and under the
526
+ # secret path when one is set (Docker / standalone direct access). A
527
+ # deployment hits either, both, or — guarded above — neither. Deriving
528
+ # the mounts from one table keeps them from drifting; the frontend uses
529
+ # relative fetches (./api/settings/...) so the handlers work at any prefix.
530
+ routes: list[tuple[str, list[str], str]] = [
531
+ ("/settings", ["GET"], "settings_page"),
532
+ ("/api/settings/tools", ["GET"], "get_tools"),
533
+ ("/api/settings/tools", ["POST"], "save_tools"),
534
+ ("/api/settings/restart", ["POST"], "restart_addon"),
535
+ ("/api/settings/info", ["GET"], "settings_info"),
536
+ ("/api/settings/features", ["GET"], "get_feature_flags"),
537
+ ("/api/settings/features", ["POST"], "save_feature_flags"),
538
+ # Theme / accessibility prefs (#1574 review) — server-side copy so
539
+ # they survive the stdio sidecar's per-spawn origin change
540
+ ("/api/settings/theme", ["GET"], "get_theme_prefs"),
541
+ ("/api/settings/theme", ["POST"], "save_theme_prefs"),
542
+ # Advanced settings endpoints
543
+ ("/api/settings/advanced", ["GET"], "get_advanced_settings"),
544
+ ("/api/settings/advanced", ["POST"], "save_advanced_settings"),
545
+ # Auto-backup endpoints (#1288)
546
+ ("/api/settings/backups", ["GET"], "list_backups"),
547
+ ("/api/settings/backups", ["DELETE"], "delete_backups_bulk"),
548
+ ("/api/settings/backups/{name}", ["GET"], "view_backup"),
549
+ ("/api/settings/backups/{name}/diff", ["GET"], "diff_backup"),
550
+ ("/api/settings/backups/{name}/restore", ["POST"], "restore_backup"),
551
+ ("/api/settings/backups/{name}", ["DELETE"], "delete_backup"),
552
+ ("/api/settings/backup-config", ["GET"], "get_backup_config"),
553
+ ("/api/settings/backup-config", ["POST"], "save_backup_config"),
554
+ # Custom filesystem directories (issue #1567) — component-owned list
555
+ ("/api/settings/fs-custom-paths", ["GET"], "get_fs_custom_paths"),
556
+ ("/api/settings/fs-custom-paths", ["POST"], "save_fs_custom_paths"),
557
+ # Tool security policies endpoints
558
+ ("/api/policy/config", ["GET"], "policy_get_config"),
559
+ ("/api/policy/config", ["PUT"], "policy_put_config"),
560
+ ("/api/policy/pending", ["GET"], "policy_get_pending"),
561
+ ("/api/policy/approve", ["POST"], "policy_post_approve"),
562
+ ("/api/policy/deny", ["POST"], "policy_post_deny"),
563
+ ("/api/policy/tool-schema", ["GET"], "policy_get_tool_schema"),
564
+ ("/api/policy/value-source", ["GET"], "policy_get_value_source"),
565
+ # Entity visibility filter endpoints (issue #1728)
566
+ ("/api/visibility/config", ["GET"], "visibility_get_config"),
567
+ ("/api/visibility/config", ["PUT"], "visibility_put_config"),
568
+ ]
569
+
570
+ def _mount(prefix: str, *, guard: bool = False) -> None:
571
+ # guard=True wraps each handler in _ingress_only so the route only
572
+ # answers HA ingress (the Supervisor) — used for the add-on root
573
+ # mount, whose port 9583 is reachable without the MCP secret.
574
+ for path, methods, handler_key in routes:
575
+ handler = handlers[handler_key]
576
+ if guard:
577
+ handler = _ingress_only(handler)
578
+ mcp.custom_route(f"{prefix}{path}", methods=methods)(handler)
579
+
580
+ if is_addon:
581
+ # Root mount lets HA ingress proxy localhost:9583/ → the settings UI
582
+ # ("Open Web UI" button). The published port 9583 also makes these
583
+ # routes reachable by direct callers that present no MCP secret, so
584
+ # the root mount is gated with _ingress_only: only the Supervisor
585
+ # (HA ingress, 172.30.32.2) may reach root; every other caller gets
586
+ # 403 and must use the secret-path mount below. The "Open Web UI"
587
+ # button is unaffected — its traffic arrives from the Supervisor.
588
+ mcp.custom_route("/", methods=["GET"])(_ingress_only(handlers["root_page"]))
589
+ _mount("", guard=True)
590
+
591
+ if secret_prefix:
592
+ # Mount under the MCP secret path so Docker / standalone clients
593
+ # need the same secret to reach the UI as they do for the MCP
594
+ # endpoint.
595
+ _mount(secret_prefix)
596
+ # Record the mount so ha_get_overview can point users at the settings
597
+ # page in HTTP transports that have no stdio sidecar URL file (#1458).
598
+ global _http_settings_prefix
599
+ _http_settings_prefix = secret_prefix