ha-mcp-dev 7.13.0.dev1973__tar.gz → 7.13.0.dev1977__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 (185) hide show
  1. {ha_mcp_dev-7.13.0.dev1973/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev1977}/PKG-INFO +2 -1
  2. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/README.md +1 -0
  3. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/pyproject.toml +2 -1
  4. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/__main__.py +220 -0
  5. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/backup_manager.py +63 -21
  6. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/backup.py +149 -20
  7. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/component_api.py +85 -16
  8. ha_mcp_dev-7.13.0.dev1977/src/ha_mcp/tools/component_config_reads.py +212 -0
  9. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/component_devices.py +41 -6
  10. ha_mcp_dev-7.13.0.dev1977/src/ha_mcp/tools/component_registries.py +144 -0
  11. ha_mcp_dev-7.13.0.dev1977/src/ha_mcp/tools/component_registry_lookup.py +161 -0
  12. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/radio/base.py +12 -3
  13. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/reference_validator.py +24 -4
  14. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_areas.py +80 -55
  15. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_blueprints.py +12 -1
  16. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_config_automations.py +31 -2
  17. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_config_dashboards.py +768 -16
  18. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_config_helpers.py +58 -8
  19. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_config_scenes.py +71 -4
  20. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_dev.py +133 -16
  21. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_entities.py +13 -3
  22. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_integrations.py +413 -58
  23. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_search.py +158 -40
  24. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_services.py +114 -5
  25. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_system.py +293 -28
  26. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_voice_assistant.py +16 -4
  27. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_zones.py +19 -3
  28. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/util_helpers.py +31 -8
  29. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/visibility/model.py +27 -0
  30. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/visibility/resolver.py +39 -0
  31. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977/src/ha_mcp_dev.egg-info}/PKG-INFO +2 -1
  32. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp_dev.egg-info/SOURCES.txt +3 -0
  33. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp_dev.egg-info/entry_points.txt +1 -0
  34. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/LICENSE +0 -0
  35. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/MANIFEST.in +0 -0
  36. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/setup.cfg +0 -0
  37. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/__init__.py +0 -0
  38. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/_pypi_marker +0 -0
  39. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/_version.py +0 -0
  40. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/auth/__init__.py +0 -0
  41. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/auth/consent_form.py +0 -0
  42. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/auth/provider.py +0 -0
  43. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/browser_landing.py +0 -0
  44. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/client/__init__.py +0 -0
  45. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/client/rest_client.py +0 -0
  46. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/client/supervisor_client.py +0 -0
  47. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/client/websocket_client.py +0 -0
  48. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/client/websocket_listener.py +0 -0
  49. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/config.py +0 -0
  50. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  51. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  52. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  53. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  54. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  55. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/errors.py +0 -0
  56. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/llm_exposure.py +0 -0
  57. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/__init__.py +0 -0
  58. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/approval_queue.py +0 -0
  59. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/evaluator.py +0 -0
  60. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/handlers.py +0 -0
  61. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/middleware.py +0 -0
  62. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/model.py +0 -0
  63. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/persistence.py +0 -0
  64. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/value_sources.py +0 -0
  65. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/py.typed +0 -0
  66. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/read_only.py +0 -0
  67. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  68. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  69. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  70. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  71. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  72. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  73. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  74. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  75. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  76. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  77. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  78. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  79. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  80. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  81. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  82. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  83. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  84. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  85. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  86. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  87. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  88. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  89. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  90. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  91. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  92. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/server.py +0 -0
  93. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/__init__.py +0 -0
  94. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  95. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  96. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  97. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  98. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  99. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  100. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  101. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  102. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  103. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_theme.py +0 -0
  104. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  105. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  106. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  107. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  108. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/settings.css +0 -0
  109. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/settings.html +0 -0
  110. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/settings.js +0 -0
  111. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/smoke_test.py +0 -0
  112. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  113. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/strict_bps.py +0 -0
  114. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/__init__.py +0 -0
  115. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/auto_backup.py +0 -0
  116. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  117. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  118. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/device_control.py +0 -0
  119. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/enhanced.py +0 -0
  120. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/helpers.py +0 -0
  121. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/radio/__init__.py +0 -0
  122. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/radio/matter.py +0 -0
  123. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/radio/thread.py +0 -0
  124. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  125. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/radio/zwave.py +0 -0
  126. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/registry.py +0 -0
  127. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  128. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  129. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  130. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  131. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  132. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  133. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  134. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  135. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  136. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  137. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_addons.py +0 -0
  138. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  139. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_calendar.py +0 -0
  140. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_camera.py +0 -0
  141. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_categories.py +0 -0
  142. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_code.py +0 -0
  143. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  144. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  145. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_energy.py +0 -0
  146. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  147. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_groups.py +0 -0
  148. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_hacs.py +0 -0
  149. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_history.py +0 -0
  150. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_labels.py +0 -0
  151. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_radio.py +0 -0
  152. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_registry.py +0 -0
  153. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_resources.py +0 -0
  154. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_service.py +0 -0
  155. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_themes.py +0 -0
  156. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_todo.py +0 -0
  157. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_traces.py +0 -0
  158. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_updates.py +0 -0
  159. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_utility.py +0 -0
  160. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  161. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  162. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/validation_middleware.py +0 -0
  163. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/transforms/__init__.py +0 -0
  164. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/transforms/categorized_search.py +0 -0
  165. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  166. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/transport_security.py +0 -0
  167. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/update_check.py +0 -0
  168. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/__init__.py +0 -0
  169. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/config_hash.py +0 -0
  170. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/data_paths.py +0 -0
  171. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/domain_handlers.py +0 -0
  172. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  173. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  174. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/operation_manager.py +0 -0
  175. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/python_sandbox.py +0 -0
  176. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/skill_loader.py +0 -0
  177. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/usage_logger.py +0 -0
  178. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/visibility/__init__.py +0 -0
  179. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/visibility/persistence.py +0 -0
  180. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  181. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  182. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  183. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/tests/__init__.py +0 -0
  184. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/tests/test_constants.py +0 -0
  185. {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/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.dev1973
3
+ Version: 7.13.0.dev1977
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
@@ -118,6 +118,7 @@ These run the server outside Home Assistant — useful for **Container** / **Cor
118
118
  - **Docker (HTTP server):** run `ghcr.io/homeassistant-ai/ha-mcp` in HTTP mode, pointed at your Home Assistant URL and a long-lived token, and connect your client to its secret URL. See the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/) for the full command and per-client config.
