ha-mcp-dev 7.8.0.dev714__tar.gz → 7.8.1.dev716__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 (144) hide show
  1. {ha_mcp_dev-7.8.0.dev714/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.8.1.dev716}/PKG-INFO +13 -5
  2. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/README.md +11 -4
  3. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/pyproject.toml +2 -1
  4. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/__main__.py +20 -1
  5. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_search.py +24 -5
  6. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_system.py +19 -0
  7. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_updates.py +19 -1
  8. ha_mcp_dev-7.8.1.dev716/src/ha_mcp/update_check.py +261 -0
  9. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716/src/ha_mcp_dev.egg-info}/PKG-INFO +13 -5
  10. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  11. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp_dev.egg-info/requires.txt +1 -0
  12. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/LICENSE +0 -0
  13. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/MANIFEST.in +0 -0
  14. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/setup.cfg +0 -0
  15. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/__init__.py +0 -0
  16. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/_pypi_marker +0 -0
  17. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/_version.py +0 -0
  18. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/auth/__init__.py +0 -0
  19. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/auth/consent_form.py +0 -0
  20. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/auth/provider.py +0 -0
  21. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/backup_manager.py +0 -0
  22. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/client/__init__.py +0 -0
  23. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/client/rest_client.py +0 -0
  24. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/client/supervisor_client.py +0 -0
  25. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/client/websocket_client.py +0 -0
  26. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/client/websocket_listener.py +0 -0
  27. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/config.py +0 -0
  28. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  29. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  30. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  31. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/errors.py +0 -0
  32. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/policy/__init__.py +0 -0
  33. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/policy/approval_queue.py +0 -0
  34. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/policy/evaluator.py +0 -0
  35. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/policy/handlers.py +0 -0
  36. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/policy/middleware.py +0 -0
  37. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/policy/model.py +0 -0
  38. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/policy/persistence.py +0 -0
  39. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/policy/value_sources.py +0 -0
  40. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/py.typed +0 -0
  41. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/read_only.py +0 -0
  42. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  43. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  44. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  45. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  46. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  47. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  48. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  49. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  50. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  51. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  52. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  53. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  54. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  55. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  56. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  57. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  58. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  59. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  60. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  61. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  62. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  63. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  64. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/server.py +0 -0
  65. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/settings.css +0 -0
  66. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/settings.js +0 -0
  67. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/settings_ui.py +0 -0
  68. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/smoke_test.py +0 -0
  69. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  70. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/__init__.py +0 -0
  71. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/auto_backup.py +0 -0
  72. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/backup.py +0 -0
  73. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  74. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  75. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/device_control.py +0 -0
  76. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/enhanced.py +0 -0
  77. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/helpers.py +0 -0
  78. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/reference_validator.py +0 -0
  79. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/registry.py +0 -0
  80. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  81. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  82. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  83. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  84. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  85. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  86. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  87. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  88. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  89. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  90. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_addons.py +0 -0
  91. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_areas.py +0 -0
  92. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  93. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  94. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_calendar.py +0 -0
  95. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_camera.py +0 -0
  96. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_categories.py +0 -0
  97. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_code.py +0 -0
  98. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  99. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  100. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  101. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  102. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  103. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  104. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_energy.py +0 -0
  105. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_entities.py +0 -0
  106. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  107. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_groups.py +0 -0
  108. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_hacs.py +0 -0
  109. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_history.py +0 -0
  110. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_integrations.py +0 -0
  111. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_labels.py +0 -0
  112. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  113. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_registry.py +0 -0
  114. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_resources.py +0 -0
  115. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_service.py +0 -0
  116. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_services.py +0 -0
  117. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_themes.py +0 -0
  118. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_todo.py +0 -0
  119. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_traces.py +0 -0
  120. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_utility.py +0 -0
  121. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  122. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  123. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/tools_zones.py +0 -0
  124. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/util_helpers.py +0 -0
  125. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/tools/validation_middleware.py +0 -0
  126. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/transforms/__init__.py +0 -0
  127. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/transforms/categorized_search.py +0 -0
  128. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  129. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/utils/__init__.py +0 -0
  130. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/utils/config_hash.py +0 -0
  131. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/utils/data_paths.py +0 -0
  132. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/utils/domain_handlers.py +0 -0
  133. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  134. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  135. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/utils/operation_manager.py +0 -0
  136. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/utils/python_sandbox.py +0 -0
  137. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/utils/skill_loader.py +0 -0
  138. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp/utils/usage_logger.py +0 -0
  139. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  140. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  141. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  142. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/tests/__init__.py +0 -0
  143. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/tests/test_constants.py +0 -0
  144. {ha_mcp_dev-7.8.0.dev714 → ha_mcp_dev-7.8.1.dev716}/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.8.0.dev714
3
+ Version: 7.8.1.dev716
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
@@ -27,6 +27,7 @@ Requires-Dist: websockets==16.0
27
27
  Requires-Dist: cryptography==49.0.0
