ha-mcp-dev 7.7.0.dev689__tar.gz → 7.7.0.dev691__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 (142) hide show
  1. {ha_mcp_dev-7.7.0.dev689/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.7.0.dev691}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/dashboard_screenshot/provision.py +3 -4
  4. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/server.py +2 -1
  5. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_dashboards.py +440 -66
  6. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  7. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/LICENSE +0 -0
  8. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/MANIFEST.in +0 -0
  9. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/README.md +0 -0
  10. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/setup.cfg +0 -0
  11. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/__init__.py +0 -0
  12. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/__main__.py +0 -0
  13. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/_pypi_marker +0 -0
  14. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/_version.py +0 -0
  15. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/auth/__init__.py +0 -0
  16. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/auth/consent_form.py +0 -0
  17. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/auth/provider.py +0 -0
  18. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/backup_manager.py +0 -0
  19. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/client/__init__.py +0 -0
  20. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/client/rest_client.py +0 -0
  21. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/client/supervisor_client.py +0 -0
  22. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/client/websocket_client.py +0 -0
  23. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/client/websocket_listener.py +0 -0
  24. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/config.py +0 -0
  25. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  26. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  27. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/errors.py +0 -0
  28. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/__init__.py +0 -0
  29. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/approval_queue.py +0 -0
  30. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/evaluator.py +0 -0
  31. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/handlers.py +0 -0
  32. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/middleware.py +0 -0
  33. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/model.py +0 -0
  34. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/persistence.py +0 -0
  35. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/value_sources.py +0 -0
  36. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/py.typed +0 -0
  37. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/read_only.py +0 -0
  38. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  39. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  40. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  41. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  42. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  43. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  44. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  45. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  46. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  47. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  48. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  49. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  50. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  51. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  52. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  53. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  54. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  55. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  56. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  57. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  58. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  59. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  60. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/settings.css +0 -0
  61. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/settings.js +0 -0
  62. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/settings_ui.py +0 -0
  63. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/smoke_test.py +0 -0
  64. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  65. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/__init__.py +0 -0
  66. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/auto_backup.py +0 -0
  67. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/backup.py +0 -0
  68. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  69. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  70. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/device_control.py +0 -0
  71. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/enhanced.py +0 -0
  72. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/helpers.py +0 -0
  73. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/reference_validator.py +0 -0
  74. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/registry.py +0 -0
  75. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  76. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  77. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  78. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  79. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  80. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  81. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  82. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  83. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  84. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_addons.py +0 -0
  85. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_areas.py +0 -0
  86. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  87. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  88. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_calendar.py +0 -0
  89. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_camera.py +0 -0
  90. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_categories.py +0 -0
  91. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_code.py +0 -0
  92. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  93. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  94. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  95. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  96. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  97. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_energy.py +0 -0
  98. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_entities.py +0 -0
  99. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  100. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_groups.py +0 -0
  101. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_hacs.py +0 -0
  102. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_history.py +0 -0
  103. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_integrations.py +0 -0
  104. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_labels.py +0 -0
  105. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  106. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_registry.py +0 -0
  107. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_resources.py +0 -0
  108. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_search.py +0 -0
  109. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_service.py +0 -0
  110. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_services.py +0 -0
  111. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_system.py +0 -0
  112. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_themes.py +0 -0
  113. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_todo.py +0 -0
  114. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_traces.py +0 -0
  115. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_updates.py +0 -0
  116. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_utility.py +0 -0
  117. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  118. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  119. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_zones.py +0 -0
  120. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/util_helpers.py +0 -0
  121. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/validation_middleware.py +0 -0
  122. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/transforms/__init__.py +0 -0
  123. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/transforms/categorized_search.py +0 -0
  124. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  125. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/__init__.py +0 -0
  126. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/config_hash.py +0 -0
  127. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/data_paths.py +0 -0
  128. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/domain_handlers.py +0 -0
  129. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  130. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  131. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/operation_manager.py +0 -0
  132. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/python_sandbox.py +0 -0
  133. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/skill_loader.py +0 -0
  134. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/usage_logger.py +0 -0
  135. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  136. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  137. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  138. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  139. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  140. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/tests/__init__.py +0 -0
  141. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/tests/test_constants.py +0 -0
  142. {ha_mcp_dev-7.7.0.dev689 → ha_mcp_dev-7.7.0.dev691}/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.7.0.dev689
