ha-mcp-dev 7.13.0.dev2002__tar.gz → 7.13.0.dev2003__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 (187) hide show
  1. {ha_mcp_dev-7.13.0.dev2002/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev2003}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/__main__.py +215 -14
  4. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/__init__.py +51 -6
  5. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/_handlers_advanced.py +2 -2
  6. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/stdio_settings_sidecar.py +13 -1
  7. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_bug_report.py +18 -9
  8. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  9. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/LICENSE +0 -0
  10. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/MANIFEST.in +0 -0
  11. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/README.md +0 -0
  12. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/setup.cfg +0 -0
  13. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/_pypi_marker +0 -0
  15. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/_version.py +0 -0
  16. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/auth/__init__.py +0 -0
  17. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/auth/consent_form.py +0 -0
  18. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/auth/provider.py +0 -0
  19. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/backup_manager.py +0 -0
  20. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/browser_landing.py +0 -0
  21. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/client/__init__.py +0 -0
  22. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/client/rest_client.py +0 -0
  23. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/client/supervisor_client.py +0 -0
  24. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/client/websocket_client.py +0 -0
  25. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/client/websocket_listener.py +0 -0
  26. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/config.py +0 -0
  27. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  28. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  29. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  30. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  31. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  32. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  33. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/errors.py +0 -0
  34. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/llm_exposure.py +0 -0
  35. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/policy/__init__.py +0 -0
  36. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/policy/approval_queue.py +0 -0
  37. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/policy/evaluator.py +0 -0
  38. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/policy/handlers.py +0 -0
  39. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/policy/middleware.py +0 -0
  40. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/policy/model.py +0 -0
  41. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/policy/persistence.py +0 -0
  42. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/policy/value_sources.py +0 -0
  43. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/py.typed +0 -0
  44. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/read_only.py +0 -0
  45. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  46. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  47. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  48. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  49. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  50. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  51. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  52. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  53. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  54. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  55. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  56. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  57. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  58. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  59. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  60. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  61. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  62. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  63. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  64. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  65. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  66. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  67. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  68. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  69. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  70. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/server.py +0 -0
  71. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  72. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  73. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  74. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  75. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  76. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  77. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  78. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  79. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/_theme.py +0 -0
  80. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  81. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  82. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  83. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  84. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  85. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/settings.css +0 -0
  86. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/settings.html +0 -0
  87. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/settings_ui/settings.js +0 -0
  88. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/smoke_test.py +0 -0
  89. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/strict_bps.py +0 -0
  90. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/__init__.py +0 -0
  91. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/auto_backup.py +0 -0
  92. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/backup.py +0 -0
  93. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  94. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/component_api.py +0 -0
  95. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/component_config_reads.py +0 -0
  96. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/component_devices.py +0 -0
  97. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/component_registries.py +0 -0
  98. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  99. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  100. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/device_control.py +0 -0
  101. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/enhanced.py +0 -0
  102. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/helpers.py +0 -0
  103. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/radio/__init__.py +0 -0
  104. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/radio/base.py +0 -0
  105. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/radio/matter.py +0 -0
  106. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/radio/thread.py +0 -0
  107. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  108. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/radio/zwave.py +0 -0
  109. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/reference_validator.py +0 -0
  110. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/registry.py +0 -0
  111. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  112. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  113. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  114. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  115. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  116. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  117. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  118. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  119. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  120. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  121. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_addons.py +0 -0
  122. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_areas.py +0 -0
  123. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  124. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_calendar.py +0 -0
  125. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_camera.py +0 -0
  126. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_categories.py +0 -0
  127. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_code.py +0 -0
  128. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  129. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  130. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  131. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  132. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  133. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  134. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_dev.py +0 -0
  135. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_energy.py +0 -0
  136. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_entities.py +0 -0
  137. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  138. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_groups.py +0 -0
  139. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_hacs.py +0 -0
  140. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_history.py +0 -0
  141. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_integrations.py +0 -0
  142. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_labels.py +0 -0
  143. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_radio.py +0 -0
  144. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_registry.py +0 -0
  145. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_resources.py +0 -0
  146. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_search.py +0 -0
  147. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_service.py +0 -0
  148. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_services.py +0 -0
  149. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_system.py +0 -0
  150. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_themes.py +0 -0
  151. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_todo.py +0 -0
  152. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_traces.py +0 -0
  153. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_updates.py +0 -0
  154. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_utility.py +0 -0
  155. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  156. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  157. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  158. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/tools_zones.py +0 -0
  159. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/util_helpers.py +0 -0
  160. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/tools/validation_middleware.py +0 -0
  161. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/transforms/__init__.py +0 -0
  162. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/transforms/categorized_search.py +0 -0
  163. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  164. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/transport_security.py +0 -0
  165. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/update_check.py +0 -0
  166. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/utils/__init__.py +0 -0
  167. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/utils/config_hash.py +0 -0
  168. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/utils/data_paths.py +0 -0
  169. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/utils/domain_handlers.py +0 -0
  170. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  171. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  172. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/utils/operation_manager.py +0 -0
  173. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/utils/python_sandbox.py +0 -0
  174. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/utils/skill_loader.py +0 -0
  175. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/utils/usage_logger.py +0 -0
  176. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/visibility/__init__.py +0 -0
  177. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/visibility/model.py +0 -0
  178. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/visibility/persistence.py +0 -0
  179. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp/visibility/resolver.py +0 -0
  180. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  181. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  182. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  183. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  184. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  185. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/tests/__init__.py +0 -0
  186. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/tests/test_constants.py +0 -0
  187. {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2003}/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.dev2002
