ha-mcp-dev 7.12.3.dev1894__tar.gz → 7.12.3.dev1898__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.

Potentially problematic release.


This version of ha-mcp-dev might be problematic. Click here for more details.

Files changed (165) hide show
  1. {ha_mcp_dev-7.12.3.dev1894/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1898}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/pyproject.toml +1 -9
  3. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/client/rest_client.py +43 -24
  4. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/policy/evaluator.py +30 -33
  5. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_addons.py +177 -72
  6. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_dev.py +54 -5
  7. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_hacs.py +152 -109
  8. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_traces.py +283 -196
  9. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_updates.py +153 -109
  10. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/utils/data_paths.py +62 -57
  11. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/utils/skill_loader.py +22 -16
  12. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  13. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/LICENSE +0 -0
  14. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/MANIFEST.in +0 -0
  15. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/README.md +0 -0
  16. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/setup.cfg +0 -0
  17. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/__init__.py +0 -0
  18. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/__main__.py +0 -0
  19. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/_pypi_marker +0 -0
  20. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/_version.py +0 -0
  21. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/auth/__init__.py +0 -0
  22. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/auth/consent_form.py +0 -0
  23. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/auth/provider.py +0 -0
  24. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/backup_manager.py +0 -0
  25. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/browser_landing.py +0 -0
  26. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/client/__init__.py +0 -0
  27. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/client/supervisor_client.py +0 -0
  28. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/client/websocket_client.py +0 -0
  29. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/client/websocket_listener.py +0 -0
  30. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/config.py +0 -0
  31. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  32. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  33. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  34. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/errors.py +0 -0
  35. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/llm_exposure.py +0 -0
  36. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/policy/__init__.py +0 -0
  37. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/policy/approval_queue.py +0 -0
  38. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/policy/handlers.py +0 -0
  39. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/policy/middleware.py +0 -0
  40. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/policy/model.py +0 -0
  41. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/policy/persistence.py +0 -0
  42. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/policy/value_sources.py +0 -0
  43. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/py.typed +0 -0
  44. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/read_only.py +0 -0
  45. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  46. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  47. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  48. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  49. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  50. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  51. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  52. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  53. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  54. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  55. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  56. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  57. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  58. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  59. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  60. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  61. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  62. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  63. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  64. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  65. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  66. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  67. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  68. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  69. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  70. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/server.py +0 -0
  71. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/settings_ui/__init__.py +0 -0
  72. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/settings_ui/settings.css +0 -0
  73. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/settings_ui/settings.html +0 -0
  74. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/settings_ui/settings.js +0 -0
  75. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/smoke_test.py +0 -0
  76. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  77. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/strict_bps.py +0 -0
  78. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/__init__.py +0 -0
  79. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/auto_backup.py +0 -0
  80. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/backup.py +0 -0
  81. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  82. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/component_api.py +0 -0
  83. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  84. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/device_control.py +0 -0
  85. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/enhanced.py +0 -0
  86. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/helpers.py +0 -0
  87. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/radio/__init__.py +0 -0
  88. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/radio/base.py +0 -0
  89. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/radio/matter.py +0 -0
  90. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/radio/thread.py +0 -0
  91. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  92. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/radio/zwave.py +0 -0
  93. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/reference_validator.py +0 -0
  94. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/registry.py +0 -0
  95. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  96. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  97. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  98. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  99. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  100. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  101. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  102. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  103. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  104. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  105. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_areas.py +0 -0
  106. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  107. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  108. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_calendar.py +0 -0
  109. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_camera.py +0 -0
  110. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_categories.py +0 -0
  111. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_code.py +0 -0
  112. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  113. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  114. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  115. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  116. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  117. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  118. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_energy.py +0 -0
  119. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_entities.py +0 -0
  120. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  121. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_groups.py +0 -0
  122. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_history.py +0 -0
  123. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_integrations.py +0 -0
  124. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_labels.py +0 -0
  125. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  126. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_radio.py +0 -0
  127. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_registry.py +0 -0
  128. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_resources.py +0 -0
  129. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_search.py +0 -0
  130. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_service.py +0 -0
  131. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_services.py +0 -0
  132. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_system.py +0 -0
  133. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_themes.py +0 -0
  134. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_todo.py +0 -0
  135. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_utility.py +0 -0
  136. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  137. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  138. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/tools_zones.py +0 -0
  139. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/util_helpers.py +0 -0
  140. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/tools/validation_middleware.py +0 -0
  141. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/transforms/__init__.py +0 -0
  142. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/transforms/categorized_search.py +0 -0
  143. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  144. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/transport_security.py +0 -0
  145. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/update_check.py +0 -0
  146. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/utils/__init__.py +0 -0
  147. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/utils/config_hash.py +0 -0
  148. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/utils/domain_handlers.py +0 -0
  149. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  150. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  151. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/utils/operation_manager.py +0 -0
  152. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/utils/python_sandbox.py +0 -0
  153. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/utils/usage_logger.py +0 -0
  154. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/visibility/__init__.py +0 -0
  155. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/visibility/model.py +0 -0
  156. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/visibility/persistence.py +0 -0
  157. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp/visibility/resolver.py +0 -0
  158. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  159. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  160. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  161. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  162. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  163. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/tests/__init__.py +0 -0
  164. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/tests/test_constants.py +0 -0
  165. {ha_mcp_dev-7.12.3.dev1894 → ha_mcp_dev-7.12.3.dev1898}/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.12.3.dev1894
