ha-mcp-dev 7.13.0.dev1959__tar.gz → 7.13.0.dev1965__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 (178) hide show
  1. {ha_mcp_dev-7.13.0.dev1959/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev1965}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/backup_manager.py +13 -2
  4. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/client/rest_client.py +9 -1
  5. ha_mcp_dev-7.13.0.dev1965/src/ha_mcp/tools/component_devices.py +153 -0
  6. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/radio/base.py +12 -3
  7. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/radio/zigbee.py +28 -4
  8. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_blueprints.py +90 -6
  9. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_entities.py +144 -22
  10. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_registry.py +216 -57
  11. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_search.py +495 -33
  12. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_voice_assistant.py +115 -0
  13. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  14. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  15. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/LICENSE +0 -0
  16. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/MANIFEST.in +0 -0
  17. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/README.md +0 -0
  18. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/setup.cfg +0 -0
  19. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/__init__.py +0 -0
  20. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/__main__.py +0 -0
  21. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/_pypi_marker +0 -0
  22. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/_version.py +0 -0
  23. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/auth/__init__.py +0 -0
  24. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/auth/consent_form.py +0 -0
  25. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/auth/provider.py +0 -0
  26. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/browser_landing.py +0 -0
  27. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/client/__init__.py +0 -0
  28. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/client/supervisor_client.py +0 -0
  29. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/client/websocket_client.py +0 -0
  30. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/client/websocket_listener.py +0 -0
  31. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/config.py +0 -0
  32. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  33. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  34. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  35. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  36. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  37. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/errors.py +0 -0
  38. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/llm_exposure.py +0 -0
  39. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/__init__.py +0 -0
  40. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/approval_queue.py +0 -0
  41. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/evaluator.py +0 -0
  42. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/handlers.py +0 -0
  43. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/middleware.py +0 -0
  44. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/model.py +0 -0
  45. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/persistence.py +0 -0
  46. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/value_sources.py +0 -0
  47. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/py.typed +0 -0
  48. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/read_only.py +0 -0
  49. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  50. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  51. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  52. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  53. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  54. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  55. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  56. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  57. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  58. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  59. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  60. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  61. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  62. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  63. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  64. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  65. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  66. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  67. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  68. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  69. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  70. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  71. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  72. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  73. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  74. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/server.py +0 -0
  75. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/__init__.py +0 -0
  76. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  77. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  78. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  79. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  80. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  81. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  82. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  83. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  84. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_theme.py +0 -0
  85. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  86. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/settings.css +0 -0
  87. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/settings.html +0 -0
  88. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/settings.js +0 -0
  89. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/smoke_test.py +0 -0
  90. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  91. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/strict_bps.py +0 -0
  92. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/__init__.py +0 -0
  93. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/auto_backup.py +0 -0
  94. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/backup.py +0 -0
  95. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  96. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/component_api.py +0 -0
  97. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  98. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/device_control.py +0 -0
  99. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/enhanced.py +0 -0
  100. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/helpers.py +0 -0
  101. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/radio/__init__.py +0 -0
  102. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/radio/matter.py +0 -0
  103. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/radio/thread.py +0 -0
  104. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/radio/zwave.py +0 -0
  105. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/reference_validator.py +0 -0
  106. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/registry.py +0 -0
  107. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  108. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  109. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  110. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  111. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  112. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  113. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  114. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  115. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  116. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  117. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_addons.py +0 -0
  118. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_areas.py +0 -0
  119. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  120. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_calendar.py +0 -0
  121. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_camera.py +0 -0
  122. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_categories.py +0 -0
  123. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_code.py +0 -0
  124. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  125. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  126. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  127. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  128. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  129. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  130. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_dev.py +0 -0
  131. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_energy.py +0 -0
  132. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  133. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_groups.py +0 -0
  134. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_hacs.py +0 -0
  135. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_history.py +0 -0
  136. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_integrations.py +0 -0
  137. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_labels.py +0 -0
  138. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_radio.py +0 -0
  139. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_resources.py +0 -0
  140. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_service.py +0 -0
  141. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_services.py +0 -0
  142. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_system.py +0 -0
  143. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_themes.py +0 -0
  144. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_todo.py +0 -0
  145. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_traces.py +0 -0
  146. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_updates.py +0 -0
  147. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_utility.py +0 -0
  148. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  149. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  150. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_zones.py +0 -0
  151. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/util_helpers.py +0 -0
  152. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/validation_middleware.py +0 -0
  153. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/transforms/__init__.py +0 -0
  154. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/transforms/categorized_search.py +0 -0
  155. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  156. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/transport_security.py +0 -0
  157. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/update_check.py +0 -0
  158. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/__init__.py +0 -0
  159. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/config_hash.py +0 -0
  160. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/data_paths.py +0 -0
  161. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/domain_handlers.py +0 -0
  162. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  163. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  164. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/operation_manager.py +0 -0
  165. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/python_sandbox.py +0 -0
  166. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/skill_loader.py +0 -0
  167. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/usage_logger.py +0 -0
  168. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/visibility/__init__.py +0 -0
  169. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/visibility/model.py +0 -0
  170. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/visibility/persistence.py +0 -0
  171. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/visibility/resolver.py +0 -0
  172. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  173. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  174. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  175. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  176. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/tests/__init__.py +0 -0
  177. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/tests/test_constants.py +0 -0
  178. {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/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.dev1959
3
+ Version: 7.13.0.dev1965
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.dev1959"
7
+ version = "7.13.0.dev1965"
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
@@ -1720,12 +1720,23 @@ async def _restore_entity_state(client: Any, entity_id: str, config: Any) -> Any
1720
1720
  # best-effort.
1721
1721
 
1722
1722
 
1723
- async def _fetch_device(client: Any, entity_id: str) -> Any:
1723
+ async def _fetch_device(client: Any, device_id: str) -> Any:
1724
+ # Route the single-device capture through the component's ``device_get`` when
1725
+ # available (one in-process read of the raw DeviceEntry) instead of dumping the
1726
+ # whole registry — the same pre-write snapshot ``ha_set_device`` /
1727
+ # ``ha_remove_device`` capture. Lazy import to avoid the backup_manager →
1728
+ # tools → backup_manager cycle. ``None`` from the helper means "component
1729
+ # unavailable"; fall back to the full-list scan.
1730
+ from .tools.component_devices import fetch_device_via_component
1731
+
1732
+ result = await fetch_device_via_component(client, device_id)
1733
+ if result is not None:
1734
+ return result.get("device")
1724
1735
  items = await _ws_send(client, {"type": "config/device_registry/list"})
1725
1736
  if not isinstance(items, list):
1726
1737
  return None
1727
1738
  for item in items:
1728
- if item.get("id") == entity_id:
1739
+ if item.get("id") == device_id:
1729
1740
  return item
1730
1741
  return None
1731
1742
 
@@ -1445,6 +1445,7 @@ class HomeAssistantClient:
1445
1445
  return {
1446
1446
  "success": False,
1447
1447
  "error": f"WebSocket request blocked (403 Forbidden): {error_str}",
1448
+ "error_code": getattr(e, "code", None),
1448
1449
  "suggestions": [
1449
1450
  "This may be caused by a reverse proxy or security filter",
1450
1451
  "Try simplifying the request (e.g., shorter templates, fewer parameters)",
@@ -1457,7 +1458,14 @@ class HomeAssistantClient:
1457
1458
  # Unknown command." is undiagnosable from a user's log
1458
1459
  # (issue #1889 took a live reproduction to attribute).
1459
1460
  logger.error(f"WebSocket message failed ({message.get('type')}): {e}")
1460
- return {"success": False, "error": str(e)}
1461
+ # Preserve HA's structured error code (e.g. ``not_found``) so callers
1462
+ # can distinguish HA's authoritative verdict from a transient failure
1463
+ # instead of only seeing the stringified message.
1464
+ return {
1465
+ "success": False,
1466
+ "error": str(e),
1467
+ "error_code": getattr(e, "code", None),
1468
+ }
1461
1469
 
1462
1470
  return {"success": False, "error": "WebSocket request failed"}
1463
1471
 
@@ -0,0 +1,153 @@
1
+ """Shared device-registry reads over the ``ha_mcp_tools`` component gate.
2
+
3
+ Several tools resolve a device the same wasteful way today — pull the ENTIRE
4
+ device registry and filter for one entry (``ha_get_device`` single lookup,
5
+ ``ha_remove_device``'s body, the ``@with_auto_backup`` capture read, and
6
+ ``ha_manage_radio``'s ``_resolve_ieee``). When the component advertises the
7
+ ``device_get`` / ``device_list`` capabilities, that whole-registry dump becomes a
8
+ single in-process read: ``device_get`` returns one ``DeviceEntry.dict_repr`` by
9
+ id, ``device_list`` returns them all — each byte-identical to a
10
+ ``config/device_registry/list`` element by construction (see
11
+ ``custom_components/ha_mcp_tools/websocket_api.py``). Consumers keep their own
12
+ transforms over that raw shape.
13
+
14
+ This module owns the caps-gated fetch so the routing discipline — probe caps,
15
+ send one frame, invalidate on ``unknown_command``, fall back to the legacy path
16
+ on any component error — lives in one place instead of being duplicated per
17
+ consumer (the pattern ``tools_search._fetch_states_via_component`` established
18
+ for the ``states`` capability). Both helpers return ``None`` to mean "component
19
+ unavailable — use the legacy path"; a component that answers authoritatively
20
+ returns its payload (with ``device`` possibly ``None`` for "no such device").
21
+
22
+ ``device_get`` also carries an opt-in per-device entity join
23
+ (:func:`fetch_device_entities_via_component`): with ``include_entities`` the
24
+ component returns the device's ``config/entity_registry/list``-shaped rows as a
25
+ SIBLING ``entities`` key, so a single-device lookup no longer dumps the whole
26
+ entity registry to list one device's entities. The join is additive within
27
+ schema_version 1, so the server tolerates its absence rather than depending on a
28
+ version bump. An older ``device_get`` that predates the param never round-trips
29
+ the entities half — the extra field is rejected by the command's
30
+ ``PREVENT_EXTRA`` base schema, surfacing as an error that maps to the ``None``
31
+ miss — and, belt-and-suspenders, a response that carries the device but no
32
+ ``entities`` key is treated the same: fall back to the legacy
33
+ ``config/entity_registry/list`` for the entity half. Neither breaks the call.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import logging
39
+ from typing import Any
40
+
41
+ from ..client.rest_client import (
42
+ HomeAssistantCommandError,
43
+ HomeAssistantCommandTimeout,
44
+ )
45
+ from ..client.websocket_client import get_websocket_client
46
+ from .component_api import (
47
+ component_supports,
48
+ get_component_caps,
49
+ invalidate_caps,
50
+ is_unknown_command,
51
+ )
52
+
53
+ logger = logging.getLogger(__name__)
54
+
55
+ WS_DEVICE_GET = "ha_mcp_tools/device_get"
56
+ WS_DEVICE_LIST = "ha_mcp_tools/device_list"
57
+
58
+
59
+ async def fetch_device_via_component(
60
+ client: Any, device_id: str, *, include_entities: bool = False
61
+ ) -> dict[str, Any] | None:
62
+ """One ``ha_mcp_tools/device_get`` read; ``None`` ⇒ use the legacy path.
63
+
64
+ Returns the component's ``{"device": <raw dict> | None}`` payload (the raw
65
+ ``DeviceEntry.dict_repr`` for the id, byte-identical to a
66
+ ``config/device_registry/list`` element) or ``None`` when the component lacks
67
+ the ``device_get`` capability, was downgraded (``unknown_command`` →
68
+ invalidate the cached caps), or errored (logged). A component that answers
69
+ with ``{"device": None}`` is authoritative — the device does not exist — so
70
+ the caller must distinguish that from a ``None`` return (which means "component
71
+ unavailable, fall back"). Falls back **silently**, mirroring
72
+ ``ha_get_state``: the legacy path returns the byte-identical device either way
73
+ and the ``log.warning`` preserves operator visibility. A
74
+ ``HomeAssistantConnectionError`` (WS down) is not caught here, so it
75
+ propagates to the caller's own error handling — the legacy path shares the
76
+ same socket and would fail identically.
77
+
78
+ With ``include_entities`` the payload also carries a sibling ``entities`` list
79
+ (the device's ``config/entity_registry/list``-shaped rows), so a single-device
80
+ lookup that needs the device's entities skips the whole-entity-registry dump.
81
+ ``include_entities`` is only sent when true, so the frames of callers that need
82
+ only the device (``_resolve_ieee`` / capture / remove) are unchanged.
83
+ """
84
+ caps = await get_component_caps(client)
85
+ if not component_supports(caps, "device_get"):
86
+ return None
87
+ kwargs: dict[str, Any] = {"device_id": device_id}
88
+ if include_entities:
89
+ kwargs["include_entities"] = True
90
+ try:
91
+ ws = await get_websocket_client(url=client.base_url, token=client.token)
92
+ raw = await ws.send_command(WS_DEVICE_GET, **kwargs)
93
+ except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
94
+ if is_unknown_command(exc):
95
+ invalidate_caps(client)
96
+ else:
97
+ logger.warning("%s failed; fell back to legacy: %r", WS_DEVICE_GET, exc)
98
+ return None
99
+ result = raw.get("result")
100
+ if not isinstance(result, dict) or "device" not in result:
101
+ return None
102
+ return result
103
+
104
+
105
+ async def fetch_device_entities_via_component(
106
+ client: Any, device_id: str
107
+ ) -> list[dict[str, Any]] | None:
108
+ """The device's entity rows via ``device_get(include_entities=True)``; ``None`` ⇒ legacy.
109
+
110
+ Returns the ``config/entity_registry/list``-shaped rows bound to ``device_id``
111
+ (disabled included) so a per-device entity lookup avoids the whole-registry
112
+ dump. Returns ``None`` — the caller falls back to
113
+ ``config/entity_registry/list`` — when the component can't serve ``device_get``
114
+ at all, OR when it served the device but the response carries no ``entities``
115
+ key (an older ``device_get`` predating ``include_entities``, or any component
116
+ that does not round-trip the entities half): the entity join is additive, so
117
+ its absence degrades to legacy rather than silently reporting zero entities.
118
+ An AUTHORITATIVE empty result — the component honored ``include_entities`` and
119
+ the device has no entities, or no such device — comes back as a present empty
120
+ list, kept distinct from that ``None`` miss.
121
+ """
122
+ result = await fetch_device_via_component(client, device_id, include_entities=True)
123
+ if result is None:
124
+ return None
125
+ entities = result.get("entities")
126
+ return entities if isinstance(entities, list) else None
127
+
128
+
129
+ async def fetch_device_list_via_component(client: Any) -> dict[str, Any] | None:
130
+ """One ``ha_mcp_tools/device_list`` read; ``None`` ⇒ use the legacy path.
131
+
132
+ Returns the component's ``{"devices": [<raw dict>, ...]}`` payload (each a raw
133
+ ``DeviceEntry.dict_repr``, the in-process equivalent of
134
+ ``config/device_registry/list``) or ``None`` on capability miss, downgrade
135
+ (``unknown_command`` → invalidate caps), or error (logged) — same
136
+ error-taxonomy and silent fallback as :func:`fetch_device_via_component`.
137
+ """
138
+ caps = await get_component_caps(client)
139
+ if not component_supports(caps, "device_list"):
140
+ return None
141
+ try:
142
+ ws = await get_websocket_client(url=client.base_url, token=client.token)
143
+ raw = await ws.send_command(WS_DEVICE_LIST)
144
+ except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
145
+ if is_unknown_command(exc):
146
+ invalidate_caps(client)
147
+ else:
148
+ logger.warning("%s failed; fell back to legacy: %r", WS_DEVICE_LIST, exc)
149
+ return None
150
+ result = raw.get("result")
151
+ if not isinstance(result, dict) or not isinstance(result.get("devices"), list):
152
+ return None
153
+ return result
@@ -21,6 +21,7 @@ from dataclasses import dataclass, field
21
21
  from typing import Any
22
22
 
23
23
  from ...errors import ErrorCode, create_error_response
24
+ from ..component_devices import fetch_device_entities_via_component
24
25
  from ..helpers import raise_tool_error
25
26
 
26
27
  logger = logging.getLogger(__name__)
@@ -107,10 +108,18 @@ async def resolve_update_entity(
107
108
  Filters the ``update.*`` entities tied to ``device_id``, preferring the given
108
109
  ``platform`` (e.g. "matter", "zha", "zwave_js") when a device exposes more
109
110
  than one. Raises ENTITY_NOT_FOUND when the device exposes no update entity.
111
+
112
+ Routes the per-device entity read through the component's
113
+ ``device_get(include_entities=True)`` when available (the device's rows in one
114
+ in-process frame, ``config/entity_registry/list`` shape) instead of dumping the
115
+ whole entity registry; falls back to ``config/entity_registry/list`` otherwise.
116
+ The ``update.*`` / platform filtering stays client-side exactly as before.
110
117
  """
111
- entities = await ws_call(
112
- client, "config/entity_registry/list", context={"device_id": device_id}
113
- )
118
+ entities = await fetch_device_entities_via_component(client, device_id)
119
+ if entities is None:
120
+ entities = await ws_call(
121
+ client, "config/entity_registry/list", context={"device_id": device_id}
122
+ )
114
123
  candidates = [
115
124
  e
116
125
  for e in (entities or [])
@@ -19,6 +19,7 @@ from typing import Any
19
19
  from fastmcp.exceptions import ToolError
20
20
 
21
21
  from ...errors import ErrorCode, create_error_response
22
+ from ..component_devices import fetch_device_via_component
22
23
  from ..helpers import raise_tool_error
23
24
  from .base import (
24
25
  ActionSpec,
@@ -123,17 +124,40 @@ SUPPORTED: dict[str, ActionSpec] = {
123
124
  }
124
125
 
125
126
 
127
+ async def _resolve_ieee_devices(client: Any, device_id: Any) -> list[dict[str, Any]]:
128
+ """The device rows ``_resolve_ieee`` scans — component ``device_get`` or legacy.
129
+
130
+ Returns just the one matching device (``[device]``, or ``[]`` when it does not
131
+ exist) via ``ha_mcp_tools/device_get`` when the component serves it, else the
132
+ whole ``config/device_registry/list`` for the caller to filter. Only a
133
+ non-empty string ``device_id`` is eligible for the component read; anything
134
+ else takes the legacy path (and is rejected by the caller's scan).
135
+ """
136
+ if isinstance(device_id, str) and device_id:
137
+ result = await fetch_device_via_component(client, device_id)
138
+ if result is not None:
139
+ device = result.get("device")
140
+ return [device] if isinstance(device, dict) else []
141
+ devices = await ws_call(
142
+ client, "config/device_registry/list", context={"device_id": device_id}
143
+ )
144
+ return list(devices or [])
145
+
146
+
126
147
  async def _resolve_ieee(client: Any, device_id: Any) -> str:
127
148
  """Resolve a ``device_id`` to its ZHA IEEE address via the device registry.
128
149
 
129
150
  Parses the ``["zha", "<ieee>"]`` registry identifier (falling back to an
130
151
  ``("ieee", ...)`` connection). Raises VALIDATION_INVALID_PARAMETER when the
131
152
  device is not a ZHA device.
153
+
154
+ Routes the single-device lookup through the component's ``device_get`` when
155
+ available (one in-process read of the raw ``DeviceEntry`` shape) instead of
156
+ dumping the whole device registry; falls back to
157
+ ``config/device_registry/list`` when the component can't serve it.
132
158
  """
133
- devices = await ws_call(
134
- client, "config/device_registry/list", context={"device_id": device_id}
135
- )
136
- for device in devices or []:
159
+ devices = await _resolve_ieee_devices(client, device_id)
160
+ for device in devices:
137
161
  if device.get("id") != device_id:
138
162
  continue
139
163
  for ident in device.get("identifiers", []):
@@ -12,7 +12,18 @@ from fastmcp.exceptions import ToolError
12
12
  from fastmcp.tools import tool
13
13
  from pydantic import Field
14
14
 
15
+ from ..client.rest_client import (
16
+ HomeAssistantCommandError,
17
+ HomeAssistantCommandTimeout,
18
+ )
19
+ from ..client.websocket_client import get_websocket_client
15
20
  from ..errors import ErrorCode, create_error_response
21
+ from .component_api import (
22
+ component_supports,
23
+ get_component_caps,
24
+ invalidate_caps,
25
+ is_unknown_command,
26
+ )
16
27
  from .helpers import (
17
28
  exception_to_structured_error,
18
29
  log_tool_usage,
@@ -105,8 +116,11 @@ class BlueprintTools:
105
116
  Get blueprint information - list all blueprints or get details for a specific one.
106
117
 
107
118
  Without a path: Lists all installed blueprints for the specified domain.
108
- With a path: Retrieves full blueprint configuration including inputs, triggers,
109
- conditions, and actions.
119
+ With a path: Returns the blueprint's metadata and input definitions. The
120
+ full body (triggers/conditions/actions for automations, sequence for
121
+ scripts) is included under `config` ONLY when the ha_mcp_tools custom
122
+ component is installed — core's blueprint API exposes metadata alone, so
123
+ without the component the body cannot be read.
110
124
 
111
125
  EXAMPLES:
112
126
  - List all automation blueprints: ha_get_blueprint(domain="automation")
@@ -120,7 +134,8 @@ class BlueprintTools:
120
134
  RETURNS (when getting specific blueprint):
121
135
  - Blueprint metadata (name, description, author, source_url)
122
136
  - Input definitions with selectors and defaults
123
- - Blueprint configuration (triggers, conditions, actions for automations; sequence for scripts)
137
+ - `config`: the full parsed blueprint body (only with the ha_mcp_tools
138
+ component; `!input` substitution points appear as `{"__input__": name}`)
124
139
  """
125
140
  try:
126
141
  # Validate domain
@@ -202,9 +217,11 @@ class BlueprintTools:
202
217
  if "input" in meta:
203
218
  result["inputs"] = meta["input"]
204
219
 
205
- # Add blueprint configuration if available
206
- if "blueprint" in blueprint_data:
207
- result["blueprint"] = blueprint_data["blueprint"]
220
+ # Core's blueprint/list returns metadata only (never a body), so the
221
+ # full triggers/conditions/actions/sequence come from the ha_mcp_tools
222
+ # component when installed. Merge it additively under `config`; without
223
+ # the component the response stays metadata + inputs.
224
+ await self._merge_blueprint_config(result, domain, path)
208
225
 
209
226
  return result
210
227
 
@@ -222,6 +239,73 @@ class BlueprintTools:
222
239
  )
223
240
  return None # unreachable: exception_to_structured_error always raises
224
241
 
242
+ async def _merge_blueprint_config(
243
+ self, result: dict[str, Any], domain: str, path: str
244
+ ) -> None:
245
+ """Fetch the component-served blueprint body and merge it into ``result``.
246
+
247
+ Adds ``config`` when the body was read, or a top-level ``warnings`` entry
248
+ when a present component returned an unreadable body; a metadata-only
249
+ outcome (no component / capability) leaves ``result`` untouched.
250
+ """
251
+ config, config_warning = await self._blueprint_config_via_component(
252
+ domain, path
253
+ )
254
+ if config is not None:
255
+ result["config"] = config
256
+ elif config_warning is not None:
257
+ result.setdefault("warnings", []).append(config_warning)
258
+
259
+ async def _blueprint_config_via_component(
260
+ self, domain: str, path: str
261
+ ) -> tuple[dict[str, Any] | None, str | None]:
262
+ """Fetch a blueprint's full parsed body via the component.
263
+
264
+ core's ``blueprint/list`` returns only ``{metadata}`` (no body), so
265
+ without the component ``ha_get_blueprint`` can serve metadata + inputs
266
+ only. When the component advertises ``blueprint_get`` it reads the on-disk
267
+ blueprint file (path-jailed, executor-offloaded) and returns the full
268
+ parsed body, merged additively under ``config``. Returns
269
+ ``(config, warning)``:
270
+
271
+ - ``(dict, None)`` — the parsed body was read.
272
+ - ``(None, None)`` — metadata-only is the expected outcome: the component
273
+ is absent / lacks the capability, was downgraded (``unknown_command`` →
274
+ invalidate the cached caps), or errored (logged).
275
+ - ``(None, warning)`` — the component is present and the server has already
276
+ confirmed the path is a real installed blueprint, yet it returned a null
277
+ ``config`` (corrupt / unparseable file, read error). Metadata-only would
278
+ otherwise be indistinguishable from component-not-installed, so a
279
+ top-level warning is surfaced instead.
280
+ """
281
+ caps = await get_component_caps(self._client)
282
+ if not component_supports(caps, "blueprint_get"):
283
+ return None, None
284
+ try:
285
+ ws = await get_websocket_client(
286
+ url=self._client.base_url, token=self._client.token
287
+ )
288
+ raw = await ws.send_command(
289
+ "ha_mcp_tools/blueprint_get", domain=domain, path=path
290
+ )
291
+ except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
292
+ if is_unknown_command(exc):
293
+ invalidate_caps(self._client)
294
+ else:
295
+ logger.warning(
296
+ "ha_mcp_tools/blueprint_get failed; served metadata-only: %r",
297
+ exc,
298
+ )
299
+ return None, None
300
+ result = raw.get("result") or {}
301
+ config = result.get("config")
302
+ if isinstance(config, dict):
303
+ return config, None
304
+ return None, (
305
+ "Blueprint body could not be read or parsed by the ha_mcp_tools "
306
+ "component; returning metadata only"
307
+ )
308
+
225
309
  async def _save_blueprint(
226
310
  self,
227
311
  url: str,
@@ -14,8 +14,19 @@ from fastmcp.exceptions import ToolError
14
14
  from fastmcp.tools import tool
15
15
  from pydantic import Field
16
16
 
17
+ from ..client.rest_client import (
18
+ HomeAssistantCommandError,
19
+ HomeAssistantCommandTimeout,
20
+ )
21
+ from ..client.websocket_client import get_websocket_client
17
22
  from ..errors import ErrorCode, create_error_response
18
23
  from .auto_backup import with_auto_backup
24
+ from .component_api import (
25
+ component_supports,
26
+ get_component_caps,
27
+ invalidate_caps,
28
+ is_unknown_command,
29
+ )
19
30
  from .helpers import (
20
31
  exception_to_structured_error,
21
32
  extract_tool_error_message,
@@ -33,6 +44,11 @@ from .util_helpers import (
33
44
 
34
45
  logger = logging.getLogger(__name__)
35
46
 
47
+ # The ha_mcp_tools/entity_enrich WS command: resolved area/floor/label NAMES +
48
+ # aliases per entity_id, byte-shaped like the component's registry join. Named
49
+ # once so the routing helper and its tests stay in lockstep.
50
+ WS_ENTITY_ENRICH = "ha_mcp_tools/entity_enrich"
51
+
36
52
  # Bounds the per-frame size of a bulk config/entity_registry/get_entries call
37
53
  # (extended entries, ~1KB each) so a large id list can't produce an over-cap
38
54
  # WebSocket frame. Matches the chunk size smart_search uses for the same
@@ -73,6 +89,80 @@ def _format_fetched_entity(entry: dict[str, Any]) -> dict[str, Any]:
73
89
  }
74
90
 
75
91
 
92
+ async def fetch_entity_enrichment_via_component(
93
+ client: Any, entity_ids: list[str]
94
+ ) -> dict[str, dict[str, Any]] | None:
95
+ """One ``ha_mcp_tools/entity_enrich`` read; ``None`` ⇒ skip enrichment (legacy).
96
+
97
+ Returns the component's ``{id: {area, floor, labels, aliases}}`` map (resolved
98
+ area/floor/label NAMES the raw registry entry lacks — it carries ``area_id`` /
99
+ label *ids*) or ``None`` when the component lacks the ``entity_enrich``
100
+ capability, was downgraded (``unknown_command`` → invalidate the cached caps),
101
+ or errored (logged). ``None`` means "no enrichment available", NOT "no such
102
+ entity": ``ha_get_entity``'s registry read already served the base record, so a
103
+ ``None`` here simply leaves the additive fields off — the enrichment is
104
+ strictly additive, so its absence changes nothing else. A
105
+ ``HomeAssistantConnectionError`` (WS down) is not caught here, so it propagates
106
+ to the tool's own error handling. Follows the same caps-gate discipline as
107
+ ``component_devices.fetch_device_via_component``.
108
+
109
+ The ids are split into ``_GET_ENTRIES_CHUNK_SIZE`` chunks (the same bound the
110
+ sibling ``config/entity_registry/get_entries`` read uses) so a large bulk
111
+ request cannot produce an over-cap WebSocket frame or stall in the optional
112
+ enrichment step. Enrichment stays all-or-nothing: any chunk failing or drifting
113
+ in shape returns ``None`` (legacy shape), never a partially-enriched response.
114
+ """
115
+ if not entity_ids:
116
+ return None
117
+ caps = await get_component_caps(client)
118
+ if not component_supports(caps, "entity_enrich"):
119
+ return None
120
+ ids = list(entity_ids)
121
+ chunks = [
122
+ ids[i : i + _GET_ENTRIES_CHUNK_SIZE]
123
+ for i in range(0, len(ids), _GET_ENTRIES_CHUNK_SIZE)
124
+ ]
125
+ merged: dict[str, dict[str, Any]] = {}
126
+ try:
127
+ ws = await get_websocket_client(url=client.base_url, token=client.token)
128
+ for chunk in chunks:
129
+ raw = await ws.send_command(WS_ENTITY_ENRICH, entity_ids=chunk)
130
+ result = raw.get("result")
131
+ if not isinstance(result, dict):
132
+ return None
133
+ entities = result.get("entities")
134
+ if not isinstance(entities, dict):
135
+ return None
136
+ merged.update(entities)
137
+ except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
138
+ if is_unknown_command(exc):
139
+ invalidate_caps(client)
140
+ else:
141
+ logger.warning("%s failed; skipped enrichment: %r", WS_ENTITY_ENRICH, exc)
142
+ return None
143
+ return merged
144
+
145
+
146
+ def _merge_entity_enrichment(
147
+ record: dict[str, Any], enrichment: dict[str, Any] | None
148
+ ) -> None:
149
+ """Additively attach resolved area/floor/label NAMES to an entity record.
150
+
151
+ The base record already carries ``area_id`` and label *ids* (``labels``); the
152
+ component join adds the resolved, device-inherited NAMES under non-clobbering
153
+ keys — ``area`` / ``floor`` / ``label_names`` — so nothing existing is
154
+ overwritten. ``aliases`` is already on the base record (same set — the
155
+ component sorts, so ordering may differ — and the base value is authoritative),
156
+ so it is not re-added. A ``None`` / empty enrichment is a no-op: on a capability
157
+ miss the fields are simply absent, leaving the legacy response shape unchanged.
158
+ """
159
+ if not enrichment:
160
+ return
161
+ record["area"] = enrichment.get("area")
162
+ record["floor"] = enrichment.get("floor")
163
+ record["label_names"] = enrichment.get("labels") or []
164
+
165
+
76
166
  def _match_registry_by_unique_id(
77
167
  entries: list[dict[str, Any]],
78
168
  unique_id: str,
@@ -1816,6 +1906,16 @@ class EntityTools:
1816
1906
  helper's current config (template body, group members, etc.) without
1817
1907
  scanning a domain list.
1818
1908
  - unique_id: Integration's unique identifier
1909
+
1910
+ Resolved-name enrichment (present only when the ha_mcp_tools component
1911
+ advertises it; otherwise these keys are absent):
1912
+ - area: Assigned area NAME (device-inherited when the entity has none;
1913
+ resolves area_id above)
1914
+ - floor: Floor NAME of the assigned area
1915
+ - label_names: Assigned label NAMES (resolves the label ids in labels)
1916
+ Resolved label names live under label_names HERE (this tool's base
1917
+ `labels` already carries the label ids); ha_search result_fields and
1918
+ ha_get_entity_exposure instead emit the resolved names under `labels`.
1819
1919
  """
1820
1920
  try:
1821
1921
  # Resolver mode (unique_id) is mutually exclusive with entity_id.
@@ -1870,35 +1970,43 @@ class EntityTools:
1870
1970
  )
1871
1971
  return None # unreachable: exception_to_structured_error always raises
1872
1972
 
1973
+ async def _get_single_entity(self, eid: str) -> dict[str, Any]:
1974
+ """Look up one entity registry entry, with additive enrichment."""
1975
+ logger.info(f"Getting entity registry entry for {eid}")
1976
+ try:
1977
+ result = await self._fetch_entity(eid)
1978
+ except ValueError as e:
1979
+ raise_tool_error(
1980
+ create_error_response(
1981
+ ErrorCode.SERVICE_CALL_FAILED,
1982
+ f"Entity not found: {e}",
1983
+ context={"entity_id": eid},
1984
+ suggestions=[
1985
+ "Use ha_search() to find valid entity IDs",
1986
+ "Check the entity_id spelling and format (e.g., 'sensor.temperature')",
1987
+ ],
1988
+ )
1989
+ )
1990
+ # Additive area/floor/label-name enrichment via the component when it
1991
+ # advertises entity_enrich; a capability miss leaves the fields off
1992
+ # (legacy shape unchanged).
1993
+ enriched = await fetch_entity_enrichment_via_component(self._client, [eid])
1994
+ if enriched is not None:
1995
+ _merge_entity_enrichment(result, enriched.get(eid))
1996
+ return {
1997
+ "success": True,
1998
+ "entity_id": eid,
1999
+ "entity_entry": result,
2000
+ }
2001
+
1873
2002
  async def _get_by_entity_id(self, entity_id: str | list[str]) -> dict[str, Any]:
1874
2003
  """Look up entities by entity_id (single or bulk list)."""
1875
2004
  entity_ids, is_bulk, early_response = _parse_get_entity_ids(entity_id)
1876
2005
  if early_response is not None:
1877
2006
  return early_response
1878
2007
 
1879
- # Single entity case
1880
2008
  if not is_bulk:
1881
- eid = entity_ids[0]
1882
- logger.info(f"Getting entity registry entry for {eid}")
1883
- try:
1884
- result = await self._fetch_entity(eid)
1885
- except ValueError as e:
1886
- raise_tool_error(
1887
- create_error_response(
1888
- ErrorCode.SERVICE_CALL_FAILED,
1889
- f"Entity not found: {e}",
1890
- context={"entity_id": eid},
1891
- suggestions=[
1892
- "Use ha_search() to find valid entity IDs",
1893
- "Check the entity_id spelling and format (e.g., 'sensor.temperature')",
1894
- ],
1895
- )
1896
- )
1897
- return {
1898
- "success": True,
1899
- "entity_id": eid,
1900
- "entity_entry": result,
1901
- }
2009
+ return await self._get_single_entity(entity_ids[0])
1902
2010
 
1903
2011
  # Bulk case - fetch all entities in one chunked get_entries call
1904
2012
  # (native HA bulk command) instead of one registry get per id.
@@ -1922,6 +2030,20 @@ class EntityTools:
1922
2030
  }
1923
2031
  )
1924
2032
 
2033
+ # Additive area/floor/label-name enrichment for the found entities via the
2034
+ # component's entity_enrich capability; missing ⇒ fields simply absent.
2035
+ found_ids: list[str] = [
2036
+ e["entity_id"]
2037
+ for e in entity_entries
2038
+ if isinstance(e.get("entity_id"), str)
2039
+ ]
2040
+ enriched = await fetch_entity_enrichment_via_component(self._client, found_ids)
2041
+ if enriched is not None:
2042
+ for entry in entity_entries:
2043
+ entry_id = entry.get("entity_id")
2044
+ if isinstance(entry_id, str):
2045
+ _merge_entity_enrichment(entry, enriched.get(entry_id))
2046
+
1925
2047
  response: dict[str, Any] = {
1926
2048
  "success": True,
1927
2049
  "count": len(entity_entries),