3
+ Version: 7.13.0.dev2003
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.dev2002"
7
+ version = "7.13.0.dev2003"
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
@@ -25,6 +25,7 @@ import hashlib # noqa: E402
25
25
  import ipaddress # noqa: E402
26
26
  import logging # noqa: E402
27
27
  import os # noqa: E402
28
+ import secrets # noqa: E402
28
29
  import signal # noqa: E402
29
30
  import stat # noqa: E402
30
31
  import sys # noqa: E402
@@ -1050,14 +1051,43 @@ def register_browser_landing(
1050
1051
  )
1051
1052
 
1052
1053
 
1053
- def _log_settings_url(host: str, port: int, path: str) -> None:
1054
+ def _log_settings_url(
1055
+ host: str,
1056
+ port: int,
1057
+ path: str,
1058
+ *,
1059
+ level: int = logging.INFO,
1060
+ note_autogen: bool = False,
1061
+ base_url: str | None = None,
1062
+ ) -> None:
1054
1063
  """Log the web settings-UI URL at HTTP startup.
1055
1064
 
1056
1065
  Non-add-on operators (Docker / standalone) otherwise have no easy way to
1057
1066
  discover the settings page or its secret-path URL (issue #1458). When the
1058
1067
  bind host is the wildcard (``0.0.0.0`` / ``::``) the process can't know its
1059
1068
  externally reachable address, so we log a ``<host>`` placeholder.
1069
+
1070
+ ``level`` and ``note_autogen`` let the OAuth/OIDC dedicated-secret path log
1071
+ an auto-generated URL at WARNING (visible even when internet-facing
1072
+ deployments run at ``LOG_LEVEL=WARNING``), since that path rotates each
1073
+ restart and is the only way for the admin to find it. ``base_url``, when
1074
+ given (OAuth/OIDC modes, where it is the required public front), is used
1075
+ verbatim instead of the local ``host:port`` — those deployments are reached
1076
+ through a tunnel/proxy, not the bind address.
1060
1077
  """
1078
+ if base_url:
1079
+ url = f"{base_url.rstrip('/')}{path.rstrip('/')}/settings"
1080
+ logger.log(
1081
+ level,
1082
+ f"Settings UI available at: {url}"
1083
+ + (
1084
+ " [auto-generated, rotates each restart; "
1085
+ "set MCP_SETTINGS_SECRET_PATH for a stable URL]"
1086
+ if note_autogen
1087
+ else ""
1088
+ ),
1089
+ )
1090
+ return
1061
1091
  is_wildcard = host in ("0.0.0.0", "::")
1062
1092
  if is_wildcard:
1063
1093
  display_host = "<host>"
