ha-mcp-dev 7.13.0.dev1994__tar.gz → 7.13.0.dev1999__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 (187) hide show
  1. {ha_mcp_dev-7.13.0.dev1994/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev1999}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/_i18n.py +34 -0
  4. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/settings.js +48 -20
  5. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_labels.py +82 -16
  6. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  7. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/LICENSE +0 -0
  8. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/MANIFEST.in +0 -0
  9. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/README.md +0 -0
  10. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/setup.cfg +0 -0
  11. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/__init__.py +0 -0
  12. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/__main__.py +0 -0
  13. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/_pypi_marker +0 -0
  14. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/_version.py +0 -0
  15. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/auth/__init__.py +0 -0
  16. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/auth/consent_form.py +0 -0
  17. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/auth/provider.py +0 -0
  18. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/backup_manager.py +0 -0
  19. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/browser_landing.py +0 -0
  20. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/client/__init__.py +0 -0
  21. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/client/rest_client.py +0 -0
  22. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/client/supervisor_client.py +0 -0
  23. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/client/websocket_client.py +0 -0
  24. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/client/websocket_listener.py +0 -0
  25. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/config.py +0 -0
  26. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  27. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  28. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  29. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  30. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  31. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  32. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/errors.py +0 -0
  33. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/llm_exposure.py +0 -0
  34. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/policy/__init__.py +0 -0
  35. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/policy/approval_queue.py +0 -0
  36. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/policy/evaluator.py +0 -0
  37. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/policy/handlers.py +0 -0
  38. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/policy/middleware.py +0 -0
  39. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/policy/model.py +0 -0
  40. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/policy/persistence.py +0 -0
  41. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/policy/value_sources.py +0 -0
  42. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/py.typed +0 -0
  43. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/read_only.py +0 -0
  44. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  45. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  46. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  47. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  48. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  49. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  50. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  51. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  52. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  53. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  54. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  55. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  56. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  57. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  58. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  59. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  60. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  61. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  62. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  63. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  64. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  65. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  66. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  67. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  68. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  69. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/server.py +0 -0
  70. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/__init__.py +0 -0
  71. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  72. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  73. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  74. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  75. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  76. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  77. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  78. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  79. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/_theme.py +0 -0
  80. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  81. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  82. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  83. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  84. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  85. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/settings.css +0 -0
  86. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/settings_ui/settings.html +0 -0
  87. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/smoke_test.py +0 -0
  88. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  89. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/strict_bps.py +0 -0
  90. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/__init__.py +0 -0
  91. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/auto_backup.py +0 -0
  92. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/backup.py +0 -0
  93. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  94. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/component_api.py +0 -0
  95. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/component_config_reads.py +0 -0
  96. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/component_devices.py +0 -0
  97. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/component_registries.py +0 -0
  98. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  99. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  100. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/device_control.py +0 -0
  101. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/enhanced.py +0 -0
  102. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/helpers.py +0 -0
  103. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/radio/__init__.py +0 -0
  104. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/radio/base.py +0 -0
  105. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/radio/matter.py +0 -0
  106. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/radio/thread.py +0 -0
  107. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  108. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/radio/zwave.py +0 -0
  109. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/reference_validator.py +0 -0
  110. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/registry.py +0 -0
  111. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  112. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  113. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  114. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  115. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  116. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  117. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  118. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  119. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  120. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  121. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_addons.py +0 -0
  122. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_areas.py +0 -0
  123. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  124. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  125. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_calendar.py +0 -0
  126. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_camera.py +0 -0
  127. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_categories.py +0 -0
  128. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_code.py +0 -0
  129. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  130. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  131. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  132. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  133. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  134. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  135. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_dev.py +0 -0
  136. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_energy.py +0 -0
  137. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_entities.py +0 -0
  138. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  139. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_groups.py +0 -0
  140. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_hacs.py +0 -0
  141. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_history.py +0 -0
  142. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_integrations.py +0 -0
  143. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_radio.py +0 -0
  144. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_registry.py +0 -0
  145. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_resources.py +0 -0
  146. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_search.py +0 -0
  147. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_service.py +0 -0
  148. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_services.py +0 -0
  149. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_system.py +0 -0
  150. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_themes.py +0 -0
  151. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_todo.py +0 -0
  152. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_traces.py +0 -0
  153. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_updates.py +0 -0
  154. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_utility.py +0 -0
  155. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  156. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  157. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  158. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/tools_zones.py +0 -0
  159. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/util_helpers.py +0 -0
  160. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/tools/validation_middleware.py +0 -0
  161. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/transforms/__init__.py +0 -0
  162. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/transforms/categorized_search.py +0 -0
  163. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  164. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/transport_security.py +0 -0
  165. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/update_check.py +0 -0
  166. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/utils/__init__.py +0 -0
  167. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/utils/config_hash.py +0 -0
  168. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/utils/data_paths.py +0 -0
  169. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/utils/domain_handlers.py +0 -0
  170. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  171. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  172. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/utils/operation_manager.py +0 -0
  173. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/utils/python_sandbox.py +0 -0
  174. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/utils/skill_loader.py +0 -0
  175. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/utils/usage_logger.py +0 -0
  176. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/visibility/__init__.py +0 -0
  177. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/visibility/model.py +0 -0
  178. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/visibility/persistence.py +0 -0
  179. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp/visibility/resolver.py +0 -0
  180. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  181. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  182. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  183. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  184. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  185. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/tests/__init__.py +0 -0
  186. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/tests/test_constants.py +0 -0
  187. {ha_mcp_dev-7.13.0.dev1994 → ha_mcp_dev-7.13.0.dev1999}/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.13.0.dev1994