3
+ Version: 7.7.0.dev691
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.7.0.dev689"
7
+ version = "7.7.0.dev691"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -33,10 +33,9 @@ from ..tools.helpers import raise_tool_error
33
33
  logger = logging.getLogger(__name__)
34
34
 
35
35
  ENGINE_PORT = 10000
36
- # The Supervisor slug is ``<repo-hash>_puppet`` for balloob's Puppet add-on;
37
- # ``_ha_mcp_screenshot`` is the legacy vendored engine, still accepted so a
38
- # mid-migration install keeps working. ``str.endswith`` takes this tuple.
39
- ENGINE_SLUG_SUFFIXES = ("_puppet", "_ha_mcp_screenshot")
36
+ # The Supervisor slug is ``<repo-hash>_puppet`` for balloob's Puppet add-on.
37
+ # ``str.endswith`` accepts a tuple, kept as one for easy future extension.
38
+ ENGINE_SLUG_SUFFIXES = ("_puppet",)
40
39
 
41
40
  _REPO_URL = "https://github.com/balloob/home-assistant-addons"
42
41
 
@@ -662,7 +662,8 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
662
662
  "Three modes: (1) list — `list_only=True` returns all "
663
663
  "storage-mode dashboards with metadata. (2) search — pass "
664
664
  "any of `entity_id`, `card_type`, `heading` to find cards "
665
- "(and their `jq_path`) inside a specific dashboard; the "
665
+ "(including nested ones, with a `python_path`) inside a "
666
+ "specific dashboard; the "
666
667
  "result includes a `config_hash` you can pair with "
667
668
  "ha_config_set_dashboard(python_transform=...) to edit "
668
669
  "matched cards surgically. (3) get — no search params "
@@ -183,18 +183,319 @@ def _badge_matches(badge: Any, entity_id: str) -> bool:
183
183
  return entity_id == badge_entity
184
184
 
185
185
 