119
119
  - **PyPI / uvx (HTTP server):** run the published `ha-mcp` package with `uvx ha-mcp@latest` (or pip) as a streamable-HTTP server the same way. Details in the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/).
120
120
  - **Local stdio (not recommended):** runs ha-mcp on your own machine over stdio. The one-command installers in the **Demo server** section below use this path; the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/) covers connecting it to your own Home Assistant.
121
+ - **OIDC authentication:** gate remote access behind an external identity provider (Authentik, Keycloak, Auth0, Google, etc.) instead of a secret URL — all authenticated users share the server's Home Assistant credentials. See [OIDC Mode](docs/oidc.md).
121
122
 
122
123
  > ⚠️ **stdio has known transport issues.** The stdio transport has connection problems that streamable HTTP does not ([#1713](https://github.com/homeassistant-ai/ha-mcp/issues/1713)). It is recommended only for demo/testing tinkering — for a real setup, use the custom component or an HTTP method above.
123
124
 
@@ -85,6 +85,7 @@ These run the server outside Home Assistant — useful for **Container** / **Cor
85
85
  - **Docker (HTTP server):** run `ghcr.io/homeassistant-ai/ha-mcp` in HTTP mode, pointed at your Home Assistant URL and a long-lived token, and connect your client to its secret URL. See the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/) for the full command and per-client config.
86
86
  - **PyPI / uvx (HTTP server):** run the published `ha-mcp` package with `uvx ha-mcp@latest` (or pip) as a streamable-HTTP server the same way. Details in the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/).
87
87
  - **Local stdio (not recommended):** runs ha-mcp on your own machine over stdio. The one-command installers in the **Demo server** section below use this path; the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/) covers connecting it to your own Home Assistant.
88
+ - **OIDC authentication:** gate remote access behind an external identity provider (Authentik, Keycloak, Auth0, Google, etc.) instead of a secret URL — all authenticated users share the server's Home Assistant credentials. See [OIDC Mode](docs/oidc.md).
88
89
 