3
+ Version: 7.13.0.dev1999
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.13.0.dev1994"
7
+ version = "7.13.0.dev1999"
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
@@ -112,6 +112,7 @@ def load_catalogs(directory: Path = LOCALES_DIR) -> dict[str, dict[str, Any]]:
112
112
  f"The settings UI requires {DEFAULT_LOCALE}.json in {directory}"
113
113
  )
114
114
  _validate_placeholder_parity(catalogs)
115
+ _validate_inline_markup(catalogs)
115
116
  return catalogs
116
117
 
117
118
 
@@ -154,6 +155,39 @@ def _validate_placeholder_parity(catalogs: dict[str, dict[str, Any]]) -> None:
154
155
  )
155
156
 
156
157
 
158
+ # Anything that looks like an HTML tag in a catalog message. A bare "<" in
159
+ # prose (e.g. "< 5") deliberately does not match — it renders fine escaped.
160
+ _TAG_LIKE_RE = re.compile(r"</?[a-zA-Z][^>]*>")
161
+ # The exact tag shapes settings.js::tHtml restores after escaping. Keep in
162
+ # sync with the restore regexes there — any other spelling (case, spacing,
163
+ # attribute order) survives escaping and shows the user literal markup text.
164
+ _ALLOWED_TAGS_RE = re.compile(
165
+ r'</?code>|</?strong>|</a>|<a href="#" data-panel-link="[a-z][a-z-]*">'
166
+ )
167
+
168
+
169
+ def _validate_inline_markup(catalogs: dict[str, dict[str, Any]]) -> None:
170
+ """Reject catalog messages whose markup ``tHtml`` cannot restore.
171
+
172
+ ``settings.js::tHtml`` escapes every translated value and restores only
173
+ the exact allowlisted tag shapes, so a translation written with
174
+ ``<CODE>`` or ``<code >`` would silently render as literal escaped text.
175
+ Fail fast at load time instead, mirroring the placeholder-parity check.
176
+ Scoped to ``messages``: tool translations are plain text rendered through
177
+ ``escapeHtml`` and carry no markup contract.
178
+ """
179
+ for locale, catalog in catalogs.items():
180
+ for key, value in catalog["messages"].items():
181
+ for tag in _TAG_LIKE_RE.findall(value):
182
+ if _ALLOWED_TAGS_RE.fullmatch(tag) is None:
183
+ raise ValueError(
184
+ f"Locale {locale} message {key!r} contains inline "
185
+ f"markup {tag!r} that the settings UI cannot render; "
186
+ f"allowed: <code>, <strong>, </a>, and "
187
+ f'<a href="#" data-panel-link="...">'
188
+ )
189
+
190
+
157
191
  CATALOGS = load_catalogs()
158
192
 
159
193
 
@@ -23,6 +23,27 @@ function t(key, params = {}, fallback = key) {
23
23
  return value;
24
24
  }
25
25
 