186
+ # Keys under which a card nests other cards, by descent rule (issue #1599):
187
+ # - ``cards`` (list): vertical/horizontal-stack, grid, and any custom wrapper
188
+ # following the stack convention.
189
+ # - ``card`` (dict): conditional and wrapper cards such as
190
+ # ``custom:auto-entities``.
191
+ # - ``custom_fields`` (dict of field-configs): ``custom:button-card`` embeds
192
+ # sub-cards under ``custom_fields.<name>.card`` (a very common pattern that
193
+ # wraps an entire view in one button-card). Each field-config is descended
194
+ # as a node, so its own ``card`` / ``cards`` are picked up by the recursion.
195
+ # - ``states`` (name->card map): ``custom:state-switch`` swaps a whole card per
196
+ # source state. Each value is itself a card, descended directly as a node.
197
+ # Picture-elements ``elements`` is deliberately NOT traversed: it is not one of
198
+ # the descent keys above, so a node carrying it is disclosed at the response
199
+ # boundary instead of being walked (see ``_UNTRAVERSED_NESTED_KEYS`` and the
200
+ # find-card warnings). A blanket "descend every dict with a ``type``" walk is
201
+ # intentionally avoided: tile ``features`` and view ``conditions`` also carry
202
+ # ``type`` and would false-match as cards.
203
+ _NESTED_CARDS_KEY = "cards"
204
+ _NESTED_CARD_KEY = "card"
205
+ _NESTED_CUSTOM_FIELDS_KEY = "custom_fields"
206
+ _NESTED_STATES_KEY = "states"
207
+ # Child-bearing keys recognised but deliberately NOT traversed. A walked card
208
+ # carrying one of these (with a truthy value) cannot be fully covered, so it is
209
+ # its *presence* — not the absence of matches — that the response discloses
210
+ # (issue #1599: disclose by presence, not by absence-inference). picture-elements
211
+ # ``elements`` is the canonical case.
212
+ _UNTRAVERSED_NESTED_KEYS = ("elements",)
213
+ # Defensive bound against pathological/malformed configs. Real dashboards nest
214
+ # only a handful of levels; this guards recursion depth far above any real use.
215
+ _MAX_CARD_DEPTH = 50
216
+
217
+
218
+ def _py_key(name: str) -> str:
219
+ """Render a mapping key as a Python subscript segment (``['name']``).
220
+
221
+ ``repr`` quotes and escapes the key, so a name containing a quote (e.g.
222
+ ``o'brien``) yields a valid literal; a raw ``['{name}']`` interpolation would
223
+ splice an unterminated string into ``python_transform``.
224
+ """
225
+ return f"[{name!r}]"
226
+
227
+
228
+ def _jq_key(name: str) -> str:
229
+ """Render a mapping key as a jq path segment.
230
+
231
+ Identifier-safe keys use dot notation (``.name``); any other key (a dot, a
232
+ space, a quote) is emitted as a bracketed JSON string (``["weird.key"]``) so
233
+ jq does not read an embedded dot as further nesting.
234
+ """
235
+ if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name):
236
+ return f".{name}"
237
+ return f"[{json.dumps(name)}]"
238
+
239
+
240
+ def _log_non_str_key(container_key: str, name: object, jq_prefix: str) -> None:
241
+ """Breadcrumb a non-string mapping key under a card-bearing container.
242
+
243
+ Dashboard config arrives as JSON, so keys are normally strings; a non-string
244
+ key (from a corrupted or hand-edited config) cannot form a valid path, so the
245
+ entry is skipped rather than crashing the walk via ``_jq_key`` / ``_py_key``.
246
+ """
247
+ logger.debug(
248
+ "Card-search skipping non-string %s key at %s (%r, %s)",
249
+ container_key,
250
+ jq_prefix,
251
+ name,
252
+ type(name).__name__,
253
+ )
254
+
255
+
256
+ def _walk_card(
257
+ card: Any,
258
+ entity_id: str | None,
259
+ card_type: str | None,
260
+ heading: str | None,
261
+ *,
262
+ jq_prefix: str,
263
+ python_prefix: str,
264
+ view_index: int,
265
+ section_index: int | None,
266
+ card_index: int | None,
267
+ depth: int = 0,
268
+ truncation: list[str] | None = None,
269
+ uncovered: list[str] | None = None,
270
+ ) -> list[dict[str, Any]]:
271
+ """Return matches for ``card`` and every card nested beneath it.
272
+
273
+ Descends ``cards`` (list), ``card`` (dict), each ``custom_fields`` value, and
274
+ each ``states`` value (custom:state-switch), for nested as well as top-level
275
+ cards, up to ``_MAX_CARD_DEPTH``.
276
+
277
+ ``jq_prefix`` / ``python_prefix`` locate ``card`` itself — the former in jq
278
+ dot-notation, the latter as a Python subscript chain usable (appended after
279
+ ``config``) directly in ``ha_config_set_dashboard(python_transform=...)``.
280
+ Nested descendants extend both prefixes per level, so the path strings are
281
+ the authoritative locator for nested cards (the flat ``view_index`` /
282
+ ``section_index`` / ``card_index`` identify the top-level container only and
283
+ are carried unchanged into nested matches for back-compat).
284
+
285
+ Only a dict carrying a ``type`` key is treated as a card; this keeps non-card
286
+ dicts reached under these keys (action targets, style blocks, entity rows)
287
+ from matching. If ``truncation`` is provided, the prefix of any subtree
288
+ skipped at the depth bound is appended to it. If ``uncovered`` is provided,
289
+ the path of any walked card carrying a non-traversed child-bearing key (see
290
+ ``_UNTRAVERSED_NESTED_KEYS``) is appended to it, so the caller can disclose
291
+ the incompleteness regardless of whether the search matched anything.
292
+ """
293
+ matches: list[dict[str, Any]] = []
294
+ if not isinstance(card, dict):
295
+ # Structurally-present but malformed slot (e.g. a string where a card
296
+ # dict is expected): skip, but breadcrumb so it is not a silent drop.
297
+ if card is not None:
298
+ logger.debug(
299
+ "Card-search skipping non-dict node at %s (%s)",
300
+ jq_prefix,
301
+ type(card).__name__,
302
+ )
303
+ return matches
304
+ if depth > _MAX_CARD_DEPTH:
305
+ # Stop, but make the truncation visible rather than silently dropping
306
+ # any cards nested below this point. Only reachable on pathological or
307
+ # malformed configs (real dashboards nest a handful of levels).
308
+ logger.warning(
309
+ "Card-search depth bound (%d) exceeded at %s; not descending further",
310
+ _MAX_CARD_DEPTH,
311
+ jq_prefix,
312
+ )
313
+ if truncation is not None:
314
+ truncation.append(jq_prefix)
315
+ return matches
316
+
317
+ if "type" in card:
318
+ if _card_matches(card, entity_id, card_type, heading):
319
+ matches.append(
320
+ {
321
+ "view_index": view_index,
322
+ "section_index": section_index,
323
+ "card_index": card_index,
324
+ "jq_path": jq_prefix,
325
+ "python_path": python_prefix,
326
+ "card_type": card.get("type"),
327
+ "card_config": card,
328
+ }
329
+ )
330
+ # Disclose un-coverable nesting by presence during the walk, not by the
331
+ # absence of matches: a card that carries e.g. picture-elements
332
+ # ``elements`` hides content this search cannot reach whether or not it
333
+ # (or anything else) matched.
334
+ if uncovered is not None:
335
+ for key in _UNTRAVERSED_NESTED_KEYS:
336
+ if card.get(key):
337
+ uncovered.append(f"{jq_prefix}.{key}")
338
+ break
339
+
340
+ nested_list = card.get(_NESTED_CARDS_KEY)
341
+ if isinstance(nested_list, list):
342
+ for i, child in enumerate(nested_list):
343
+ matches.extend(
344
+ _walk_card(
345
+ child,
346
+ entity_id,
347
+ card_type,
348
+ heading,
349
+ jq_prefix=f"{jq_prefix}.{_NESTED_CARDS_KEY}[{i}]",
350
+ python_prefix=f"{python_prefix}['{_NESTED_CARDS_KEY}'][{i}]",
351
+ view_index=view_index,
352
+ section_index=section_index,
353
+ card_index=card_index,
354
+ depth=depth + 1,
355
+ truncation=truncation,
356
+ uncovered=uncovered,
357
+ )
358
+ )
359
+ elif nested_list is not None:
360
+ # ``cards`` key present but not a list — structurally malformed slot.
361
+ logger.debug(
362
+ "Card-search skipping non-list '%s' under %s (%s)",
363
+ _NESTED_CARDS_KEY,
364
+ jq_prefix,
365
+ type(nested_list).__name__,
366
+ )
367
+
368
+ nested_card = card.get(_NESTED_CARD_KEY)
369
+ if isinstance(nested_card, dict):
370
+ matches.extend(
371
+ _walk_card(
372
+ nested_card,
373
+ entity_id,
374
+ card_type,
375
+ heading,
376
+ jq_prefix=f"{jq_prefix}.{_NESTED_CARD_KEY}",
377
+ python_prefix=f"{python_prefix}['{_NESTED_CARD_KEY}']",
378
+ view_index=view_index,
379
+ section_index=section_index,
380
+ card_index=card_index,
381
+ depth=depth + 1,
382
+ truncation=truncation,
383
+ uncovered=uncovered,
384
+ )
385
+ )
386
+ elif nested_card is not None:
387
+ # ``card`` key present but not a dict — structurally malformed slot.
388
+ logger.debug(
389
+ "Card-search skipping non-dict '%s' under %s (%s)",
390
+ _NESTED_CARD_KEY,
391
+ jq_prefix,
392
+ type(nested_card).__name__,
393
+ )
394
+
395
+ # custom:button-card and similar embed sub-cards under custom_fields.<name>.
396
+ # Descend each field-config as a node; its own card/cards (and the type gate)
397
+ # are handled by the recursion, so a field that is not itself a card
398
+ # contributes nothing but is still traversed for nested cards. Keys are
399
+ # rendered quote/dot-safe so a field name like ``o'brien`` yields a usable
400
+ # python_path/jq_path (issue #1599: handle a quote/dot in the field name).
401
+ custom_fields = card.get(_NESTED_CUSTOM_FIELDS_KEY)
402
+ if isinstance(custom_fields, dict):
403
+ for name, field in custom_fields.items():
404
+ if not isinstance(name, str):
405
+ _log_non_str_key(_NESTED_CUSTOM_FIELDS_KEY, name, jq_prefix)
406
+ continue
407
+ matches.extend(
408
+ _walk_card(
409
+ field,
410
+ entity_id,
411
+ card_type,
412
+ heading,
413
+ jq_prefix=f"{jq_prefix}.{_NESTED_CUSTOM_FIELDS_KEY}{_jq_key(name)}",
414
+ python_prefix=(
415
+ f"{python_prefix}['{_NESTED_CUSTOM_FIELDS_KEY}']{_py_key(name)}"
416
+ ),
417
+ view_index=view_index,
418
+ section_index=section_index,
419
+ card_index=card_index,
420
+ depth=depth + 1,
421
+ truncation=truncation,
422
+ uncovered=uncovered,
423
+ )
424
+ )
425
+ elif custom_fields is not None:
426
+ logger.debug(
427
+ "Card-search skipping non-dict '%s' under %s (%s)",
428
+ _NESTED_CUSTOM_FIELDS_KEY,
429
+ jq_prefix,
430
+ type(custom_fields).__name__,
431
+ )
432
+
433
+ # custom:state-switch swaps a whole card per source state under states.<name>.
434
+ # Each value is itself a card (not a field-config wrapper), descended
435
+ # directly — the same quote/dot-safe key rendering applies for state names
436
+ # like ``on'hold`` (issue #1599: state-switch nests a card per state).
437
+ states = card.get(_NESTED_STATES_KEY)
438
+ if isinstance(states, dict):
439
+ for name, child in states.items():
440
+ if not isinstance(name, str):
441
+ _log_non_str_key(_NESTED_STATES_KEY, name, jq_prefix)
442
+ continue
443
+ matches.extend(
444
+ _walk_card(
445
+ child,
446
+ entity_id,
447
+ card_type,
448
+ heading,
449
+ jq_prefix=f"{jq_prefix}.{_NESTED_STATES_KEY}{_jq_key(name)}",
450
+ python_prefix=(
451
+ f"{python_prefix}['{_NESTED_STATES_KEY}']{_py_key(name)}"
452
+ ),
453
+ view_index=view_index,
454
+ section_index=section_index,
455
+ card_index=card_index,
456
+ depth=depth + 1,
457
+ truncation=truncation,
458
+ uncovered=uncovered,
459
+ )
460
+ )
461
+ elif states is not None:
462
+ logger.debug(
463
+ "Card-search skipping non-dict '%s' under %s (%s)",
464
+ _NESTED_STATES_KEY,
465
+ jq_prefix,
466
+ type(states).__name__,
467
+ )
468
+
469
+ return matches
470
+
471
+
186
472
  def _find_cards_in_config(
187
473
  config: dict[str, Any],
188
474
  entity_id: str | None = None,
189
475
  card_type: str | None = None,
190
476
  heading: str | None = None,
477
+ truncation: list[str] | None = None,
478
+ uncovered: list[str] | None = None,
191
479
  ) -> list[dict[str, Any]]:
