ha-mcp-dev 7.7.0.dev697__tar.gz → 7.7.0.dev699__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 (143) hide show
  1. {ha_mcp_dev-7.7.0.dev697/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.7.0.dev699}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/__main__.py +106 -9
  4. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/client/rest_client.py +56 -31
  5. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_hacs.py +8 -6
  6. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  7. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/LICENSE +0 -0
  8. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/MANIFEST.in +0 -0
  9. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/README.md +0 -0
  10. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/setup.cfg +0 -0
  11. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/__init__.py +0 -0
  12. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/_pypi_marker +0 -0
  13. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/_version.py +0 -0
  14. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/auth/__init__.py +0 -0
  15. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/auth/consent_form.py +0 -0
  16. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/auth/provider.py +0 -0
  17. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/backup_manager.py +0 -0
  18. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/client/__init__.py +0 -0
  19. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/client/supervisor_client.py +0 -0
  20. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/client/websocket_client.py +0 -0
  21. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/client/websocket_listener.py +0 -0
  22. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/config.py +0 -0
  23. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  24. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  25. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  26. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/errors.py +0 -0
  27. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/policy/__init__.py +0 -0
  28. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/policy/approval_queue.py +0 -0
  29. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/policy/evaluator.py +0 -0
  30. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/policy/handlers.py +0 -0
  31. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/policy/middleware.py +0 -0
  32. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/policy/model.py +0 -0
  33. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/policy/persistence.py +0 -0
  34. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/policy/value_sources.py +0 -0
  35. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/py.typed +0 -0
  36. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/read_only.py +0 -0
  37. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  38. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  39. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  40. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  41. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  42. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  43. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  44. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  45. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  46. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  47. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  48. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  49. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  50. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  51. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  52. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  53. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  54. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  55. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  56. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  57. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  58. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  59. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/server.py +0 -0
  60. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/settings.css +0 -0
  61. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/settings.js +0 -0
  62. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/settings_ui.py +0 -0
  63. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/smoke_test.py +0 -0
  64. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  65. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/__init__.py +0 -0
  66. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/auto_backup.py +0 -0
  67. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/backup.py +0 -0
  68. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  69. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  70. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/device_control.py +0 -0
  71. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/enhanced.py +0 -0
  72. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/helpers.py +0 -0
  73. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/reference_validator.py +0 -0
  74. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/registry.py +0 -0
  75. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  76. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  77. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  78. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  79. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  80. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  81. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  82. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  83. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  84. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  85. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_addons.py +0 -0
  86. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_areas.py +0 -0
  87. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  88. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  89. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_calendar.py +0 -0
  90. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_camera.py +0 -0
  91. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_categories.py +0 -0
  92. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_code.py +0 -0
  93. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  94. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  95. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  96. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  97. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  98. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  99. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_energy.py +0 -0
  100. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_entities.py +0 -0
  101. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  102. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_groups.py +0 -0
  103. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_history.py +0 -0
  104. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_integrations.py +0 -0
  105. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_labels.py +0 -0
  106. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  107. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_registry.py +0 -0
  108. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_resources.py +0 -0
  109. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_search.py +0 -0
  110. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_service.py +0 -0
  111. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_services.py +0 -0
  112. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_system.py +0 -0
  113. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_themes.py +0 -0
  114. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_todo.py +0 -0
  115. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_traces.py +0 -0
  116. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_updates.py +0 -0
  117. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_utility.py +0 -0
  118. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  119. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  120. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/tools_zones.py +0 -0
  121. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/util_helpers.py +0 -0
  122. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/tools/validation_middleware.py +0 -0
  123. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/transforms/__init__.py +0 -0
  124. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/transforms/categorized_search.py +0 -0
  125. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  126. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/utils/__init__.py +0 -0
  127. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/utils/config_hash.py +0 -0
  128. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/utils/data_paths.py +0 -0
  129. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/utils/domain_handlers.py +0 -0
  130. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  131. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  132. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/utils/operation_manager.py +0 -0
  133. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/utils/python_sandbox.py +0 -0
  134. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/utils/skill_loader.py +0 -0
  135. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp/utils/usage_logger.py +0 -0
  136. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  137. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  138. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  139. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  140. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  141. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/tests/__init__.py +0 -0
  142. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/tests/test_constants.py +0 -0
  143. {ha_mcp_dev-7.7.0.dev697 → ha_mcp_dev-7.7.0.dev699}/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.7.0.dev697