26
+ // Translation for HTML contexts (innerHTML sinks). Catalog strings are data,
27
+ // not markup: the whole value is HTML-escaped first, then only the allowlist
28
+ // of inline formatting tags the catalogs use (<code>, <strong>, and the
29
+ // internal tab links) is restored, so a translated string can never
30
+ // contribute any other markup. Placeholders are substituted AFTER escaping:
31
+ // params are trusted HTML fragments built by the caller (escape any dynamic
32
+ // text inside them with escapeHtml at the call site).
33
+ function tHtml(key, params = {}, fallback = key) {
34
+ const raw = Object.prototype.hasOwnProperty.call(I18N_PAYLOAD.messages || {}, key)
35
+ ? I18N_PAYLOAD.messages[key]
36
+ : fallback;
37
+ let value = escapeHtml(raw)
38
+ .replace(/&lt;(\/?)(code|strong)&gt;/g, '<$1$2>')
39
+ .replace(/&lt;a href=&quot;#&quot; data-panel-link=&quot;([a-z][a-z-]*)&quot;&gt;/g, '<a href="#" data-panel-link="$1">')
40
+ .replace(/&lt;\/a&gt;/g, '</a>');
41
+ Object.keys(params).forEach(name => {
42
+ value = value.replaceAll(`{${name}}`, String(params[name]));
43
+ });
44
+ return value;
45
+ }
46
+
26
47
  function localizeMeta(section, field, meta) {
27
48
  return {
28
49
  ...meta,
@@ -76,7 +97,14 @@ function applyStaticTranslations(root = document) {
76
97
  el.textContent = t(el.dataset.i18n, {}, el.textContent);
77
98
  });
78
99
  root.querySelectorAll('[data-i18n-html]').forEach(el => {
79
- el.innerHTML = t(el.dataset.i18nHtml, {}, el.innerHTML);
100
+ // Rewrite only from the catalog: the server-rendered markup already is
101
+ // the English copy, and routing it back through the escape+allowlist
102
+ // pipeline would depend on attribute serialization being round-trip
103
+ // stable, which it is not guaranteed to be.
104
+ const key = el.dataset.i18nHtml;
105
+ if (Object.prototype.hasOwnProperty.call(I18N_PAYLOAD.messages || {}, key)) {
106
+ el.innerHTML = tHtml(key);
107
+ }
80
108
  });
81
109
  for (const attribute of ['placeholder', 'title', 'aria-label']) {
82
110
  const dataName = `i18n${attribute.split('-').map(part => part[0].toUpperCase() + part.slice(1)).join('')}`;
@@ -908,17 +936,17 @@ function render() {
908
936
  else badges += `<span class="badge unknown">${escapeHtml(category) || '?'}</span>`;
909
937
 
910
938
  const gatedNote = disabledBy
911
- ? `<div class="disabled-by-note">${tr(
939
+ ? `<div class="disabled-by-note">${tHtml(
912
940
  'tools.notes.beta_disabled',
913
941
  {setting: `<code>${escapeHtml(disabledBy)}</code>`},
914
- `Beta. Set <code>${escapeHtml(disabledBy)}</code> in the dev App (add-on) config or the matching env var (see docs/beta.md).`
942
+ 'Beta. Set {setting} in the dev App (add-on) config or the matching env var (see docs/beta.md).'
915
943
  )}</div>`
916
944
  : '';
917
945
  const envPinnedNote = isEnvPinned
918
- ? `<div class="feature-locked-note">${tr(
946
+ ? `<div class="feature-locked-note">${tHtml(
919
947
  'tools.notes.env_pinned',
920
948
  {variable: `<code>${escapeHtml(envPinVar)}</code>`},
921
- `env-pinned via <code>${envPinVar}</code>. Unset the env var to edit here.`
949
+ 'Pinned by {variable}. Unset the environment variable to edit here.'
922
950
  )}</div>`
923
951
  : '';
924
952
  const bpsLockedNote = isBpsLocked
@@ -1277,10 +1305,10 @@ const BACKUP_FIELD_LABELS = {
1277
1305
  };
1278
1306
 
1279
1307
  const BACKUP_ORIGIN_LABELS = {
1280
- addon: t('backup.origins.addon', {}, 'Synced to Supervisor. Restart required after save.'),
1308
+ addon: escapeHtml(t('backup.origins.addon', {}, 'Synced to Supervisor. Restart required after save.')),
1281
1309
  env: null, // banner generated dynamically with the env var name
1282
- file: t('backup.origins.file', {}, 'Persisted locally; takes effect immediately.'),
1283
- default: t('backup.origins.default', {}, 'Using default; first save creates a local override file.'),
1310
+ file: escapeHtml(t('backup.origins.file', {}, 'Persisted locally; takes effect immediately.')),
1311
+ default: escapeHtml(t('backup.origins.default', {}, 'Using default; first save creates a local override file.')),
1284
1312
  };
1285
1313
 
1286
1314
  async function loadBackupConfig() {
@@ -1476,8 +1504,8 @@ function renderFsCustomPathsSubForm(parentEl, masterOn, fsOn) {
1476
1504
  : '.storage, secrets.yaml';
1477
1505
  info.innerHTML =
1478
1506
  `<div class="feature-name">${escapeHtml(t('filesystem.custom.title', {}, 'Custom filesystem directories (advanced)'))}</div>` +
1479
- `<div class="feature-help">${t('filesystem.custom.help', {}, 'Extra directories (one per line) that the file tools may READ and WRITE, either relative to your config dir or an absolute allowed HAOS sibling volume. Each entry grants both read and write. Applies immediately; no restart needed.')}</div>` +
1480
- `<div class="feature-help">${t('filesystem.custom.blocked', {paths: `<code>${escapeHtml(denyList)}</code>`}, `Always blocked (cannot be added): <code>${escapeHtml(denyList)}</code>, path traversal (<code>..</code>), and any absolute path outside the HAOS sibling volumes.`)}</div>`;
1507
+ `<div class="feature-help">${tHtml('filesystem.custom.help', {}, 'Extra directories (one per line) that the file tools may READ and WRITE, either relative to your config dir or an absolute allowed HAOS sibling volume. Each entry grants both read and write. Applies immediately; no restart needed.')}</div>` +
1508
+ `<div class="feature-help">${tHtml('filesystem.custom.blocked', {paths: `<code>${escapeHtml(denyList)}</code>`}, 'Always blocked (cannot be added): {paths}, path traversal (<code>..</code>), and any absolute path outside the HAOS sibling volumes.')}</div>`;
1481
1509
 
1482
1510
  const control = document.createElement('div');
1483
1511
  control.className = 'feature-control';
@@ -1922,19 +1950,19 @@ const ORIGIN_INFO_NOTE = {
1922
1950
  function envLockedNoteHtml(envVar, fieldName) {
1923
1951
  const envVarTag = `<code>${escapeHtml(envVar)}</code>`;
1924
1952
  if (!IS_ADDON_MODE) {
1925
- return t('origins.env_var_unset', {variable: envVarTag}, `Set via env var ${envVarTag}; unset it to edit here.`);
1953
+ return tHtml('origins.env_var_unset', {variable: envVarTag}, 'Set via env var {variable}; unset it to edit here.');
1926
1954
  }
1927
1955
  if (fieldName === 'enable_beta_features') {
1928
- return t(
1956
+ return tHtml(
1929
1957
  'origins.beta_legacy_bridge',
1930
1958
  {variable: envVarTag},
1931
- `Auto-enabled in App (add-on) mode (legacy bridge; your options.json predates the master toggle schema entry). Set <code>enable_beta_features</code> explicitly in the App (add-on) Configuration tab to take direct control. (env: ${envVarTag})`
1959
+ 'Auto-enabled in App (add-on) mode (legacy bridge; your options.json predates the master toggle schema entry). Set <code>enable_beta_features</code> explicitly in the App (add-on) Configuration tab to take direct control. (env: {variable})'
1932
1960
  );
1933
1961
  }
1934
- return t(
1962
+ return tHtml(
1935
1963
  'origins.addon_runtime',
1936
1964
  {variable: envVarTag},
1937
- `Set by the App (add-on) runtime environment, managed by Home Assistant Supervisor; cannot be changed from this web UI. (env: ${envVarTag})`
1965
+ 'Set by the App (add-on) runtime environment, managed by Home Assistant Supervisor; cannot be changed from this web UI. (env: {variable})'
1938
1966
  );
1939
1967
  }
1940
1968
 
@@ -2035,7 +2063,7 @@ function renderFeatureFlags(flags) {
2035
2063
  `${escapeHtml(ORIGIN_INFO_NOTE[f.origin])}</div>`
2036
2064
  : '';
2037
2065
  info.innerHTML =
2038
- `<div class="feature-name" id="label-feature-${fieldName}">${escapeHtml(meta.label)}</div>` +
2066
+ `<div class="feature-name" id="label-feature-${escapeHtml(fieldName)}">${escapeHtml(meta.label)}</div>` +
2039
2067
  `<div class="feature-help">${escapeHtml(meta.help)}</div>` +
2040
2068
  lockedNote + infoNote;
2041
2069
 
@@ -2209,7 +2237,7 @@ function renderSubFlagRows(flags, parentEl, subFieldNames, { cssClass, lockedByG
2209
2237
  `${escapeHtml(ORIGIN_INFO_NOTE[f.origin])}</div>`
2210
2238
  : '';
2211
2239
  info.innerHTML =
2212
- `<div class="feature-name" id="label-feature-${fieldName}">${escapeHtml(meta.label)}</div>` +
2240
+ `<div class="feature-name" id="label-feature-${escapeHtml(fieldName)}">${escapeHtml(meta.label)}</div>` +
2213
2241
  `<div class="feature-help">${escapeHtml(meta.help)}</div>` +
2214
2242
  lockedNote + infoNote;
2215
2243
 
@@ -2275,7 +2303,7 @@ function renderCodeModeSubRows(parentEl, masterOn, codeModeOn) {
2275
2303
  let lockedNote = '';
2276
2304
  if (f.field === 'code_mode_saved_tools_path') {
2277
2305
  if (IS_ADDON_MODE) {
2278
- lockedNote = `<div class="feature-locked-note">${t(
2306
+ lockedNote = `<div class="feature-locked-note">${tHtml(
2279
2307
  'advanced.code_mode_saved_tools_path.addon_locked',
2280
2308
  {},
2281
2309
  'Hardcoded to <code>/data/saved_tools.json</code> in App (add-on) mode and cannot be changed (fixed here so saved tools survive App (add-on) updates).'
@@ -2295,7 +2323,7 @@ function renderCodeModeSubRows(parentEl, masterOn, codeModeOn) {
2295
2323
  `<div class="feature-locked-note">${envLockedNoteHtml(f.env_var, f.field)}</div>`;
2296
2324
  }
2297
2325
  info.innerHTML =
2298
- `<div class="feature-name" id="label-feature-${f.field}">${escapeHtml(meta.label)}</div>` +
2326
+ `<div class="feature-name" id="label-feature-${escapeHtml(f.field)}">${escapeHtml(meta.label)}</div>` +
2299
2327
  `<div class="feature-help">${escapeHtml(meta.help)}</div>` +
2300
2328
  lockedNote;
2301
2329
 
@@ -2472,7 +2500,7 @@ function renderPolicyCards(policy) {
2472
2500
  // see/edit them all.
2473
2501
  const byTool = {};
2474
2502
  rules.forEach((r, idx) => {
2475
- const key = r.tool_name + '' + idx;
2503
+ const key = r.tool_name + '\0' + idx;
2476
2504
  byTool[key] = {tool_name: r.tool_name, rule: r, originalIndex: idx};
2477
2505
  });
2478
2506
  Object.keys(byTool).forEach(key => {
@@ -31,6 +31,73 @@ class LabelTools:
31
31
  def __init__(self, client: Any) -> None:
32
32
  self._client = client
33
33
 
34
+ async def _list_labels(
35
+ self, context: dict[str, Any] | None = None
36
+ ) -> list[dict[str, Any]]:
37
+ """Return all labels from the registry (shared by get/set).
38
+
39
+ Raises ToolError (SERVICE_CALL_FAILED) if the list call fails or returns
40
+ an unexpected non-list envelope — a degraded response must not collapse
41
+ to an empty registry, or callers would confidently report a real label
42
+ as missing (mirrors ``backup_manager._require_list``).
43
+ """
44
+ result = await self._client.send_websocket_message(
45
+ {"type": "config/label_registry/list"}
46
+ )
47
+ if not result.get("success"):
48
+ raise_tool_error(
49
+ create_error_response(
50
+ ErrorCode.SERVICE_CALL_FAILED,
51
+ result.get("error", "Failed to get labels"),
52
+ context=context,
53
+ )
54
+ )
55
+ # No ``[]`` default: a success envelope that omits ``result`` (or sends
56
+ # a non-list) is a degraded response, not an empty registry — defaulting
57
+ # would let callers confidently report a real label as missing.
58
+ labels = result.get("result")
59
+ if not isinstance(labels, list):
60
+ raise_tool_error(
61
+ create_error_response(
62
+ ErrorCode.SERVICE_CALL_FAILED,
63
+ "Label registry returned a missing or non-list result",
64
+ context=context,
65
+ )
66
+ )
67
+ return labels
68
+
69
+ async def _require_existing_label(self, label_id: str, name: str) -> None:
70
+ """Raise RESOURCE_NOT_FOUND if ``label_id`` is not in the registry.
71
+
72
+ Enforces the strict update-only contract (issue #1860): update of an
73
+ unknown id yields an opaque "Unknown error", and create cannot honor a
74
+ caller-supplied id (HA derives it from the name), so an unknown id is a
75
+ clear error with a create hint rather than a silently divergent upsert.
76
+ """
77
+ existing = await self._list_labels(context={"name": name, "label_id": label_id})
78
+ if any(lbl.get("label_id") == label_id for lbl in existing):
79
+ return
80
+ raise_tool_error(
81
+ create_error_response(
82
+ ErrorCode.RESOURCE_NOT_FOUND,
83
+ f"Label not found: {label_id}",
84
+ context={
85
+ "name": name,
86
+ "label_id": label_id,
87
+ "available_label_ids": [
88
+ lbl.get("label_id") for lbl in existing[:10]
89
+ ],
90
+ },
91
+ suggestions=[
92
+ "To create a new label, omit label_id — Home Assistant "
93
+ + "derives the id from the name (e.g. 'vendor:tapo' becomes "
94
+ + "'vendor_tapo')",
95
+ "To update an existing label, pass its exact current "
96
+ + "label_id (list all with ha_config_get_label())",
97
+ ],
98
+ )
99
+ )
100
+
34
101
  @tool(
35
102
  name="ha_config_get_label",
36
103
  tags={"Labels & Categories"},
@@ -82,22 +149,7 @@ class LabelTools:
82
149
  ],
83
150
  context={"action": "get"},
84
151
  )
85
- message: dict[str, Any] = {
86
- "type": "config/label_registry/list",
87
- }
88
-
89
- result = await self._client.send_websocket_message(message)
90
-
91
- if not result.get("success"):
92
- raise_tool_error(
93
- create_error_response(
94
- ErrorCode.SERVICE_CALL_FAILED,
95
- result.get("error", "Failed to get labels"),
96
- context={"label_id": label_id},
97
- )
98
- )
99
-
100
- labels = result.get("result", [])
152
+ labels = await self._list_labels(context={"label_id": label_id})
101
153
 
102
154
  if label_id is None:
103
155
  return {
@@ -223,6 +275,13 @@ class LabelTools:
223
275
  ],
224
276
  context={"action": "set", "name": name},
225
277
  )
278
+ # Strict update-only contract (issue #1860): routing to
279
+ # label_registry/update with an unknown id returns an opaque
280
+ # "Unknown error", and label_registry/create cannot honor a
281
+ # caller-supplied id (HA derives it from the name). Verify the
282
+ # id exists up front and return actionable guidance instead of
283
+ # dispatching an update that fails cryptically.
284
+ await self._require_existing_label(label_id, name)
226
285
  action = "update" if label_id else "create"
227
286
 
228
287
  message: dict[str, Any] = {
@@ -254,6 +313,13 @@ class LabelTools:
254
313
  "message": f"Successfully {action_past} label: {name}",
255
314
  }
256
315
  else:
316
+ # The unknown-id case is caught up front by
317
+ # _require_existing_label. This substring match only catches HA
318
+ # error texts containing "not found"/"doesn't exist"; HA's label
319
+ # update does NOT emit those for an unknown/deleted id (it
320
+ # surfaces "Unknown error"), so it is a best-effort guard for
321
+ # other/future phrasings only — the check-then-update delete race
322
+ # is not handled here and still surfaces as SERVICE_CALL_FAILED.
257
323
  error_str = str(result.get("error", "")).lower()
258
324
  if "not found" in error_str or "doesn't exist" in error_str:
259
325
  raise_tool_error(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.13.0.dev1994
3
+ Version: 7.13.0.dev1999
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