ha-mcp-dev 7.8.0.dev709__tar.gz → 7.8.0.dev711__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.8.0.dev709/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.8.0.dev711}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/backup_manager.py +108 -9
  4. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/auto_backup.py +14 -11
  5. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_entities.py +1 -0
  6. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_registry.py +3 -0
  7. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_utility.py +105 -10
  8. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  9. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/LICENSE +0 -0
  10. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/MANIFEST.in +0 -0
  11. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/README.md +0 -0
  12. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/setup.cfg +0 -0
  13. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/__main__.py +0 -0
  15. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/_pypi_marker +0 -0
  16. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/_version.py +0 -0
  17. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/auth/__init__.py +0 -0
  18. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/auth/consent_form.py +0 -0
  19. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/auth/provider.py +0 -0
  20. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/client/__init__.py +0 -0
  21. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/client/rest_client.py +0 -0
  22. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/client/supervisor_client.py +0 -0
  23. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/client/websocket_client.py +0 -0
  24. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/client/websocket_listener.py +0 -0
  25. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/config.py +0 -0
  26. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  27. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  28. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  29. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/errors.py +0 -0
  30. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/__init__.py +0 -0
  31. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/approval_queue.py +0 -0
  32. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/evaluator.py +0 -0
  33. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/handlers.py +0 -0
  34. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/middleware.py +0 -0
  35. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/model.py +0 -0
  36. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/persistence.py +0 -0
  37. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/value_sources.py +0 -0
  38. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/py.typed +0 -0
  39. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/read_only.py +0 -0
  40. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  41. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  42. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  43. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  44. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  45. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  46. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  47. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  48. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  49. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  50. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  51. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  52. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  53. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  54. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  55. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  56. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  57. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  58. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  59. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  60. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  61. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  62. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/server.py +0 -0
  63. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/settings.css +0 -0
  64. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/settings.js +0 -0
  65. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/settings_ui.py +0 -0
  66. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/smoke_test.py +0 -0
  67. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  68. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/__init__.py +0 -0
  69. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/backup.py +0 -0
  70. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  71. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  72. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/device_control.py +0 -0
  73. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/enhanced.py +0 -0
  74. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/helpers.py +0 -0
  75. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/reference_validator.py +0 -0
  76. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/registry.py +0 -0
  77. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  78. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  79. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  80. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  81. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  82. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  83. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  84. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  85. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  86. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  87. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_addons.py +0 -0
  88. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_areas.py +0 -0
  89. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  90. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  91. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_calendar.py +0 -0
  92. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_camera.py +0 -0
  93. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_categories.py +0 -0
  94. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_code.py +0 -0
  95. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  96. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  97. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  98. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  99. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  100. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  101. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_energy.py +0 -0
  102. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  103. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_groups.py +0 -0
  104. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_hacs.py +0 -0
  105. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_history.py +0 -0
  106. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_integrations.py +0 -0
  107. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_labels.py +0 -0
  108. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  109. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_resources.py +0 -0
  110. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_search.py +0 -0
  111. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_service.py +0 -0
  112. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_services.py +0 -0
  113. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_system.py +0 -0
  114. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_themes.py +0 -0
  115. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_todo.py +0 -0
  116. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_traces.py +0 -0
  117. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_updates.py +0 -0
  118. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  119. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  120. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_zones.py +0 -0
  121. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/util_helpers.py +0 -0
  122. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/validation_middleware.py +0 -0
  123. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/transforms/__init__.py +0 -0
  124. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/transforms/categorized_search.py +0 -0
  125. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  126. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/__init__.py +0 -0
  127. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/config_hash.py +0 -0
  128. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/data_paths.py +0 -0
  129. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/domain_handlers.py +0 -0
  130. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  131. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  132. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/operation_manager.py +0 -0
  133. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/python_sandbox.py +0 -0
  134. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/skill_loader.py +0 -0
  135. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/usage_logger.py +0 -0
  136. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  137. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  138. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  139. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  140. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  141. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/tests/__init__.py +0 -0
  142. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/tests/test_constants.py +0 -0
  143. {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/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.dev709
3
+ Version: 7.8.0.dev711
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.8.0.dev709"
7
+ version = "7.8.0.dev711"
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"
@@ -991,19 +991,46 @@ async def _fetch_dashboard(client: Any, entity_id: str) -> Any:
991
991
  """
992
992
  from fastmcp.exceptions import ToolError
993
993
 
994
- from .tools.tools_config_dashboards import _get_dashboard_config_internal
994
+ from .tools.tools_config_dashboards import (
995
+ _get_dashboard_config_internal,
996
+ _resolve_dashboard,
997
+ )
998
+
999
+ # The set/delete tools accept BOTH the canonical hyphenated url_path
1000
+ # AND HA's internal (underscored) dashboard id, eagerly resolving the
1001
+ # latter before writing. ``_get_dashboard_config_internal`` does NOT
1002
+ # lazy-resolve, so an internal-id identifier 404s with "Unknown config
1003
+ # specified" and the pre-write snapshot is silently skipped. Pre-resolve
1004
+ # to the canonical url_path so capture works for whichever form the
1005
+ # caller passed (matching the form the write tool ultimately targets).
1006
+ fetch_path = entity_id
1007
+ try:
1008
+ match, _ = await _resolve_dashboard(client, entity_id)
1009
+ if match and match.get("url_path"):
1010
+ fetch_path = match["url_path"]
1011
+ except (HomeAssistantError, ToolError) as err:
1012
+ # Resolver failure (transport/shape) — fall through with the
1013
+ # original identifier; the canonical form is often already correct.
1014
+ logger.debug(
1015
+ "Auto-backup: dashboard resolve failed for %r: %s — using as-is",
1016
+ entity_id,
1017
+ err,
1018
+ )
995
1019
 
996
1020
  try:
997
- config, _config_hash = await _get_dashboard_config_internal(client, entity_id)
1021
+ config, _config_hash = await _get_dashboard_config_internal(client, fetch_path)
998
1022
  except ToolError as err:
999
- # ToolError carries the structured failure payload; treat
1000
- # missing-dashboard responses as "entity doesn't exist yet".
1023
+ # ToolError carries the structured failure payload; treat a
1024
+ # missing/unknown dashboard as "nothing to back up" (also covers a
1025
+ # brand-new dashboard on the create path). "Unknown config
1026
+ # specified" is HA's message for an unresolved url_path.
1001
1027
  msg = str(err).lower()
1002
- if "not_found" in msg or "config_not_found" in msg:
1028
+ if "not_found" in msg or "config_not_found" in msg or "unknown config" in msg:
1003
1029
  return None
1004
1030
  raise
1005
1031
  except HomeAssistantError as err:
1006
- if "not_found" in str(err).lower() or "config_not_found" in str(err).lower():
1032
+ msg = str(err).lower()
1033
+ if "not_found" in msg or "config_not_found" in msg or "unknown config" in msg:
1007
1034
  return None
1008
1035
  raise
1009
1036
  return config
@@ -1305,8 +1332,11 @@ async def _restore_area_or_floor(client: Any, entity_id: str, config: Any) -> An
1305
1332
 
1306
1333
 
1307
1334
  async def _fetch_todo_item(client: Any, entity_id: str) -> Any:
1308
- cal, _, uid = entity_id.partition("::")
1309
- if not cal or not uid:
1335
+ # The second segment is whatever the tool's ``item`` param carried.
1336
+ # ha_set_todo_item / ha_remove_todo_item accept EITHER the item uid OR
1337
+ # its exact summary/name, so this can be either form.
1338
+ cal, _, item_ref = entity_id.partition("::")
1339
+ if not cal or not item_ref:
1310
1340
  return None
1311
1341
  payload = {
1312
1342
  "type": "execute_script",
@@ -1332,7 +1362,11 @@ async def _fetch_todo_item(client: Any, entity_id: str) -> Any:
1332
1362
  result = _require_dict(result, "execute_script")
1333
1363
  items = result.get("response", {}).get("items", {}).get(cal, {}).get("items", [])
1334
1364
  for item in items:
1335
- if item.get("uid") == uid:
1365
+ # Match either form. Matching only on uid silently skipped the
1366
+ # snapshot whenever the caller passed the human-readable summary
1367
+ # (the documented/common case, e.g. ha_remove_todo_item(list, "Buy
1368
+ # milk")) — uid != summary, so the loop found nothing -> None.
1369
+ if item.get("uid") == item_ref or item.get("summary") == item_ref:
1336
1370
  return {"todo_entity_id": cal, **item}
1337
1371
  return None
1338
1372
 
@@ -1366,6 +1400,42 @@ async def _restore_entity_state(client: Any, entity_id: str, config: Any) -> Any
1366
1400
  return await _rest_post(client, f"states/{entity_id}", payload)
1367
1401
 
1368
1402
 
1403
+ # Devices — config/device_registry/{list,update}. ``ha_set_device`` mutates
1404
+ # the user-editable registry fields (name_by_user / area_id / disabled_by /
1405
+ # labels); restore re-applies exactly those. A device deleted by
1406
+ # ``ha_remove_device`` cannot be recreated through the registry, so for that
1407
+ # path the snapshot is an informational pre-delete record and restore is
1408
+ # best-effort.
1409
+
1410
+
1411
+ async def _fetch_device(client: Any, entity_id: str) -> Any:
1412
+ items = await _ws_send(client, {"type": "config/device_registry/list"})
1413
+ if not isinstance(items, list):
1414
+ return None
1415
+ for item in items:
1416
+ if item.get("id") == entity_id:
1417
+ return item
1418
+ return None
1419
+
1420
+
1421
+ async def _restore_device(client: Any, entity_id: str, config: Any) -> Any:
1422
+ # Re-apply the captured registry state. Uses the same field NAMES as
1423
+ # ``_update_device_internal`` but, unlike that partial-update path, always
1424
+ # sends all four — restore reverts the device to the snapshot, so a
1425
+ # captured ``None`` area/name is intentionally re-applied (cleared).
1426
+ return await _ws_send(
1427
+ client,
1428
+ {
1429
+ "type": "config/device_registry/update",
1430
+ "device_id": entity_id,
1431
+ "name_by_user": config.get("name_by_user"),
1432
+ "area_id": config.get("area_id"),
1433
+ "disabled_by": config.get("disabled_by"),
1434
+ "labels": config.get("labels", []),
1435
+ },
1436
+ )
1437
+
1438
+
1369
1439
  # Integration enable/disable — restore re-applies the disabled flag.
1370
1440
 
1371
1441
 
@@ -1435,6 +1505,34 @@ async def _fetch_helper(client: Any, entity_id: str, helper_type: str) -> Any:
1435
1505
  for item in items:
1436
1506
  if item.get("id") == object_id or item.get("id") == entity_id:
1437
1507
  return item
1508
+ # Fallback for renamed helpers: after an entity_id rename the object_id
1509
+ # no longer equals the storage collection id (which stays the original
1510
+ # create-time id == the registry unique_id), so the direct match above
1511
+ # misses and the snapshot was silently skipped. Resolve the unique_id
1512
+ # via the entity registry and match on that — the same key the helper
1513
+ # update tool itself resolves to.
1514
+ eid = entity_id if "." in entity_id else f"{helper_type}.{entity_id}"
1515
+ try:
1516
+ entry = await _ws_send(
1517
+ client, {"type": "config/entity_registry/get", "entity_id": eid}
1518
+ )
1519
+ except HomeAssistantError as err:
1520
+ # Only a genuine "entity not found" means there's nothing to back up;
1521
+ # transport/auth/5xx errors must propagate so maybe_snapshot logs a
1522
+ # WARNING rather than silently skipping. Same POLICY as _fetch_automation,
1523
+ # but matched on the message substring because config/entity_registry/get
1524
+ # failures arrive as a WS command error with no status_code to switch on.
1525
+ # Best-effort: if HA's not-found wording ever changes, a real miss
1526
+ # degrades to a WARNING + skip (never a swallowed fatal error).
1527
+ msg = str(err).lower()
1528
+ if "not_found" in msg or "not found" in msg:
1529
+ return None
1530
+ raise
1531
+ unique_id = entry.get("unique_id") if isinstance(entry, dict) else None
1532
+ if unique_id:
1533
+ for item in items:
1534
+ if str(item.get("id")) == str(unique_id):
1535
+ return item
1438
1536
  return None
1439
1537
 
1440
1538
 
@@ -1505,6 +1603,7 @@ def register_default_handlers(mgr: BackupManager, _client: Any) -> None:
1505
1603
  )
1506
1604
  mgr.register(DomainHandler("todo_item", _fetch_todo_item, _restore_todo_item))
1507
1605
  mgr.register(DomainHandler("entity", _fetch_entity_state, _restore_entity_state))
1606
+ mgr.register(DomainHandler("device", _fetch_device, _restore_device))
1508
1607
  mgr.register(DomainHandler("integration", _fetch_integration, _restore_integration))
1509
1608
  for helper_type in _KNOWN_HELPER_TYPES:
1510
1609
  mgr.register(_make_helper_handler(helper_type))
@@ -75,17 +75,20 @@ def automation_backup_target(kw: dict[str, Any]) -> str:
75
75
  config_id = config.get("id")
76
76
  if config_id:
77
77
  return str(config_id)
78
- identifier = _resolve_str(kw.get("identifier"))
79
- # Strip the leading ``automation.`` prefix when the caller passed an
80
- # entity_id form (typical for ``python_transform`` calls that don't
81
- # carry a config body). Without this, snapshot files duplicate the
82
- # domain segment as ``automation.automation.<slug>.<ts>.yaml`` the
83
- # body's entity_id keeps the prefix, only the filename / list key
84
- # tighten up. HA's automation upsert accepts either form for the
85
- # ``identifier`` param, so restore is unaffected.
86
- if identifier.startswith("automation."):
87
- identifier = identifier[len("automation.") :]
88
- return identifier
78
+ # Return the identifier UNCHANGED — do NOT strip the ``automation.``
79
+ # prefix. Capture and restore resolve the target through
80
+ # ``client.get_automation_config`` -> ``_resolve_automation_id``, which
81
+ # converts an entity_id ("automation.<slug>") to the real numeric
82
+ # ``unique_id`` via a state lookup ONLY when the prefix is present;
83
+ # otherwise it assumes the string already IS a unique_id. Stripping the
84
+ # prefix produced a bare object_id slug that the resolver mis-treats as
85
+ # a unique_id -> GET /config/automation/config/<slug> 404s -> the
86
+ # pre-write snapshot is silently skipped (and, had it resolved, restore
87
+ # would POST to the wrong key and create a stray automation). The
88
+ # doubled domain segment in the snapshot filename
89
+ # ("automation.automation.<slug>.<ts>.yaml") is purely cosmetic and is
90
+ # exactly what the remove path (id_param="identifier") already produces.
91
+ return _resolve_str(kw.get("identifier"))
89
92
 
90
93
 
91
94
  def with_auto_backup(
@@ -1316,6 +1316,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1316
1316
  "title": "Remove Entity",
1317
1317
  },
1318
1318
  )
1319
+ @with_auto_backup(domain="entity", id_param="entity_id", client=client)
1319
1320
  @log_tool_usage
1320
1321
  async def ha_remove_entity(
1321
1322
  entity_id: Annotated[
@@ -14,6 +14,7 @@ from pydantic import Field
14
14
 
15
15
  from ..client.rest_client import HomeAssistantAPIError, HomeAssistantConnectionError
16
16
  from ..errors import ErrorCode, create_error_response
17
+ from .auto_backup import with_auto_backup
17
18
  from .helpers import (
18
19
  exception_to_structured_error,
19
20
  log_tool_usage,
@@ -599,6 +600,7 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
599
600
  tags={"Device Registry"},
600
601
  annotations={"destructiveHint": True, "title": "Set Device"},
601
602
  )
603
+ @with_auto_backup(domain="device", id_param="device_id", client=client)
602
604
  @log_tool_usage
603
605
  async def ha_set_device(
604
606
  device_id: Annotated[
@@ -700,6 +702,7 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
700
702
  "title": "Remove Device",
701
703
  },
702
704
  )
705
+ @with_auto_backup(domain="device", id_param="device_id", client=client)
703
706
  @log_tool_usage
704
707
  async def ha_remove_device(
705
708
  device_id: Annotated[
@@ -122,8 +122,14 @@ class UtilityTools:
122
122
  entity_id: str | None,
123
123
  end_time: str | None,
124
124
  slug: str | None,
125
+ order: Literal["newest", "oldest"],
125
126
  ) -> list[str]:
126
127
  warnings: list[str] = []
128
+ if source == "logger" and order != "newest":
129
+ warnings.append(
130
+ "Parameter 'order' does not apply to source='logger' "
131
+ "(entries are sorted by integration name); ignored"
132
+ )
127
133
  if source != "logbook" and any(p is not None for p in [entity_id, end_time]):
128
134
  ignored = [
129
135
  p
@@ -200,6 +206,7 @@ class UtilityTools:
200
206
  compact: bool,
201
207
  level: str | None,
202
208
  slug: str | None,
209
+ order: Literal["newest", "oldest"],
203
210
  ) -> dict[str, Any]:
204
211
  if source == "logbook":
205
212
  return await self._get_logbook(
@@ -210,20 +217,29 @@ class UtilityTools:
210
217
  offset=offset,
211
218
  search=search,
212
219
  compact=compact,
220
+ order=order,
213
221
  )
214
222
  if source == "system":
215
- return await self._get_system_log(limit=limit, search=search, level=level)
223
+ return await self._get_system_log(
224
+ limit=limit, search=search, level=level, order=order
225
+ )
216
226
  if source == "error_log":
217
- return await self._get_error_log(limit=limit, search=search, level=level)
227
+ return await self._get_error_log(
228
+ limit=limit, search=search, level=level, order=order
229
+ )
218
230
  if source == "logger":
231
+ # logger reports per-integration levels, not time-ordered events;
232
+ # 'order' does not apply (a warning is emitted upstream).
219
233
  return await self._get_logger_info(limit=limit, search=search)
220
234
  if source == "system_service":
221
235
  assert slug is not None # guaranteed by _validate_log_slug
222
236
  return await self._get_system_service_log(
223
- service=slug, limit=limit, search=search
237
+ service=slug, limit=limit, search=search, order=order
224
238
  )
225
239
  assert slug is not None # guaranteed by _validate_log_slug
226
- return await self._get_supervisor_log(slug=slug, limit=limit, search=search)
240
+ return await self._get_supervisor_log(
241
+ slug=slug, limit=limit, search=search, order=order
242
+ )
227
243
 
228
244
  async def get_logs(
229
245
  self,
@@ -237,9 +253,12 @@ class UtilityTools:
237
253
  compact: bool,
238
254
  level: str | None,
239
255
  slug: str | None,
256
+ order: Literal["newest", "oldest"] = "newest",
240
257
  ) -> dict[str, Any]:
241
258
  level = self._validate_log_level(level)
242
- warnings = self._collect_log_warnings(source, level, entity_id, end_time, slug)
259
+ warnings = self._collect_log_warnings(
260
+ source, level, entity_id, end_time, slug, order
261
+ )
243
262
  self._validate_log_slug(source, slug)
244
263
  result = await self._fetch_log_source(
245
264
  source,
@@ -252,6 +271,7 @@ class UtilityTools:
252
271
  compact,
253
272
  level,
254
273
  slug,
274
+ order,
255
275
  )
256
276
  if warnings:
257
277
  result["warnings"] = warnings
@@ -277,6 +297,7 @@ class UtilityTools:
277
297
  entity_id: str | None,
278
298
  search: str | None,
279
299
  compact_bool: bool,
300
+ order: Literal["newest", "oldest"] = "newest",
280
301
  ) -> str:
281
302
  """Build reproducible pagination hint string for logbook results."""
282
303
  next_offset = offset_int + effective_limit
@@ -293,6 +314,8 @@ class UtilityTools:
293
314
  param_parts.append(f"search={search}")
294
315
  if not compact_bool:
295
316
  param_parts.append("compact=False")
317
+ if order != "newest":
318
+ param_parts.append(f"order={order}")
296
319
  param_str = ", ".join(param_parts)
297
320
  return (
298
321
  f"Showing entries {offset_int + 1}-{offset_int + len(paginated_entries)} of {total_entries}. "
@@ -308,6 +331,7 @@ class UtilityTools:
308
331
  offset: int = 0,
309
332
  search: str | None = None,
310
333
  compact: bool = True,
334
+ order: Literal["newest", "oldest"] = "newest",
311
335
  ) -> dict[str, Any]:
312
336
  """Fetch logbook entries with search and pagination."""
313
337
  hours_back_int, effective_limit, offset_int = self._coerce_logbook_params(
@@ -342,8 +366,20 @@ class UtilityTools:
342
366
  total_entries = len(response) if isinstance(response, list) else 1
343
367
 
344
368
  if isinstance(response, list):
345
- paginated_entries = response[offset_int : offset_int + effective_limit]
346
- has_more = (offset_int + effective_limit) < total_entries
369
+ # HA's /logbook returns entries oldest-first. Take a window from
370
+ # the end for newest-first (default), or from the start for
371
+ # oldest-first, with offset paging deeper in the chosen order.
372
+ if order == "newest":
373
+ end = total_entries - offset_int
374
+ start = max(end - effective_limit, 0)
375
+ paginated_entries = (
376
+ list(reversed(response[start:end])) if end > 0 else []
377
+ )
378
+ else:
379
+ paginated_entries = response[
380
+ offset_int : offset_int + effective_limit
381
+ ]
382
+ has_more = offset_int + len(paginated_entries) < total_entries
347
383
  else:
348
384
  paginated_entries = response
349
385
  has_more = False
@@ -368,6 +404,7 @@ class UtilityTools:
368
404
  else 1,
369
405
  "limit": effective_limit,
370
406
  "offset": offset_int,
407
+ "order": order,
371
408
  "has_more": has_more,
372
409
  }
373
410
  if filters_applied:
@@ -383,6 +420,7 @@ class UtilityTools:
383
420
  entity_id,
384
421
  search,
385
422
  compact,
423
+ order,
386
424
  )
387
425
 
388
426
  return await add_timezone_metadata(self._client, logbook_data)
@@ -416,11 +454,28 @@ class UtilityTools:
416
454
  )
417
455
  raise # unreachable: exception_to_structured_error always raises
418
456
 
457
+ @staticmethod
458
+ def _system_log_sort_key(entry: Any) -> float:
459
+ """Total-order-safe sort key for system_log entries.
460
+
461
+ ``system_log/list`` does not guarantee a numeric ``timestamp`` on every
462
+ record. Coerce a missing / non-numeric / non-dict entry to ``0.0`` so
463
+ sorting never raises a cross-type ``TypeError`` (bools are excluded so
464
+ a stray ``True`` doesn't read as ``1.0``).
465
+ """
466
+ if not isinstance(entry, dict):
467
+ return 0.0
468
+ ts = entry.get("timestamp")
469
+ if isinstance(ts, bool) or not isinstance(ts, (int, float)):
470
+ return 0.0
471
+ return float(ts)
472
+
419
473
  async def _get_system_log(
420
474
  self,
421
475
  limit: int | None = None,
422
476
  search: str | None = None,
423
477
  level: str | None = None,
478
+ order: Literal["newest", "oldest"] = "newest",
424
479
  ) -> dict[str, Any]:
425
480
  """Fetch structured system log entries via system_log/list."""
426
481
  effective_limit = self._coerce_limit(limit)
@@ -461,6 +516,17 @@ class UtilityTools:
461
516
  ]
462
517
  filters_applied["search"] = search
463
518
 
519
+ # system_log/list entries carry a 'timestamp' (epoch float, last
520
+ # occurrence), but HA does not guarantee it on every record. Sort
521
+ # with a total-order-safe key so 'order' is deterministic regardless
522
+ # of HA's native ordering (newest-first by default) and a missing /
523
+ # non-numeric / non-dict entry can never raise a cross-type
524
+ # TypeError out of this method's narrow except clause.
525
+ entries.sort(
526
+ key=self._system_log_sort_key,
527
+ reverse=(order == "newest"),
528
+ )
529
+
464
530
  total_entries = len(entries)
465
531
  entries = entries[:effective_limit]
466
532
 
@@ -471,6 +537,7 @@ class UtilityTools:
471
537
  "total_entries": total_entries,
472
538
  "returned_entries": len(entries),
473
539
  "limit": effective_limit,
540
+ "order": order,
474
541
  }
475
542
  if filters_applied:
476
543
  data["filters_applied"] = filters_applied
@@ -500,6 +567,7 @@ class UtilityTools:
500
567
  limit: int | None = None,
501
568
  search: str | None = None,
502
569
  level: str | None = None,
570
+ order: Literal["newest", "oldest"] = "newest",
503
571
  ) -> dict[str, Any]:
504
572
  """Fetch raw error log text from home-assistant.log."""
505
573
  effective_limit = self._coerce_limit(
@@ -527,8 +595,11 @@ class UtilityTools:
527
595
  filters_applied["search"] = search
528
596
 
529
597
  total_lines = len(lines)
530
- # Return the LAST N lines (most recent)
598
+ # Always take the most-recent window (the tail of the chronological
599
+ # file); 'order' controls only the display direction of that window.
531
600
  lines = lines[-effective_limit:]
601
+ if order == "newest":
602
+ lines = list(reversed(lines))
532
603
 
533
604
  data: dict[str, Any] = {
534
605
  "success": True,
@@ -537,6 +608,7 @@ class UtilityTools:
537
608
  "total_lines": total_lines,
538
609
  "returned_lines": len(lines),
539
610
  "limit": effective_limit,
611
+ "order": order,
540
612
  "note": "Returned the most recent log lines matching filters",
541
613
  }
542
614
  if filters_applied:
@@ -665,6 +737,7 @@ class UtilityTools:
665
737
  slug: str,
666
738
  limit: int | None = None,
667
739
  search: str | None = None,
740
+ order: Literal["newest", "oldest"] = "newest",
668
741
  ) -> dict[str, Any]:
669
742
  """Fetch add-on container logs.
670
743
 
@@ -692,8 +765,11 @@ class UtilityTools:
692
765
  filters_applied["search"] = search
693
766
 
694
767
  total_lines = len(lines)
695
- # Return the LAST N lines (most recent)
768
+ # Always take the most-recent window (the tail); 'order' controls
769
+ # only the display direction of that window.
696
770
  lines = lines[-effective_limit:]
771
+ if order == "newest":
772
+ lines = list(reversed(lines))
697
773
 
698
774
  data: dict[str, Any] = {
699
775
  "success": True,
@@ -703,6 +779,7 @@ class UtilityTools:
703
779
  "total_lines": total_lines,
704
780
  "returned_lines": len(lines),
705
781
  "limit": effective_limit,
782
+ "order": order,
706
783
  }
707
784
  if filters_applied:
708
785
  data["filters_applied"] = filters_applied
@@ -797,6 +874,7 @@ class UtilityTools:
797
874
  service: str,
798
875
  limit: int | None = None,
799
876
  search: str | None = None,
877
+ order: Literal["newest", "oldest"] = "newest",
800
878
  ) -> dict[str, Any]:
801
879
  """Fetch HA system-service logs from Supervisor's per-service endpoint.
802
880
 
@@ -826,8 +904,11 @@ class UtilityTools:
826
904
  filters_applied["search"] = search
827
905
 
828
906
  total_lines = len(lines)
829
- # Return the LAST N lines (most recent)
907
+ # Always take the most-recent window (the tail); 'order' controls
908
+ # only the display direction of that window.
830
909
  lines = lines[-effective_limit:]
910
+ if order == "newest":
911
+ lines = list(reversed(lines))
831
912
 
832
913
  data: dict[str, Any] = {
833
914
  "success": True,
@@ -837,6 +918,7 @@ class UtilityTools:
837
918
  "total_lines": total_lines,
838
919
  "returned_lines": len(lines),
839
920
  "limit": effective_limit,
921
+ "order": order,
840
922
  }
841
923
  if filters_applied:
842
924
  data["filters_applied"] = filters_applied
@@ -1035,6 +1117,17 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1035
1117
  # Shared parameters
1036
1118
  limit: int | None = None,
1037
1119
  search: str | None = None,
1120
+ order: Annotated[
1121
+ Literal["newest", "oldest"],
1122
+ Field(
1123
+ description=(
1124
+ "Sort order for time-ordered sources (logbook, system, "
1125
+ "error_log, supervisor, system_service): 'newest' (default) "
1126
+ "returns most-recent first; 'oldest' returns chronological-"
1127
+ "first. Ignored for source='logger'."
1128
+ )
1129
+ ),
1130
+ ] = "newest",
1038
1131
  # Logbook-specific (ignored for other sources)
1039
1132
  hours_back: Annotated[int, Field(ge=1)] = 1,
1040
1133
  entity_id: str | None = None,
@@ -1059,6 +1152,7 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1059
1152
  - "logger": Effective log level per integration via logger/log_info (confirms logger.set_level changes took effect)
1060
1153
 
1061
1154
  **Shared params:** limit, search (keyword filter on entries/lines; matches integration domain for source='logger')
1155
+ **Order:** order='newest' (default) returns most-recent first; order='oldest' returns chronological-first. Applies to all time-ordered sources (logbook, system, error_log, supervisor, system_service); ignored for source='logger'. For raw-text sources (error_log, supervisor, system_service) it sets the read direction of the most-recent window.
1062
1156
  **Logbook params:** hours_back, entity_id, end_time, offset, compact (default True — strips attribute dicts to save context)
1063
1157
  **System/error_log params:** level (ERROR, WARNING, INFO, DEBUG)
1064
1158
  **Supervisor params:** slug = add-on slug, e.g. "core_mosquitto" (use
@@ -1078,6 +1172,7 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1078
1172
  compact=compact,
1079
1173
  level=level,
1080
1174
  slug=slug,
1175
+ order=order,
1081
1176
  )
1082
1177
 
1083
1178
  @mcp.tool(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.8.0.dev709
3
+ Version: 7.8.0.dev711
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