3
+ Version: 7.7.0.dev699
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.7.0.dev697"
7
+ version = "7.7.0.dev699"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -290,17 +290,28 @@ def _setup_standard_mode() -> None:
290
290
  _log_startup_version()
291
291
 
292
292
 
293
- def _http_run_kwargs(transport: str, host: str, port: int, path: str) -> dict:
294
- """Build common run_async kwargs for HTTP-based transports."""
295
- return {
293
+ def _http_run_kwargs(transport: str, host: str, port: int, path: str) -> dict[str, Any]:
294
+ """Build common run_async kwargs for HTTP-based transports.
295
+
296
+ ``stateless_http`` is a Streamable-HTTP concept and is only valid for the
297
+ ``http``/``streamable-http`` transports. Passing it alongside
298
+ ``transport="sse"`` makes fastmcp's ``run_async`` raise
299
+ ``ValueError("SSE transport does not support stateless mode")``. Gating it to
300
+ non-SSE transports keeps SSE startup working. (Before this fix that raise was
301
+ also swallowed into a silent exit 0; ``_run_with_shutdown`` now surfaces a
302
+ self-terminating server task's exception instead.) See #1544.
303
+ """
304
+ kwargs: dict[str, Any] = {
296
305
  "transport": transport,
297
306
  "host": host,
298
307
  "port": port,
299
308
  "path": path,
300
309
  "show_banner": _get_show_banner(),
301
- "stateless_http": True,
302
310
  "uvicorn_config": {"log_config": _get_timestamped_uvicorn_log_config()},
303
311
  }
312
+ if transport != "sse":
313
+ kwargs["stateless_http"] = True
314
+ return kwargs
304
315
 
305
316
 
306
317
  def _create_server() -> "HomeAssistantSmartMCPServer":
@@ -402,6 +413,46 @@ class ToolValidationLogFilter(logging.Filter):
402
413
  return True
403
414
 
404
415
 
416
+ class ProbeAccessLogFilter(logging.Filter):
417
+ """Drop benign, non-MCP HTTP probe noise from the uvicorn access log.
418
+
419
+ * ``GET``/``HEAD`` ``/favicon.ico`` -> ``404``: browsers auto-request a
420
+ favicon that doesn't exist. Pure noise, always dropped.
421
+ * ``GET``/``HEAD`` on the MCP path -> ``405``: a non-MCP caller (browser,
422
+ health check, reverse proxy, or a connector's SSE-style pre-flight) hit a
423
+ POST-only Streamable HTTP endpoint. The raw access line is dropped and the
424
+ landing handler logs one annotated "(NORMAL for most non-SSE connections)"
425
+ line in its place. Dropped only when ``drop_mcp_405`` is set — SSE callers
426
+ pass False, since there a GET answers 200 and a GET-405 is a genuine fault.
427
+ """
428
+
429
+ def __init__(self, mcp_path: str, *, drop_mcp_405: bool = True) -> None:
430
+ super().__init__()
431
+ self._mcp_path = mcp_path.rstrip("/") or "/"
432
+ self._drop_mcp_405 = drop_mcp_405
433
+
434
+ def filter(self, record: logging.LogRecord) -> bool:
435
+ # uvicorn.access records carry structured args: (client, method, path,
436
+ # http_version, status_int). Match on those, not the formatted string.
437
+ args = record.args
438
+ if not isinstance(args, tuple) or len(args) != 5:
439
+ return True
440
+ method, raw_path, status = args[1], args[2], args[4]
441
+ if method not in ("GET", "HEAD"):
442
+ return True
443
+ path = str(raw_path).split("?", 1)[0].rstrip("/") or "/"
444
+ if status == 404 and path == "/favicon.ico":
445
+ return False # browser favicon auto-request — pure noise
446
+ # By-design probe 405 on the MCP path; the handler logs an annotated line
447
+ # instead. This trusts that the landing route is the only GET/HEAD responder
448
+ # on the MCP path (true today). Kept in SSE mode (drop_mcp_405=False), where
449
+ # a GET answers 200 and a 405 is a real fault.
450
+ is_dropped_probe = (
451
+ status == 405 and path == self._mcp_path and self._drop_mcp_405
452
+ )
453
+ return not is_dropped_probe
454
+
455
+
405
456
  def _setup_logging(log_level_str: str, force: bool = False) -> None:
406
457
  """Configure root logger with consistent timestamp format."""
407
458
  logging.basicConfig(
@@ -532,9 +583,23 @@ async def _run_with_shutdown(server_coro: Coroutine[Any, Any, Any]) -> None:
532
583
  # Expected: we just cancelled server_task above; swallow its
533
584
  # CancelledError so shutdown can proceed to cleanup.
534
585
  pass
586
+ elif server_task in done:
587
+ # Server task finished on its own (no shutdown signal). Re-raise any
588
+ # exception it captured so a hard startup failure surfaces as a
589
+ # logged sys.exit(1) instead of a silent exit 0 — without this the
590
+ # exception on the already-done task is never retrieved. See #1544.
591
+ server_task.result()
535
592
 
536
593
  except asyncio.CancelledError:
537
- logger.info("Server task cancelled")
594
+ # A shutdown-initiated cancel is a graceful stop. A cancel without a
595
+ # shutdown signal — including one re-raised by server_task.result()
596
+ # above — is a hard stop masquerading as success; re-raise it so it
597
+ # becomes a logged sys.exit(1) rather than a silent exit 0. See #1544.
598
+ if _shutdown_event is not None and _shutdown_event.is_set():
599
+ logger.info("Server task cancelled")
600
+ else:
601
+ logger.error("Server task cancelled without a shutdown signal")
602
+ raise
538
603
  finally:
539
604
  try:
540
605
  await asyncio.wait_for(
@@ -543,7 +608,12 @@ async def _run_with_shutdown(server_coro: Coroutine[Any, Any, Any]) -> None:
543
608
  except TimeoutError:
544
609
  logger.warning("Resource cleanup timed out")
545
610
 
546
- await _cancel_tasks(server_task, shutdown_task)
611
+ try:
612
+ await _cancel_tasks(server_task, shutdown_task)
613
+ except Exception as e:
614
+ # Teardown must never mask the exception being propagated from the
615
+ # try block (Python drops the original if finally raises).
616
+ logger.warning(f"Task cancellation during shutdown failed: {e}")
547
617
 
548
618
 
549
619
  def _run_entrypoint(coro: Coroutine[Any, Any, Any], label: str) -> None:
@@ -557,7 +627,7 @@ def _run_entrypoint(coro: Coroutine[Any, Any, Any], label: str) -> None:
557
627
  except SystemExit:
558
628
  raise
559
629
  except Exception as e:
560
- logger.error(f"{label} error: {e}")
630
+ logger.error(f"{label} error: {e}", exc_info=True)
561
631
  sys.exit(1)
562
632
 
563
633
  sys.exit(0)
@@ -858,7 +928,12 @@ async def _run_http_with_graceful_shutdown(
858
928
  _registered_landing_paths: set[str] = set()
859
929
 
860
930
 
861
- def register_browser_landing(mcp_instance: "FastMCP | _DeferredMCP", path: str) -> None:
931
+ def register_browser_landing(
932
+ mcp_instance: "FastMCP | _DeferredMCP",
933
+ path: str,
934
+ *,
935
+ quiet_probe_log: bool = True,
936
+ ) -> None:
862
937
  """Register a GET handler that returns 405 with a helpful message.
863
938
 
864
939
  Browsers and misconfigured clients that send GET instead of POST will see
@@ -869,6 +944,10 @@ def register_browser_landing(mcp_instance: "FastMCP | _DeferredMCP", path: str)
869
944
  Args:
870
945
  mcp_instance: The FastMCP server to register the route on.
871
946
  path: The MCP endpoint path (e.g. "/mcp" or a secret path).
947
+ quiet_probe_log: When True (default, for Streamable HTTP), drop the
948
+ by-design GET/HEAD-405 probe line on the MCP path from the uvicorn
949
+ access log (the handler logs an annotated replacement). Pass False
950
+ for SSE, where a GET answers 200 and a 405 is a genuine fault.
872
951
  """
873
952
  if path in _registered_landing_paths:
874
953
  logger.warning(
@@ -905,6 +984,12 @@ def register_browser_landing(mcp_instance: "FastMCP | _DeferredMCP", path: str)
905
984
  # Custom routes are registered at lowest precedence (after the MCP route).
906
985
  @mcp_instance.custom_route(path, methods=["GET"])
907
986
  async def _browser_landing(_: Request) -> PlainTextResponse:
987
+ # Any GET here is a non-MCP caller (browser, health check, proxy, or a
988
+ # connector's SSE-style pre-flight) hitting this POST-only Streamable HTTP
989
+ # endpoint, which answers 405 by design. Log one annotated line so the 405
990
+ # reads as expected; ProbeAccessLogFilter drops the raw uvicorn access line
991
+ # so there's no cryptic duplicate.
992
+ logger.info("GET %s -> 405 (NORMAL for most non-SSE connections)", path)
908
993
  return PlainTextResponse(
909
994
  _landing_message,
910
995
  status_code=405,
@@ -913,6 +998,16 @@ def register_browser_landing(mcp_instance: "FastMCP | _DeferredMCP", path: str)
913
998
  headers={"Allow": "POST, DELETE"},
914
999
  )
915
1000
 
1001
+ # Tidy uvicorn's access log: always drop browser favicon 404s, and drop the
1002
+ # raw by-design GET/HEAD-405 probe line on the MCP path (the handler above logs
1003
+ # an annotated replacement). The 405 drop is skipped for SSE
1004
+ # (quiet_probe_log=False), where a GET answers 200 and a 405 is a real fault.
1005
+ # Attach to uvicorn.access directly — it has propagate=False, so a root-logger
1006
+ # filter would miss it.
1007
+ logging.getLogger("uvicorn.access").addFilter(
1008
+ ProbeAccessLogFilter(path, drop_mcp_405=quiet_probe_log)
1009
+ )
1010
+
916
1011
 
917
1012
  def _log_settings_url(host: str, port: int, path: str) -> None:
918
1013
  """Log the web settings-UI URL at HTTP startup.
@@ -946,7 +1041,9 @@ def _run_http_server(transport: str, default_port: int = 8086) -> None:
946
1041
 
947
1042
  host, port, path = _get_http_runtime(default_port)
948
1043
  _warn_if_default_path_exposed(host, port, path)
949
- register_browser_landing(_get_mcp(), path)
1044
+ # SSE transport answers GET with 200 (the event stream), so a GET->405 there
1045
+ # would be a real fault, not a benign probe — keep its access log intact.
1046
+ register_browser_landing(_get_mcp(), path, quiet_probe_log=transport != "sse")
950
1047
  register_settings_routes(_get_mcp(), _get_server(), secret_path=path)
951
1048
  _log_settings_url(host, port, path)
952
1049
 
@@ -33,6 +33,12 @@ def _is_ssl_error(exc: BaseException) -> bool:
33
33
 
34
34
  logger = logging.getLogger(__name__)
35
35
 
36
+ # Transient gateway statuses from a reverse proxy / Supervisor ingress — HA Core
37
+ # restarting or briefly overloaded behind it. The upstream couldn't be reached,
38
+ # so the request did not execute and retrying is safe even for writes.
39
+ _RETRYABLE_STATUS = frozenset({502, 503, 504})
40
+ _MAX_REQUEST_ATTEMPTS = 3
41
+
36
42
 
37
43
  class HomeAssistantError(Exception):
38
44
  """Base exception for Home Assistant API errors."""
@@ -180,7 +186,8 @@ class HomeAssistantClient:
180
186
 
181
187
  Handles auth, HTTP 4xx/5xx, and transport errors in one place.
182
188
  Callers parse the body themselves (JSON via `_request`, text via
183
- `get_addon_logs`, etc.).
189
+ `get_addon_logs`, etc.). Transient gateway errors (502/503/504) are
190
+ retried with bounded exponential backoff before surfacing.
184
191
 
185
192
  Raises:
186
193
  HomeAssistantAuthError: 401 response.
@@ -188,44 +195,62 @@ class HomeAssistantClient:
188
195
  response_data set from JSON body when possible).
189
196
  HomeAssistantConnectionError: Network, timeout, or transport error.
190
197
  """
191
- try:
192
- response = await self.httpx_client.request(method, endpoint, **kwargs)
198
+ backoff = 0.5
199
+ for attempt in range(1, _MAX_REQUEST_ATTEMPTS + 1):
200
+ try:
201
+ response = await self.httpx_client.request(method, endpoint, **kwargs)
193
202
 
194
- if response.status_code == 401:
195
- raise HomeAssistantAuthError("Invalid authentication token")
203
+ if response.status_code == 401:
204
+ raise HomeAssistantAuthError("Invalid authentication token")
196
205
 
197
- if response.status_code >= 400:
198
- try:
199
- error_data = response.json()
200
- except Exception:
201
- error_data = {"message": response.text}
206
+ if response.status_code >= 400:
207
+ try:
208
+ error_data = response.json()
209
+ except Exception:
210
+ error_data = {"message": response.text}
202
211
 
203
- message = error_data.get("message")
204
- if not message or not message.strip():
205
- message = response.reason_phrase or "<empty body>"
212
+ message = error_data.get("message")
213
+ if not message or not message.strip():
214
+ message = response.reason_phrase or "<empty body>"
206
215
 
207
- raise HomeAssistantAPIError(
208
- f"API error: {response.status_code} - {message}",
209
- status_code=response.status_code,
210
- response_data=error_data,
211
- )
216
+ if (
217
+ response.status_code in _RETRYABLE_STATUS
218
+ and attempt < _MAX_REQUEST_ATTEMPTS
219
+ ):
220
+ logger.warning(
221
+ f"Transient {response.status_code} from Home Assistant "
222
+ f"(attempt {attempt}/{_MAX_REQUEST_ATTEMPTS}), retrying "
223
+ f"in {backoff}s: {message}"
224
+ )
225
+ await asyncio.sleep(backoff)
226
+ backoff *= 2
227
+ continue
212
228
 
213
- return response
229
+ raise HomeAssistantAPIError(
230
+ f"API error: {response.status_code} - {message}",
231
+ status_code=response.status_code,
232
+ response_data=error_data,
233
+ )
214
234
 
215
- except httpx.ConnectError as e:
216
- if _is_ssl_error(e) and self.verify_ssl:
235
+ return response
236
+
237
+ except httpx.ConnectError as e:
238
+ if _is_ssl_error(e) and self.verify_ssl:
239
+ raise HomeAssistantConnectionError(
240
+ f"TLS verification failed for {self.base_url}: {e}. "
241
+ "If this is a self-signed certificate or hostname "
242
+ "mismatch, set HA_VERIFY_SSL=false to skip verification."
243
+ ) from e
217
244
  raise HomeAssistantConnectionError(
218
- f"TLS verification failed for {self.base_url}: {e}. "
219
- "If this is a self-signed certificate or hostname "
220
- "mismatch, set HA_VERIFY_SSL=false to skip verification."
245
+ f"Failed to connect to Home Assistant: {e}"
221
246
  ) from e
222
- raise HomeAssistantConnectionError(
223
- f"Failed to connect to Home Assistant: {e}"
224
- ) from e
225
- except httpx.TimeoutException as e:
226
- raise HomeAssistantConnectionError(f"Request timeout: {e}") from e
227
- except httpx.HTTPError as e:
228
- raise HomeAssistantConnectionError(f"HTTP error: {e}") from e
247
+ except httpx.TimeoutException as e:
248
+ raise HomeAssistantConnectionError(f"Request timeout: {e}") from e
249
+ except httpx.HTTPError as e:
250
+ raise HomeAssistantConnectionError(f"HTTP error: {e}") from e
251
+
252
+ # Unreachable: the final attempt takes the non-retry branch and raises.
253
+ raise AssertionError("_raw_request retry loop exhausted without returning")
229
254
 
230
255
  async def _request(
231
256
  self, method: str, endpoint: str, **kwargs: Any
@@ -674,12 +674,14 @@ def _filter_and_score_repos(
674
674
  HACS_REPOSITORY_SIGNAL = "hacs_dispatch_repository"
675
675
 
676
676
  # Wall-clock budget for ``wait_for_repo_registration``. Generous
677
- # because the constraint is "HACS finishes registration"; the prior
678
- # 10 s budget (10 attempts × 1.0 s) was exhausted on the HAOS E2E
679
- # channel under load, so a headroom backstop avoids re-tripping
680
- # the same flake. The subscription nudges us, so this is a wall-
681
- # clock cap rather than the dominant cost.
682
- HACS_REPO_REGISTRATION_TIMEOUT = 30.0
677
+ # because the constraint is "HACS finishes registration": adding a
678
+ # fresh repo makes HACS clone/index it over the network, which on a
679
+ # slow link (or a loaded HAOS E2E runner) can exceed 30 s — the prior
680
+ # value, which flaked ``test_install_mcp_tools_*`` with "Could not
681
+ # find repository ID after adding". The subscription nudges us the
682
+ # instant registration lands, so the happy path returns in seconds and
683
+ # this larger cap only ever costs wall-clock on a genuinely slow add.
684
+ HACS_REPO_REGISTRATION_TIMEOUT = 60.0
683
685
 
684
686
  # Budget for the initial ``hacs/subscribe`` ack. Smaller than the
685
687
  # overall registration timeout so a slow subscribe doesn't consume
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.7.0.dev697
3
+ Version: 7.7.0.dev699
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