192
480
  """
193
481
  Find cards, badges, and header cards in a dashboard config matching the search criteria.
194
482
 
195
483
  Returns a list of matches with location info and card/badge/header config.
196
484
  Searches cards (in sections and flat views), view-level badges, and
197
- sections-view header cards (views[n].header.card).
485
+ sections-view header cards (views[n].header.card). Card search recurses into
486
+ nested containers (``cards`` lists in stacks/grids, ``card`` dicts in
487
+ conditional/wrapper cards, ``custom_fields`` sub-cards in button-card, and
488
+ ``states`` sub-cards in custom:state-switch), so a nested card is found like
489
+ a top-level one (issue #1599) — up to a depth bound.
490
+
491
+ Each match carries both ``jq_path`` (jq dot-notation) and ``python_path``
492
+ (a Python subscript chain appended after ``config`` for
493
+ ``ha_config_set_dashboard(python_transform)``); these locate nested as well
494
+ as top-level cards. The flat ``*_index`` fields identify the top-level
495
+ container only. If ``truncation`` is provided, the prefixes of any subtrees
496
+ skipped at the depth bound are appended to it. If ``uncovered`` is provided,
497
+ the paths of any walked cards carrying a non-traversed child-bearing key
498
+ (e.g. picture-elements ``elements``) are appended to it.
198
499
  """