3
+ Version: 7.12.3.dev1898
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.12.3.dev1894"
7
+ version = "7.12.3.dev1898"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  # Upper bound widened to <3.15 so the package installs on Home Assistant's
@@ -176,9 +176,7 @@ ignore = [
176
176
  "packaging/mcpb/generate_manifest.py" = ["C901"]
177
177
  "scripts/extract_tools.py" = ["C901"]
178
178
  "src/ha_mcp/backup_manager.py" = ["C901"]
179
- "src/ha_mcp/client/rest_client.py" = ["C901"]
180
179
  "src/ha_mcp/config.py" = ["C901"]
181
- "src/ha_mcp/policy/evaluator.py" = ["C901"]
182
180
  "src/ha_mcp/policy/handlers.py" = ["C901"]
183
181
  "src/ha_mcp/server.py" = ["C901"]
184
182
  "src/ha_mcp/settings_ui/__init__.py" = ["C901"]
@@ -188,7 +186,6 @@ ignore = [
188
186
  "src/ha_mcp/tools/radio/zigbee.py" = ["C901"]
189
187
  "src/ha_mcp/tools/radio/zwave.py" = ["C901"]
190
188
  "src/ha_mcp/tools/reference_validator.py" = ["C901"]
191
- "src/ha_mcp/tools/tools_addons.py" = ["C901"]
192
189
  "src/ha_mcp/tools/tools_areas.py" = ["C901"]
193
190
  "src/ha_mcp/tools/tools_code.py" = ["C901"]
194
191
  "src/ha_mcp/tools/tools_config_automations.py" = ["C901"]
@@ -196,18 +193,13 @@ ignore = [
196
193
  "src/ha_mcp/tools/tools_config_scripts.py" = ["C901"]
197
194
  "src/ha_mcp/tools/tools_dev.py" = ["C901"]
198
195
  "src/ha_mcp/tools/tools_energy.py" = ["C901"]
199
- "src/ha_mcp/tools/tools_hacs.py" = ["C901"]
200
196
  "src/ha_mcp/tools/tools_history.py" = ["C901"]
201
197
  "src/ha_mcp/tools/tools_integrations.py" = ["C901"]
202
198
  "src/ha_mcp/tools/tools_system.py" = ["C901"]
203
- "src/ha_mcp/tools/tools_traces.py" = ["C901"]
204
- "src/ha_mcp/tools/tools_updates.py" = ["C901"]
205
199
  "src/ha_mcp/tools/tools_yaml_config.py" = ["C901"]
206
200
  "src/ha_mcp/tools/tools_zones.py" = ["C901"]
207
201
  "src/ha_mcp/transforms/categorized_search.py" = ["C901"]
208
- "src/ha_mcp/utils/data_paths.py" = ["C901"]
209
202
  "src/ha_mcp/utils/fuzzy_search.py" = ["C901"]
210
- "src/ha_mcp/utils/skill_loader.py" = ["C901"]
211
203
  "src/ha_mcp/visibility/resolver.py" = ["C901"]
212
204
  "tests/src/e2e/conftest.py" = ["C901"]
213
205
  "tests/src/e2e/error_handling/test_network_errors.py" = ["C901"]
@@ -191,6 +191,25 @@ class HomeAssistantClient:
191
191
  await self.httpx_client.aclose()
192
192
  logger.debug("Closed Home Assistant client")
193
193
 
194
+ @staticmethod
195
+ def _error_message_from_response(
196
+ response: httpx.Response,
197
+ ) -> tuple[str, dict[str, Any]]:
198
+ """Build a human message and structured error data from a 4xx/5xx body.
199
+
200
+ Prefers the JSON ``message`` field; falls back to reason phrase or a
201
+ placeholder when the body is empty or unparseable.
202
+ """
203
+ try:
204
+ error_data = response.json()
205
+ except Exception:
206
+ error_data = {"message": response.text}
207
+
208
+ message = error_data.get("message")
209
+ if not message or not message.strip():
210
+ message = response.reason_phrase or "<empty body>"
211
+ return message, error_data
212
+
194
213
  async def _raw_request(
195
214
  self, method: str, endpoint: str, **kwargs: Any
196
215
  ) -> httpx.Response:
@@ -216,14 +235,7 @@ class HomeAssistantClient:
216
235
  raise HomeAssistantAuthError("Invalid authentication token")
217
236
 
218
237
  if response.status_code >= 400:
219
- try:
220
- error_data = response.json()
221
- except Exception:
222
- error_data = {"message": response.text}
223
-
224
- message = error_data.get("message")
225
- if not message or not message.strip():
226
- message = response.reason_phrase or "<empty body>"
238
+ message, error_data = self._error_message_from_response(response)
227
239
 
228
240
  if (
229
241
  response.status_code in _RETRYABLE_STATUS
@@ -665,6 +677,28 @@ class HomeAssistantClient:
665
677
  )
666
678
  return response.text
667
679
 
680
+ @staticmethod
681
+ def _supervisor_error_message(text_body: str, reason_phrase: str) -> str:
682
+ """Extract a human message from a Supervisor 4xx/5xx body.
683
+
684
+ Supervisor returns ``{"result":"error","message":"..."}`` on some paths;
685
+ prefer that message, then fall back to the raw text, reason phrase, or a
686
+ placeholder.
687
+ """
688
+ message = ""
689
+ try:
690
+ envelope = json.loads(text_body) if text_body else None
691
+ if isinstance(envelope, dict):
692
+ msg = envelope.get("message")
693
+ if isinstance(msg, str) and msg:
694
+ message = msg
695
+ except json.JSONDecodeError:
696
+ # Body wasn't a JSON envelope; fall back to raw text below.
697
+ pass
698
+ if not message:
699
+ message = text_body.strip() or reason_phrase or "<empty body>"
700
+ return message
701
+
668
702
  async def _supervisor_logs_get(self, path: str, lines: int | None = None) -> str:
669
703
  """Fetch ``text/plain`` logs from a Supervisor REST endpoint.
670
704
 
@@ -751,22 +785,7 @@ class HomeAssistantClient:
751
785
  )
752
786
  if response.status_code >= 400:
753
787
  text_body = response.text
754
- # Supervisor returns {"result":"error","message":"..."} JSON on
755
- # some 4xx paths. Try parsing that first so the user sees the
756
- # human message instead of a JSON blob; then fall back to the
757
- # text body, then reason_phrase, then a placeholder.
758
- message = ""
759
- try:
760
- envelope = json.loads(text_body) if text_body else None
761
- if isinstance(envelope, dict):
762
- msg = envelope.get("message")
763
- if isinstance(msg, str) and msg:
764
- message = msg
765
- except json.JSONDecodeError:
766
- # Body wasn't a JSON envelope; fall back to raw text below.
767
- pass
768
- if not message:
769
- message = text_body.strip() or response.reason_phrase or "<empty body>"
788
+ message = self._supervisor_error_message(text_body, response.reason_phrase)
770
789
  logger.warning(
771
790
  "Supervisor returned %s for /%s/logs: %s",
772
791
  response.status_code,
@@ -56,6 +56,33 @@ def _ci(x: Any) -> Any:
56
56
  return x.lower() if isinstance(x, str) else x
57
57
 
58
58
 
59
+ def _contains_matches(val: Any, pv: Any) -> bool:
60
+ if isinstance(val, str) and isinstance(pv, str):
61
+ return pv.lower() in val.lower()
62
+ # Mirror the case-insensitive treatment that ``eq`` / ``in`` /
63
+ # ``not_in`` already apply: a rule listing ``["light.kitchen"]``
64
+ # must match an LLM passing ``"Light.Kitchen"``. Per-element
65
+ # ``_ci`` guards non-string entries so mixed-type collections
66
+ # (e.g. ``[1, "two"]``) keep their natural equality semantics.
67
+ return isinstance(val, (list, tuple, set)) and any(_ci(pv) == _ci(x) for x in val)
68
+
69
+
70
+ def _numeric_matches(val: Any, op: str, pv: Any) -> bool:
71
+ try:
72
+ return bool(val > pv) if op == "gt" else bool(val < pv)
73
+ except TypeError:
74
+ # Numeric rule against a non-numeric arg value — log so
75
+ # users can tell their "temperature > 30" rule isn't
76
+ # silently never firing because the arg is a string.
77
+ logger.debug(
78
+ "policy: %s type-mismatch (val=%r pv=%r) — predicate skipped",
79
+ op,
80
+ val,
81
+ pv,
82
+ )
83
+ return False
84
+
85
+
59
86
  def _op_matches(val: Any, op: str, pv: Any) -> bool:
60
87
  """Apply one op to one concrete value. Predicate dispatches over
61
88
  the candidate values (which may be many for wildcard paths).
@@ -82,39 +109,9 @@ def _op_matches(val: Any, op: str, pv: Any) -> bool:
82
109
  and re.search(pv, val, re.IGNORECASE) is not None
83
110
  )
84
111
  case "contains":
85
- if isinstance(val, str) and isinstance(pv, str):
86
- return pv.lower() in val.lower()
87
- # Mirror the case-insensitive treatment that ``eq`` / ``in`` /
88
- # ``not_in`` already apply: a rule listing ``["light.kitchen"]``
89
- # must match an LLM passing ``"Light.Kitchen"``. Per-element
90
- # ``_ci`` guards non-string entries so mixed-type collections
91
- # (e.g. ``[1, "two"]``) keep their natural equality semantics.
92
- return isinstance(val, (list, tuple, set)) and any(
93
- _ci(pv) == _ci(x) for x in val
94
- )
95
- case "gt":
96
- try:
97
- return bool(val > pv)
98
- except TypeError:
99
- # Numeric rule against a non-numeric arg value — log so
100
- # users can tell their "temperature > 30" rule isn't
101
- # silently never firing because the arg is a string.
102
- logger.debug(
103
- "policy: gt type-mismatch (val=%r pv=%r) — predicate skipped",
104
- val,
105
- pv,
106
- )
107
- return False
108
- case "lt":
109
- try:
110
- return bool(val < pv)
111
- except TypeError:
112
- logger.debug(
113
- "policy: lt type-mismatch (val=%r pv=%r) — predicate skipped",
114
- val,
115
- pv,
116
- )
117
- return False
112
+ return _contains_matches(val, pv)
113
+ case "gt" | "lt":
114
+ return _numeric_matches(val, op, pv)
118
115
  return False
119
116
 
120
117
 
@@ -2273,6 +2273,151 @@ class AddOnTools:
2273
2273
  )
2274
2274
  return await self._execute_repository_action(action, repository.strip())
2275
2275
 
2276
+ @staticmethod
2277
+ def _reject_action_mode_conflicts(
2278
+ action: str,
2279
+ path: str | None,
2280
+ config_data: dict[str, Any],
2281
+ array_patch: dict[str, Any] | None,
2282
+ ) -> None:
2283
+ """Raise if lifecycle-action mode is combined with another mode's params."""
2284
+ conflicts = []
2285
+ if path is not None:
2286
+ conflicts.append("path")
2287
+ if config_data:
2288
+ conflicts.append("config parameters")
2289
+ if array_patch is not None:
2290
+ conflicts.append("array_patch")
2291
+ if conflicts:
2292
+ raise_tool_error(
2293
+ create_validation_error(
2294
+ f"action='{action}' (lifecycle mode) cannot be combined "
2295
+ f"with {', '.join(conflicts)}. Use one mode at a time.",
2296
+ parameter="action",
2297
+ )
2298
+ )
2299
+
2300
+ def _reject_config_mode_proxy_params(
2301
+ self,
2302
+ *,
2303
+ method: str,
2304
+ body: dict[str, Any] | str | None,
2305
+ debug: bool,
2306
+ port: int | None,
2307
+ offset: int,
2308
+ limit: int | None,
2309
+ websocket: bool,
2310
+ wait_for_close: bool,
2311
+ message_limit: int | None,
2312
+ message_offset: int,
2313
+ summarize: bool,
2314
+ python_transform: str | None,
2315
+ array_patch: dict[str, Any] | None,
2316
+ request_headers: dict[str, str] | None,
2317
+ ) -> None:
2318
+ """Raise if any proxy-mode-only param is set while config mode is active."""
2319
+ proxy_overrides = self._proxy_overrides_basic(
2320
+ method, body, debug, port, offset, limit, websocket
2321
+ ) + self._proxy_overrides_ws_and_extra(
2322
+ wait_for_close,
2323
+ message_limit,
2324
+ message_offset,
2325
+ summarize,
2326
+ python_transform,
2327
+ array_patch,
2328
+ request_headers,
2329
+ )
2330
+ if proxy_overrides:
2331
+ raise_tool_error(
2332
+ create_validation_error(
2333
+ f"Proxy-mode parameters cannot be used in config mode: {', '.join(d for _, d in proxy_overrides)}. "
2334
+ "Remove these parameters or switch to proxy mode by providing 'path'.",
2335
+ parameter=proxy_overrides[0][0],
2336
+ )
2337
+ )
2338
+
2339
+ async def _execute_ws_proxy(
2340
+ self,
2341
+ slug: str,
2342
+ path: str,
2343
+ body: dict[str, Any] | str | None,
2344
+ debug: bool,
2345
+ port: int | None,
2346
+ wait_for_close: bool,
2347
+ message_limit: int | None,
2348
+ message_offset: int,
2349
+ summarize: bool,
2350
+ python_transform: str | None,
2351
+ ) -> dict[str, Any]:
2352
+ result = await _call_addon_ws(
2353
+ client=self._client,
2354
+ slug=slug,
2355
+ path=path,
2356
+ body=body,
2357
+ timeout=120 if wait_for_close else 10,
2358
+ debug=debug,
2359
+ port=port,
2360
+ wait_for_close=wait_for_close,
2361
+ message_limit=message_limit,
2362
+ message_offset=message_offset,
2363
+ summarize=summarize,
2364
+ python_transform=python_transform,
2365
+ )
2366
+ if not result.get("success"):
2367
+ raise_tool_error(result)
2368
+ return result
2369
+
2370
+ async def _execute_http_proxy(
2371
+ self,
2372
+ *,
2373
+ slug: str,
2374
+ path: str,
2375
+ method: str,
2376
+ body: dict[str, Any] | str | None,
2377
+ debug: bool,
2378
+ port: int | None,
2379
+ offset: int,
2380
+ limit: int | None,
2381
+ python_transform: str | None,
2382
+ request_headers: dict[str, str] | None,
2383
+ message_limit: int | None,
2384
+ message_offset: int,
2385
+ summarize: bool,
2386
+ ) -> dict[str, Any]:
2387
+ valid_methods = {"GET", "POST", "PUT", "DELETE", "PATCH"}
2388
+ if method.upper() not in valid_methods:
2389
+ raise_tool_error(
2390
+ create_validation_error(
2391
+ f"Invalid HTTP method: {method}. Must be one of: {', '.join(sorted(valid_methods))}",
2392
+ parameter="method",
2393
+ )
2394
+ )
2395
+ if message_limit is not None or message_offset != 0 or not summarize:
2396
+ raise_tool_error(
2397
+ create_validation_error(
2398
+ "message_limit / message_offset / summarize apply only to "
2399
+ "WebSocket mode. Set websocket=True or remove them.",
2400
+ parameter="message_limit",
2401
+ )
2402
+ )
2403
+
2404
+ result = await _call_addon_api(
2405
+ client=self._client,
2406
+ slug=slug,
2407
+ path=path,
2408
+ method=method,
2409
+ body=body,
2410
+ debug=debug,
2411
+ port=port,
2412
+ offset=offset,
2413
+ limit=limit,
2414
+ python_transform=python_transform,
2415
+ extra_headers=request_headers,
2416
+ )
2417
+ if not result.get("success"):
2418
+ raise_tool_error(result)
2419
+ return result
2420
+
2276
2421
  async def manage_addon(
2277
2422
  self,
2278
2423
  slug: str,
@@ -2326,45 +2471,28 @@ class AddOnTools:
2326
2471
  # Lifecycle mode takes precedence and is mutually exclusive with the
2327
2472
  # proxy / config / array-patch modes.
2328
2473
  if action is not None:
2329
- conflicts = []
2330
- if path is not None:
2331
- conflicts.append("path")
2332
- if config_data:
2333
- conflicts.append("config parameters")
2334
- if array_patch is not None:
2335
- conflicts.append("array_patch")
2336
- if conflicts:
2337
- raise_tool_error(
2338
- create_validation_error(
2339
- f"action='{action}' (lifecycle mode) cannot be combined "
2340
- f"with {', '.join(conflicts)}. Use one mode at a time.",
2341
- parameter="action",
2342
- )
2343
- )
2474
+ self._reject_action_mode_conflicts(action, path, config_data, array_patch)
2344
2475
  return await self._execute_action_mode(slug, action)
2345
2476
 
2346
2477
  self._validate_manage_mode(path, config_data)
2347
2478
 
2348
2479
  if config_data:
2349
- proxy_overrides = self._proxy_overrides_basic(
2350
- method, body, debug, port, offset, limit, websocket
2351
- ) + self._proxy_overrides_ws_and_extra(
2352
- wait_for_close,
2353
- message_limit,
2354
- message_offset,
2355
- summarize,
2356
- python_transform,
2357
- array_patch,
2358
- request_headers,
2480
+ self._reject_config_mode_proxy_params(
2481
+ method=method,
2482
+ body=body,
2483
+ debug=debug,
2484
+ port=port,
2485
+ offset=offset,
2486
+ limit=limit,
2487
+ websocket=websocket,
2488
+ wait_for_close=wait_for_close,
2489
+ message_limit=message_limit,
2490
+ message_offset=message_offset,
2491
+ summarize=summarize,
2492
+ python_transform=python_transform,
2493
+ array_patch=array_patch,
2494
+ request_headers=request_headers,
2359
2495
  )
2360
- if proxy_overrides:
2361
- raise_tool_error(
2362
- create_validation_error(
2363
- f"Proxy-mode parameters cannot be used in config mode: {', '.join(d for _, d in proxy_overrides)}. "
2364
- "Remove these parameters or switch to proxy mode by providing 'path'.",
2365
- parameter=proxy_overrides[0][0],
2366
- )
2367
- )
2368
2496
  return await self._execute_config_mode(slug, config_data)
2369
2497
 
2370
2498
  # _call_addon_ws does not accept caller headers — reject the combo rather
@@ -2399,43 +2527,20 @@ class AddOnTools:
2399
2527
  )
2400
2528
 
2401
2529
  if websocket:
2402
- result = await _call_addon_ws(
2403
- client=self._client,
2404
- slug=slug,
2405
- path=path,
2406
- body=body,
2407
- timeout=120 if wait_for_close else 10,
2408
- debug=debug,
2409
- port=port,
2410
- wait_for_close=wait_for_close,
2411
- message_limit=message_limit,
2412
- message_offset=message_offset,
2413
- summarize=summarize,
2414
- python_transform=python_transform,
2415
- )
2416
- if not result.get("success"):
2417
- raise_tool_error(result)
2418
- return result
2419
-
2420
- valid_methods = {"GET", "POST", "PUT", "DELETE", "PATCH"}
2421
- if method.upper() not in valid_methods:
2422
- raise_tool_error(
2423
- create_validation_error(
2424
- f"Invalid HTTP method: {method}. Must be one of: {', '.join(sorted(valid_methods))}",
2425
- parameter="method",
2426
- )
2427
- )
2428
- if message_limit is not None or message_offset != 0 or not summarize:
2429
- raise_tool_error(
2430
- create_validation_error(
2431
- "message_limit / message_offset / summarize apply only to "
2432
- "WebSocket mode. Set websocket=True or remove them.",
2433
- parameter="message_limit",
2434
- )
2530
+ return await self._execute_ws_proxy(
2531
+ slug,
2532
+ path,
2533
+ body,
2534
+ debug,
2535
+ port,
2536
+ wait_for_close,
2537
+ message_limit,
2538
+ message_offset,
2539
+ summarize,
2540
+ python_transform,
2435
2541
  )
2436
2542
 
2437
- result = await _call_addon_api(
2438
- client=self._client,
2543
+ return await self._execute_http_proxy(
2439
2544
  slug=slug,
2440
2545
  path=path,
2441
2546
  method=method,
@@ -2445,11 +2550,11 @@ class AddOnTools:
2445
2550
  offset=offset,
2446
2551
  limit=limit,
2447
2552
  python_transform=python_transform,
2448
- extra_headers=request_headers,
2553
+ request_headers=request_headers,
2554
+ message_limit=message_limit,
2555
+ message_offset=message_offset,
2556
+ summarize=summarize,
2449
2557
  )
2450
- if not result.get("success"):
2451
- raise_tool_error(result)
2452
- return result
2453
2558
 
2454
2559
 
2455
2560
  def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -> None:
@@ -44,8 +44,25 @@ COMPONENT_DOMAIN = "ha_mcp_tools"
44
44
  # (custom_components/ha_mcp_tools/const.py OPT_CHANNEL / OPT_PIP_SPEC).
45
45
  _OPT_CHANNEL = "channel"
46
46
  _OPT_PIP_SPEC = "pip_spec"
47
+ _OPT_SERVER_URL = "server_url"
48
+ _OPT_EXTERNAL_URL = "external_url"
49
+ _OPT_WEBHOOK_ID_OVERRIDE = "webhook_id_override"
50
+ _OPT_SECRET_PATH_OVERRIDE = "secret_path_override"
47
51
  _VALID_CHANNELS = ("stable", "dev")
48
52
 
53
+ # Optional text fields the component's options flow pre-fills via
54
+ # suggested_value (so the UI can clear them). Because an OMITTED optional field
55
+ # reads as "cleared" rather than "unchanged", a partial update_source submit
56
+ # must resend these at their current values or it would blank the user's
57
+ # server-URL / connect-secret overrides.
58
+ _PRESERVED_OPTION_KEYS = (
59
+ _OPT_PIP_SPEC,
60
+ _OPT_SERVER_URL,
61
+ _OPT_EXTERNAL_URL,
62
+ _OPT_WEBHOOK_ID_OVERRIDE,
63
+ _OPT_SECRET_PATH_OVERRIDE,
64
+ )
65
+
49
66
  # Delay before a self-affecting action (embedded entry reload / options
50
67
  # submit) fires, so this tool's JSON response flushes to the MCP client
51
68
  # before the serving thread is torn down. Mirrors
@@ -85,6 +102,23 @@ def _spawn_background(coro: Any) -> None:
85
102
  task.add_done_callback(_BACKGROUND_TASKS.discard)
86
103
 
87
104
 
105
+ def _field_prefill(item: dict[str, Any]) -> Any:
106
+ """Return a serialized options-flow field's current value.
107
+
108
+ Reads ``description.suggested_value`` first: a persisted option is
109
+ serialized there (as ``add_suggested_values_to_schema`` does; this component
110
+ sets it directly on the ``vol.Optional`` marker), and the clearable text
111
+ fields carry their value there rather than as a schema ``default`` (a
112
+ ``default`` equal to the value would make the field impossible to clear).
113
+ Falls back to ``default`` then ``value`` for the dropdown/toggle fields.
114
+ Mirrors ``tools_integrations.options_from_form_flow``.
115
+ """
116
+ description = item.get("description")
117
+ if isinstance(description, dict) and description.get("suggested_value") is not None:
118
+ return description["suggested_value"]
119
+ return item.get("default", item.get("value"))
120
+
121
+
88
122
  async def find_server_config_entry(
89
123
  client: Any,
90
124
  ) -> tuple[str, dict[str, Any], dict[str, Any]] | None:
@@ -95,8 +129,9 @@ async def find_server_config_entry(
95
129
  (services) entry's flow aborts immediately. Returns
96
130
  ``(entry_id, open_flow, current_options)`` with the options flow left
97
131
  OPEN (callers must submit or abort it), or ``None`` when no server
98
- entry exists. ``current_options`` maps schema field names to their
99
- defaults — i.e. the entry's current option values.
132
+ entry exists. ``current_options`` maps schema field names to their current
133
+ values (persisted ``suggested_value`` first, else the schema ``default`` or
134
+ ``value``, via ``_field_prefill``).
100
135
 
101
136
  Module-level (not a DevTools method) so the settings UI's embedded
102
137
  restart handler can share it.
@@ -131,7 +166,7 @@ async def find_server_config_entry(
131
166
  continue
132
167
  schema = flow.get("data_schema") or []
133
168
  fields: dict[str, Any] = {
134
- str(item["name"]): item.get("default")
169
+ str(item["name"]): _field_prefill(item)
135
170
  for item in schema
136
171
  if isinstance(item, dict) and item.get("name")
137
172
  }
@@ -400,7 +435,16 @@ class DevTools:
400
435
  await asyncio.sleep(_SELF_ACTION_FLUSH_DELAY_S)
401
436
  try:
402
437
  result = await self._client.submit_options_flow_step(flow_id, user_input)
403
- logger.info("Deferred options submit result: %s", result.get("type"))
438
+ if result.get("type") == "create_entry":
439
+ logger.info("Deferred options submit applied")
440
+ else:
441
+ # Fire-and-forget: no caller is left to raise to, so a rejected
442
+ # self-restart must at least be discoverable in the log.
443
+ logger.warning(
444
+ "Deferred options submit was not applied (type=%s, errors=%s)",
445
+ result.get("type"),
446
+ result.get("errors") or result.get("reason"),
447
+ )
404
448
  except Exception:
405
449
  logger.exception("Deferred options-flow submit failed")
406
450
 
@@ -819,7 +863,12 @@ class DevTools:
819
863
  )
820
864
  )
821
865
  entry_id, flow, current = found
822
- user_input: dict[str, Any] = {}
866
+ # Resend the user's current overrides (see _PRESERVED_OPTION_KEYS) so a
867
+ # channel/pip-spec change here does not blank them — an omitted optional
868
+ # field reads as "cleared", not "unchanged".
869
+ user_input: dict[str, Any] = {
870
+ key: current[key] for key in _PRESERVED_OPTION_KEYS if current.get(key)
871
+ }
823
872
  if channel is not None:
824
873
  user_input[_OPT_CHANNEL] = channel
825
874
  if pip_spec is not None: