ha-mcp-dev 7.6.0.dev614__tar.gz → 7.6.0.dev616__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 (124) hide show
  1. {ha_mcp_dev-7.6.0.dev614/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.6.0.dev616}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/smart_search.py +151 -0
  4. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_config_automations.py +1 -1
  5. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_config_dashboards.py +1 -2
  6. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_config_helpers.py +2 -2
  7. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_config_scenes.py +1 -1
  8. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_config_scripts.py +1 -1
  9. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_entities.py +7 -0
  10. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_integrations.py +109 -62
  11. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_search.py +7 -0
  12. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  13. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/LICENSE +0 -0
  14. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/MANIFEST.in +0 -0
  15. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/README.md +0 -0
  16. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/setup.cfg +0 -0
  17. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/__init__.py +0 -0
  18. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/__main__.py +0 -0
  19. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/_pypi_marker +0 -0
  20. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/_version.py +0 -0
  21. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/auth/__init__.py +0 -0
  22. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/auth/consent_form.py +0 -0
  23. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/auth/provider.py +0 -0
  24. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/backup_manager.py +0 -0
  25. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/client/__init__.py +0 -0
  26. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/client/rest_client.py +0 -0
  27. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/client/supervisor_client.py +0 -0
  28. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/client/websocket_client.py +0 -0
  29. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/client/websocket_listener.py +0 -0
  30. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/config.py +0 -0
  31. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/errors.py +0 -0
  32. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/policy/__init__.py +0 -0
  33. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/policy/approval_queue.py +0 -0
  34. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/policy/evaluator.py +0 -0
  35. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/policy/handlers.py +0 -0
  36. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/policy/middleware.py +0 -0
  37. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/policy/model.py +0 -0
  38. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/policy/persistence.py +0 -0
  39. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/policy/value_sources.py +0 -0
  40. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/py.typed +0 -0
  41. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  42. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  43. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  44. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  45. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  46. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  47. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  48. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  49. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  50. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  51. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  52. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  53. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  54. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  55. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  56. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  57. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  58. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  59. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  60. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  61. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  62. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  63. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/server.py +0 -0
  64. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/settings_ui.py +0 -0
  65. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/smoke_test.py +0 -0
  66. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  67. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/__init__.py +0 -0
  68. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/auto_backup.py +0 -0
  69. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/backup.py +0 -0
  70. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  71. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/device_control.py +0 -0
  72. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/enhanced.py +0 -0
  73. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/helpers.py +0 -0
  74. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/reference_validator.py +0 -0
  75. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/registry.py +0 -0
  76. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_addons.py +0 -0
  77. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_areas.py +0 -0
  78. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  79. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  80. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_calendar.py +0 -0
  81. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_camera.py +0 -0
  82. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_categories.py +0 -0
  83. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_code.py +0 -0
  84. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  85. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_energy.py +0 -0
  86. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  87. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_groups.py +0 -0
  88. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_hacs.py +0 -0
  89. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_history.py +0 -0
  90. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_labels.py +0 -0
  91. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  92. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_registry.py +0 -0
  93. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_resources.py +0 -0
  94. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_service.py +0 -0
  95. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_services.py +0 -0
  96. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_system.py +0 -0
  97. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_todo.py +0 -0
  98. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_traces.py +0 -0
  99. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_updates.py +0 -0
  100. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_utility.py +0 -0
  101. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  102. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  103. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/tools_zones.py +0 -0
  104. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/tools/util_helpers.py +0 -0
  105. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/transforms/__init__.py +0 -0
  106. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/transforms/categorized_search.py +0 -0
  107. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  108. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/utils/__init__.py +0 -0
  109. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/utils/config_hash.py +0 -0
  110. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/utils/data_paths.py +0 -0
  111. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/utils/domain_handlers.py +0 -0
  112. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  113. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  114. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/utils/operation_manager.py +0 -0
  115. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/utils/python_sandbox.py +0 -0
  116. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp/utils/usage_logger.py +0 -0
  117. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  118. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  119. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  120. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  121. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  122. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/tests/__init__.py +0 -0
  123. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/tests/test_constants.py +0 -0
  124. {ha_mcp_dev-7.6.0.dev614 → ha_mcp_dev-7.6.0.dev616}/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.6.0.dev614
3
+ Version: 7.6.0.dev616
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.6.0.dev614"
7
+ version = "7.6.0.dev616"
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"
@@ -6,6 +6,7 @@ import asyncio
6
6
  import logging
7
7
  import os
8
8
  import random
9
+ import re
9
10
  import time
10
11
  from typing import Any
11
12
 
@@ -23,6 +24,8 @@ from ..utils.fuzzy_search import (
23
24
  )
24
25
  from .helpers import exception_to_structured_error, safe_info, safe_progress
25
26
  from .tools_config_dashboards import fetch_dashboards_list
27
+ from .tools_config_entry_flow import FLOW_HELPER_TYPES
28
+ from .tools_integrations import fetch_entry_options
26
29
 
27
30
  logger = logging.getLogger(__name__)
28
31
 
@@ -1755,6 +1758,22 @@ class SmartSearchTools:
1755
1758
  elif isinstance(result, Exception):
1756
1759
  logger.debug(f"Helper list fetch failed: {result}")
1757
1760
 
1761
+ # Flow-based helpers (template, group, utility_meter,
1762
+ # derivative, ...) are config entries, not storage records,
1763
+ # and have no `<type>/list` WebSocket endpoint. Pull them via
1764
+ # the standard /config/config_entries/entry REST surface and
1765
+ # probe each entry's options flow so the helper's current
1766
+ # config (template body, group members, source entity, ...)
1767
+ # is searchable alongside the input_* helpers above.
1768
+ results["helpers"].extend(
1769
+ await self._search_flow_helpers(
1770
+ query_lower,
1771
+ exact_match,
1772
+ semaphore,
1773
+ include_config=include_config,
1774
+ )
1775
+ )
1776
+
1758
1777
  phase_done += 1
1759
1778
  await safe_progress(
1760
1779
  ctx,
@@ -1969,6 +1988,138 @@ class SmartSearchTools:
1969
1988
  },
1970
1989
  )
1971
1990
 
1991
+ async def _search_flow_helpers(
1992
+ self,
1993
+ query_lower: str,
1994
+ exact_match: bool,
1995
+ semaphore: asyncio.Semaphore,
1996
+ *,
1997
+ include_config: bool,
1998
+ ) -> list[dict[str, Any]]:
1999
+ """Search UI-created flow-based helpers (template, group, …).
2000
+
2001
+ Flow-helpers live as config entries (not storage records) and have
2002
+ no ``<type>/list`` endpoint. Lists them via the standard config
2003
+ entries REST endpoint, then probes each entry's options flow so the
2004
+ helper's current config — template body, group members, source
2005
+ entity, etc. — is searchable.
2006
+
2007
+ Cost: 1 REST call + one options-flow probe per flow-helper config
2008
+ entry, parallelised under ``semaphore``. The probe is skipped when
2009
+ the title alone already scores the maximum (a deeper config match can
2010
+ only raise the total, never lower it); any title that leaves headroom
2011
+ is still probed for accurate scoring and ``match_in_config``.
2012
+ """
2013
+ try:
2014
+ response = await self.client._request("GET", "/config/config_entries/entry")
2015
+ except Exception as exc:
2016
+ logger.debug(f"flow-helper search: list_entries failed: {exc}")
2017
+ return []
2018
+
2019
+ if not isinstance(response, list):
2020
+ return []
2021
+
2022
+ flow_entries = [
2023
+ e
2024
+ for e in response
2025
+ if isinstance(e, dict)
2026
+ and e.get("domain") in FLOW_HELPER_TYPES
2027
+ and e.get("supports_options")
2028
+ ]
2029
+ if not flow_entries:
2030
+ return []
2031
+
2032
+ async def score_entry(entry: dict[str, Any]) -> dict[str, Any] | None:
2033
+ entry_id = entry.get("entry_id")
2034
+ if not isinstance(entry_id, str):
2035
+ return None
2036
+ domain = entry.get("domain", "")
2037
+ title = entry.get("title") or entry_id
2038
+
2039
+ # Score the name against a title-derived slug, never the opaque
2040
+ # config-entry ULID: a random ULID substring would otherwise
2041
+ # produce false-positive name matches (e.g. a 3-char query that
2042
+ # happens to occur inside the base32 id). The slug mirrors the
2043
+ # storage-helper path, which scores a name-derived id rather than
2044
+ # an opaque key. entry_id is still returned to the caller; it just
2045
+ # isn't a search target.
2046
+ title_slug = re.sub(r"[^a-z0-9]+", "_", title.lower()).strip("_")
2047
+ title_pseudo_eid = f"{domain}.{title_slug}" if title_slug else domain
2048
+ name_score = self.fuzzy_searcher._calculate_entity_score(
2049
+ title_pseudo_eid, title, domain, query_lower
2050
+ )
2051
+
2052
+ options: dict[str, Any] = {}
2053
+ # Only a perfect title match (score 100) makes the deeper options
2054
+ # probe redundant — the probe can only raise the total, never lower
2055
+ # it, so anything below 100 is worth probing (in both exact and
2056
+ # fuzzy modes) for accurate scoring and ``match_in_config``.
2057
+ need_probe = include_config or (
2058
+ self._score_deep_match(
2059
+ title_pseudo_eid,
2060
+ title,
2061
+ name_score,
2062
+ 0,
2063
+ query_lower,
2064
+ exact_match,
2065
+ )[0]
2066
+ < 100
2067
+ )
2068
+ if need_probe:
2069
+ async with semaphore:
2070
+ options = await fetch_entry_options(
2071
+ self.client, entry_id, quiet=True
2072
+ )
2073
+
2074
+ # Search the title, domain, and probed options — but not the opaque
2075
+ # entry_id (it would match random ULID substrings; it is returned
2076
+ # in the result for the caller regardless).
2077
+ haystack: dict[str, Any] = {
2078
+ "title": title,
2079
+ "domain": domain,
2080
+ "options": options,
2081
+ }
2082
+ config_score = self._search_in_dict(haystack, query_lower, exact_match)
2083
+ total_score, threshold, match_in_name = self._score_deep_match(
2084
+ title_pseudo_eid,
2085
+ title,
2086
+ name_score,
2087
+ config_score,
2088
+ query_lower,
2089
+ exact_match,
2090
+ )
2091
+ if total_score < threshold:
2092
+ return None
2093
+
2094
+ result: dict[str, Any] = {
2095
+ "entry_id": entry_id,
2096
+ "helper_type": domain,
2097
+ "name": title,
2098
+ "score": total_score,
2099
+ "match_in_name": match_in_name,
2100
+ "match_in_config": config_score >= threshold,
2101
+ }
2102
+ if include_config:
2103
+ result["config"] = options
2104
+ return result
2105
+
2106
+ scored = await asyncio.gather(
2107
+ *(score_entry(e) for e in flow_entries),
2108
+ return_exceptions=True,
2109
+ )
2110
+ out: list[dict[str, Any]] = []
2111
+ for item in scored:
2112
+ if isinstance(item, dict):
2113
+ out.append(item)
2114
+ elif isinstance(item, Exception):
2115
+ # The probe swallows its own transient/API errors, so anything
2116
+ # reaching here is a scoring/extraction bug (e.g. a shape
2117
+ # assumption breaking on a future HA version). Log at warning so
2118
+ # it's discoverable — one bad entry must not sink the whole
2119
+ # multi-source deep_search, so we drop it and keep going.
2120
+ logger.warning(f"flow-helper scoring failed: {item!r}")
2121
+ return out
2122
+
1972
2123
  def _score_deep_match(
1973
2124
  self,
1974
2125
  entity_id: str,
@@ -401,7 +401,7 @@ class AutomationConfigTools:
401
401
  async def ha_config_set_automation(
402
402
  self,
403
403
  config: Annotated[
404
- str | dict[str, Any] | None,
404
+ dict[str, Any] | None,
405
405
  Field(
406
406
  description="Complete automation configuration with required fields: 'alias', 'trigger', 'action'. "
407
407
  "Optional: 'description', 'condition', 'mode', 'max', 'initial_state', 'variables'. "
@@ -847,10 +847,9 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
847
847
  ),
848
848
  ],
849
849
  config: Annotated[
850
- str | dict[str, Any] | None,
850
+ dict[str, Any] | None,
851
851
  Field(
852
852
  description="Dashboard configuration with views and cards. "
853
- "Can be dict or JSON string. "
854
853
  "Omit or set to None to create dashboard without initial config. "
855
854
  "Mutually exclusive with python_transform."
856
855
  ),
@@ -2297,7 +2297,7 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2297
2297
  ),
2298
2298
  ] = None,
2299
2299
  config: Annotated[
2300
- str | dict | None,
2300
+ dict[str, Any] | None,
2301
2301
  Field(
2302
2302
  description=(
2303
2303
  "Config dict for flow-based helper types and "
@@ -2305,7 +2305,7 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2305
2305
  "(template, group, utility_meter, derivative, min_max, threshold, "
2306
2306
  "integration, statistics, trend, random, filter, tod, "
2307
2307
  "generic_thermostat, switch_as_x, generic_hygrostat). "
2308
- "Accepts JSON string or dict. Ignored for simple helper types. "
2308
+ "Ignored for simple helper types. "
2309
2309
  "Field set is delivered as data_schema on the first validation error."
2310
2310
  ),
2311
2311
  default=None,
@@ -424,7 +424,7 @@ class ConfigSceneTools:
424
424
  str, Field(description="Scene identifier (e.g., 'movie_night')")
425
425
  ],
426
426
  config: Annotated[
427
- str | dict[str, Any] | None,
427
+ dict[str, Any] | None,
428
428
  Field(
429
429
  description=(
430
430
  "Scene configuration dictionary. Must include 'entities' "
@@ -383,7 +383,7 @@ class ConfigScriptTools:
383
383
  ),
384
384
  ],
385
385
  config: Annotated[
386
- str | dict[str, Any] | None,
386
+ dict[str, Any] | None,
387
387
  Field(
388
388
  description="Script configuration dictionary. Must include EITHER 'sequence' (for regular scripts) OR 'use_blueprint' (for blueprint-based scripts). "
389
389
  "Optional fields: 'alias', 'description', 'icon', 'mode', 'max', 'fields'. "
@@ -1189,6 +1189,12 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1189
1189
  via the ha_set_entity(expose_to=...) parameter, not the options dict.
1190
1190
  - platform: Integration platform (e.g., "hue", "zwave_js")
1191
1191
  - device_id: Associated device ID (null if standalone)
1192
+ - config_entry_id: Parent config entry's ID (null for YAML-only
1193
+ entities). When non-null — e.g. for UI-created template/group/
1194
+ utility_meter/derivative/... helpers — pass it to
1195
+ ``ha_get_integration(entry_id=..., include_options=True)`` to read the
1196
+ helper's current config (template body, group members, etc.) without
1197
+ scanning a domain list.
1192
1198
  - unique_id: Integration's unique identifier
1193
1199
  """
1194
1200
  try:
@@ -1254,6 +1260,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1254
1260
  "options": entry.get("options", {}),
1255
1261
  "platform": entry.get("platform"),
1256
1262
  "device_id": entry.get("device_id"),
1263
+ "config_entry_id": entry.get("config_entry_id"),
1257
1264
  "unique_id": entry.get("unique_id"),
1258
1265
  }
1259
1266
 
@@ -101,6 +101,98 @@ assert set(get_args(HelperTypeLiteral)) == (
101
101
  )
102
102
 
103
103
 
104
+ def options_from_form_flow(flow: dict[str, Any]) -> dict[str, Any]:
105
+ """Extract ``{field_name: current_value}`` from a form-type OptionsFlow.
106
+
107
+ Reads each ``data_schema`` entry's ``default`` key, falling back to
108
+ ``value`` (constant-type fields ship ``value`` instead of ``default``)
109
+ and then ``description.suggested_value`` (UI-created template, group,
110
+ utility_meter, and other flow-based helpers stash the current value
111
+ there — voluptuous renders ``suggested_value=...`` into the
112
+ ``description`` sub-object, not as a top-level field key). Fields with
113
+ a missing or ``None`` value are skipped.
114
+ """
115
+ out: dict[str, Any] = {}
116
+ # Defensive: HA should always return a list of dict fields, but guard
117
+ # against malformed shapes so a bad response degrades to {} instead of
118
+ # raising AttributeError (e.g. a string data_schema would iterate chars).
119
+ data_schema = flow.get("data_schema")
120
+ if not isinstance(data_schema, list):
121
+ return out
122
+ for field in data_schema:
123
+ if not isinstance(field, dict):
124
+ continue
125
+ name = field.get("name")
126
+ if name is None:
127
+ continue
128
+ value = field.get("default", field.get("value"))
129
+ if value is None:
130
+ description = field.get("description")
131
+ if isinstance(description, dict):
132
+ value = description.get("suggested_value")
133
+ if value is not None:
134
+ out[name] = value
135
+ return out
136
+
137
+
138
+ async def fetch_entry_options(
139
+ client: Any, entry_id: str, *, quiet: bool = False
140
+ ) -> dict[str, Any]:
141
+ """Read the current ``options`` for a config entry via its OptionsFlow.
142
+
143
+ Home Assistant does not expose ``ConfigEntry.options`` through any
144
+ read-only REST or WebSocket endpoint — ``/api/config/config_entries/entry``
145
+ deliberately omits the field. The closest approximation that the HA UI
146
+ itself uses is the ``default`` values populated into the OptionsFlow's
147
+ first-step ``data_schema``: integrations build that schema from the
148
+ existing options dict, so the defaults match the persisted state.
149
+
150
+ Starts the flow, harvests ``{name: default}`` from the first step, and
151
+ aborts the flow in ``finally`` so it doesn't sit half-open.
152
+
153
+ Returns ``{}`` on any failure (unsupported entry, non-form first step
154
+ such as a menu, init/abort errors) so callers can treat the return as
155
+ the canonical "options" field without further checks.
156
+
157
+ Probe failures log at ``warning`` (so breakage of a deliberate
158
+ single-entry probe is discoverable) unless ``quiet=True``, which demotes
159
+ them to ``debug`` for bulk fan-out callers (e.g. ``smart_search`` probes
160
+ one entry per flow-helper on every ``ha_deep_search``; a per-entry
161
+ warning there would spam the log on routine searches).
162
+
163
+ Exposed at module level (not as a method) so non-class callers such as
164
+ ``smart_search._search_flow_helpers`` can probe flow-helper config
165
+ without instantiating ``IntegrationTools``.
166
+ """
167
+ log_probe_failure = logger.debug if quiet else logger.warning
168
+ flow_id: str | None = None
169
+ try:
170
+ flow = await client.start_options_flow(entry_id)
171
+ flow_id = flow.get("flow_id")
172
+ flow_type = flow.get("type")
173
+ if flow_type != "form":
174
+ log_probe_failure(
175
+ f"OptionsFlow for {entry_id} returned type={flow_type!r}, "
176
+ f"not a form — cannot extract option defaults"
177
+ )
178
+ return {}
179
+ return options_from_form_flow(flow)
180
+ except Exception as exc:
181
+ log_probe_failure(
182
+ f"Failed to fetch options for {entry_id}: {type(exc).__name__}: {exc}"
183
+ )
184
+ return {}
185
+ finally:
186
+ if flow_id:
187
+ try:
188
+ await client.abort_options_flow(flow_id)
189
+ except Exception as abort_err:
190
+ log_probe_failure(
191
+ f"Failed to abort options flow {flow_id}: "
192
+ f"{type(abort_err).__name__}: {abort_err}"
193
+ )
194
+
195
+
104
196
  async def _get_entry_id_for_flow_helper(
105
197
  client: Any,
106
198
  helper_type: str,
@@ -213,7 +305,13 @@ class IntegrationTools:
213
305
  Field(
214
306
  description="Include the options object for each entry. "
215
307
  "Automatically enabled when domain filter is set. "
216
- "Useful for auditing template definitions and helper configurations.",
308
+ "For UI-created flow-based helpers (template, group, "
309
+ "utility_meter, derivative, ...), the current config — "
310
+ "template body, group members, source entity, etc. — is "
311
+ "surfaced here by probing the options flow. Prefer this over "
312
+ "include_schema when you only need to read the current values; "
313
+ "use include_schema when you also need the field types or "
314
+ "selector metadata.",
217
315
  default=False,
218
316
  ),
219
317
  ] = False,
@@ -614,7 +712,7 @@ class IntegrationTools:
614
712
 
615
713
  # Surface `options` on every per-entry response (HA's REST endpoint
616
714
  # omits the field). For entries with supports_options=True we probe
617
- # via OptionsFlow — see `_fetch_entry_options`. When include_schema
715
+ # via OptionsFlow — see `fetch_entry_options`. When include_schema
618
716
  # is also requested, `_fetch_options_schema` below populates options
619
717
  # from the same flow init so we don't pay for two round-trips.
620
718
  if isinstance(result, dict):
@@ -758,68 +856,17 @@ class IntegrationTools:
758
856
 
759
857
  @staticmethod
760
858
  def _options_from_form_flow(flow: dict[str, Any]) -> dict[str, Any]:
761
- """Extract ``{field_name: current_value}`` from a form-type OptionsFlow.
762
-
763
- Reads each ``data_schema`` entry's ``default`` key, falling back to
764
- ``value`` only when the ``default`` key is absent (constant-type
765
- fields ship ``value`` instead of ``default``). Fields with a missing
766
- or ``None`` value are skipped.
767
- """
768
- out: dict[str, Any] = {}
769
- for field in flow.get("data_schema") or []:
770
- name = field.get("name")
771
- if name is None:
772
- continue
773
- value = field.get("default", field.get("value"))
774
- if value is not None:
775
- out[name] = value
776
- return out
859
+ """Class-method alias for :func:`options_from_form_flow`."""
860
+ return options_from_form_flow(flow)
777
861
 
778
862
  async def _fetch_entry_options(self, entry_id: str) -> dict[str, Any]:
779
- """Read the current ``options`` for a config entry via its OptionsFlow.
780
-
781
- Home Assistant does not expose ``ConfigEntry.options`` through any
782
- read-only REST or WebSocket endpoint ``/api/config/config_entries/entry``
783
- deliberately omits the field. The closest approximation that the HA UI
784
- itself uses is the ``default`` values populated into the OptionsFlow's
785
- first-step ``data_schema``: integrations build that schema from the
786
- existing options dict, so the defaults match the persisted state.
787
-
788
- Starts the flow, harvests ``{name: default}`` from the first step,
789
- and aborts the flow in ``finally`` so it doesn't sit half-open.
790
-
791
- Returns ``{}`` on any failure (unsupported entry, non-form first step
792
- such as a menu, init/abort errors) so callers can treat the return as
793
- the canonical "options" field without further checks. Unexpected
794
- exception types are logged at ``warning`` so probe breakage is
795
- discoverable.
863
+ """Instance wrapper around :func:`fetch_entry_options`.
864
+
865
+ Kept so existing call sites (and the ``include_schema`` path) read
866
+ naturally as ``self._fetch_entry_options(...)``; the probe logic and
867
+ full rationale live on the module-level function.
796
868
  """
797
- flow_id: str | None = None
798
- try:
799
- flow = await self._client.start_options_flow(entry_id)
800
- flow_id = flow.get("flow_id")
801
- flow_type = flow.get("type")
802
- if flow_type != "form":
803
- logger.debug(
804
- f"OptionsFlow for {entry_id} returned type={flow_type!r}, "
805
- f"not a form — cannot extract option defaults"
806
- )
807
- return {}
808
- return self._options_from_form_flow(flow)
809
- except Exception as exc:
810
- logger.warning(
811
- f"Failed to fetch options for {entry_id}: {type(exc).__name__}: {exc}"
812
- )
813
- return {}
814
- finally:
815
- if flow_id:
816
- try:
817
- await self._client.abort_options_flow(flow_id)
818
- except Exception as abort_err:
819
- logger.warning(
820
- f"Failed to abort options flow {flow_id}: "
821
- f"{type(abort_err).__name__}: {abort_err}"
822
- )
869
+ return await fetch_entry_options(self._client, entry_id)
823
870
 
824
871
  async def _fetch_options_schema(self, entry_id: str, resp: dict[str, Any]) -> None:
825
872
  """Start an options flow to read the schema, then abort it.
@@ -899,7 +946,7 @@ class IntegrationTools:
899
946
 
900
947
  # `_format_entry` is sync and cannot probe the OptionsFlow; options
901
948
  # are filled in by a second async pass below for entries that
902
- # advertise supports_options=True. See `_fetch_entry_options`.
949
+ # advertise supports_options=True. See `fetch_entry_options`.
903
950
  formatted_entries = [
904
951
  self._format_entry(entry, include_opts, logger_levels) for entry in entries
905
952
  ]
@@ -1473,6 +1473,13 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1473
1473
  **NOTE:** Dashboards and badges are NOT searched by default. Add 'dashboard' to
1474
1474
  search_types to include them.
1475
1475
 
1476
+ The 'helper' search covers both input_* helpers (input_boolean, input_number, ...)
1477
+ and UI-created flow-based helpers (template, group, utility_meter, derivative, ...).
1478
+ For flow-helpers, results carry the parent config entry id under ``entry_id``.
1479
+ When ``include_config=False`` (the default), pair with
1480
+ ``ha_get_integration(entry_id=..., include_options=True)`` to retrieve the full
1481
+ config; set ``include_config=True`` to get it inline in one call.
1482
+
1476
1483
  Args:
1477
1484
  query: Search query (exact substring by default, or fuzzy with exact_match=False)
1478
1485
  search_types: Types to search (default: ["automation", "script", "scene", "helper"])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.6.0.dev614
3
+ Version: 7.6.0.dev616
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