199
500
  matches: list[dict[str, Any]] = []
200
501
 
@@ -215,38 +516,47 @@ def _find_cards_in_config(
215
516
  badges = view.get("badges", [])
216
517
  for badge_idx, badge in enumerate(badges):
217
518
  if _badge_matches(badge, entity_id):
218
- badge_config = (
219
- badge if isinstance(badge, dict) else {"entity": badge}
220
- )
221
- matches.append(
222
- {
223
- "view_index": view_idx,
224
- "section_index": None,
225
- "card_index": None,
226
- "badge_index": badge_idx,
227
- "jq_path": f".views[{view_idx}].badges[{badge_idx}]",
228
- "card_type": "badge",
229
- "card_config": badge_config,
230
- }
231
- )
519
+ is_dict_badge = isinstance(badge, dict)
520
+ badge_config = badge if is_dict_badge else {"entity": badge}
521
+ badge_match: dict[str, Any] = {
522
+ "view_index": view_idx,
523
+ "section_index": None,
524
+ "card_index": None,
525
+ "badge_index": badge_idx,
526
+ "jq_path": f".views[{view_idx}].badges[{badge_idx}]",
527
+ "card_type": "badge",
528
+ "card_config": badge_config,
529
+ }
530
+ # A bare-string badge (the common form) is not subscript-
531
+ # assignable, so a python_path spliced into python_transform
532
+ # would raise TypeError. Only advertise python_path for dict
533
+ # badges; string badges must be converted to dict form first.
534
+ if is_dict_badge:
535
+ badge_match["python_path"] = (
536
+ f"['views'][{view_idx}]['badges'][{badge_idx}]"
537
+ )
538
+ matches.append(badge_match)
232
539
 
233
540
  # Search sections-view header card (views[n].header.card)
234
541
  # The header accepts a card (typically Markdown) that can contain entity refs
235
542
  header = view.get("header", {})
236
543
  if isinstance(header, dict):
237
544
  header_card = header.get("card")
238
- if isinstance(header_card, dict) and _card_matches(
239
- header_card, entity_id, card_type, heading
240
- ):
241
- matches.append(
242
- {
243
- "view_index": view_idx,
244
- "section_index": None,
245
- "card_index": None,
246
- "jq_path": f".views[{view_idx}].header.card",
247
- "card_type": header_card.get("type"),
248
- "card_config": header_card,
249
- }
545
+ if isinstance(header_card, dict):
546
+ matches.extend(
547
+ _walk_card(
548
+ header_card,
549
+ entity_id,
550
+ card_type,
551
+ heading,
552
+ jq_prefix=f".views[{view_idx}].header.card",
553
+ python_prefix=f"['views'][{view_idx}]['header']['card']",
554
+ view_index=view_idx,
555
+ section_index=None,
556
+ card_index=None,
557
+ truncation=truncation,
558
+ uncovered=uncovered,
559
+ )
250
560
  )
251
561
 
252
562
  view_type = view.get("type", "masonry")
@@ -259,36 +569,40 @@ def _find_cards_in_config(
259
569
  continue
260
570
  cards = section.get("cards", [])
261
571
  for card_idx, card in enumerate(cards):
262
- if not isinstance(card, dict):
263
- continue
264
- if _card_matches(card, entity_id, card_type, heading):
265
- matches.append(
266
- {
267
- "view_index": view_idx,
268
- "section_index": section_idx,
269
- "card_index": card_idx,
270
- "jq_path": f".views[{view_idx}].sections[{section_idx}].cards[{card_idx}]",
271
- "card_type": card.get("type"),
272
- "card_config": card,
273
- }
572
+ matches.extend(
573
+ _walk_card(
574
+ card,
575
+ entity_id,
576
+ card_type,
577
+ heading,
578
+ jq_prefix=f".views[{view_idx}].sections[{section_idx}].cards[{card_idx}]",
579
+ python_prefix=f"['views'][{view_idx}]['sections'][{section_idx}]['cards'][{card_idx}]",
580
+ view_index=view_idx,
581
+ section_index=section_idx,
582
+ card_index=card_idx,
583
+ truncation=truncation,
584
+ uncovered=uncovered,
274
585
  )
586
+ )
275
587
  else:
276
588
  # Flat view (masonry, panel, sidebar)
277
589
  cards = view.get("cards", [])
278
590
  for card_idx, card in enumerate(cards):
279
- if not isinstance(card, dict):
280
- continue
281
- if _card_matches(card, entity_id, card_type, heading):
282
- matches.append(
283
- {
284
- "view_index": view_idx,
285
- "section_index": None,
286
- "card_index": card_idx,
287
- "jq_path": f".views[{view_idx}].cards[{card_idx}]",
288
- "card_type": card.get("type"),
289
- "card_config": card,
290
- }
591
+ matches.extend(
592
+ _walk_card(
593
+ card,
594
+ entity_id,
595
+ card_type,
596
+ heading,
597
+ jq_prefix=f".views[{view_idx}].cards[{card_idx}]",
598
+ python_prefix=f"['views'][{view_idx}]['cards'][{card_idx}]",
599
+ view_index=view_idx,
600
+ section_index=None,
601
+ card_index=card_idx,
602
+ truncation=truncation,
603
+ uncovered=uncovered,
291
604
  )
605
+ )
292
606
 
293
607
  return matches
294
608
 
@@ -687,9 +1001,12 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
687
1001
  bool,
688
1002
  Field(
689
1003
  description="In search mode: include each matched card's own configuration "
690
- "object in results (increases output size). Does not affect whether the full "
691
- "dashboard config is returned search mode always returns matches only, "
692
- "not the full dashboard. Ignored outside search mode."
1004
+ "object in results (increases output size). Note that a matched container "
1005
+ "card's config contains its descendants, which are themselves separate "
1006
+ "matches with their own config, so deeply-nested stacks multiply the "
1007
+ "payload — keep the default (False) unless you need the bodies. Does not "
1008
+ "affect whether the full dashboard config is returned — search mode always "
1009
+ "returns matches only, not the full dashboard. Ignored outside search mode."
693
1010
  ),
694
1011
  ] = False,
695
1012
  include_screenshot: Annotated[
@@ -716,9 +1033,20 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
716
1033
  Lists all storage-mode dashboards with metadata (url_path, title, icon).
717
1034
 
718
1035
  MODE 2 — Search: any of entity_id / card_type / heading provided
719
- Finds cards, badges, and header cards matching the criteria.
720
- Returns matches with jq_path for use with ha_config_set_dashboard(python_transform=...).
1036
+ Finds cards, badges, and header cards matching the criteria, including
1037
+ cards nested inside stacks, grids, conditional cards, button-card
1038
+ custom_fields, and state-switch states. Each match carries a
1039
+ python_path and a jq_path that locate the card for nested as well as
1040
+ top-level cards. The python_path is a Python subscript chain to be
1041
+ appended after `config` — e.g.
1042
+ python_transform=f'config{m["python_path"]}["icon"] = "mdi:x"' (it is
1043
+ NOT valid on its own without the `config` prefix). jq_path is the same
1044
+ location in jq dot-notation.
721
1045
  Multiple criteria are AND-ed. Always fetches fresh config (force=True).
1046
+ Search covers cards/card/custom_fields/states containers up to a depth
1047
+ bound; if the dashboard carries a non-traversed child-bearing shape
1048
+ (e.g. picture-elements `elements`), the result carries a `warnings`
1049
+ entry naming where, so its hidden content is not mistaken for absent.
722
1050
  Strategy dashboards are not searchable (no explicit cards).
723
1051
 
724
1052
  MODE 3 — Get: Active when list_only=False and no search parameters are provided.
@@ -742,7 +1070,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
742
1070
  2. ha_config_set_dashboard(
743
1071
  url_path="my-dash",
744
1072
  config_hash=find["config_hash"],
745
- python_transform=f'config{find["matches"][0]["jq_path"]}["icon"] = "mdi:lamp"'
1073
+ python_transform=f'config{find["matches"][0]["python_path"]}["icon"] = "mdi:lamp"'
746
1074
  )
747
1075
 
748
1076
  Note: YAML-mode dashboards (defined in configuration.yaml) are not included in list.
@@ -859,7 +1187,16 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
859
1187
  )
860
1188
  )
861
1189
 
862
- matches = _find_cards_in_config(config, entity_id, card_type, heading)
1190
+ truncation: list[str] = []
1191
+ uncovered: list[str] = []
1192
+ matches = _find_cards_in_config(
1193
+ config,
1194
+ entity_id,
1195
+ card_type,
1196
+ heading,
1197
+ truncation=truncation,
1198
+ uncovered=uncovered,
1199
+ )
863
1200
 
864
1201
  if not include_config:
865
1202
  for match in matches:
@@ -867,6 +1204,46 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
867
1204
 
868
1205
  config_hash: str | None = compute_config_hash(config)
869
1206
 
1207
+ # Warn-don't-truncate (AGENTS.md Return Values): the walker covers
1208
+ # cards / card / custom_fields / states containers and stops at
1209
+ # the depth bound, so neither a depth-truncated search nor a
1210
+ # search over a dashboard carrying a non-traversed child-bearing
1211
+ # shape may read as an authoritative complete result. Disclosure
1212
+ # keys off the *presence* of such a shape (collected during the
1213
+ # walk), not off a 0-match — a matching un-walkable container no
1214
+ # longer suppresses the warning, and a true negative over a
1215
+ # fully-coverable dashboard no longer cries wolf.
1216
+ warnings: list[str] = []
1217
+ if truncation:
1218
+ warnings.append(
1219
+ f"Search stopped at the nesting depth bound "
1220
+ f"(_MAX_CARD_DEPTH={_MAX_CARD_DEPTH}) in "
1221
+ f"{len(truncation)} place(s); cards nested deeper were not "
1222
+ "searched, so results may be incomplete."
1223
+ )
1224
+ if uncovered:
1225
+ locations = ", ".join(sorted(set(uncovered)))
1226
+ warnings.append(
1227
+ "Cards nesting content under keys this search does not "
1228
+ "traverse (e.g. picture-elements 'elements') are present at: "
1229
+ f"{locations}. That nested content is not searched; fetch the "
1230
+ "full config (ha_config_get_dashboard without search params) "
1231
+ "to inspect those."
1232
+ )
1233
+
1234
+ if matches:
1235
+ hint = (
1236
+ "Use python_path with "
1237
+ "ha_config_set_dashboard(python_transform=...) for targeted "
1238
+ "updates"
1239
+ )
1240
+ else:
1241
+ hint = (
1242
+ "No matches in searched containers. Try other criteria, or "
1243
+ "fetch the full config (no search params) to inspect nesting "
1244
+ "shapes this search does not cover."
1245
+ )
1246
+
870
1247
  search_result: dict[str, Any] = {
871
1248
  "success": True,
872
1249
  "action": "find_card",
@@ -879,13 +1256,10 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
879
1256
  },
880
1257
  "matches": matches,
881
1258
  "match_count": len(matches),
882
- "hint": (
883
- "Use jq_path with ha_config_set_dashboard(python_transform=...) "
884
- "for targeted updates"
885
- if matches
886
- else "No matches found. Try broader search criteria."
887
- ),
1259
+ "hint": hint,
888
1260
  }
1261
+ if warnings:
1262
+ search_result["warnings"] = warnings
889
1263
  if search_resolved_from is not None:
890
1264
  search_result["resolved_from"] = search_resolved_from
891
1265
  _note_screenshot_ignored(
@@ -1371,14 +1745,14 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1371
1745
  message, suggestions = format_sandbox_error(e, python_transform)
1372
1746
  # A path-shape mismatch (IndexError/KeyError) is almost always
1373
1747
  # a hallucinated path; steer the retry toward search mode so
1374
- # the next transform is built from a verified jq_path.
1748
+ # the next transform is built from a verified python_path.
1375
1749
  if isinstance(e, PythonSandboxExecutionError) and isinstance(
1376
1750
  e.__cause__, (IndexError, KeyError)
1377
1751
  ):
1378
1752
  suggestions = [
1379
1753
  "Call ha_config_get_dashboard with card_type=..., "
1380
1754
  "entity_id=..., or heading=... to get the verified "
1381
- "jq_path for the target card, then build "
1755
+ "python_path for the target card, then build "
1382
1756
  "python_transform from that path",
1383
1757
  *suggestions,
1384
1758
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.7.0.dev689
3
+ Version: 7.7.0.dev691
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