28
28
  Requires-Dist: pydantic-monty==0.0.18
29
29
  Requires-Dist: tzdata>=2024.1
30
+ Requires-Dist: packaging>=24.0
30
31
  Dynamic: license-file
31
32
 
32
33
  > **Breaking change (v7.3.0):** `ha_config_set_yaml` has been moved to [beta](docs/beta.md).
@@ -319,9 +320,16 @@ Skills can still be installed manually for clients that prefer local skill files
319
320
 
320
321
  ## 🔍 Tool Discovery for AI Agents
321
322
 
322
- By default, the full tool catalog (~86 tools) is listed to the client through the standard MCP `tools/list` response. Clients with deferred / on-demand tool loading (Claude Sonnet, Claude Opus,) handle that fine — tools are pulled into context only when needed, so idle context cost is near-zero.
323
+ By default, the full tool catalog (~84 tools) is listed to the client through the standard MCP `tools/list` response. Clients with deferred / on-demand tool loading (Claude Sonnet, Claude Opus) handle that fine — tools are pulled into context only when needed, so idle context cost is near-zero.
323
324
 
324
- For models *without* deferred tool support — Claude Haiku, Gemini, ChatGPT OpenAI-compatible local models, smaller open-weights models — listing 86 tools eats ~46K tokens of idle context. To address that, the server ships with a **search-based discovery mode** built on top of FastMCP's BM25 search transform.
325
+ For models *without* deferred tool support — Claude Haiku, Gemini, ChatGPT OpenAI-compatible local models, smaller open-weights models — listing the full tool catalog up front adds a lot of idle context and can overwhelm smaller models. To address that, the server ships with a **search-based discovery mode** built on top of FastMCP's BM25 search transform.
326
+
327
+ ### Smaller or local LLMs (Ollama, etc.)
328
+
329
+ If your model can't see the tools or your Home Assistant, it may be getting handed the whole tool catalog at once and struggling with it. It's recommended to try the following to see if it helps:
330
+
331
+ - **Enable tool search** (`ENABLE_TOOL_SEARCH=true`, or the add-on option below). Instead of listing every tool up front, the server defers the catalog behind a search interface so the model pulls in only the tools it needs, when it needs them.
332
+ - **Raise the model's context window above the default.** Local runtimes ship with small defaults (Ollama's `num_ctx` is one example) that can't hold a large tool set plus the conversation — increase it well beyond the default.
325
333
 
326
334
  ### Enable search-based discovery
327
335
 
@@ -338,14 +346,14 @@ The proxy split lets MCP clients apply different permission policies per categor
338
346
 
339
347
  | Setting | Default | Description |
340
348
  |---------|---------|-------------|
341
- | `ENABLE_TOOL_SEARCH` | `false` | Replace full tool catalog with search-based discovery (~46K ~5K idle tokens). |
349
+ | `ENABLE_TOOL_SEARCH` | `false` | Replace full tool catalog with search-based discovery (tools deferred behind on-demand search). |
342
350
  | `TOOL_SEARCH_MAX_RESULTS` | `5` | Max results returned by `ha_search_tools` (range 2–10). |
343
351
  | `PINNED_TOOLS` | empty | Comma-separated tool names to keep always visible. The web settings UI is the primary way to manage this. |
344
352
 
345
353
  ### When to enable
346
354
 
347
355
  - **Claude Haiku, OpenAI-compatible local models, Gemini, ChatGPT or any model without native deferred tool support** — large idle-context savings.
348
- - MCP clients that cap total tool count (some cap at 100) — surfaces a minimal set (~10 tools) instead of 86.
356
+ - MCP clients that cap total tool count (some cap at 100) — surfaces a minimal set (~10 tools) instead of 84.
349
357
  - **Cost-sensitive deployments** — fewer idle tokens per turn.
350
358
 
351
359
  Leave it off when using Claude Sonnet/Opus or any client with deferred tool loading; the full catalog has no idle cost there and direct calls skip the search step. If you choose to use our toolsearch then you should disable the native Claude Opus/Sonnet toolsearch, which is called deferred tools in the settings.
@@ -288,9 +288,16 @@ Skills can still be installed manually for clients that prefer local skill files
288
288
 
289
289
  ## 🔍 Tool Discovery for AI Agents
290
290
 
291
- By default, the full tool catalog (~86 tools) is listed to the client through the standard MCP `tools/list` response. Clients with deferred / on-demand tool loading (Claude Sonnet, Claude Opus,) handle that fine — tools are pulled into context only when needed, so idle context cost is near-zero.
291
+ By default, the full tool catalog (~84 tools) is listed to the client through the standard MCP `tools/list` response. Clients with deferred / on-demand tool loading (Claude Sonnet, Claude Opus) handle that fine — tools are pulled into context only when needed, so idle context cost is near-zero.
292
292
 
293
- For models *without* deferred tool support — Claude Haiku, Gemini, ChatGPT OpenAI-compatible local models, smaller open-weights models — listing 86 tools eats ~46K tokens of idle context. To address that, the server ships with a **search-based discovery mode** built on top of FastMCP's BM25 search transform.
293
+ For models *without* deferred tool support — Claude Haiku, Gemini, ChatGPT OpenAI-compatible local models, smaller open-weights models — listing the full tool catalog up front adds a lot of idle context and can overwhelm smaller models. To address that, the server ships with a **search-based discovery mode** built on top of FastMCP's BM25 search transform.
294
+
295
+ ### Smaller or local LLMs (Ollama, etc.)
296
+
297
+ If your model can't see the tools or your Home Assistant, it may be getting handed the whole tool catalog at once and struggling with it. It's recommended to try the following to see if it helps:
298
+
299
+ - **Enable tool search** (`ENABLE_TOOL_SEARCH=true`, or the add-on option below). Instead of listing every tool up front, the server defers the catalog behind a search interface so the model pulls in only the tools it needs, when it needs them.
300
+ - **Raise the model's context window above the default.** Local runtimes ship with small defaults (Ollama's `num_ctx` is one example) that can't hold a large tool set plus the conversation — increase it well beyond the default.
294
301
 
295
302
  ### Enable search-based discovery
296
303
 
@@ -307,14 +314,14 @@ The proxy split lets MCP clients apply different permission policies per categor
307
314
 
308
315
  | Setting | Default | Description |
309
316
  |---------|---------|-------------|
310
- | `ENABLE_TOOL_SEARCH` | `false` | Replace full tool catalog with search-based discovery (~46K ~5K idle tokens). |
317
+ | `ENABLE_TOOL_SEARCH` | `false` | Replace full tool catalog with search-based discovery (tools deferred behind on-demand search). |
311
318
  | `TOOL_SEARCH_MAX_RESULTS` | `5` | Max results returned by `ha_search_tools` (range 2–10). |
312
319
  | `PINNED_TOOLS` | empty | Comma-separated tool names to keep always visible. The web settings UI is the primary way to manage this. |
313
320
 
314
321
  ### When to enable
315
322
 
316
323
  - **Claude Haiku, OpenAI-compatible local models, Gemini, ChatGPT or any model without native deferred tool support** — large idle-context savings.
317
- - MCP clients that cap total tool count (some cap at 100) — surfaces a minimal set (~10 tools) instead of 86.
324
+ - MCP clients that cap total tool count (some cap at 100) — surfaces a minimal set (~10 tools) instead of 84.
318
325
  - **Cost-sensitive deployments** — fewer idle tokens per turn.
319
326
 
320
327
  Leave it off when using Claude Sonnet/Opus or any client with deferred tool loading; the full catalog has no idle cost there and direct calls skip the search step. If you choose to use our toolsearch then you should disable the native Claude Opus/Sonnet toolsearch, which is called deferred tools in the settings.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.8.0.dev714"
7
+ version = "7.8.1.dev716"
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"
@@ -33,6 +33,7 @@ dependencies = [
33
33
  "cryptography==49.0.0",
34
34
  "pydantic-monty==0.0.18",
35
35
  "tzdata>=2024.1",
36
+ "packaging>=24.0",
36
37
  ]
37
38
 
38
39
  [project.urls]
@@ -479,16 +479,24 @@ def _setup_logging(log_level_str: str, force: bool = False) -> None:
479
479
 
480
480
 
481
481
  def _log_startup_version() -> None:
482
- """Log ha-mcp version at startup, plus a dev-channel banner when relevant.
482
+ """Log ha-mcp version at startup, plus dev-channel / update banners.
483
483
 
484
484
  The dev banner only fires for standalone dev installs (Docker ``:dev`` /
485
485
  ``:latest``, or ``pip install ha-mcp-dev``). It is suppressed under the HA
486
486
  Supervisor because add-on users already pick dev vs stable in the HAOS UI.
487
+
488
+ The update banner fires on every startup when a newer release is available,
489
+ on every deployment — pip/Docker/stdio compare against PyPI, the HA add-on
490
+ (stable AND dev) against the Supervisor add-on store — mirroring FastMCP's
491
+ ``log_server_banner``. ``get_update_info`` is a no-op for the ``unknown``
492
+ version (PyPI path) and the ``HA_MCP_DISABLE_UPDATE_CHECK`` opt-out, and
493
+ never raises.
487
494
  """
488
495
  from ha_mcp._version import get_version, is_dev_version, is_running_in_addon
489
496
 
490
497
  version = get_version()
491
498
  logger.info(f"ha-mcp {version}")
499
+
492
500
  if is_dev_version(version) and not is_running_in_addon():
493
501
  logger.warning(
494
502
  "This is the dev channel. For the stable release use the "
@@ -496,6 +504,17 @@ def _log_startup_version() -> None:
496
504
  "(or 'pip install ha-mcp' on PyPI)."
497
505
  )
498
506
 
507
+ from ha_mcp.update_check import get_update_info, update_command_hint
508
+
509
+ info = get_update_info()
510
+ if info is not None and info.update_available:
511
+ logger.warning(
512
+ "A newer ha-mcp release is available: %s (you have %s). %s",
513
+ info.latest,
514
+ info.current,
515
+ update_command_hint(info.current),
516
+ )
517
+
499
518
 
500
519
  def _get_timestamped_uvicorn_log_config() -> dict:
501
520
  """Return a Uvicorn log config with human-readable timestamps added."""
@@ -1896,13 +1896,14 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1896
1896
  "notification_count, notifications, repair_count, "
1897
1897
  "dismissed_repair_count, repairs, repairs_error, "
1898
1898
  "tool_discovery, settings_url, settings_url_hint, "
1899
- "read_only_mode, read_only_mode_hint. Note: "
1899
+ "read_only_mode, read_only_mode_hint, ha_mcp_update. Note: "
1900
1900
  "``settings_url`` (stdio mode), ``settings_url_hint`` "
1901
- "(HTTP/Docker/OAuth mode), and the ``read_only_mode`` / "
1901
+ "(HTTP/Docker/OAuth mode), the ``read_only_mode`` / "
1902
1902
  "``read_only_mode_hint`` pair (only while Read Only Mode "
1903
- "is on) are emitted regardless of ``fields=`` projection "
1904
- "so the settings page and the active mode stay "
1905
- "discoverable; see the tool description."
1903
+ "is on), and ``ha_mcp_update`` (when an update check applies) "
1904
+ "are emitted regardless of ``fields=`` projection so the "
1905
+ "settings page, the active mode, and a newer ha-mcp release "
1906
+ "stay discoverable; see the tool description."
1906
1907
  ),
1907
1908
  ),
1908
1909
  ] = None,
@@ -1936,6 +1937,13 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1936
1937
  instead carries a ``settings_url_hint`` string telling the user where
1937
1938
  the page is mounted and to read the full URL from the startup logs.
1938
1939
  Hand whichever of the two fields is present to the user.
1940
+
1941
+ The response also carries an ``ha_mcp_update`` object
1942
+ ``{current, latest, update_available}`` reporting whether a newer ha-mcp
1943
+ release is available (PyPI for pip/Docker, the Supervisor add-on store
1944
+ for the add-on) — proactively tell the user when ``update_available`` is
1945
+ true. Emitted regardless of ``fields=``; omitted only for the
1946
+ ``unknown`` version and when ``HA_MCP_DISABLE_UPDATE_CHECK`` is set.
1939
1947
  """
1940
1948
  # Validate fields= early so a malformed value returns VALIDATION_FAILED
1941
1949
  # with parameter="fields" (ha_get_overview has no outer try/except, so
@@ -2172,6 +2180,17 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2172
2180
  "configuration."
2173
2181
  )
2174
2182
 
2183
+ # Surface the MCP server's own update status after projection (like
2184
+ # settings_url / read_only_mode) so it survives any fields= filter — the
2185
+ # model should learn about a newer ha-mcp release even from a minimal
2186
+ # overview, the most common session-start call. Best-effort and never
2187
+ # raises; see ha_mcp.update_check.
2188
+ from ..update_check import get_update_field
2189
+
2190
+ mcp_update = await get_update_field()
2191
+ if mcp_update is not None:
2192
+ projected["ha_mcp_update"] = mcp_update
2193
+
2175
2194
  return projected
2176
2195
 
