ha-mcp-dev 8.3.0.dev2387__tar.gz → 8.3.0.dev2393__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 (277) hide show
  1. {ha_mcp_dev-8.3.0.dev2387/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.3.0.dev2393}/PKG-INFO +1 -1
  2. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/pyproject.toml +1 -1
  3. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_entities.py +63 -0
  4. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_search.py +225 -41
  5. ha_mcp_dev-8.3.0.dev2393/src/ha_mcp/utils/entity_membership.py +59 -0
  6. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  7. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  8. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/LICENSE +0 -0
  9. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/MANIFEST.in +0 -0
  10. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/README.md +0 -0
  11. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/setup.cfg +0 -0
  12. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/__init__.py +0 -0
  13. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/__main__.py +0 -0
  14. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_pypi_marker +0 -0
  15. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_rich_compat.py +0 -0
  16. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/__init__.py +0 -0
  17. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/requirements.txt +0 -0
  18. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
  19. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
  20. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
  21. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
  22. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
  23. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
  24. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
  25. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
  26. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
  27. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
  28. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
  29. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
  30. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
  31. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/client.py +0 -0
  32. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
  33. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
  34. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
  35. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
  36. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
  37. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
  38. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
  39. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
  40. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
  41. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
  42. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
  43. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
  44. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
  45. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
  46. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
  47. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
  48. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
  49. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
  50. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
  51. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
  52. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
  53. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
  54. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/server.py +0 -0
  55. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
  56. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
  57. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
  58. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
  59. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
  60. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
  61. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
  62. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
  63. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
  64. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
  65. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
  66. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
  67. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
  68. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
  69. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
  70. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
  71. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
  72. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
  73. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/version.py +0 -0
  74. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_version.py +0 -0
  75. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/auth/__init__.py +0 -0
  76. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/auth/consent_form.py +0 -0
  77. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/auth/oidc_compat.py +0 -0
  78. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/auth/provider.py +0 -0
  79. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/backup_manager.py +0 -0
  80. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/browser_landing.py +0 -0
  81. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/client/__init__.py +0 -0
  82. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/client/rest_client.py +0 -0
  83. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/client/supervisor_client.py +0 -0
  84. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/client/websocket_client.py +0 -0
  85. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/client/websocket_listener.py +0 -0
  86. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/config.py +0 -0
  87. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  88. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  89. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  90. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  91. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  92. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  93. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/errors.py +0 -0
  94. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/hacs_auto_refresh.py +0 -0
  95. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/llm_exposure.py +0 -0
  96. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/__init__.py +0 -0
  97. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/approval_queue.py +0 -0
  98. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/editing.py +0 -0
  99. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/evaluator.py +0 -0
  100. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/handlers.py +0 -0
  101. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/middleware.py +0 -0
  102. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/model.py +0 -0
  103. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/persistence.py +0 -0
  104. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/value_sources.py +0 -0
  105. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/py.typed +0 -0
  106. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/read_only.py +0 -0
  107. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/redaction.py +0 -0
  108. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/renamed_tools.py +0 -0
  109. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  110. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  111. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  112. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  113. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  114. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  115. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  116. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  117. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  118. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  119. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  120. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  121. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  122. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  123. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  124. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  125. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  126. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  127. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  128. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  129. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  130. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  131. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  132. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  133. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  134. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/server.py +0 -0
  135. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/__init__.py +0 -0
  136. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  137. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  138. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  139. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  140. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  141. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  142. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  143. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  144. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  145. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_theme.py +0 -0
  146. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  147. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  148. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
  149. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  150. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  151. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
  152. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/es.json +0 -0
  153. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
  154. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/it.json +0 -0
  155. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
  156. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
  157. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  158. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
  159. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  160. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/settings.css +0 -0
  161. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/settings.html +0 -0
  162. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/settings.js +0 -0
  163. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/smoke_test.py +0 -0
  164. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  165. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/strict_bps.py +0 -0
  166. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/__init__.py +0 -0
  167. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/auto_backup.py +0 -0
  168. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/backup.py +0 -0
  169. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  170. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/best_practice_result.py +0 -0
  171. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/best_practice_variables.py +0 -0
  172. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/component_api.py +0 -0
  173. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/component_config_entries.py +0 -0
  174. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/component_config_reads.py +0 -0
  175. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/component_devices.py +0 -0
  176. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/component_registries.py +0 -0
  177. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  178. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  179. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
  180. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
  181. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
  182. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_identity.py +0 -0
  183. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
  184. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
  185. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/device_control.py +0 -0
  186. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/error_log_parsing.py +0 -0
  187. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/hacs_registration.py +0 -0
  188. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/helpers.py +0 -0
  189. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
  190. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/radio/__init__.py +0 -0
  191. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/radio/base.py +0 -0
  192. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/radio/matter.py +0 -0
  193. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/radio/thread.py +0 -0
  194. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  195. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/radio/zwave.py +0 -0
  196. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/reference_validator.py +0 -0
  197. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/registry.py +0 -0
  198. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
  199. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  200. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  201. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  202. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  203. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  204. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  205. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  206. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  207. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  208. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_addons.py +0 -0
  209. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_areas.py +0 -0
  210. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
  211. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  212. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  213. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_calendar.py +0 -0
  214. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_camera.py +0 -0
  215. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_categories.py +0 -0
  216. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_code.py +0 -0
  217. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  218. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  219. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  220. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  221. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  222. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  223. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_dev.py +0 -0
  224. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_energy.py +0 -0
  225. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_entities.py +0 -0
  226. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  227. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_groups.py +0 -0
  228. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_hacs.py +0 -0
  229. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_history.py +0 -0
  230. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_integrations.py +0 -0
  231. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_labels.py +0 -0
  232. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_radio.py +0 -0
  233. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_registry.py +0 -0
  234. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_resources.py +0 -0
  235. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_security_policy.py +0 -0
  236. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_service.py +0 -0
  237. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_services.py +0 -0
  238. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_system.py +0 -0
  239. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_themes.py +0 -0
  240. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_todo.py +0 -0
  241. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_traces.py +0 -0
  242. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_updates.py +0 -0
  243. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_utility.py +0 -0
  244. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  245. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  246. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  247. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_zones.py +0 -0
  248. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/util_helpers.py +0 -0
  249. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/validation_middleware.py +0 -0
  250. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/transforms/__init__.py +0 -0
  251. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/transforms/categorized_search.py +0 -0
  252. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  253. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/transport_security.py +0 -0
  254. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/update_check.py +0 -0
  255. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/__init__.py +0 -0
  256. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/config_hash.py +0 -0
  257. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/config_write_lock.py +0 -0
  258. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/data_paths.py +0 -0
  259. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/domain_handlers.py +0 -0
  260. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  261. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  262. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/operation_manager.py +0 -0
  263. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/python_sandbox.py +0 -0
  264. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/skill_loader.py +0 -0
  265. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/usage_logger.py +0 -0
  266. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/visibility/__init__.py +0 -0
  267. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/visibility/enforcement.py +0 -0
  268. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/visibility/model.py +0 -0
  269. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/visibility/persistence.py +0 -0
  270. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/visibility/resolver.py +0 -0
  271. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  272. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  273. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  274. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  275. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/tests/__init__.py +0 -0
  276. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/tests/test_constants.py +0 -0
  277. {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/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: 8.3.0.dev2387
3
+ Version: 8.3.0.dev2393
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 = "8.3.0.dev2387"
7
+ version = "8.3.0.dev2393"
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
@@ -2,8 +2,10 @@
2
2
 
3
3
  import asyncio
4
4
  import logging
5
+ from collections.abc import Mapping
5
6
  from typing import Any
6
7
 
8
+ from ...utils.entity_membership import normalize_member_entity_ids
7
9
  from ...utils.fuzzy_search import calculate_partial_ratio, calculate_ratio
8
10
  from ...visibility.resolver import (
9
11
  device_registry_needed_for_visibility,
@@ -21,6 +23,35 @@ logger = logging.getLogger(__name__)
21
23
  _GET_ENTRIES_CHUNK_SIZE = 500
22
24
 
23
25
 
26
+ def _redact_hidden_memberships(
27
+ entities: list[dict[str, Any]], visibility_hidden: set[str]
28
+ ) -> list[dict[str, Any]]:
29
+ """Mark membership whose direct IDs intersect the visibility deny-set.
30
+
31
+ tools_search._add_membership_fields consumes this private sentinel after
32
+ smart-search projection, preserving is_group while withholding member IDs.
33
+ """
34
+ if not visibility_hidden:
35
+ return entities
36
+ redacted: list[dict[str, Any]] = []
37
+ for entity in entities:
38
+ attributes = entity.get("attributes")
39
+ members = normalize_member_entity_ids(attributes)
40
+ if members is not None and visibility_hidden.intersection(members):
41
+ redacted.append(
42
+ {
43
+ **entity,
44
+ "attributes": {
45
+ **(attributes if isinstance(attributes, Mapping) else {}),
46
+ "_ha_mcp_membership_redacted": True,
47
+ },
48
+ }
49
+ )
50
+ else:
51
+ redacted.append(entity)
52
+ return redacted
53
+
54
+
24
55
  class EntitySearchMixin(_SearchBase):
25
56
  """``smart_entity_search`` and ``get_entities_by_area`` plus helpers."""
26
57
 
@@ -33,6 +64,7 @@ class EntitySearchMixin(_SearchBase):
33
64
  domain_filter: str | None = None,
34
65
  include_hidden: bool = True,
35
66
  *,
67
+ include_membership: bool = False,
36
68
  prefetched_states: list[dict[str, Any]] | None = None,
37
69
  prefetched_registry: Any = None,
38
70
  ) -> dict[str, Any]:
@@ -49,6 +81,8 @@ class EntitySearchMixin(_SearchBase):
49
81
  set in the entity registry are still returned but receive
50
82
  a score penalty so they sort below comparable visible
51
83
  matches. Pass False to filter them out entirely.
84
+ include_membership: Whether to retain membership attributes and redaction markers
85
+ for opt-in search result fields.
52
86
  prefetched_states: Pre-fetched ``get_states()`` list shared by the
53
87
  ha_search orchestrator when both search branches run; ``None``
54
88
  means fetch here.
@@ -68,6 +102,7 @@ class EntitySearchMixin(_SearchBase):
68
102
  entities, visibility_warnings = await self._fetch_search_entities(
69
103
  domain_filter,
70
104
  include_hidden,
105
+ include_membership=include_membership,
71
106
  prefetched_states=prefetched_states,
72
107
  prefetched_registry=prefetched_registry,
73
108
  )
@@ -261,6 +296,7 @@ class EntitySearchMixin(_SearchBase):
261
296
  domain_filter: str | None,
262
297
  include_hidden: bool,
263
298
  *,
299
+ include_membership: bool = False,
264
300
  prefetched_states: list[dict[str, Any]] | None = None,
265
301
  prefetched_registry: Any = None,
266
302
  ) -> tuple[list[dict[str, Any]], list[str]]:
@@ -334,6 +370,17 @@ class EntitySearchMixin(_SearchBase):
334
370
  registry_result, states_result, self.client, device_result
335
371
  )
336
372
  registry_slim = self._build_registry_slim(registry_result)
373
+ if include_membership:
374
+ denied_member_ids = visibility_hidden | (
375
+ {
376
+ entity_id
377
+ for entity_id, entry in registry_slim.items()
378
+ if entry.get("hidden_by") is not None
379
+ }
380
+ if not include_hidden
381
+ else set()
382
+ )
383
+ entities = _redact_hidden_memberships(entities, denied_member_ids)
337
384
  survivor_ids, survivor_states = self._filter_hidden_entities(
338
385
  entities, registry_slim, include_hidden, visibility_hidden
339
386
  )
@@ -401,6 +448,8 @@ class EntitySearchMixin(_SearchBase):
401
448
  area_query: str,
402
449
  group_by_domain: bool = True,
403
450
  include_hidden: bool = True,
451
+ *,
452
+ include_membership: bool = False,
404
453
  ) -> dict[str, Any]:
405
454
  """
406
455
  Get entities grouped by area/room using the HA registries for accurate area resolution.
@@ -417,6 +466,8 @@ class EntitySearchMixin(_SearchBase):
417
466
  set in the entity registry are still grouped under their
418
467
  area but receive a score penalty when ranked. Pass False
419
468
  to filter them out entirely.
469
+ include_membership: Whether to retain membership attributes and redaction markers
470
+ for opt-in search result fields.
420
471
 
421
472
  Returns:
422
473
  Dictionary with area-grouped entities
@@ -509,6 +560,17 @@ class EntitySearchMixin(_SearchBase):
509
560
  entity_area_resolved, hidden_entity_ids = self._resolve_entity_areas(
510
561
  entity_reg_map, device_area_map, include_hidden, visibility_hidden
511
562
  )
563
+ if include_membership:
564
+ denied_member_ids = visibility_hidden | (
565
+ {
566
+ entity_id
567
+ for entity_id, entry in entity_reg_map.items()
568
+ if entry.get("hidden_by") is not None
569
+ }
570
+ if not include_hidden
571
+ else set()
572
+ )
573
+ entities = _redact_hidden_memberships(entities, denied_member_ids)
512
574
  state_map = self._build_state_map(entities)
513
575
  formatted_areas, total_entities = self._format_area_entities(
514
576
  matched_area_ids,
@@ -870,6 +932,7 @@ class EntitySearchMixin(_SearchBase):
870
932
  ),
871
933
  "state": state_info.get("state", "unknown"),
872
934
  "_hidden_by": "hidden" if entity_id in hidden_entity_ids else None,
935
+ "_attributes": state_info.get("attributes", {}),
873
936
  }
874
937
  if include_domain:
875
938
  record["domain"] = entity_id.split(".", maxsplit=1)[0]
@@ -6,6 +6,7 @@ This module provides entity search, system overview, deep search, and state retr
6
6
 
7
7
  import asyncio
8
8
  import logging
9
+ from collections.abc import Mapping
9
10
  from dataclasses import dataclass
10
11
  from typing import Annotated, Any, Literal, cast
11
12
 
@@ -24,6 +25,7 @@ from ..client.websocket_client import get_websocket_client
24
25
  from ..config import get_global_settings
25
26
  from ..errors import create_validation_error
26
27
  from ..transforms.categorized_search import DEFAULT_PINNED_TOOLS
28
+ from ..utils.entity_membership import normalize_member_entity_ids
27
29
  from ..utils.fuzzy_search import apply_hidden_penalty
28
30
  from ..visibility.resolver import (
29
31
  device_registry_needed_for_visibility,
@@ -596,12 +598,22 @@ def _apply_by_domain_grouping(
596
598
  by_domain = {d: ents[:per_domain_limit_int] for d, ents in by_domain.items()}
597
599
  if parsed_result_fields is not None:
598
600
  by_domain = {
599
- d: _project_records(ents, parsed_result_fields)
601
+ d: _project_records(ents, _effective_result_fields(parsed_result_fields))
600
602
  for d, ents in by_domain.items()
601
603
  }
602
604
  data["by_domain"] = by_domain
603
605
 
604
606
 
607
+ def _effective_result_fields(parsed_result_fields: list[str]) -> list[str]:
608
+ """Retain is_group whenever member_entity_ids is projected."""
609
+ if (
610
+ "member_entity_ids" in parsed_result_fields
611
+ and "is_group" not in parsed_result_fields
612
+ ):
613
+ return [*parsed_result_fields, "is_group"]
614
+ return parsed_result_fields
615
+
616
+
605
617
  def _apply_result_fields_to_response(
606
618
  data: dict[str, Any],
607
619
  parsed_result_fields: list[str] | None,
@@ -610,8 +622,17 @@ def _apply_result_fields_to_response(
610
622
  if parsed_result_fields is None or "results" not in data:
611
623
  return
612
624
  orig = data["results"]
613
- data["results"] = _project_records(orig, parsed_result_fields)
614
- _warn = _result_fields_warning(orig, data["results"], parsed_result_fields)
625
+ data["results"] = _project_records(
626
+ orig, _effective_result_fields(parsed_result_fields)
627
+ )
628
+ warning_fields = [
629
+ field for field in parsed_result_fields if field != "member_entity_ids"
630
+ ]
631
+ _warn = (
632
+ _result_fields_warning(orig, data["results"], warning_fields)
633
+ if warning_fields
634
+ else None
635
+ )
615
636
  if _warn:
616
637
  data.setdefault("warnings", []).append(_warn)
617
638
 
@@ -738,12 +759,15 @@ _ENTITY_RECORD_KEYS = (
738
759
  # (SearchTools._fetch_entity_enrichment). Ordered so a projected record lists them
739
760
  # consistently regardless of the caller's result_fields order.
740
761
  _ENRICHMENT_FIELDS: tuple[str, ...] = ("area", "floor", "labels", "aliases")
741
-
742
- # Every field name result_fields= accepts — base record keys plus the opt-in
743
- # enrichment keys. A requested name outside this set is rejected up front with the
744
- # standard validation error rather than silently projecting to empty records.
745
- _ALLOWED_RESULT_FIELDS: frozenset[str] = frozenset(_ENTITY_RECORD_KEYS) | frozenset(
746
- _ENRICHMENT_FIELDS
762
+ _MEMBERSHIP_FIELDS: tuple[str, ...] = ("is_group", "member_entity_ids")
763
+
764
+ # Every field name result_fields= accepts — base record keys plus opt-in
765
+ # enrichment and membership keys. Unknown names are rejected up front rather
766
+ # than silently projecting to empty records.
767
+ _ALLOWED_RESULT_FIELDS: frozenset[str] = (
768
+ frozenset(_ENTITY_RECORD_KEYS)
769
+ | frozenset(_ENRICHMENT_FIELDS)
770
+ | frozenset(_MEMBERSHIP_FIELDS)
747
771
  )
748
772
 
749
773
 
@@ -790,6 +814,50 @@ def _requested_enrichment(parsed_result_fields: list[str] | None) -> tuple[str,
790
814
  return tuple(f for f in _ENRICHMENT_FIELDS if f in requested)
791
815
 
792
816
 
817
+ def _requested_membership(parsed_result_fields: list[str] | None) -> tuple[str, ...]:
818
+ """Return requested membership fields, retaining the group discriminator.
819
+
820
+ A member-only projection still includes is_group so a redacted group,
821
+ an empty group, and a leaf entity remain distinguishable.
822
+ """
823
+ if not parsed_result_fields:
824
+ return ()
825
+ requested = set(parsed_result_fields)
826
+ if "member_entity_ids" in requested:
827
+ requested.add("is_group")
828
+ return tuple(field for field in _MEMBERSHIP_FIELDS if field in requested)
829
+
830
+
831
+ def _add_membership_fields(
832
+ record: dict[str, Any],
833
+ attributes: Any,
834
+ requested: tuple[str, ...],
835
+ *,
836
+ denied_member_ids: set[str],
837
+ ) -> None:
838
+ """Add requested generic membership metadata to one search record.
839
+
840
+ Smart-search paths may mark attributes in _redact_hidden_memberships before
841
+ this final projection. Callers that consume that sentinel pass an explicit
842
+ empty denied set.
843
+ """
844
+ if not requested:
845
+ return
846
+ members = normalize_member_entity_ids(attributes)
847
+ redacted = bool(
848
+ isinstance(attributes, Mapping)
849
+ and attributes.get("_ha_mcp_membership_redacted")
850
+ ) or bool(
851
+ members is not None
852
+ and denied_member_ids
853
+ and denied_member_ids.intersection(members)
854
+ )
855
+ if "is_group" in requested:
856
+ record["is_group"] = members is not None
857
+ if "member_entity_ids" in requested and members is not None and not redacted:
858
+ record["member_entity_ids"] = members
859
+
860
+
793
861
  def _ws_result_map(resp: Any) -> dict[str, dict[str, Any]]:
794
862
  """The ``{entity_id: entry}`` map from a ``config/entity_registry/get_entries`` reply."""
795
863
  if isinstance(resp, dict) and resp.get("success"):
@@ -974,6 +1042,11 @@ def _build_component_search_request(req: _ResolvedSearch) -> dict[str, Any]:
974
1042
  "limit": req.limit,
975
1043
  "offset": req.offset,
976
1044
  }
1045
+ membership_fields = _requested_membership(
1046
+ _parse_component_result_fields(req.result_fields)
1047
+ )
1048
+ if membership_fields:
1049
+ request["result_fields"] = list(membership_fields)
977
1050
  if req.query_text:
978
1051
  request["query"] = req.query_text
979
1052
  domain_filter = _normalized_domain_filter(req.domain_filter)
@@ -1061,18 +1134,22 @@ def _shape_component_search_response(
1061
1134
 
1062
1135
  if req.registry_eligible:
1063
1136
  parsed_result_fields = _parse_component_result_fields(req.result_fields)
1064
- # Base record is the six documented keys. result_fields may additionally
1065
- # request enrichment fields (area/floor/labels/aliases) that the component
1066
- # already computed per hit via its registry join — retain exactly those
1067
- # requested keys before the result_fields projection so the enrichment
1068
- # survives it, while a search that requests none still emits the default
1069
- # six-key shape (parity with the legacy path).
1137
+ # Base records contain the six documented keys. Requested enrichment
1138
+ # fields retain the component's area/floor/labels/aliases registry join;
1139
+ # requested membership fields append its opt-in state-derived metadata.
1140
+ # With neither class requested, the default six-key shape is unchanged.
1070
1141
  record_keys = (
1071
1142
  *_ENTITY_RECORD_KEYS,
1072
1143
  *_requested_enrichment(parsed_result_fields),
1144
+ *_requested_membership(parsed_result_fields),
1073
1145
  )
1146
+ conditional_keys = set(_requested_membership(parsed_result_fields))
1074
1147
  entities = [
1075
- {key: rec.get(key) for key in record_keys}
1148
+ {
1149
+ key: rec[key] if key in conditional_keys else rec.get(key)
1150
+ for key in record_keys
1151
+ if key not in conditional_keys or key in rec
1152
+ }
1076
1153
  for rec in _as_record_list(component_result.get("entities"))
1077
1154
  ]
1078
1155
  entity_has_more = bool(component_result.get("entity_has_more", False))
@@ -1310,7 +1387,7 @@ def _build_domain_only_by_domain(
1310
1387
  """Build the by_domain dict for domain-listing mode (all results are one domain)."""
1311
1388
  items = results[:per_domain_limit] if per_domain_limit is not None else results
1312
1389
  if parsed_result_fields is not None:
1313
- items = _project_records(items, parsed_result_fields)
1390
+ items = _project_records(items, _effective_result_fields(parsed_result_fields))
1314
1391
  return {domain: items}
1315
1392
 
1316
1393
 
@@ -1491,6 +1568,9 @@ def _match_exact_search_entity(
1491
1568
  visibility_hidden: set[str],
1492
1569
  hidden_ids: set[str],
1493
1570
  include_hidden: bool,
1571
+ *,
1572
+ membership_fields: tuple[str, ...] = (),
1573
+ denied_member_ids: set[str],
1494
1574
  ) -> dict[str, Any] | None:
1495
1575
  """Score a single entity for ``_exact_match_search``, or None if it's excluded/no match."""
