ha-mcp-dev 7.12.3.dev1928__tar.gz → 7.12.3.dev1929__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 (177) hide show
  1. {ha_mcp_dev-7.12.3.dev1928/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1929}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/evaluator.py +18 -0
  4. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_code.py +3 -49
  5. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_search.py +4 -1
  6. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_service.py +289 -21
  7. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_system.py +1 -1
  8. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/util_helpers.py +40 -0
  9. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  10. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/LICENSE +0 -0
  11. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/MANIFEST.in +0 -0
  12. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/README.md +0 -0
  13. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/setup.cfg +0 -0
  14. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/__init__.py +0 -0
  15. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/__main__.py +0 -0
  16. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/_pypi_marker +0 -0
  17. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/_version.py +0 -0
  18. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/auth/__init__.py +0 -0
  19. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/auth/consent_form.py +0 -0
  20. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/auth/provider.py +0 -0
  21. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/backup_manager.py +0 -0
  22. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/browser_landing.py +0 -0
  23. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/client/__init__.py +0 -0
  24. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/client/rest_client.py +0 -0
  25. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/client/supervisor_client.py +0 -0
  26. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/client/websocket_client.py +0 -0
  27. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/client/websocket_listener.py +0 -0
  28. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/config.py +0 -0
  29. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  30. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  31. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  32. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  33. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  34. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/errors.py +0 -0
  35. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/llm_exposure.py +0 -0
  36. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/__init__.py +0 -0
  37. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/approval_queue.py +0 -0
  38. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/handlers.py +0 -0
  39. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/middleware.py +0 -0
  40. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/model.py +0 -0
  41. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/persistence.py +0 -0
  42. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/value_sources.py +0 -0
  43. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/py.typed +0 -0
  44. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/read_only.py +0 -0
  45. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  46. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  47. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  48. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  49. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  50. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  51. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  52. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  53. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  54. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  55. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  56. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  57. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  58. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  59. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  60. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  61. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  62. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  63. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  64. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  65. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  66. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  67. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  68. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  69. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/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.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/server.py +0 -0
  71. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/__init__.py +0 -0
  72. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  73. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  74. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  75. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  76. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  77. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  78. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  79. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  80. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_theme.py +0 -0
  81. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  82. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/settings.css +0 -0
  83. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/settings.html +0 -0
  84. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/settings.js +0 -0
  85. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/smoke_test.py +0 -0
  86. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  87. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/strict_bps.py +0 -0
  88. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/__init__.py +0 -0
  89. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/auto_backup.py +0 -0
  90. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/backup.py +0 -0
  91. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  92. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/component_api.py +0 -0
  93. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  94. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/device_control.py +0 -0
  95. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/enhanced.py +0 -0
  96. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/helpers.py +0 -0
  97. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/radio/__init__.py +0 -0
  98. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/radio/base.py +0 -0
  99. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/radio/matter.py +0 -0
  100. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/radio/thread.py +0 -0
  101. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  102. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/radio/zwave.py +0 -0
  103. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/reference_validator.py +0 -0
  104. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/registry.py +0 -0
  105. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  106. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  107. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  108. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  109. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  110. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  111. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  112. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  113. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  114. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  115. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_addons.py +0 -0
  116. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_areas.py +0 -0
  117. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  118. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  119. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_calendar.py +0 -0
  120. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_camera.py +0 -0
  121. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_categories.py +0 -0
  122. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  123. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  124. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  125. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  126. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  127. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  128. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_dev.py +0 -0
  129. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_energy.py +0 -0
  130. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_entities.py +0 -0
  131. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  132. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_groups.py +0 -0
  133. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_hacs.py +0 -0
  134. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_history.py +0 -0
  135. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_integrations.py +0 -0
  136. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_labels.py +0 -0
  137. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  138. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_radio.py +0 -0
  139. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_registry.py +0 -0
  140. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_resources.py +0 -0
  141. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_services.py +0 -0
  142. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_themes.py +0 -0
  143. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_todo.py +0 -0
  144. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_traces.py +0 -0
  145. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_updates.py +0 -0
  146. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_utility.py +0 -0
  147. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  148. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  149. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_zones.py +0 -0
  150. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/validation_middleware.py +0 -0
  151. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/transforms/__init__.py +0 -0
  152. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/transforms/categorized_search.py +0 -0
  153. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  154. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/transport_security.py +0 -0
  155. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/update_check.py +0 -0
  156. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/__init__.py +0 -0
  157. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/config_hash.py +0 -0
  158. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/data_paths.py +0 -0
  159. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/domain_handlers.py +0 -0
  160. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  161. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  162. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/operation_manager.py +0 -0
  163. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/python_sandbox.py +0 -0
  164. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/skill_loader.py +0 -0
  165. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/usage_logger.py +0 -0
  166. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/visibility/__init__.py +0 -0
  167. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/visibility/model.py +0 -0
  168. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/visibility/persistence.py +0 -0
  169. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/visibility/resolver.py +0 -0
  170. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  171. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  172. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  173. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  174. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  175. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/tests/__init__.py +0 -0
  176. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/tests/test_constants.py +0 -0
  177. {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/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.dev1928
3
+ Version: 7.12.3.dev1929
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.dev1928"
7
+ version = "7.12.3.dev1929"
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
@@ -145,4 +145,22 @@ def find_matching_rule(
145
145
  def evaluate(tool_name: str, args: dict[str, Any], policy: Policy) -> Verdict:
146
146
  if find_matching_rule(tool_name, args, policy) is not None:
147
147
  return Verdict.REQUIRE_APPROVAL
148
+ # ha_call_service exposes a raw WebSocket escape hatch (``ws_command``) that
149
+ # carries no ``domain``/``service`` argument, so a rule keyed on
150
+ # ``args.domain``/``args.service`` cannot match it and it would otherwise slip
151
+ # through the fail-open default. If the operator has ANY rule that applies to
152
+ # ha_call_service -- one scoped to it by name, or a wildcard ``*`` rule, which
153
+ # ``match_rule`` treats as applying to every tool -- treat an unmatched
154
+ # ws_command call as require-approval (fail safe) so the escape hatch cannot
155
+ # sneak past that oversight. Blocking the call (require-approval) rather than
156
+ # silently allowing it is the safe error direction for a raw WS escape hatch,
157
+ # especially since the write-command blocklist is a deliberately
158
+ # non-exhaustive wrapper-bypass list that leans on this gate.
159
+ # Operators who want finer control can add a rule keyed on ``args.ws_command``.
160
+ if (
161
+ tool_name == "ha_call_service"
162
+ and args.get("ws_command")
163
+ and any(rule.tool_name in ("ha_call_service", "*") for rule in policy.rules)
164
+ ):
165
+ return Verdict.REQUIRE_APPROVAL
148
166
  return Verdict.ALLOW
@@ -36,6 +36,7 @@ from fastmcp.server.context import Context
36
36
  from ..config import get_global_settings
37
37
  from ..errors import ErrorCode, create_error_response
38
38
  from .helpers import log_tool_usage, raise_tool_error
39
+ from .util_helpers import BLOCKED_WS_WRITE_COMMANDS
39
40
 
40
41
  logger = logging.getLogger(__name__)
41
42
 
@@ -156,53 +157,6 @@ _BLOCKED_HA_INTERNAL_EVENTS: frozenset[str] = frozenset(
156
157
  }
157
158
  )
158
159
 
159
- # WebSocket commands the sandbox must not send. Each one either changes
160
- # persistent state in a way that bypasses a wrapping tool's validation
161
- # (lovelace, registry mutations) or has no sandbox-appropriate use case
162
- # at all (``config/core/update`` rewrites the HA installation's location/
163
- # timezone/currency/lat-long).
164
- _BLOCKED_WS_COMMANDS: frozenset[str] = frozenset(
165
- {
166
- "config/core/update",
167
- "lovelace/config/save",
168
- "lovelace/dashboards/create",
169
- "lovelace/dashboards/delete",
170
- "lovelace/dashboards/update",
171
- "config/area_registry/delete",
172
- "config/area_registry/disable",
173
- "config/area_registry/update",
174
- "config/device_registry/delete",
175
- "config/device_registry/disable",
176
- "config/device_registry/update",
177
- # Device registry deletion is registered as ``remove_config_entry`` on
178
- # HA Core, not ``delete`` — see ``tools_registry.py:753`` for the
179
- # actually-emitted command. ``ha_remove_device`` wraps it; raw
180
- # ``ws_send`` would skip those checks.
181
- "config/device_registry/remove_config_entry",
182
- "config/entity_registry/delete",
183
- "config/entity_registry/disable",
184
- "config/entity_registry/update",
185
- # Entity registry deletion is registered as ``remove`` on HA Core,
186
- # not ``delete`` — see ``tools_entities.py:1130`` for the
187
- # actually-emitted command. ``ha_remove_entity`` wraps it.
188
- "config/entity_registry/remove",
189
- # Floor / label / category registries follow the same rationale as
190
- # area / device / entity above: each has a wrapping MCP tool
191
- # (``ha_set_area_or_floor``, ``ha_config_set_label``,
192
- # ``ha_config_set_category``) that performs invariant checks the
193
- # raw WS command skips.
194
- "config/floor_registry/create",
195
- "config/floor_registry/delete",
196
- "config/floor_registry/update",
197
- "config/label_registry/create",
198
- "config/label_registry/delete",
199
- "config/label_registry/update",
200
- "config/category_registry/create",
201
- "config/category_registry/delete",
202
- "config/category_registry/update",
203
- }
204
- )
205
-
206
160
 
207
161
  def _classify_sandbox_error(exc: Exception) -> tuple[ErrorCode, str, list[str]]:
208
162
  """Map a sandbox exception to ``(code, message, suggestions)``.
@@ -859,7 +813,7 @@ class _SandboxBridge:
859
813
  code is dynamic and may pass non-dict values; the runtime guard
860
814
  below converts that into an error dict.
861
815
 
862
- Commands listed in ``_BLOCKED_WS_COMMANDS`` are rejected with an
816
+ Commands listed in ``BLOCKED_WS_WRITE_COMMANDS`` are rejected with an
863
817
  explanatory error: those either rewrite persistent state in ways
864
818
  that have no sandbox-appropriate use case (``config/core/update``)
865
819
  or bypass the validation in their wrapping MCP tool
@@ -876,7 +830,7 @@ class _SandboxBridge:
876
830
  msg_type = message.get("type")
877
831
  if not isinstance(msg_type, str):
878
832
  return {"error": "ws_send(message) requires a 'type' field"}
879
- if msg_type in _BLOCKED_WS_COMMANDS:
833
+ if msg_type in BLOCKED_WS_WRITE_COMMANDS:
880
834
  logger.info("sandbox.ws_send.blocked type=%r", msg_type)
881
835
  return {
882
836
  "error": (
@@ -2689,7 +2689,10 @@ class SearchTools:
2689
2689
  default=False,
2690
2690
  description=(
2691
2691
  "Include user-dismissed/ignored repairs (default: False). "
2692
- "Matches the HA Repairs UI which hides dismissed items by default."
2692
+ "Matches the HA Repairs UI which hides dismissed items by default. "
2693
+ "To dismiss/ignore a repair, call ha_call_service with "
2694
+ 'ws_command="repairs/ignore_issue" and data={"domain": ..., '
2695
+ '"issue_id": ..., "ignore": true}.'
2693
2696
  ),
2694
2697
  ),
2695
2698
  ] = False,
@@ -15,6 +15,8 @@ from pydantic import Field
15
15
 
16
16
  from ..client.rest_client import HomeAssistantConnectionError
17
17
  from ..errors import (
18
+ ErrorCode,
19
+ create_error_response,
18
20
  create_validation_error,
19
21
  )
20
22
  from .helpers import (
@@ -24,6 +26,7 @@ from .helpers import (
24
26
  register_tool_methods,
25
27
  )
26
28
  from .util_helpers import (
29
+ BLOCKED_WS_WRITE_COMMANDS,
27
30
  JSON_STRING_COERCION,
28
31
  compact_service_result,
29
32
  parse_json_param,
@@ -126,6 +129,50 @@ _SERVICE_TO_STATE: dict[str, str] = {
126
129
  }
127
130
 
128
131
 
132
+ # WebSocket commands that stream events or reply in two phases (an initial ack
133
+ # then the real payload as a follow-up event) rather than resolving to a single
134
+ # terminal result. ha_call_service's ws_command escape hatch only supports
135
+ # one-shot request/response commands, so these are rejected: the "subscribe"
136
+ # substring catches subscription commands, and the set names known two-phase /
137
+ # streaming commands whose names don't contain "subscribe". The set is a floor,
138
+ # not an exhaustive list -- HA's WS command naming isn't consistent enough for a
139
+ # name check alone to be fully reliable.
140
+ _WS_COMMAND_EVENT_BLOCKLIST = frozenset(
141
+ {
142
+ "render_template", # event-based; use ha_eval_template instead
143
+ "system_health/info", # two-phase (see tools_system._fetch_health_info)
144
+ "assist_pipeline/run", # streams pipeline events
145
+ }
146
+ )
147
+
148
+ # Substrings that mark a WS command as streaming / subscription-based even when
149
+ # its name isn't in the blocklist above. Such commands ack once and then push
150
+ # follow-up events on the same id; the one-shot send_command path returns the
151
+ # ack and leaks the subscription. "subscribe" covers subscribe_* and */subscribe;
152
+ # "stream" covers history/stream, logbook/event_stream, camera/stream, ...;
153
+ # "start_preview" covers template/start_preview and the config-flow preview family.
154
+ _WS_STREAMING_SUBSTRINGS = ("subscribe", "stream", "start_preview")
155
+
156
+ # One-shot WS commands that re-enter Home Assistant's service invocation. Routing
157
+ # them through the escape hatch would bypass the service-mode guards (notably the
158
+ # reserved ha_mcp_tools domain block), so they are rejected -- use the
159
+ # domain/service parameters for service calls instead.
160
+ _WS_COMMAND_SERVICE_INVOKERS = frozenset({"call_service", "execute_script"})
161
+
162
+ # Reserved WebSocket envelope keys the transport owns. Allowing them inside data
163
+ # would let a caller override the validated command type (defeating every check
164
+ # below) or collide with the transport's message id, so they are rejected.
165
+ _WS_RESERVED_ENVELOPE_KEYS = frozenset({"type", "id"})
166
+
167
+
168
+ def _is_streaming_ws_command(command_type: str) -> bool:
169
+ """Return True for subscription / streaming / two-phase WS commands."""
170
+ lowered = command_type.lower()
171
+ if lowered in _WS_COMMAND_EVENT_BLOCKLIST:
172
+ return True
173
+ return any(sub in lowered for sub in _WS_STREAMING_SUBSTRINGS)
174
+
175
+
129
176
  def _build_service_suggestions(
130
177
  domain: str, service: str, entity_id: str | None
131
178
  ) -> list[str]:
@@ -162,6 +209,80 @@ class ServiceTools:
162
209
  service_data["entity_id"] = entity_id
163
210
  return service_data
164
211
 
212
+ @staticmethod
213
+ def _validate_service_call_params(
214
+ domain: str | None, service: str | None
215
+ ) -> tuple[str, str]:
216
+ """Validate service-mode params and return the (domain, service) pair.
217
+
218
+ Raises a structured ToolError when domain/service are missing (the caller
219
+ likely wants the ws_command escape hatch) or when the domain targets the
220
+ reserved ha_mcp_tools namespace.
221
+ """
222
+ if not domain or not service:
223
+ raise_tool_error(
224
+ create_validation_error(
225
+ "domain and service are required for a service call. To send "
226
+ "a raw WebSocket command instead, pass ws_command.",
227
+ parameter="domain" if not domain else "service",
228
+ )
229
+ )
230
+ # ha_mcp_tools.* services are restricted to the ha-mcp server's dedicated
231
+ # wrappers (which inject the required caller token). Block ha_call_service
232
+ # from forwarding to that domain — it would otherwise be a bypass path
233
+ # around the dedicated tools. HA core's service registry lowercases the
234
+ # domain on fallback lookup (homeassistant/core.py
235
+ # ServiceRegistry.async_call), so normalise here to make sure a mixed-case
236
+ # `HA_MCP_TOOLS` can't slip past this exact-string check and still resolve
237
+ # downstream.
238
+ if domain.strip().lower() == "ha_mcp_tools":
239
+ raise_tool_error(
240
+ create_validation_error(
241
+ (
242
+ "ha_call_service cannot invoke services in the "
243
+ "'ha_mcp_tools' domain. Use the dedicated MCP tool "
244
+ "instead: ha_list_files, ha_read_file, ha_write_file, "
245
+ "ha_delete_file, or ha_config_set_yaml."
246
+ ),
247
+ parameter="domain",
248
+ )
249
+ )
250
+ return domain, service
251
+
252
+ @staticmethod
253
+ def _reject_incompatible_ws_params(
254
+ entity_id: str | None,
255
+ return_response: bool,
256
+ verbose: bool,
257
+ result_fields: str | list[str] | None,
258
+ result_attribute_keys: str | list[str] | None,
259
+ ) -> None:
260
+ """Reject service-mode-only params when the ws_command escape hatch is used.
261
+
262
+ These shape a registered-service call and have no meaning for a raw
263
+ WebSocket command; silently ignoring them would be a confusing no-op, so
264
+ (mirroring the domain/service "not both" guard) they must be omitted.
265
+ """
266
+ offenders = [
267
+ name
268
+ for name, is_set in (
269
+ ("entity_id", entity_id is not None),
270
+ ("return_response", return_response),
271
+ ("verbose", verbose),
272
+ ("result_fields", result_fields is not None),
273
+ ("result_attribute_keys", result_attribute_keys is not None),
274
+ )
275
+ if is_set
276
+ ]
277
+ if offenders:
278
+ raise_tool_error(
279
+ create_validation_error(
280
+ "These parameters apply only to service calls and must be "
281
+ f"omitted when ws_command is set: {', '.join(offenders)}.",
282
+ parameter="ws_command",
283
+ )
284
+ )
285
+
165
286
  @staticmethod
166
287
  def _parse_result_projection_params(
167
288
  result_fields: str | list[str] | None,
@@ -372,6 +493,125 @@ class ServiceTools:
372
493
  suggestions=suggestions,
373
494
  )
374
495
 
496
+ async def _call_ws_command(
497
+ self,
498
+ ws_command: str,
499
+ data: str | dict[str, Any] | None,
500
+ *,
501
+ domain: str | None,
502
+ service: str | None,
503
+ ) -> dict[str, Any]:
504
+ """Send a one-shot WebSocket command via ha_call_service's escape hatch.
505
+
506
+ Reaches Home Assistant WebSocket commands that are not registered
507
+ services (e.g. ``repairs/ignore_issue``). Only one-shot
508
+ request/response commands are supported — streaming / subscription
509
+ commands are rejected up front.
510
+ """
511
+ command_type = ws_command.strip()
512
+ if domain is not None or service is not None:
513
+ raise_tool_error(
514
+ create_validation_error(
515
+ "Provide either domain + service (a registered service call) "
516
+ "OR ws_command (a raw WebSocket command), not both.",
517
+ parameter="ws_command",
518
+ )
519
+ )
520
+ if not command_type:
521
+ raise_tool_error(
522
+ create_validation_error(
523
+ "ws_command must be a non-empty WebSocket command type, "
524
+ "e.g. 'repairs/ignore_issue'.",
525
+ parameter="ws_command",
526
+ )
527
+ )
528
+ if _is_streaming_ws_command(command_type):
529
+ raise_tool_error(
530
+ create_validation_error(
531
+ f"ws_command '{command_type}' is a streaming or two-phase "
532
+ "command; ha_call_service only sends one-shot "
533
+ "request/response commands. For template rendering use "
534
+ "ha_eval_template.",
535
+ parameter="ws_command",
536
+ )
537
+ )
538
+ if command_type.lower() in _WS_COMMAND_SERVICE_INVOKERS:
539
+ raise_tool_error(
540
+ create_validation_error(
541
+ f"ws_command '{command_type}' invokes Home Assistant services "
542
+ "and would bypass ha_call_service's safeguards. Use the "
543
+ "domain/service parameters for service calls instead.",
544
+ parameter="ws_command",
545
+ )
546
+ )
547
+ if command_type.lower().startswith("ha_mcp_tools/"):
548
+ raise_tool_error(
549
+ create_validation_error(
550
+ "ha_call_service cannot invoke 'ha_mcp_tools/*' WebSocket "
551
+ "commands. Use the dedicated ha-mcp tools instead.",
552
+ parameter="ws_command",
553
+ )
554
+ )
555
+ if command_type.lower() in BLOCKED_WS_WRITE_COMMANDS:
556
+ raise_tool_error(
557
+ create_validation_error(
558
+ f"ws_command '{command_type}' mutates persistent state that a "
559
+ "dedicated tool guards with backups and conflict checks. Use "
560
+ "the corresponding ha-mcp tool (e.g. ha_config_set_dashboard, "
561
+ "ha_set_area_or_floor, ha_remove_entity) instead.",
562
+ parameter="ws_command",
563
+ )
564
+ )
565
+ command_params = (
566
+ _parse_json_dict_param(
567
+ data, type_error_message="ws_command data must be a JSON object"
568
+ )
569
+ or {}
570
+ )
571
+ reserved = _WS_RESERVED_ENVELOPE_KEYS & command_params.keys()
572
+ if reserved:
573
+ raise_tool_error(
574
+ create_validation_error(
575
+ "data must not contain the reserved WebSocket envelope key(s) "
576
+ f"{', '.join(sorted(reserved))}; the command type is set by "
577
+ "ws_command, and the message id is managed by the transport.",
578
+ parameter="data",
579
+ )
580
+ )
581
+ # send_websocket_message catches its own transport errors and always
582
+ # returns a {"success": ...} dict (never raises), so the failure is
583
+ # handled by the result-shape check below -- matching the other
584
+ # send_websocket_message call sites in the codebase.
585
+ result = await self._client.send_websocket_message(
586
+ {"type": command_type, **command_params}
587
+ )
588
+
589
+ if not isinstance(result, dict) or not result.get("success", False):
590
+ error_msg = (
591
+ result.get("error") if isinstance(result, dict) else None
592
+ ) or "WebSocket command failed"
593
+ raise_tool_error(
594
+ create_error_response(
595
+ ErrorCode.SERVICE_CALL_FAILED,
596
+ str(error_msg),
597
+ context={"ws_command": command_type},
598
+ suggestions=[
599
+ "Verify the command type and its parameters (e.g. "
600
+ + "repairs/ignore_issue needs domain, issue_id, ignore)",
601
+ "Confirm the target still exists (a repair must be "
602
+ + "present to ignore it)",
603
+ ],
604
+ )
605
+ )
606
+
607
+ return {
608
+ "success": True,
609
+ "ws_command": command_type,
610
+ "parameters": command_params or None,
611
+ "result": result.get("result"),
612
+ "message": f"Successfully executed WebSocket command '{command_type}'",
613
+ }
614
+
375
615
  @tool(
376
616
  name="ha_call_service",
377
617
  tags={"Service & Device Control"},
@@ -380,8 +620,8 @@ class ServiceTools:
380
620
  @log_tool_usage
381
621
  async def ha_call_service(
382
622
  self,
383
- domain: str,
384
- service: str,
623
+ domain: str | None = None,
624
+ service: str | None = None,
385
625
  entity_id: str | None = None,
386
626
  data: Annotated[dict[str, Any] | None, JSON_STRING_COERCION] = None,
387
627
  return_response: bool = False,
@@ -425,6 +665,21 @@ class ServiceTools:
425
665
  ),
426
666
  ),
427
667
  ] = None,
668
+ ws_command: Annotated[
669
+ str | None,
670
+ Field(
671
+ default=None,
672
+ description=(
673
+ "Advanced escape hatch: send a raw one-shot Home Assistant "
674
+ "WebSocket command that is NOT a registered service (e.g. "
675
+ "'repairs/ignore_issue' to dismiss a Repairs issue). When set, "
676
+ "omit domain/service and the other service params; put the "
677
+ "command's parameters in data. Streaming/two-phase and "
678
+ "service-invoking commands (call_service, execute_script) are "
679
+ "rejected."
680
+ ),
681
+ ),
682
+ ] = None,
428
683
  ) -> dict[str, Any]:
429
684
  """
430
685
  Execute Home Assistant services to control entities and trigger automations.
@@ -462,27 +717,40 @@ class ServiceTools:
462
717
 
463
718
  Common patterns: Use ha_get_state() to check current values before making changes.
464
719
  Use ha_search() to find correct entity IDs.
720
+
721
+ **WebSocket command escape hatch (advanced):**
722
+ A few Home Assistant operations are WebSocket-only commands, not
723
+ registered services — most notably dismissing a Repairs issue. Pass
724
+ ``ws_command`` (instead of domain/service) to send one, with its
725
+ parameters in ``data``:
726
+ ```python
727
+ # Dismiss a repair (get domain/issue_id from ha_get_overview repairs
728
+ # or ha_get_system_health include="repairs")
729
+ ha_call_service(ws_command="repairs/ignore_issue",
730
+ data={"domain": "sun", "issue_id": "abc", "ignore": True})
731
+ ```
732
+ Only one-shot request/response commands are supported; streaming/two-phase
733
+ and service-invoking commands are rejected, and the other service
734
+ parameters (entity_id, return_response, etc.) don't apply.
465
735
  """
466
- # ha_mcp_tools.* services are restricted to the ha-mcp server's
467
- # dedicated wrappers (which inject the required caller token). Block
468
- # ha_call_service from forwarding to that domain — it would otherwise
469
- # be a bypass path around the dedicated tools.
470
- # HA core's service registry lowercases the domain on fallback lookup
471
- # (homeassistant/core.py ServiceRegistry.async_call), so normalise
472
- # here to make sure a mixed-case `HA_MCP_TOOLS` can't slip past this
473
- # exact-string check and still resolve downstream.
474
- if isinstance(domain, str) and domain.strip().lower() == "ha_mcp_tools":
475
- raise_tool_error(
476
- create_validation_error(
477
- (
478
- "ha_call_service cannot invoke services in the "
479
- "'ha_mcp_tools' domain. Use the dedicated MCP tool "
480
- "instead: ha_list_files, ha_read_file, ha_write_file, "
481
- "ha_delete_file, or ha_config_set_yaml."
482
- ),
483
- parameter="domain",
484
- )
736
+ # WebSocket-command escape hatch (issue #1839): reach one-shot WS
737
+ # commands that aren't registered services (e.g. repairs/ignore_issue).
738
+ if ws_command is not None:
739
+ self._reject_incompatible_ws_params(
740
+ entity_id,
741
+ return_response,
742
+ verbose,
743
+ result_fields,
744
+ result_attribute_keys,
745
+ )
746
+ return await self._call_ws_command(
747
+ ws_command, data, domain=domain, service=service
485
748
  )
749
+
750
+ # Service mode requires domain + service (optional at the signature level
751
+ # only to make room for the ws_command escape hatch) and rejects the
752
+ # reserved ha_mcp_tools domain.
753
+ domain, service = self._validate_service_call_params(domain, service)
486
754
  try:
487
755
  service_data = self._parse_service_data(data, entity_id)
488
756
 
@@ -541,7 +541,7 @@ class SystemTools:
541
541
 
542
542
  **Parameters:**
543
543
  - include: Optional comma-separated list of additional data to include.
544
- - "repairs": Repair items from Settings > System > Repairs (active only by default; pass `include_dismissed_repairs=True` for all)
544
+ - "repairs": Repair items from Settings > System > Repairs (active only by default; pass `include_dismissed_repairs=True` for all). To dismiss/ignore a repair, call `ha_call_service(ws_command="repairs/ignore_issue", data={"domain": <domain>, "issue_id": <issue_id>, "ignore": true})`.
545
545
  - "zha_network": ZHA Zigbee devices with radio signal summary (name, LQI, RSSI)
546
546
  - "zha_network_full": ZHA Zigbee devices with all device details (can be large on 100+ device networks; prefer "zha_network" for summary)
547
547
  - "zwave_network": Z-Wave JS network status and node summary (status, security, routing)
@@ -480,6 +480,46 @@ def unwrap_service_response(result: dict[str, Any]) -> dict[str, Any]:
480
480
  return sr if isinstance(sr, dict) else result
481
481
 
482
482
 
483
+ # WebSocket commands that mutate persistent state in a way that bypasses a
484
+ # wrapping MCP tool's validation (auto-backup, config-hash optimistic locking,
485
+ # registry invariant checks), or that have no escape-hatch-appropriate use case
486
+ # (`config/core/update` rewrites the installation's location/timezone/currency/
487
+ # lat-long). Shared by the code sandbox's `ws_send` bridge (tools_code.py) and
488
+ # ha_call_service's `ws_command` escape hatch (tools_service.py) so the two raw
489
+ # WebSocket surfaces stay in lockstep. Registry deletion command names differ on
490
+ # HA Core: device deletion is `remove_config_entry` and entity deletion is
491
+ # `remove` (not `delete`) -- ha_remove_device / ha_remove_entity emit those.
492
+ BLOCKED_WS_WRITE_COMMANDS: frozenset[str] = frozenset(
493
+ {
494
+ "config/core/update",
495
+ "lovelace/config/save",
496
+ "lovelace/dashboards/create",
497
+ "lovelace/dashboards/delete",
498
+ "lovelace/dashboards/update",
499
+ "config/area_registry/delete",
500
+ "config/area_registry/disable",
501
+ "config/area_registry/update",
502
+ "config/device_registry/delete",
503
+ "config/device_registry/disable",
504
+ "config/device_registry/update",
505
+ "config/device_registry/remove_config_entry",
506
+ "config/entity_registry/delete",
507
+ "config/entity_registry/disable",
508
+ "config/entity_registry/update",
509
+ "config/entity_registry/remove",
510
+ "config/floor_registry/create",
511
+ "config/floor_registry/delete",
512
+ "config/floor_registry/update",
513
+ "config/label_registry/create",
514
+ "config/label_registry/delete",
515
+ "config/label_registry/update",
516
+ "config/category_registry/create",
517
+ "config/category_registry/delete",
518
+ "config/category_registry/update",
519
+ }
520
+ )
521
+
522
+
483
523
  # Fields surfaced from each repair issue. Includes `ignored` / `dismissed_version`
484
524
  # so callers can distinguish active vs. user-dismissed repairs when both are
485
525
  # returned (e.g., `include_dismissed_repairs=True`).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.12.3.dev1928
3
+ Version: 7.12.3.dev1929
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