2177
2196
  async def ha_deep_search(
@@ -375,6 +375,15 @@ class SystemTools:
375
375
  Returns health check results from integrations, system resources, and connectivity.
376
376
  Available information varies by installation type and loaded integrations.
377
377
 
378
+ The result also carries an ``ha_mcp_update`` object —
379
+ ``{current, latest, update_available}`` — reporting whether a newer
380
+ ha-mcp release is available (from PyPI for pip/Docker, or the Supervisor
381
+ add-on store for the add-on), so you can proactively tell the user to
382
+ upgrade. Present on every install type including the HA add-on (so a user
383
+ who missed the Supervisor's update prompt still hears about it); omitted
384
+ only for the ``unknown`` version and when ``HA_MCP_DISABLE_UPDATE_CHECK``
385
+ is set.
386
+
378
387
  **Parameters:**
379
388
  - include: Optional comma-separated list of additional data to include.
380
389
  - "repairs": Repair items from Settings > System > Repairs (active only by default; pass `include_dismissed_repairs=True` for all)
@@ -700,6 +709,16 @@ class SystemTools:
700
709
  result.setdefault("warnings", []).extend(section_warnings)
701
710
  result["dead_entities"] = dead_section
702
711
 
712
+ # Surface the MCP server's own update status so the model can relay
713
+ # it in chat. ``get_update_field`` is best-effort, thread-offloaded,
714
+ # and never raises (omits the field on any hiccup); see
715
+ # ha_mcp.update_check for gating/throttle details.
716
+ from ..update_check import get_update_field
717
+
718
+ mcp_update = await get_update_field()
719
+ if mcp_update is not None:
720
+ result["ha_mcp_update"] = mcp_update
721
+
703
722
  return result
704
723
 
705
724
  except ToolError:
@@ -524,7 +524,7 @@ class UpdateTools:
524
524
 
525
525
  categories = {k: v for k, v in categories.items() if v}
526
526
 
527
- return {
527
+ result: dict[str, Any] = {
528
528
  "success": True,
529
529
  "updates_available": len(available_updates),
530
530
  "skipped_count": len(skipped_updates),
@@ -533,6 +533,19 @@ class UpdateTools:
533
533
  "include_skipped": include_skipped,
534
534
  }
535
535
 
536
+ # Surface the MCP server's OWN update status alongside HA's updates so the
537
+ # model is more likely to mention "you're on X, but Y is out" — this tool
538
+ # is HA-centric, but spreading the ha-mcp self-update signal across the
539
+ # status surfaces raises the odds an AI relays it. ``get_update_field`` is
540
+ # best-effort and never raises; see ha_mcp.update_check for details.
541
+ from ..update_check import get_update_field
542
+
543
+ mcp_update = await get_update_field()
544
+ if mcp_update is not None:
545
+ result["ha_mcp_update"] = mcp_update
546
+
547
+ return result
548
+
536
549
  async def _get_update_details(
537
550
  self, entity_id: str, include_release_notes: bool = False
538
551
  ) -> dict[str, Any]:
@@ -702,6 +715,11 @@ class UpdateTools:
702
715
  - updates_available: Count of available updates
703
716
  - updates: List of update entities with version info
704
717
  - categories: Updates grouped by category (core, addons, devices, hacs, os)
718
+ - ha_mcp_update: This MCP server's own update status
719
+ {current, latest, update_available} — so you can flag a newer ha-mcp
720
+ release (from PyPI for pip/Docker, the Supervisor add-on store for the
721
+ add-on). Present on all install types; omitted only for the unknown
722
+ version and when HA_MCP_DISABLE_UPDATE_CHECK is set.
705
723
 
706
724
  RETURNS (when getting specific update):
707
725
  - Update details including installed/latest versions
@@ -0,0 +1,261 @@
1
+ """Self-update notifier for ha-mcp.
2
+
3
+ An operator can sit on an old build without ever knowing. This does a
4
+ fail-silent check, holds the result in memory for the process, and surfaces a
5
+ newer release via a startup log banner and the ``ha_get_overview`` /
6
+ ``ha_get_system_health`` / ``ha_get_updates`` tool fields.
7
+
8
+ The comparison reference depends on how ha-mcp is deployed, because the running
9
+ version only means something against the matching source:
10
+
11
+ * **pip / Docker / stdio** — the running version IS a PyPI version, so it is
12
+ compared against PyPI: stable installs against ``ha-mcp``, dev installs
13
+ (``.dev``) against ``ha-mcp-dev``.
14
+ * **HA add-on (stable AND dev)** — the add-on is built from source and updated
15
+ through the Supervisor add-on store, so its ``HA_MCP_BUILD_VERSION`` is on the
16
+ add-on's own counter, NOT PyPI's. The reference is therefore the Supervisor
17
+ add-on store (``GET /addons/self/info`` → ``version`` / ``version_latest`` /
18
+ ``update_available``), which is the same counter — so the dev add-on, like
19
+ every other deployment, correctly says "you're on X, Y is out."
20
+
21
+ The check runs once per process — memoized in memory, no disk, no throttle. For
22
+ stdio that is once per session (the server is spawned per conversation); for a
23
+ long-running Docker/web server or the add-on, once at boot. It is a no-op for the
24
+ ``unknown`` version (PyPI path) and the ``HA_MCP_DISABLE_UPDATE_CHECK`` opt-out.
25
+ Every network call is best-effort: any failure yields "no update info" rather
26
+ than raising, so callers never need to guard it.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import asyncio
32
+ import functools
33
+ import logging
34
+ import os
35
+ from dataclasses import dataclass
36
+ from typing import Any, TypedDict
37
+
38
+ import httpx
39
+ from packaging.version import InvalidVersion, Version
40
+
41
+ from ._version import (
42
+ get_supervisor_base_url,
43
+ get_version,
44
+ is_dev_version,
45
+ is_running_in_addon,
46
+ )
47
+ from .stdio_settings_sidecar import _TRUTHY # shared HA_MCP_DISABLE_* truthy set
48
+
49
+ logger = logging.getLogger(__name__)
50
+
51
+ DISABLE_ENV = "HA_MCP_DISABLE_UPDATE_CHECK"
52
+ _PYPI_JSON_URL = "https://pypi.org/pypi/{package}/json"
53
+ _STABLE_PACKAGE = "ha-mcp"
54
+ _DEV_PACKAGE = "ha-mcp-dev"
55
+ # Tight timeout so the once-per-process check can never add more than a blip of
56
+ # latency to a cold stdio spawn.
57
+ _HTTP_TIMEOUT_SECONDS = 2.0
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class UpdateInfo:
62
+ """Result of a self-update check.
63
+
64
+ ``update_available`` is the field callers branch on; ``current`` and
65
+ ``latest`` are carried so a banner or tool field can show both versions.
66
+ """
67
+
68
+ current: str
69
+ latest: str
70
+ update_available: bool
71
+
72
+
73
+ def _is_disabled() -> bool:
74
+ # Reuse the shared _TRUTHY set so HA_MCP_DISABLE_UPDATE_CHECK parses
75
+ # identically to the sibling HA_MCP_DISABLE_SETTINGS_UI flag: only a truthy
76
+ # value disables; 0/false/no/off/blank (and anything unrecognized) keep the
77
+ # check enabled, so a user who sets =0 to "keep it on" isn't surprised.
78
+ return os.environ.get(DISABLE_ENV, "").strip().lower() in _TRUTHY
79
+
80
+
81
+ def _is_newer(latest: str, current: str) -> bool:
82
+ """Return True only when ``latest`` is a strictly higher PEP 440 release.
83
+
84
+ ``packaging.version`` orders dev/pre/post correctly — e.g.
85
+ ``7.8.0.dev714 < 7.8.0.dev720 < 7.8.0`` and ``7.8.0 < 7.9.0`` — so this works
86
+ for both the stable and dev channels. An unparseable version on either side
87
+ reads as "can't compare" → not newer (never a bogus banner).
88
+ """
89
+ try:
90
+ return Version(latest) > Version(current)
91
+ except InvalidVersion:
92
+ return False
93
+
94
+
95
+ def _fetch_latest_from_pypi(package: str) -> str | None:
96
+ """Fetch ``package``'s latest published version from PyPI, or None on failure."""
97
+ try:
98
+ # follow_redirects=True mirrors the sibling fetch in tools_updates.py and
99
+ # survives any future PyPI redirect (the JSON API serves 200 directly today).
100
+ resp = httpx.get(
101
+ _PYPI_JSON_URL.format(package=package),
102
+ timeout=_HTTP_TIMEOUT_SECONDS,
103
+ follow_redirects=True,
104
+ )
105
+ resp.raise_for_status()
106
+ version = resp.json()["info"]["version"]
107
+ return version if isinstance(version, str) else None
108
+ except (httpx.HTTPError, KeyError, ValueError, TypeError) as err:
109
+ logger.debug("ha-mcp update check skipped (PyPI fetch failed: %s)", err)
110
+ return None
111
+
112
+
113
+ def _fetch_supervisor_addon_info() -> dict[str, Any] | None:
114
+ """Return this add-on's Supervisor info dict, or None on any failure.
115
+
116
+ GET ``/addons/self/info`` carries ``version`` (installed), ``version_latest``
117
+ (the add-on store's latest), and ``update_available`` — all on the add-on's
118
+ own version counter. Sync + fail-silent, mirroring ``_fetch_latest_from_pypi``.
119
+ """
120
+ token = os.environ.get("SUPERVISOR_TOKEN")
121
+ if not token:
122
+ return None
123
+ try:
124
+ resp = httpx.get(
125
+ f"{get_supervisor_base_url()}/addons/self/info",
126
+ headers={"Authorization": f"Bearer {token}"},
127
+ timeout=_HTTP_TIMEOUT_SECONDS,
128
+ )
129
+ resp.raise_for_status()
130
+ body = resp.json()
131
+ # Supervisor REST envelope is {"result": "ok", "data": {...}}; some mocks
132
+ # return the data dict directly — handle both (mirrors settings_ui).
133
+ data = body.get("data") if isinstance(body, dict) and "data" in body else body
134
+ return data if isinstance(data, dict) else None
135
+ except (httpx.HTTPError, ValueError, TypeError) as err:
136
+ logger.debug("ha-mcp update check skipped (Supervisor fetch failed: %s)", err)
137
+ return None
138
+
139
+
140
+ def _resolve_from_supervisor() -> UpdateInfo | None:
141
+ """Build UpdateInfo for the add-on from the Supervisor add-on store."""
142
+ info = _fetch_supervisor_addon_info()
143
+ if info is None:
144
+ return None
145
+ current = info.get("version")
146
+ latest = info.get("version_latest")
147
+ if not isinstance(current, str) or not isinstance(latest, str):
148
+ return None
149
+ return UpdateInfo(
150
+ current=current,
151
+ latest=latest,
152
+ update_available=bool(info.get("update_available")),
153
+ )
154
+
155
+
156
+ def _resolve_update_info() -> UpdateInfo | None:
157
+ """Update-check logic; see ``get_update_info`` for the memo + never-raises wrapper."""
158
+ if _is_disabled():
159
+ return None
160
+ # In the add-on (stable OR dev), the authoritative reference is the
161
+ # Supervisor add-on store, which tracks the SAME version counter as the
162
+ # installed add-on. PyPI is the wrong reference there: the add-on builds
163
+ # ha-mcp from source on its own cadence (HA_MCP_BUILD_VERSION), unrelated to
164
+ # the ha-mcp/ha-mcp-dev PyPI counters — comparing them yields false
165
+ # positives/negatives. Non-add-on deployments (pip / Docker / stdio) report a
166
+ # real PyPI version and take the PyPI path below.
167
+ if is_running_in_addon():
168
+ return _resolve_from_supervisor()
169
+ current = get_version()
170
+ if current == "unknown":
171
+ return None
172
+ # A dev install (``.dev`` version) tracks the renamed ``ha-mcp-dev`` package;
173
+ # a stable install tracks ``ha-mcp``.
174
+ package = _DEV_PACKAGE if is_dev_version(current) else _STABLE_PACKAGE
175
+ latest = _fetch_latest_from_pypi(package)
176
+ if latest is None:
177
+ return None
178
+ return UpdateInfo(
179
+ current=current,
180
+ latest=latest,
181
+ update_available=_is_newer(latest, current),
182
+ )
183
+
184
+
185
+ @functools.lru_cache(maxsize=1)
186
+ def get_update_info() -> UpdateInfo | None:
187
+ """Return self-update info for this process, or None when no check applies.
188
+
189
+ Memoized in memory (``lru_cache``) so the network check runs once per process
190
+ — the startup banner warms it and the tool fields reuse it without re-hitting
191
+ PyPI. No disk, no throttle: a long-running server reflects its boot-time check
192
+ until restart, which is fine (those operators aren't watching startup logs).
193
+ Never raises — an unexpected failure degrades to None so the unguarded
194
+ startup-banner call can't break startup. Tests reset via
195
+ ``get_update_info.cache_clear()``.
196
+ """
197
+ try:
198
+ return _resolve_update_info()
199
+ except Exception as err: # pragma: no cover - contract backstop
200
+ logger.debug("ha-mcp update check skipped (unexpected error: %s)", err)
201
+ return None
202
+
203
+
204
+ class UpdateField(TypedDict):
205
+ """The ``ha_mcp_update`` object embedded in status-tool responses."""
206
+
207
+ current: str
208
+ latest: str
209
+ update_available: bool
210
+
211
+
212
+ async def get_update_field() -> UpdateField | None:
213
+ """Return an embeddable self-update dict for tool responses, or None.
214
+
215
+ Off-loads the (memoized, networks-at-most-once-per-process) check to a thread
216
+ so it never blocks the event loop, and shapes the result for embedding under
217
+ an ``ha_mcp_update`` key. Never raises — a hiccup yields None (the tool omits
218
+ the field) and is logged at debug. Shared by every status tool that surfaces
219
+ the notice (``ha_get_overview`` / ``ha_get_system_health`` / ``ha_get_updates``)
220
+ so the shaping and event-loop offload live in one place.
221
+ """
222
+ try:
223
+ # Once the lru_cache is warm (normally at startup), the result is a
224
+ # sub-microsecond cache hit, so call it directly. Only the cold first
225
+ # call — which may hit PyPI — is offloaded to a thread so it can't block
226
+ # the event loop.
227
+ if get_update_info.cache_info().currsize > 0:
228
+ info = get_update_info()
229
+ else:
230
+ info = await asyncio.to_thread(get_update_info)
231
+ except Exception as err: # pragma: no cover - defensive
232
+ logger.debug("ha-mcp self-update check skipped: %s", err)
233
+ return None
234
+ if info is None:
235
+ return None
236
+ return {
237
+ "current": info.current,
238
+ "latest": info.latest,
239
+ "update_available": info.update_available,
240
+ }
241
+
242
+
243
+ def _running_in_docker() -> bool:
244
+ return os.path.exists("/.dockerenv") or os.path.exists("/run/.containerenv")
245
+
246
+
247
+ def update_command_hint(current: str) -> str:
248
+ """Return a deployment- and channel-aware one-liner for how to upgrade."""
249
+ if is_running_in_addon():
250
+ # Add-on users update through the Supervisor UI, not pip/docker.
251
+ return "Update from Settings -> Add-ons -> Home Assistant MCP Server."
252
+ dev = is_dev_version(current)
253
+ if _running_in_docker():
254
+ # Dev images are tagged :dev (rolling); :stable is the stable channel.
255
+ tag = "dev" if dev else "stable"
256
+ return (
257
+ f"Pull the new image: docker pull "
258
+ f"ghcr.io/homeassistant-ai/ha-mcp:{tag} (then restart)."
259
+ )
260
+ package = _DEV_PACKAGE if dev else _STABLE_PACKAGE
261
+ return f"Upgrade with: pip install -U {package}."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.8.0.dev714
3
+ Version: 7.8.1.dev716
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
@@ -27,6 +27,7 @@ Requires-Dist: websockets==16.0
27
27
  Requires-Dist: cryptography==49.0.0
28
28
  Requires-Dist: pydantic-monty==0.0.18
29
29
  Requires-Dist: tzdata>=2024.1
30
+ Requires-Dist: packaging>=24.0
30
31
  Dynamic: license-file
31
32
 
32
33
  > **Breaking change (v7.3.0):** `ha_config_set_yaml` has been moved to [beta](docs/beta.md).
@@ -319,9 +320,16 @@ Skills can still be installed manually for clients that prefer local skill files
319
320
 
320
321
  ## 🔍 Tool Discovery for AI Agents
321
322
 
322
- By default, the full tool catalog (~86 tools) is listed to the client through the standard MCP `tools/list` response. Clients with deferred / on-demand tool loading (Claude Sonnet, Claude Opus,) handle that fine — tools are pulled into context only when needed, so idle context cost is near-zero.
323
+ By default, the full tool catalog (~84 tools) is listed to the client through the standard MCP `tools/list` response. Clients with deferred / on-demand tool loading (Claude Sonnet, Claude Opus) handle that fine — tools are pulled into context only when needed, so idle context cost is near-zero.
323
324
 
324
- For models *without* deferred tool support — Claude Haiku, Gemini, ChatGPT OpenAI-compatible local models, smaller open-weights models — listing 86 tools eats ~46K tokens of idle context. To address that, the server ships with a **search-based discovery mode** built on top of FastMCP's BM25 search transform.
325
+ For models *without* deferred tool support — Claude Haiku, Gemini, ChatGPT OpenAI-compatible local models, smaller open-weights models — listing the full tool catalog up front adds a lot of idle context and can overwhelm smaller models. To address that, the server ships with a **search-based discovery mode** built on top of FastMCP's BM25 search transform.
326
+
327
+ ### Smaller or local LLMs (Ollama, etc.)
328
+
329
+ If your model can't see the tools or your Home Assistant, it may be getting handed the whole tool catalog at once and struggling with it. It's recommended to try the following to see if it helps:
330
+
331
+ - **Enable tool search** (`ENABLE_TOOL_SEARCH=true`, or the add-on option below). Instead of listing every tool up front, the server defers the catalog behind a search interface so the model pulls in only the tools it needs, when it needs them.
332
+ - **Raise the model's context window above the default.** Local runtimes ship with small defaults (Ollama's `num_ctx` is one example) that can't hold a large tool set plus the conversation — increase it well beyond the default.
325
333
 
326
334
  ### Enable search-based discovery
327
335
 
@@ -338,14 +346,14 @@ The proxy split lets MCP clients apply different permission policies per categor
338
346
 
339
347
  | Setting | Default | Description |
340
348
  |---------|---------|-------------|
341
- | `ENABLE_TOOL_SEARCH` | `false` | Replace full tool catalog with search-based discovery (~46K ~5K idle tokens). |
349
+ | `ENABLE_TOOL_SEARCH` | `false` | Replace full tool catalog with search-based discovery (tools deferred behind on-demand search). |
342
350
  | `TOOL_SEARCH_MAX_RESULTS` | `5` | Max results returned by `ha_search_tools` (range 2–10). |
343
351
  | `PINNED_TOOLS` | empty | Comma-separated tool names to keep always visible. The web settings UI is the primary way to manage this. |
344
352
 
345
353
  ### When to enable
346
354
 
347
355
  - **Claude Haiku, OpenAI-compatible local models, Gemini, ChatGPT or any model without native deferred tool support** — large idle-context savings.
348
- - MCP clients that cap total tool count (some cap at 100) — surfaces a minimal set (~10 tools) instead of 86.
356
+ - MCP clients that cap total tool count (some cap at 100) — surfaces a minimal set (~10 tools) instead of 84.
349
357
  - **Cost-sensitive deployments** — fewer idle tokens per turn.
350
358
 
351
359
  Leave it off when using Claude Sonnet/Opus or any client with deferred tool loading; the full catalog has no idle cost there and direct calls skip the search step. If you choose to use our toolsearch then you should disable the native Claude Opus/Sonnet toolsearch, which is called deferred tools in the settings.
@@ -17,6 +17,7 @@ src/ha_mcp/settings.js
17
17
  src/ha_mcp/settings_ui.py
18
18
  src/ha_mcp/smoke_test.py
19
19
  src/ha_mcp/stdio_settings_sidecar.py
20
+ src/ha_mcp/update_check.py
20
21
  src/ha_mcp/auth/__init__.py
21
22
  src/ha_mcp/auth/consent_form.py
22
23
  src/ha_mcp/auth/provider.py
@@ -7,3 +7,4 @@ websockets==16.0
7
7
  cryptography==49.0.0
8
8
  pydantic-monty==0.0.18
9
9
  tzdata>=2024.1
10
+ packaging>=24.0