1496
1576
  entity_id = entity.get("entity_id", "")
@@ -1518,7 +1598,7 @@ def _match_exact_search_entity(
1518
1598
  score = 100 if is_exact else 80
1519
1599
  if is_hidden:
1520
1600
  score = apply_hidden_penalty(score, "_hidden")
1521
- return {
1601
+ record = {
1522
1602
  "entity_id": entity_id,
1523
1603
  "friendly_name": friendly_name,
1524
1604
  "domain": domain,
@@ -1526,6 +1606,13 @@ def _match_exact_search_entity(
1526
1606
  "score": score,
1527
1607
  "match_type": "exact_match",
1528
1608
  }
1609
+ _add_membership_fields(
1610
+ record,
1611
+ attributes,
1612
+ membership_fields,
1613
+ denied_member_ids=denied_member_ids,
1614
+ )
1615
+ return record
1529
1616
 
1530
1617
 
1531
1618
  async def _exact_match_search(
@@ -1539,6 +1626,7 @@ async def _exact_match_search(
1539
1626
  *,
1540
1627
  prefetched_states: list[dict[str, Any]] | None = None,
1541
1628
  prefetched_registry: Any = None,
1629
+ membership_fields: tuple[str, ...] = (),
1542
1630
  ) -> dict[str, Any]:
1543
1631
  """
1544
1632
  Search entities by substring on entity_id + friendly_name.
@@ -1607,6 +1695,9 @@ async def _exact_match_search(
1607
1695
  )
1608
1696
 
1609
1697
  query_lower = query.lower().strip()
1698
+ denied_member_ids = visibility_hidden | (
1699
+ hidden_ids if not include_hidden else set()
1700
+ )
1610
1701
 
1611
1702
  results = []
1612
1703
  for entity in all_entities:
@@ -1617,6 +1708,8 @@ async def _exact_match_search(
1617
1708
  visibility_hidden,
1618
1709
  hidden_ids,
1619
1710
  include_hidden,
1711
+ membership_fields=membership_fields,
1712
+ denied_member_ids=denied_member_ids,
1620
1713
  )
1621
1714
  if match is not None:
1622
1715
  results.append(match)
@@ -1808,8 +1901,15 @@ class SearchTools:
1808
1901
  "Project each entity-registry record to only the specified "
1809
1902
  'keys (e.g. ["entity_id", "state"]). None = full records. '
1810
1903
  "Base keys: entity_id, friendly_name, domain, state, score, "
1811
- "match_type. Opt-in enrichment keys (joined on request): "
1812
- "area, floor, labels, aliases. An unknown key is rejected."
1904
+ "match_type. Opt-in enrichment/membership keys (computed on request): "
1905
+ "area, floor, labels, aliases, is_group, member_entity_ids. "
1906
+ "Membership is recognized only when HA explicitly exposes a "
1907
+ "valid group_entities or legacy entity_id collection; "
1908
+ "member IDs are sorted, direct (not recursively expanded), "
1909
+ "and omitted if visibility/include_hidden excludes a member. "
1910
+ "is_group remains true when member IDs are withheld; requesting "
1911
+ "member_entity_ids also retains is_group. "
1912
+ "An unknown key is rejected."
1813
1913
  ),
1814
1914
  ),
1815
1915
  ] = None,
@@ -1871,6 +1971,13 @@ class SearchTools:
1871
1971
  Use this whenever you need to find something in HA without deciding
1872
1972
  entity-name vs config-body search up front.
1873
1973
 
1974
+ For control requests with exclusions such as "except", "excluding", or
1975
+ "but not", include `is_group` and `member_entity_ids` in `result_fields`.
1976
+ Do not control an aggregate whose members include an excluded entity;
1977
+ prefer leaf entities when the exception cannot be verified safely.
1978
+ A withheld member list still returns is_group=true; absence of
1979
+ member_entity_ids must not be interpreted as a leaf entity.
1980
+
1874
1981
  When NOT to use:
1875
1982
  - To read a known entity_id's state: use `ha_get_state` (cheaper).
1876
1983
  - To inspect one automation/script/scene config by id: use the
@@ -1901,6 +2008,10 @@ class SearchTools:
1901
2008
  Examples:
1902
2009
  - List sensors in an area: ha_search(domain_filter="sensor", area_filter="Living Room")
1903
2010
  - Find a light by name: ha_search("kitchen", domain_filter="light")
2011
+ - Find lights safely before an "all except one" control request:
2012
+ ha_search("living room", domain_filter="light",
2013
+ result_fields=["entity_id", "friendly_name", "is_group",
2014
+ "member_entity_ids"])
1904
2015
  - Which automations use an entity: ha_search("light.bed_light")
1905
2016
  - Scenes touching a light: ha_search("light.kitchen", search_types=["scene"])
1906
2017
  - Narrow the response to the entity bucket: ha_search("kitchen", fields=["entities"])
@@ -2016,7 +2127,10 @@ class SearchTools:
2016
2127
  and _component_serves_search_types(req)
2017
2128
  ):
2018
2129
  caps = await get_component_caps(self._client)
2019
- if component_supports(caps, "search"):
2130
+ if component_supports(caps, "search") and (
2131
+ not _requested_membership(parsed_result_fields)
2132
+ or component_supports(caps, "search_entity_membership")
2133
+ ):
2020
2134
  (
2021
2135
  route_component,
2022
2136
  visibility,
@@ -2356,7 +2470,7 @@ class SearchTools:
2356
2470
  'E.g. ["entity_id", "state"] returns slim entity records. '
2357
2471
  "None = full records (default). "
2358
2472
  "Base keys: entity_id, friendly_name, domain, state, score, match_type. "
2359
- "Opt-in enrichment keys (joined on request): area, floor, labels, aliases. "
2473
+ "Opt-in enrichment/membership keys (computed on request): area, floor, labels, aliases, is_group, member_entity_ids. "
2360
2474
  "An unknown key is rejected."
2361
2475
  ),
2362
2476
  ),
@@ -2402,6 +2516,11 @@ class SearchTools:
2402
2516
  area_filter,
2403
2517
  group_by_domain=True,
2404
2518
  include_hidden=include_hidden_bool,
2519
+ **(
2520
+ {"include_membership": True}
2521
+ if _requested_membership(parsed_result_fields)
2522
+ else {}
2523
+ ),
2405
2524
  )
2406
2525
  if query and query.strip():
2407
2526
  area_search = await self._search_area_with_query(
@@ -2754,7 +2873,7 @@ class SearchTools:
2754
2873
  entities_for_search = [
2755
2874
  {
2756
2875
  "entity_id": entity.get("entity_id", ""),
2757
- "attributes": {"friendly_name": entity.get("friendly_name", "")},
2876
+ "attributes": entity.get("_attributes", {}),
2758
2877
  "state": entity.get("state", "unknown"),
2759
2878
  "_aliases": aliases_map.get(entity.get("entity_id", ""), []),
2760
2879
  "_hidden_by": entity.get("_hidden_by"),
@@ -2780,6 +2899,21 @@ class SearchTools:
2780
2899
  for match in matches
2781
2900
  ]
2782
2901
 
2902
+ membership_fields = _requested_membership(parsed_result_fields)
2903
+ attributes_by_id = {
2904
+ entity.get("entity_id"): entity.get("_attributes")
2905
+ for entity in all_area_entities
2906
+ }
2907
+ for record in results:
2908
+ # smart_entity_search already applied visibility redaction and
2909
+ # preserved its sentinel on the private attributes payload.
2910
+ _add_membership_fields(
2911
+ record,
2912
+ attributes_by_id.get(record["entity_id"]),
2913
+ membership_fields,
2914
+ denied_member_ids=set(),
2915
+ )
2916
+
2783
2917
  if state_filter:
2784
2918
  results = [r for r in results if _state_matches(r, state_filter)]
2785
2919
 
@@ -2856,10 +2990,21 @@ class SearchTools:
2856
2990
  "domain": domain,
2857
2991
  "score": apply_hidden_penalty(100, entity.get("_hidden_by")),
2858
2992
  "match_type": "area_match",
2993
+ "attributes": entity.get("_attributes"),
2859
2994
  }
2860
2995
  for entity in entities
2861
2996
  )
2862
2997
 
2998
+ membership_fields = _requested_membership(parsed_result_fields)
2999
+ for record in all_results:
3000
+ # search_entities_by_area already applied visibility redaction and
3001
+ # preserved its sentinel on the private attributes payload.
3002
+ _add_membership_fields(
3003
+ record,
3004
+ record.pop("attributes", None),
3005
+ membership_fields,
3006
+ denied_member_ids=set(),
3007
+ )
2863
3008
  all_results.sort(key=lambda x: (-x["score"], x["entity_id"]))
2864
3009
  if state_filter:
2865
3010
  all_results = [r for r in all_results if _state_matches(r, state_filter)]
@@ -3033,6 +3178,10 @@ class SearchTools:
3033
3178
  and eid not in visibility_hidden
3034
3179
  and (include_hidden_bool or eid not in hidden_ids)
3035
3180
  ]
3181
+ membership_fields = _requested_membership(parsed_result_fields)
3182
+ denied_member_ids = visibility_hidden | (
3183
+ hidden_ids if not include_hidden_bool else set()
3184
+ )
3036
3185
 
3037
3186
  # Score: 100 baseline for domain membership (exact, not fuzzy);
3038
3187
  # penalised for hidden entries so they sort below visible peers.
@@ -3043,16 +3192,21 @@ class SearchTools:
3043
3192
  score = apply_hidden_penalty(
3044
3193
  100, "_hidden" if entity_id in hidden_ids else None
3045
3194
  )
3046
- scored_entities.append(
3047
- {
3048
- "entity_id": entity_id,
3049
- "friendly_name": attributes.get("friendly_name", entity_id),
3050
- "domain": domain_filter,
3051
- "state": entity.get("state", "unknown"),
3052
- "score": score,
3053
- "match_type": "domain_listing",
3054
- }
3195
+ record = {
3196
+ "entity_id": entity_id,
3197
+ "friendly_name": attributes.get("friendly_name", entity_id),
3198
+ "domain": domain_filter,
3199
+ "state": entity.get("state", "unknown"),
3200
+ "score": score,
3201
+ "match_type": "domain_listing",
3202
+ }
3203
+ _add_membership_fields(
3204
+ record,
3205
+ attributes,
3206
+ membership_fields,
3207
+ denied_member_ids=denied_member_ids,
3055
3208
  )
3209
+ scored_entities.append(record)
3056
3210
  scored_entities.sort(key=lambda x: (-x["score"], x["entity_id"]))
3057
3211
  if state_filter:
3058
3212
  scored_entities = [
@@ -3122,6 +3276,10 @@ class SearchTools:
3122
3276
  and _state_matches(e, state_filter)
3123
3277
  and (include_hidden_bool or eid not in hidden_ids)
3124
3278
  ]
3279
+ membership_fields = _requested_membership(parsed_result_fields)
3280
+ denied_member_ids = visibility_hidden | (
3281
+ hidden_ids if not include_hidden_bool else set()
3282
+ )
3125
3283
 
3126
3284
  # Score: 100 baseline for state membership (exact, not fuzzy); penalised
3127
3285
  # for hidden entries so they sort below visible peers. ``domain`` is
@@ -3133,16 +3291,21 @@ class SearchTools:
3133
3291
  score = apply_hidden_penalty(
3134
3292
  100, "_hidden" if entity_id in hidden_ids else None
3135
3293
  )
3136
- scored_entities.append(
3137
- {
3138
- "entity_id": entity_id,
3139
- "friendly_name": attributes.get("friendly_name", entity_id),
3140
- "domain": entity_id.split(".")[0],
3141
- "state": entity.get("state", "unknown"),
3142
- "score": score,
3143
- "match_type": "state_listing",
3144
- }
3294
+ record = {
3295
+ "entity_id": entity_id,
3296
+ "friendly_name": attributes.get("friendly_name", entity_id),
3297
+ "domain": entity_id.split(".")[0],
3298
+ "state": entity.get("state", "unknown"),
3299
+ "score": score,
3300
+ "match_type": "state_listing",
3301
+ }
3302
+ _add_membership_fields(
3303
+ record,
3304
+ attributes,
3305
+ membership_fields,
3306
+ denied_member_ids=denied_member_ids,
3145
3307
  )
3308
+ scored_entities.append(record)
3146
3309
  scored_entities.sort(key=lambda x: (-x["score"], x["entity_id"]))
3147
3310
  results = scored_entities[offset : offset + limit]
3148
3311
 
@@ -3216,6 +3379,7 @@ class SearchTools:
3216
3379
  state_filter=state_filter,
3217
3380
  prefetched_states=prefetched_states,
3218
3381
  prefetched_registry=prefetched_registry,
3382
+ membership_fields=_requested_membership(parsed_result_fields),
3219
3383
  )
3220
3384
  else:
3221
3385
  # Fuzzy mode: BM25 → substring fallback on exception only.
@@ -3226,6 +3390,14 @@ class SearchTools:
3226
3390
  offset=offset,
3227
3391
  domain_filter=domain_filter,
3228
3392
  include_hidden=include_hidden_bool,
3393
+ include_attributes=bool(
3394
+ _requested_membership(parsed_result_fields)
3395
+ ),
3396
+ **(
3397
+ {"include_membership": True}
3398
+ if _requested_membership(parsed_result_fields)
3399
+ else {}
3400
+ ),
3229
3401
  prefetched_states=prefetched_states,
3230
3402
  prefetched_registry=prefetched_registry,
3231
3403
  )
@@ -3251,6 +3423,7 @@ class SearchTools:
3251
3423
  state_filter=state_filter,
3252
3424
  prefetched_states=prefetched_states,
3253
3425
  prefetched_registry=prefetched_registry,
3426
+ membership_fields=_requested_membership(parsed_result_fields),
3254
3427
  )
3255
3428
  warning = "Fuzzy search unavailable, using substring match"
3256
3429
  search_type = "exact_match"
@@ -3258,6 +3431,17 @@ class SearchTools:
3258
3431
  _normalize_regular_search_result(
3259
3432
  result, search_type, domain_filter, offset, limit
3260
3433
  )
3434
+ if search_type == "fuzzy_search":
3435
+ membership_fields = _requested_membership(parsed_result_fields)
3436
+ for record in result.get("results", []):
3437
+ # smart_entity_search already applied visibility redaction and
3438
+ # preserved its sentinel on the private attributes payload.
3439
+ _add_membership_fields(
3440
+ record,
3441
+ record.pop("attributes", None),
3442
+ membership_fields,
3443
+ denied_member_ids=set(),
3444
+ )
3261
3445
 
3262
3446
  # Apply state_filter to fuzzy results BEFORE grouping so by_domain
3263
3447
  # stays consistent with results[]. For fuzzy_search, state_filter is