@@ -1068,7 +1098,158 @@ def _log_settings_url(host: str, port: int, path: str) -> None:
1068
1098
  display_host = host
1069
1099
  url = f"http://{display_host}:{port}{path.rstrip('/')}/settings"
1070
1100
  note = " (substitute this server's address for <host>)" if is_wildcard else ""
1071
- logger.info(f"Settings UI available at: {url}{note}")
1101
+ if note_autogen:
1102
+ note += (
1103
+ " [auto-generated, rotates each restart; "
1104
+ "set MCP_SETTINGS_SECRET_PATH for a stable URL]"
1105
+ )
1106
+ logger.log(level, f"Settings UI available at: {url}{note}")
1107
+
1108
+
1109
+ # Truthy / falsy env-var spellings for HA_MCP_DISABLE_SETTINGS_UI, matching the
1110
+ # parsing in stdio_settings_sidecar.py so the toggle behaves the same across
1111
+ # transports. A value in neither set is unrecognized: fail closed (disable) with
1112
+ # a warning, since this is a security kill switch and failing open on a typo
1113
+ # would leave an unauthenticated surface up.
1114
+ _SETTINGS_TRUTHY = {"1", "true", "yes", "on"}
1115
+ _SETTINGS_FALSY = {"0", "false", "no", "off", ""}
1116
+
1117
+
1118
+ def _settings_ui_disabled() -> bool:
1119
+ """Return True when ``HA_MCP_DISABLE_SETTINGS_UI`` turns the settings UI off.
1120
+
1121
+ Honored by every long-lived HTTP transport (standard ``ha-mcp-web`` /
1122
+ ``ha-mcp-sse`` as well as OAuth/OIDC) and, with matching semantics, by the
1123
+ stdio sidecar. As a security kill switch it fails **closed**: any value that
1124
+ is neither a recognized truthy nor falsy spelling disables the UI and warns
1125
+ (so the operator learns why it vanished), rather than leaving an
1126
+ unauthenticated surface up on a typo. Unset — or an explicit falsy value —
1127
+ keeps the UI served.
1128
+ """
1129
+ disable = os.getenv("HA_MCP_DISABLE_SETTINGS_UI", "").strip().lower()
1130
+ if disable in _SETTINGS_FALSY:
1131
+ return False
1132
+ if disable not in _SETTINGS_TRUTHY:
1133
+ logger.warning(
1134
+ "HA_MCP_DISABLE_SETTINGS_UI=%r is not a recognized on/off value; "
1135
+ "disabling the settings UI (fail-closed). Use one of %s to disable, "
1136
+ "or one of %s to keep it served.",
1137
+ disable,
1138
+ ", ".join(sorted(_SETTINGS_TRUTHY)),
1139
+ ", ".join(sorted(s for s in _SETTINGS_FALSY if s)),
1140
+ )
1141
+ return True
1142
+
1143
+
1144
+ def _resolve_settings_secret_path() -> str | None:
1145
+ """Resolve the dedicated settings-UI secret path for OAuth/OIDC modes.
1146
+
1147
+ These modes are internet-facing, so the settings UI must never sit at the
1148
+ guessable default MCP path, where FastMCP custom routes bypass the OAuth /
1149
+ OIDC auth middleware (GHSA-mx64-982r-65vg). It gets its own secret path:
1150
+
1151
+ - ``HA_MCP_DISABLE_SETTINGS_UI`` truthy → ``None`` (do not mount at all).
1152
+ - ``MCP_SETTINGS_SECRET_PATH`` set → that path (stable across restarts).
1153
+ - neither → an auto-generated ``/private_<token>`` path (logged at startup).
1154
+
1155
+ Returns the path to mount the settings UI under, or ``None`` when disabled.
1156
+ """
1157
+ if _settings_ui_disabled():
1158
+ return None
1159
+ explicit = os.getenv("MCP_SETTINGS_SECRET_PATH", "").strip()
1160
+ if explicit:
1161
+ return explicit
1162
+ return "/private_" + secrets.token_urlsafe(16)
1163
+
1164
+
1165
+ def _register_settings_ui_secret_path(
1166
+ mcp: "FastMCP",
1167
+ server: "HomeAssistantSmartMCPServer",
1168
+ host: str,
1169
+ port: int,
1170
+ mcp_path: str,
1171
+ base_url: str,
1172
+ ) -> None:
1173
+ """Register the settings UI behind a dedicated secret path (OAuth/OIDC modes).
1174
+
1175
+ Standard HTTP mode serves the settings UI under the MCP secret path itself,
1176
+ but in OAuth/OIDC modes that path is known to clients and OAuth/OIDC — not
1177
+ path secrecy — is the auth boundary. So the settings UI gets its own secret
1178
+ path from :func:`_resolve_settings_secret_path`, mounted with
1179
+ ``advertise_prefix=False`` so the path is never returned to MCP clients via
1180
+ ``ha_get_overview``. The admin learns the URL from the startup log.
1181
+
1182
+ ``mcp_path`` is the MCP endpoint path. The settings path is validated against
1183
+ it and rejected (fail closed — UI not mounted) if it is malformed or reuses
1184
+ the MCP path, either of which would re-expose the unauthenticated routes
1185
+ (GHSA-mx64-982r-65vg).
1186
+ """
1187
+ from ha_mcp.settings_ui import register_settings_routes
1188
+
1189
+ settings_path = _resolve_settings_secret_path()
1190
+ if settings_path is None:
1191
+ logger.info(
1192
+ "Settings UI disabled (HA_MCP_DISABLE_SETTINGS_UI); routes not registered."
1193
+ )
1194
+ return
1195
+
1196
+ explicit = bool(os.getenv("MCP_SETTINGS_SECRET_PATH", "").strip())
1197
+
1198
+ # An explicit path may be malformed; the auto-generated /private_<token> is
1199
+ # always well-formed. Starlette asserts routes start with "/", so a missing
1200
+ # leading slash would crash startup — normalize it (and tell the admin).
1201
+ if not settings_path.startswith("/"):
1202
+ logger.warning(
1203
+ "MCP_SETTINGS_SECRET_PATH=%r has no leading '/'; normalizing to %r.",
1204
+ settings_path,
1205
+ "/" + settings_path,
1206
+ )
1207
+ settings_path = "/" + settings_path
1208
+ if "{" in settings_path or "}" in settings_path:
1209
+ # Starlette compiles {name} into a wildcard capture, so a braced path
1210
+ # (an unrendered /private_{token} template, or a literal paste of the
1211
+ # docs' /private_<token> convention) would mount the settings routes as a
1212
+ # publicly-matching wildcard — the exact exposure this guards against.
1213
+ logger.error(
1214
+ "MCP_SETTINGS_SECRET_PATH (%r) contains route-template characters "
1215
+ "('{{'/'}}'), which Starlette would turn into a wildcard route, "
1216
+ "re-exposing the settings UI (GHSA-mx64-982r-65vg). Use a literal path "
1217
+ "or unset it to auto-generate; settings UI not registered.",
1218
+ settings_path,
1219
+ )
1220
+ return
1221
+ if not settings_path.rstrip("/"):
1222
+ logger.error(
1223
+ "MCP_SETTINGS_SECRET_PATH must be a non-empty absolute path like "
1224
+ "'/private_xxx'; settings UI not registered."
1225
+ )
1226
+ return
1227
+ if settings_path.rstrip("/") == mcp_path.rstrip("/"):
1228
+ logger.error(
1229
+ "MCP_SETTINGS_SECRET_PATH (%r) must not equal the MCP path (%r): the "
1230
+ "settings routes bypass OAuth/OIDC auth, so reusing the client-known MCP "
1231
+ "path would re-expose them (GHSA-mx64-982r-65vg). Set a distinct path or "
1232
+ "unset it to auto-generate; settings UI not registered.",
1233
+ settings_path,
1234
+ mcp_path,
1235
+ )
1236
+ return
1237
+
1238
+ register_settings_routes(
1239
+ mcp, server, secret_path=settings_path, advertise_prefix=False
1240
+ )
1241
+ # Auto-generated paths rotate each restart and are the only way the admin can
1242
+ # find the UI, so log them at WARNING to survive LOG_LEVEL=WARNING; an
1243
+ # explicit path the admin already knows stays at INFO. OAuth/OIDC are fronted
1244
+ # by MCP_BASE_URL (a public HTTPS tunnel/proxy), so log that, not host:port.
1245
+ _log_settings_url(
1246
+ host,
1247
+ port,
1248
+ settings_path,
1249
+ level=logging.INFO if explicit else logging.WARNING,
1250
+ note_autogen=not explicit,
1251
+ base_url=base_url,
1252
+ )
1072
1253
 