89
90
  > ⚠️ **stdio has known transport issues.** The stdio transport has connection problems that streamable HTTP does not ([#1713](https://github.com/homeassistant-ai/ha-mcp/issues/1713)). It is recommended only for demo/testing tinkering — for a real setup, use the custom component or an HTTP method above.
90
91
 
@@ -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.dev1973"
7
+ version = "7.13.0.dev1977"
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
@@ -59,6 +59,7 @@ hamcp-test-env = "tests.test_env_manager:main"
59
59
  ha-mcp-web = "ha_mcp.__main__:main_web"
60
60
  ha-mcp-sse = "ha_mcp.__main__:main_sse"
61
61
  ha-mcp-oauth = "ha_mcp.__main__:main_oauth"
62
+ ha-mcp-oidc = "ha_mcp.__main__:main_oidc"
62
63
 
63
64
  [tool.setuptools]
64
65
  package-dir = {"" = "src", "tests" = "tests"}
@@ -979,6 +979,39 @@ def _healthz_enabled() -> bool:
979
979
  return os.getenv("MCP_HEALTHZ", "").strip().lower() in ("1", "true", "yes", "on")
980
980
 
981
981
 
982
+ def _oidc_verify_id_token_enabled() -> bool:
983
+ """True when OIDC_VERIFY_ID_TOKEN opts in to ID-token verification.
984
+
985
+ Off by default: FastMCP's OIDCProxy verifies the access token as a JWT,
986
+ which works for providers like Authentik and Keycloak. Providers that
987
+ issue opaque access tokens (Google always; Auth0 without an API
988
+ audience) need ``verify_id_token=True`` so FastMCP verifies the ID token
989
+ instead.
990
+ """
991
+ return os.getenv("OIDC_VERIFY_ID_TOKEN", "").strip().lower() in (
992
+ "1",
993
+ "true",
994
+ "yes",
995
+ "on",
996
+ )
997
+
998
+
999
+ def _oidc_allowed_client_redirect_uris() -> list[str] | None:
1000
+ """Parse OIDC_ALLOWED_CLIENT_REDIRECT_URIS into a list, or None if unset.
1001
+
1002
+ Comma-separated list of redirect URI patterns FastMCP's OIDCProxy will
1003
+ accept from dynamically-registered clients. With no allow-list set,
1004
+ FastMCP still validates a client's redirect URI against that same
1005
+ client's own registered redirect URIs — the exposure is that open DCR
1006
+ lets an attacker register their own client with their own redirect URI;
1007
+ internet-facing deployments should set this to constrain what any
1008
+ dynamically-registered client may register in the first place.
1009
+ """
1010
+ raw = os.getenv("OIDC_ALLOWED_CLIENT_REDIRECT_URIS", "")
1011
+ uris = [uri.strip() for uri in raw.split(",") if uri.strip()]
1012
+ return uris or None
1013
+
1014
+
982
1015
  def register_browser_landing(
983
1016
  mcp_instance: "FastMCP | _DeferredMCP",
984
1017
  path: str,
@@ -1225,5 +1258,192 @@ async def _run_oauth_server(
1225
1258
  )
1226
1259
 
1227
1260
 
1261
+ def main_oidc() -> None:
1262
+ """Run server with OIDC authentication over HTTP.
1263
+
1264
+ This mode enables authentication via an external OIDC provider
1265
+ (Authentik, Keycloak, Auth0, Google, etc.). All authenticated users
1266
+ share the same Home Assistant instance via the configured credentials.
1267
+
1268
+ Unlike OAuth mode which collects per-user HA credentials via a consent form,
1269
+ OIDC mode is purely an access gate — users authenticate through the OIDC provider,
1270
+ then all requests use the shared HA credentials from environment variables.
1271
+
1272
+ Environment:
1273
+ - OIDC_CONFIG_URL (required): OIDC discovery URL (.well-known/openid-configuration)
1274
+ - OIDC_CLIENT_ID (required): OAuth client ID registered with your OIDC provider
1275
+ - OIDC_CLIENT_SECRET (required): OAuth client secret from your OIDC provider
1276
+ - MCP_BASE_URL (required): Public HTTPS URL where this server is accessible
1277
+ - HOMEASSISTANT_URL (required): Home Assistant instance URL
1278
+ - HOMEASSISTANT_TOKEN (required): Home Assistant long-lived access token or supervisor token
1279
+ - OIDC_JWT_SIGNING_KEY (optional): Secret key for signing FastMCP JWTs. Sessions
1280
+ persist across restarts by default (the key is derived from OIDC_CLIENT_SECRET);
1281
+ set this to decouple the signing key from the client secret. Generate with:
1282
+ python -c "import secrets; print(secrets.token_urlsafe(32))"
1283
+ - OIDC_ALLOWED_CLIENT_REDIRECT_URIS (optional): Comma-separated list of redirect URI
1284
+ patterns accepted from dynamically-registered clients. Strongly recommended for
1285
+ internet-facing deployments.
1286
+ - OIDC_VERIFY_ID_TOKEN (optional, default: false): Set true for providers that issue
1287
+ opaque access tokens (e.g. Google, or Auth0 without an API audience).
1288
+ - OIDC_AUDIENCE (optional): Expected `aud` claim for IdP-issued access tokens.
1289
+ Without it (and with OIDC_VERIFY_ID_TOKEN off), FastMCP's JWT verifier checks
1290
+ issuer, signature, and expiry but not audience. With OIDC_VERIFY_ID_TOKEN=true,
1291
+ verification pins `aud` to OIDC_CLIENT_ID instead and this value is only
1292
+ forwarded to the IdP's authorize/token endpoints (it is what makes Auth0
1293
+ issue JWT rather than opaque access tokens).
1294
+ - MCP_HOST (optional, default: "0.0.0.0"; set 127.0.0.1 to restrict to loopback
1295
+ behind a same-host reverse proxy)
1296
+ - MCP_PORT (optional, default: 8086)
1297
+ - MCP_SECRET_PATH (optional, default: "/mcp")
1298
+ - LOG_LEVEL (optional, default: INFO)
1299
+ """
1300
+ # Configure logging for OIDC mode (force=True needed since logging may already be configured)
1301
+ log_level = os.getenv("LOG_LEVEL", "INFO").upper()
1302
+ _setup_logging(log_level, force=True)
1303
+ for logger_name in ["ha_mcp", "ha_mcp.auth", "ha_mcp.auth.provider"]:
1304
+ logging.getLogger(logger_name).setLevel(getattr(logging, log_level))
1305
+ logger.info(f"OIDC mode logging configured at {log_level} level")
1306
+ _log_startup_version()
1307
+
1308
+ # Validate HA credentials (OIDC mode needs them — unlike OAuth mode)
1309
+ from ha_mcp.config import get_settings
1310
+
1311
+ settings = get_settings()
1312
+ _validate_standard_credentials(settings)
1313
+
1314
+ # Validate OIDC-specific env vars
1315
+ oidc_config_url = os.getenv("OIDC_CONFIG_URL")
1316
+ oidc_client_id = os.getenv("OIDC_CLIENT_ID")
1317
+ oidc_client_secret = os.getenv("OIDC_CLIENT_SECRET")
1318
+ base_url = os.getenv("MCP_BASE_URL")
1319
+
1320
+ missing = []
1321
+ if not oidc_config_url:
1322
+ missing.append("OIDC_CONFIG_URL")
1323
+ if not oidc_client_id:
1324
+ missing.append("OIDC_CLIENT_ID")
1325
+ if not oidc_client_secret:
1326
+ missing.append("OIDC_CLIENT_SECRET")
1327
+ if not base_url:
1328
+ missing.append("MCP_BASE_URL")
1329
+
1330
+ if missing:
1331
+ logger.error(
1332
+ f"Missing required environment variables for OIDC mode: {', '.join(missing)}"
1333
+ )
1334
+ logger.error(
1335
+ "OIDC mode requires an external OIDC provider (Authentik, Keycloak, Auth0, etc.)"
1336
+ )
1337
+ sys.exit(1)
1338
+
1339
+ assert oidc_config_url is not None
1340
+ assert oidc_client_id is not None
1341
+ assert oidc_client_secret is not None
1342
+ assert base_url is not None
1343
+
1344
+ host, port, path = _get_http_runtime(default_port=8086)
1345
+
1346
+ _run_entrypoint(
1347
+ _run_oidc_server(
1348
+ config_url=oidc_config_url,
1349
+ client_id=oidc_client_id,
1350
+ client_secret=oidc_client_secret,
1351
+ base_url=base_url,
1352
+ host=host,
1353
+ port=port,
1354
+ path=path,
1355
+ ),
1356
+ "OIDC server",
1357
+ )
1358
+
1359
+
1360
+ async def _run_oidc_server(
1361
+ config_url: str,
1362
+ client_id: str,
1363
+ client_secret: str,
1364
+ base_url: str,
1365
+ host: str,
1366
+ port: int,
1367
+ path: str,
1368
+ ) -> None:
1369
+ """Run the OIDC-authenticated MCP server.
1370
+
1371
+ Unlike OAuth mode which uses OAuthProxyClient for per-user credential routing,
1372
+ OIDC mode uses the standard HomeAssistantClient with shared credentials.
1373
+ OIDC is purely an access gate — all authenticated users share the same HA instance.
1374
+
1375
+ Args:
1376
+ config_url: OIDC discovery URL (.well-known/openid-configuration)
1377
+ client_id: OAuth client ID from the OIDC provider
1378
+ client_secret: OAuth client secret from the OIDC provider
1379
+ base_url: Public HTTPS URL where this server is accessible
1380
+ host: Bind host (typically 0.0.0.0; override via MCP_HOST)
1381
+ port: Port to listen on
1382
+ path: MCP endpoint path
1383
+ """
1384
+ from fastmcp.server.auth.oidc_proxy import OIDCProxy
1385
+
1386
+ from ha_mcp.server import HomeAssistantSmartMCPServer
1387
+ from ha_mcp.transport_security import ensure_host_origin_guard_default_off
1388
+
1389
+ # OIDC mode is deployed behind a public reverse proxy or LAN hostname;
1390
+ # disable FastMCP's Host/Origin rebinding guard so proxied requests are
1391
+ # not rejected with 421/403 before OIDC auth runs.
1392
+ ensure_host_origin_guard_default_off()
1393
+
1394
+ proxy_kwargs: dict[str, Any] = {
1395
+ "config_url": config_url,
1396
+ "client_id": client_id,
1397
+ "client_secret": client_secret,
1398
+ "base_url": base_url,
1399
+ # "external" tells FastMCP that consent is handled by the upstream
1400
+ # IdP (Authentik, Keycloak, etc.) -- unlike `False`, this does not
1401
+ # log a security warning at startup that consent is disabled.
1402
+ "require_authorization_consent": "external",
1403
+ # Preserve `or None`: an empty-but-set env var must not bypass
1404
+ # FastMCP's derive-from-client-secret default for jwt_signing_key.
1405
+ "jwt_signing_key": os.getenv("OIDC_JWT_SIGNING_KEY") or None,
1406
+ }
1407
+
1408
+ allowed_redirect_uris = _oidc_allowed_client_redirect_uris()
1409
+ if allowed_redirect_uris:
1410
+ proxy_kwargs["allowed_client_redirect_uris"] = allowed_redirect_uris
1411
+ else:
1412
+ logger.warning(
1413
+ "OIDC_ALLOWED_CLIENT_REDIRECT_URIS is not set: any dynamically-registered "
1414
+ "(DCR) client's own redirect URIs are accepted. Internet-facing "
1415
+ "deployments should set it."
1416
+ )
1417
+
1418
+ if _oidc_verify_id_token_enabled():
1419
+ proxy_kwargs["verify_id_token"] = True
1420
+
1421
+ audience = os.getenv("OIDC_AUDIENCE", "").strip()
1422
+ if audience:
1423
+ proxy_kwargs["audience"] = audience
1424
+
1425
+ # Create OIDC auth provider — auto-discovers endpoints from config_url
1426
+ auth = OIDCProxy(**proxy_kwargs)
1427
+
1428
+ # Standard server with shared credentials (no proxy client needed)
1429
+ global _server
1430
+ _server = HomeAssistantSmartMCPServer()
1431
+ mcp_instance = _server.mcp
1432
+ mcp_instance.auth = auth
1433
+
1434
+ 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.
1439
+ if _healthz_enabled():
1440
+ _register_healthz_route(mcp_instance)
1441
+ logger.info(f"Starting OIDC-enabled MCP server at {base_url}{path}")
1442
+
1443
+ await _run_with_shutdown(
1444
+ mcp_instance.run_async(**_http_run_kwargs("http", host, port, path))
1445
+ )
1446
+
1447
+
1228
1448
  if __name__ == "__main__":
1229
1449
  main()
@@ -1296,14 +1296,18 @@ async def _restore_scene(client: Any, entity_id: str, config: Any) -> Any:
1296
1296
  async def _fetch_dashboard(client: Any, entity_id: str) -> Any:
1297
1297
  """Fetch a dashboard config via the same helper the get tool uses.
1298
1298
 
1299
- Delegates to ``tools_config_dashboards._get_dashboard_config_internal``
1300
- which handles the WS envelope, force-cache-bypass, and structured
1301
- error wrapping consistently with how the rest of the dashboard surface
1302
- fetches state. Imported lazily to avoid an import cycle.
1299
+ The identifier is pre-resolved to its canonical url_path via the shared
1300
+ ``_resolve_dashboard`` (component ``list`` when available), then the config is
1301
+ read through the component ``get`` (one in-process frame) with a fall back to
1302
+ the legacy ``lovelace/config`` read (``_get_dashboard_config_internal``, which
1303
+ handles the WS envelope, force-cache-bypass, and structured error wrapping).
1304
+ The component refuses YAML bodies, so those capture through legacy unchanged.
1305
+ Imported lazily to avoid an import cycle.
1303
1306
  """
1304
1307
  from fastmcp.exceptions import ToolError
1305
1308
 
1306
1309
  from .tools.tools_config_dashboards import (
1310
+ _component_dashboard_config,
1307
1311
  _get_dashboard_config_internal,
1308
1312
  _resolve_dashboard,
1309
1313
  )
@@ -1329,6 +1333,12 @@ async def _fetch_dashboard(client: Any, entity_id: str) -> Any:
1329
1333
  err,
1330
1334
  )
1331
1335
 
1336
+ # Component fast path (freshness-safe in-memory read); None ⇒ legacy below,
1337
+ # which also covers YAML dashboards and not-found (nothing to back up).
1338
+ component_config = await _component_dashboard_config(client, fetch_path)
1339
+ if component_config is not None:
1340
+ return component_config
1341
+
1332
1342
  try:
1333
1343
  config, _config_hash = await _get_dashboard_config_internal(client, fetch_path)
1334
1344
  except ToolError as err:
@@ -1442,10 +1452,21 @@ def _strip_readonly(config: dict[str, Any], *extra: str) -> dict[str, Any]:
1442
1452
 
1443
1453
 
1444
1454
  async def _fetch_label(client: Any, entity_id: str) -> Any:
1445
- items = _require_list(
1446
- await _ws_send(client, {"type": "config/label_registry/list"}),
1447
- "config/label_registry/list",
1448
- )
1455
+ # Route the capture through the component's ``registries`` capability when
1456
+ # available (one in-process read of the label registry) instead of dumping
1457
+ # the whole registry via WS. Lazy import to avoid the backup_manager →
1458
+ # tools → backup_manager cycle (same pattern as ``_fetch_device``). ``None``
1459
+ # from the helper means "component unavailable"; fall back to the full list.
1460
+ from .tools.component_registries import fetch_registries_via_component
1461
+
1462
+ component_result = await fetch_registries_via_component(client, ["label"])
1463
+ if component_result is not None:
1464
+ items = component_result.get("labels") or []
1465
+ else:
1466
+ items = _require_list(
1467
+ await _ws_send(client, {"type": "config/label_registry/list"}),
1468
+ "config/label_registry/list",
1469
+ )
1449
1470
  for item in items:
1450
1471
  if item.get("label_id") == entity_id:
1451
1472
  return item
@@ -1466,12 +1487,22 @@ async def _fetch_category(client: Any, entity_id: str) -> Any:
1466
1487
  scope, _, cat_id = entity_id.partition(":")
1467
1488
  if not cat_id:
1468
1489
  return None
1469
- items = _require_list(
1470
- await _ws_send(
1471
- client, {"type": "config/category_registry/list", "scope": scope}
1472
- ),
1473
- "config/category_registry/list",
1490
+ # Same component-first routing as ``_fetch_label``; categories are scoped,
1491
+ # so the requested scope rides ``category_scopes``.
1492
+ from .tools.component_registries import fetch_registries_via_component
1493
+
1494
+ component_result = await fetch_registries_via_component(
1495
+ client, ["category"], category_scopes=[scope]
1474
1496
  )
1497
+ if component_result is not None:
1498
+ items = (component_result.get("categories") or {}).get(scope, [])
1499
+ else:
1500
+ items = _require_list(
1501
+ await _ws_send(
1502
+ client, {"type": "config/category_registry/list", "scope": scope}
1503
+ ),
1504
+ "config/category_registry/list",
1505
+ )
1475
1506
  for item in items:
1476
1507
  if item.get("category_id") == cat_id:
1477
1508
  return {"scope": scope, **item}
@@ -1607,19 +1638,30 @@ async def _fetch_area_or_floor(client: Any, entity_id: str) -> Any:
1607
1638
  kind, _, real_id = entity_id.partition(":")
1608
1639
  if not real_id:
1609
1640
  return None
1641
+ # Same component-first routing as ``_fetch_label`` / ``_fetch_category``.
1642
+ from .tools.component_registries import fetch_registries_via_component
1643
+
1610
1644
  if kind == "area":
1611
- items = _require_list(
1612
- await _ws_send(client, {"type": "config/area_registry/list"}),
1613
- "config/area_registry/list",
1614
- )
1645
+ component_result = await fetch_registries_via_component(client, ["area"])
1646
+ if component_result is not None:
1647
+ items = component_result.get("areas") or []
1648
+ else:
1649
+ items = _require_list(
1650
+ await _ws_send(client, {"type": "config/area_registry/list"}),
1651
+ "config/area_registry/list",
1652
+ )
1615
1653
  for item in items:
1616
1654
  if item.get("area_id") == real_id:
1617
1655
  return {"kind": "area", **item}
1618
1656
  elif kind == "floor":
1619
- items = _require_list(
1620
- await _ws_send(client, {"type": "config/floor_registry/list"}),
1621
- "config/floor_registry/list",
1622
- )
1657
+ component_result = await fetch_registries_via_component(client, ["floor"])
1658
+ if component_result is not None:
1659
+ items = component_result.get("floors") or []
1660
+ else:
1661
+ items = _require_list(
1662
+ await _ws_send(client, {"type": "config/floor_registry/list"}),
1663
+ "config/floor_registry/list",
1664
+ )
1623
1665
  for item in items:
1624
1666
  if item.get("floor_id") == real_id:
1625
1667
  return {"kind": "floor", **item}
@@ -30,10 +30,21 @@ from ..backup_manager import (
30
30
  MandatoryBackupError,
31
31
  get_backup_manager,
32
32
  )
33
- from ..client.rest_client import HomeAssistantClient, HomeAssistantError
34
- from ..client.websocket_client import HomeAssistantWebSocketClient
33
+ from ..client.rest_client import (
34
+ HomeAssistantClient,
35
+ HomeAssistantCommandError,
36
+ HomeAssistantCommandTimeout,
37
+ HomeAssistantError,
38
+ )
39
+ from ..client.websocket_client import HomeAssistantWebSocketClient, get_websocket_client
35
40
  from ..config import get_global_settings
36
41
  from ..errors import ErrorCode, create_error_response
42
+ from .component_api import (
43
+ component_supports,
44
+ get_component_caps,
45
+ invalidate_caps,
46
+ is_unknown_command,
47
+ )
37
48
  from .helpers import (
38
49
  exception_to_structured_error,
39
50
  get_connected_ws_client,
@@ -89,6 +100,9 @@ _BACKUP_PROGRESS_INTERVAL_S = 10
89
100
  # _wait_timeout) can be too tight for a slow or rate-limited remote agent —
90
101
  # same class of false-timeout problem this file already fixes for creation.
91
102
  _BACKUP_DELETE_WAIT_S = 60.0
103
+ # WS command name is a module-local constant per the component-routing seam
104
+ # pattern (see component_api.py / component_devices.py).
105
+ WS_BACKUP_PREP = "ha_mcp_tools/backup_prep"
92
106
 
93
107
 
94
108
  def _get_backup_hint_text() -> str:
@@ -217,6 +231,91 @@ async def _get_backup_password(
217
231
  return cast(str, default_password)
218
232
 
219
233
 
234
+ async def _backup_prep_via_component(
235
+ client: HomeAssistantClient,
236
+ ) -> dict[str, Any] | None:
237
+ """One ``ha_mcp_tools/backup_prep`` read; ``None`` ⇒ run the legacy two-call path.
238
+
239
+ Returns the component's ``{agent_ids, local_agent_id, default_password}``
240
+ payload, replacing the sequential ``backup/agents/info`` (local-agent
241
+ discovery, :func:`_get_local_backup_agent_id`) + ``backup/config/info``
242
+ (default password, :func:`_get_backup_password`) WS round-trips with one
243
+ in-process read. ``None`` on capability miss, downgrade (``unknown_command``
244
+ → invalidate the cached caps), or command error/timeout (logged) — the
245
+ caller falls back to the legacy sequential calls. A
246
+ ``HomeAssistantConnectionError`` (pooled-WS drop) or the plain ``Exception``
247
+ ``get_websocket_client()`` raises on a failed (re)connect is caught here and
248
+ mapped to ``None``: the legacy probes (``_get_local_backup_agent_id`` /
249
+ ``_get_backup_password``) run on a DEDICATED ``get_connected_ws_client`` socket
250
+ already connected before this read, NOT this pooled one — so a wedged pooled
251
+ socket must fall back to the legacy calls rather than fail create/restore.
252
+ Same caps-gate discipline as ``component_devices.fetch_device_via_component``.
253
+ """
254
+ caps = await get_component_caps(client)
255
+ if not component_supports(caps, "backup_prep"):
256
+ return None
257
+ try:
258
+ ws = await get_websocket_client(url=client.base_url, token=client.token)
259
+ raw = await ws.send_command(WS_BACKUP_PREP)
260
+ except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
261
+ if is_unknown_command(exc):
262
+ invalidate_caps(client)
263
+ else:
264
+ logger.warning("%s failed; fell back to legacy: %r", WS_BACKUP_PREP, exc)
265
+ return None
266
+ except Exception as exc:
267
+ # HomeAssistantConnectionError (pooled-WS drop) OR the plain Exception
268
+ # get_websocket_client() raises on a failed (re)connect. The legacy probes
269
+ # ride a dedicated already-connected socket, so fall back rather than fail
270
+ # create/restore on a wedged pooled socket.
271
+ logger.warning(
272
+ "%s connection error; falling back to legacy: %r", WS_BACKUP_PREP, exc
273
+ )
274
+ return None
275
+ result = raw.get("result")
276
+ if not isinstance(result, dict) or "local_agent_id" not in result:
277
+ logger.debug(
278
+ "%s returned a malformed result (missing 'local_agent_id' key); "
279
+ "falling back to legacy",
280
+ WS_BACKUP_PREP,
281
+ )
282
+ return None
283
+ return result
284
+
285
+
286
+ def _raise_no_local_backup_agent_error(agent_ids: list[Any] | None) -> NoReturn:
287
+ """Same "no local agent" error ``_get_local_backup_agent_id`` raises.
288
+
289
+ Used when the ``backup_prep`` component read comes back with
290
+ ``local_agent_id: None`` — the SAME outcome as the legacy probe finding no
291
+ agent named ``"local"``, so both paths fail identically.
292
+ """
293
+ raise_tool_error(
294
+ create_error_response(
295
+ ErrorCode.SERVICE_CALL_FAILED,
296
+ "No local backup agent found",
297
+ context={"available_agents": [a for a in (agent_ids or []) if a]},
298
+ suggestions=[
299
+ "Backup creation requires a local agent (hassio.local on "
300
+ "Supervised, backup.local on Core); none is registered",
301
+ ],
302
+ )
303
+ )
304
+
305
+
306
+ def _raise_no_default_password_error() -> NoReturn:
307
+ """Same error ``_get_backup_password`` raises when no default password is configured."""
308
+ raise_tool_error(
309
+ create_error_response(
310
+ ErrorCode.SERVICE_CALL_FAILED,
311
+ "No default backup password configured in Home Assistant",
312
+ suggestions=[
313
+ "Configure automatic backups in Home Assistant settings to set a default password"
314
+ ],
315
+ )
316
+ )
317
+
318
+
220
319
  def _parse_backup_date(raw: Any) -> datetime | None:
221
320
  """Parse an HA backup `date` (ISO-8601, may use `Z` suffix) to a tz-aware
222
321
  datetime, returning None on missing/malformed input. Naive timestamps are
@@ -530,13 +629,24 @@ async def create_backup(
530
629
  )
531
630
  ws_client = cast(HomeAssistantWebSocketClient, ws_client)
532
631
 
533
- # Get backup password (raises ToolError on failure)
534
- password = await _get_backup_password(ws_client)
632
+ # One component read replaces the sequential password + local-agent
633
+ # probes when the ha_mcp_tools component supports backup_prep.
634
+ prep = await _backup_prep_via_component(client)
635
+ if prep is not None:
636
+ password = prep.get("default_password")
637
+ if not password:
638
+ _raise_no_default_password_error()
639
+ local_agent = prep.get("local_agent_id")
640
+ if not local_agent:
641
+ _raise_no_local_backup_agent_error(prep.get("agent_ids"))
642
+ else:
643
+ # Get backup password (raises ToolError on failure)
644
+ password = await _get_backup_password(ws_client)
535
645
 
536
- # Discover the local backup agent at call time. HA Core registers
537
- # `backup.local`; HA Supervised registers `hassio.local`. Hardcoding
538
- # either breaks the other deployment.
539
- local_agent = await _get_local_backup_agent_id(ws_client)
646
+ # Discover the local backup agent at call time. HA Core registers
647
+ # `backup.local`; HA Supervised registers `hassio.local`. Hardcoding
648
+ # either breaks the other deployment.
649
+ local_agent = await _get_local_backup_agent_id(ws_client)
540
650
 
541
651
  # Generate backup name if not provided
542
652
  if not name:
@@ -768,19 +878,38 @@ async def restore_backup(
768
878
  )
769
879
  )
770
880
 
771
- # Discover the local backup agent (Supervisor's hassio.local on
772
- # Supervised, backup.local on Core). Used for both the safety backup
773
- # and the restore call below.
774
- local_agent = await _get_local_backup_agent_id(ws_client)
881
+ # One component read replaces the sequential local-agent + password
882
+ # probes when the ha_mcp_tools component supports backup_prep.
883
+ prep = await _backup_prep_via_component(client)
884
+ if prep is not None:
885
+ # Local backup agent (Supervisor's hassio.local on Supervised,
886
+ # backup.local on Core). Used for both the safety backup and the
887
+ # restore call below.
888
+ local_agent = prep.get("local_agent_id")
889
+ if not local_agent:
890
+ _raise_no_local_backup_agent_error(prep.get("agent_ids"))
891
+
892
+ # Create safety backup BEFORE restoring
893
+ logger.info("Creating safety backup before restore...")
894
+ password = prep.get("default_password")
895
+ if not password:
896
+ # No default password - log warning but continue (restore might still work)
897
+ logger.warning("No default password - proceeding without safety backup")
898
+ password = None
899
+ else:
900
+ # Discover the local backup agent (Supervisor's hassio.local on
901
+ # Supervised, backup.local on Core). Used for both the safety backup
902
+ # and the restore call below.
903
+ local_agent = await _get_local_backup_agent_id(ws_client)
775
904
 
776
- # Create safety backup BEFORE restoring
777
- logger.info("Creating safety backup before restore...")
778
- try:
779
- password = await _get_backup_password(ws_client)
780
- except ToolError:
781
- # Password error - log warning but continue (restore might still work)
782
- logger.warning("No default password - proceeding without safety backup")
783
- password = None
905
+ # Create safety backup BEFORE restoring
906
+ logger.info("Creating safety backup before restore...")
907
+ try:
908
+ password = await _get_backup_password(ws_client)
909
+ except ToolError:
910
+ # Password error - log warning but continue (restore might still work)
911
+ logger.warning("No default password - proceeding without safety backup")
912
+ password = None
784
913
 
785
914
  safety_backup_id, safety_warnings = await _create_safety_backup(
786
915
  ws_client, password, local_agent, ctx=ctx