1073
1254
 
1074
1255
  def _run_http_server(transport: str, default_port: int = 8086) -> None:
@@ -1089,8 +1270,13 @@ def _run_http_server(transport: str, default_port: int = 8086) -> None:
1089
1270
  register_browser_landing(_get_mcp(), path, quiet_probe_log=transport != "sse")
1090
1271
  if _healthz_enabled():
1091
1272
  _register_healthz_route(_get_mcp())
1092
- register_settings_routes(_get_mcp(), _get_server(), secret_path=path)
1093
- _log_settings_url(host, port, path)
1273
+ if _settings_ui_disabled():
1274
+ logger.info(
1275
+ "Settings UI disabled (HA_MCP_DISABLE_SETTINGS_UI); routes not registered."
1276
+ )
1277
+ else:
1278
+ register_settings_routes(_get_mcp(), _get_server(), secret_path=path)
1279
+ _log_settings_url(host, port, path)
1094
1280
 
1095
1281
  _run_entrypoint(
1096
1282
  _run_http_with_graceful_shutdown(transport, host, port, path),
@@ -1107,6 +1293,8 @@ def main_web() -> None:
1107
1293
  - MCP_HOST (optional, default: "0.0.0.0"; set 127.0.0.1 to restrict to loopback)
1108
1294
  - MCP_PORT (optional, default: 8086)
1109
1295
  - MCP_SECRET_PATH (optional, default: "/mcp")
1296
+ - HA_MCP_DISABLE_SETTINGS_UI (optional): set truthy to not serve the web
1297
+ settings UI at all
1110
1298
  """
1111
1299
  _setup_standard_mode()
1112
1300
  _run_http_server("http", default_port=8086)
@@ -1121,6 +1309,8 @@ def main_sse() -> None:
1121
1309
  - MCP_HOST (optional, default: "0.0.0.0"; set 127.0.0.1 to restrict to loopback)
1122
1310
  - MCP_PORT (optional, default: 8087)
1123
1311
  - MCP_SECRET_PATH (optional, default: "/mcp")
1312
+ - HA_MCP_DISABLE_SETTINGS_UI (optional): set truthy to not serve the web
1313
+ settings UI at all
1124
1314
  """
1125
1315
  _setup_standard_mode()
1126
1316
  _run_http_server("sse", default_port=8087)
@@ -1138,7 +1328,12 @@ def main_oauth() -> None:
1138
1328
  - MCP_BASE_URL (required): Public URL where this server is accessible (e.g., https://your-tunnel.com)
1139
1329
  - MCP_HOST (optional, default: "0.0.0.0"; set 127.0.0.1 to restrict to loopback)
1140
1330
  - MCP_PORT (optional, default: 8086)
1141
- - MCP_SECRET_PATH (optional, default: "/mcp")
1331
+ - MCP_SECRET_PATH (optional, default: "/mcp") — the MCP endpoint path only;
1332
+ the settings UI uses its own secret path (below), not this one
1333
+ - MCP_SETTINGS_SECRET_PATH (optional): dedicated secret path for the web
1334
+ settings UI. Auto-generated and logged at startup when unset.
1335
+ - HA_MCP_DISABLE_SETTINGS_UI (optional): set truthy to not serve the
1336
+ settings UI at all
1142
1337
  - LOG_LEVEL (optional, default: INFO)
1143
1338
 
1144
1339
  Note: HOMEASSISTANT_TOKEN is NOT required in this mode.
@@ -1243,10 +1438,9 @@ async def _run_oauth_server(
1243
1438
  if _healthz_enabled():
1244
1439
  _register_healthz_route(mcp)
1245
1440
 
1246
- from ha_mcp.settings_ui import register_settings_routes
1247
-
1248
- register_settings_routes(mcp, _server, secret_path=path)
1249
- _log_settings_url(host, port, path)
1441
+ # The settings UI must not sit at the OAuth-known MCP path (custom routes
1442
+ # bypass the OAuth auth middleware); mount it behind a dedicated secret path.
1443
+ _register_settings_ui_secret_path(mcp, _server, host, port, path, base_url)
1250
1444
 
1251
1445
  tools = await mcp.list_tools()
1252
1446
  logger.info(
@@ -1294,7 +1488,12 @@ def main_oidc() -> None:
1294
1488
  - MCP_HOST (optional, default: "0.0.0.0"; set 127.0.0.1 to restrict to loopback
1295
1489
  behind a same-host reverse proxy)
1296
1490
  - MCP_PORT (optional, default: 8086)
1297
- - MCP_SECRET_PATH (optional, default: "/mcp")
1491
+ - MCP_SECRET_PATH (optional, default: "/mcp") — the MCP endpoint path only;
1492
+ the settings UI uses its own secret path (below), not this one
1493
+ - MCP_SETTINGS_SECRET_PATH (optional): dedicated secret path for the web
1494
+ settings UI. Auto-generated and logged at startup when unset.
1495
+ - HA_MCP_DISABLE_SETTINGS_UI (optional): set truthy to not serve the
1496
+ settings UI at all
1298
1497
  - LOG_LEVEL (optional, default: INFO)
1299
1498
  """
1300
1499
  # Configure logging for OIDC mode (force=True needed since logging may already be configured)
@@ -1432,10 +1631,12 @@ async def _run_oidc_server(
1432
1631
  mcp_instance.auth = auth
1433
1632
 
1434
1633
  logger.info("Server created with OIDC authentication")
1435
- # Unlike standard/OAuth HTTP modes, OIDC mode intentionally registers no
1436
- # browser landing page and no settings-UI sidecar: an unauthenticated
1437
- # browser GET gets a bare 405 rather than a friendly page, and there is
1438
- # no settings UI surface here.
1634
+ # OIDC mode intentionally registers no browser landing page: an
1635
+ # unauthenticated browser GET gets a bare 405 rather than a friendly page.
1636
+ # The settings UI *is* served, but only behind a dedicated secret path
1637
+ # (never the OIDC-known MCP path, whose custom routes bypass the auth
1638
+ # middleware — GHSA-mx64-982r-65vg).
1639
+ _register_settings_ui_secret_path(mcp_instance, _server, host, port, path, base_url)
1439
1640
  if _healthz_enabled():
1440
1641
  _register_healthz_route(mcp_instance)
1441
1642
  logger.info(f"Starting OIDC-enabled MCP server at {base_url}{path}")
@@ -520,16 +520,34 @@ def get_http_settings_prefix() -> str | None:
520
520
  """Return the settings-UI mount prefix for HTTP transports, or None.
521
521
 
522
522
  Set by :func:`register_settings_routes` when the page is mounted on a
523
- long-lived HTTP server. ``ha_get_overview`` reads it to hint at the
524
- settings page when there is no stdio sidecar URL to hand the user.
523
+ long-lived HTTP server *and* advertising is enabled. ``ha_get_overview``
524
+ reads it to hint at the settings page when there is no stdio sidecar URL to
525
+ hand the user. It is None when the mount is deliberately not advertised
526
+ (OAuth/OIDC dedicated-secret mode) — use :func:`is_http_settings_mounted`,
527
+ not this, to tell HTTP from the stdio sidecar.
525
528
  """
526
529
  return _http_settings_prefix
527
530
 
528
531
 
532
+ # Whether the settings UI is served over HTTP (add-on / Docker / OAuth / OIDC)
533
+ # as opposed to the stdio sidecar. Distinct from _http_settings_prefix, which is
534
+ # the *advertised* URL and is None when a mount is deliberately hidden from MCP
535
+ # clients (advertise_prefix=False). Consumers that only need "is this stdio?"
536
+ # must read this, not the prefix (GHSA-mx64-982r-65vg fix left the prefix None
537
+ # on a real HTTP mount).
538
+ _http_settings_mounted: bool = False
539
+
540
+
541
+ def is_http_settings_mounted() -> bool:
542
+ """Return True when the settings UI is HTTP-mounted (not the stdio sidecar)."""
543
+ return _http_settings_mounted
544
+
545
+
529
546
  def register_settings_routes(
530
547
  mcp: FastMCP,
531
548
  server: HomeAssistantSmartMCPServer,
532
549
  secret_path: str = "",
550
+ advertise_prefix: bool = True,
533
551
  ) -> None:
534
552
  """Register the settings UI HTTP routes on the FastMCP Starlette app.
535
553
 
@@ -550,9 +568,27 @@ def register_settings_routes(
550
568
  ``/mcp``). Required for non-add-on HTTP modes; if empty in
551
569
  non-add-on mode, the function logs a warning and registers
552
570
  nothing rather than expose the routes publicly.
571
+ advertise_prefix: When True (default), record the secret-path mount in
572
+ ``_http_settings_prefix`` so ``ha_get_overview`` can hint at the
573
+ settings URL. OAuth/OIDC modes pass False: there the settings UI
574
+ sits under a *dedicated* secret path that must never be handed to
575
+ MCP clients (GHSA-mx64-982r-65vg), so the mount happens but the
576
+ prefix is not recorded.
553
577
  """
554
578
  handlers = build_settings_handlers(server)
555
579
  secret_prefix = secret_path.rstrip("/") if secret_path else ""
580
+ if secret_prefix and ("{" in secret_prefix or "}" in secret_prefix):
581
+ # Starlette compiles {name} into a [^/]+ wildcard capture, so a braced
582
+ # secret path (e.g. an unrendered /private_{token} template) would mount
583
+ # these routes at a publicly-matching wildcard. Drop the secret mount
584
+ # rather than expose it (GHSA-mx64-982r-65vg); the add-on root mount below
585
+ # is unaffected.
586
+ logger.error(
587
+ "settings UI secret path %r contains route-template characters "
588
+ "('{{'/'}}'); not mounting under it (it would become a wildcard route).",
589
+ secret_prefix,
590
+ )
591
+ secret_prefix = ""
556
592
  is_addon = is_running_in_addon()
557
593
 
558
594
  if not is_addon and not secret_prefix:
@@ -563,6 +599,12 @@ def register_settings_routes(
563
599
  )
564
600
  return
565
601
 
602
+ # Past this point at least one HTTP mount happens (add-on root and/or the
603
+ # secret path). Record that so consumers can distinguish HTTP from the stdio
604
+ # sidecar even when the prefix itself is not advertised (advertise_prefix=False).
605
+ global _http_settings_mounted
606
+ _http_settings_mounted = True
607
+
566
608
  # Every route this function mounts except the add-on-only root mount is defined
567
609
  # once in this table and mounted under each active prefix below: at root
568
610
  # in add-on mode (so HA ingress can proxy localhost:9583/), and under the
@@ -636,7 +678,10 @@ def register_settings_routes(
636
678
  # need the same secret to reach the UI as they do for the MCP
637
679
  # endpoint.
638
680
  _mount(secret_prefix)
639
- # Record the mount so ha_get_overview can point users at the settings
640
- # page in HTTP transports that have no stdio sidecar URL file (#1458).
641
- global _http_settings_prefix
642
- _http_settings_prefix = secret_prefix
681
+ if advertise_prefix:
682
+ # Record the mount so ha_get_overview can point users at the
683
+ # settings page in HTTP transports that have no stdio sidecar URL
684
+ # file (#1458). Suppressed in OAuth/OIDC modes, where the dedicated
685
+ # secret path must not leak to MCP clients (GHSA-mx64-982r-65vg).
686
+ global _http_settings_prefix
687
+ _http_settings_prefix = secret_prefix
@@ -136,7 +136,7 @@ async def _get_advanced_settings(
136
136
  and saves route through Supervisor so the add-on Configuration tab and
137
137
  the web UI share state.
138
138
  """
139
- from ha_mcp.settings_ui import get_http_settings_prefix
139
+ from ha_mcp.settings_ui import is_http_settings_mounted
140
140
 
141
141
  from ..config import ADVANCED_SETTINGS_FIELDS, _read_feature_flag_override_file
142
142
 
@@ -172,7 +172,7 @@ async def _get_advanced_settings(
172
172
  {
173
173
  "fields": fields,
174
174
  "is_addon": is_running_in_addon(),
175
- "is_stdio": get_http_settings_prefix() is None,
175
+ "is_stdio": not is_http_settings_mounted(),
176
176
  }
177
177
  )
178
178
 
@@ -61,6 +61,7 @@ logger = logging.getLogger(__name__)
61
61
  # importing from the tools subpackage because this module is loaded
62
62
  # during early stdio startup, before any tool code is touched.
63
63
  _TRUTHY = {"1", "true", "yes", "on"}
64
+ _FALSY = {"0", "false", "no", "off", ""}
64
65
 
65
66
 
66
67
  def _sidecar_dir() -> Path:
@@ -109,7 +110,18 @@ def read_sidecar_url() -> str | None:
109
110
 
110
111
  def _is_disabled() -> bool:
111
112
  """Check whether the sidecar should be skipped."""
112
- if os.environ.get("HA_MCP_DISABLE_SETTINGS_UI", "").strip().lower() in _TRUTHY:
113
+ val = os.environ.get("HA_MCP_DISABLE_SETTINGS_UI", "").strip().lower()
114
+ if val in _TRUTHY:
115
+ return True
116
+ if val and val not in _FALSY:
117
+ # Fail closed on an unrecognized value (same semantics as the HTTP
118
+ # transports' _settings_ui_disabled), and warn so the operator sees why
119
+ # the settings UI never came up.
120
+ logger.warning(
121
+ "HA_MCP_DISABLE_SETTINGS_UI=%r is not a recognized on/off value; "
122
+ "skipping the settings-UI sidecar (fail-closed).",
123
+ val,
124
+ )
113
125
  return True
114
126
  return _disabled_sentinel().exists()
115
127
 
@@ -443,15 +443,24 @@ def _sanitize_log_text(text: str) -> str:
443
443
  # configured value (catches custom paths) and the generated
444
444
  # ``/private_<token>`` convention. Only bug-report output is scrubbed here;
445
445
  # the raw logs this text is copied from are left intact.
446
- configured = os.getenv("MCP_SECRET_PATH", "").rstrip("/")
447
- if configured and configured != "/mcp":
448
- # Anchor on a path-segment boundary so a short/substring-prone configured
449
- # value (e.g. "/ha") cannot corrupt unrelated text (e.g. "/happy").
450
- text = re.sub(
451
- re.escape(configured) + r"(?![A-Za-z0-9_-])",
452
- "[REDACTED_SECRET_PATH]",
453
- text,
454
- )
446
+ # The dedicated settings-UI secret path (OAuth/OIDC) is a second secret whose
447
+ # leak grants the same unauthenticated settings access, so redact a custom
448
+ # value the same way. The auto-generated /private_<token> form is caught by
449
+ # the generic pattern below.
450
+ for env_name in ("MCP_SECRET_PATH", "MCP_SETTINGS_SECRET_PATH"):
451
+ # Strip before rstrip: the resolver strips MCP_SETTINGS_SECRET_PATH before
452
+ # mounting, so a whitespace-bearing value mounts at the stripped path — the
453
+ # redaction pattern must match that, not the raw value (GHSA-mx64-982r-65vg).
454
+ configured = os.getenv(env_name, "").strip().rstrip("/")
455
+ if configured and configured != "/mcp":
456
+ # Anchor on a path-segment boundary so a short/substring-prone
457
+ # configured value (e.g. "/ha") cannot corrupt unrelated text
458
+ # (e.g. "/happy").
459
+ text = re.sub(
460
+ re.escape(configured) + r"(?![A-Za-z0-9_-])",
461
+ "[REDACTED_SECRET_PATH]",
462
+ text,
463
+ )
455
464
  text = re.sub(r"/private_[A-Za-z0-9_-]+", "[REDACTED_SECRET_PATH]", text)
456
465
  return text
457
466
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.13.0.dev2002
3
+ Version: 7.13.0.dev2003
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