ha-mcp-dev 7.5.0.dev594__tar.gz → 7.5.0.dev596__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.5.0.dev594/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev596}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_integrations.py +12 -22
  4. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_search.py +5 -81
  5. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_service.py +143 -14
  6. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_system.py +78 -60
  7. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/util_helpers.py +157 -0
  8. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  9. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/LICENSE +0 -0
  10. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/MANIFEST.in +0 -0
  11. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/README.md +0 -0
  12. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/setup.cfg +0 -0
  13. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/__main__.py +0 -0
  15. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/_pypi_marker +0 -0
  16. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/_version.py +0 -0
  17. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/auth/__init__.py +0 -0
  18. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/auth/consent_form.py +0 -0
  19. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/auth/provider.py +0 -0
  20. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/backup_manager.py +0 -0
  21. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/client/__init__.py +0 -0
  22. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/client/rest_client.py +0 -0
  23. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/client/supervisor_client.py +0 -0
  24. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/client/websocket_client.py +0 -0
  25. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/client/websocket_listener.py +0 -0
  26. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/config.py +0 -0
  27. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/errors.py +0 -0
  28. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/__init__.py +0 -0
  29. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/approval_queue.py +0 -0
  30. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/evaluator.py +0 -0
  31. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/handlers.py +0 -0
  32. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/middleware.py +0 -0
  33. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/model.py +0 -0
  34. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/persistence.py +0 -0
  35. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/value_sources.py +0 -0
  36. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/py.typed +0 -0
  37. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  38. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  39. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  40. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  41. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  42. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  43. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  44. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  45. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  46. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  47. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  48. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  49. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  50. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  51. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  52. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  53. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  54. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  55. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  56. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  57. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  58. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  59. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/server.py +0 -0
  60. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/settings_ui.py +0 -0
  61. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/smoke_test.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/__init__.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/auto_backup.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/backup.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/device_control.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/enhanced.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/helpers.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/reference_validator.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/registry.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/smart_search.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_addons.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_areas.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_calendar.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_camera.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_categories.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_code.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_energy.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_entities.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_groups.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_hacs.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_history.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_labels.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_registry.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_resources.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_services.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_todo.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_traces.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_updates.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_utility.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_zones.py +0 -0
  105. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/transforms/__init__.py +0 -0
  106. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/transforms/categorized_search.py +0 -0
  107. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  108. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/__init__.py +0 -0
  109. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/config_hash.py +0 -0
  110. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/data_paths.py +0 -0
  111. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/domain_handlers.py +0 -0
  112. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  113. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  114. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/operation_manager.py +0 -0
  115. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/python_sandbox.py +0 -0
  116. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/usage_logger.py +0 -0
  117. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  118. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  119. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  120. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  121. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  122. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/tests/__init__.py +0 -0
  123. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/tests/test_constants.py +0 -0
  124. {ha_mcp_dev-7.5.0.dev594 → ha_mcp_dev-7.5.0.dev596}/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.5.0.dev594
3
+ Version: 7.5.0.dev596
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.5.0.dev594"
7
+ version = "7.5.0.dev596"
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"
@@ -7,7 +7,7 @@ integrations (config entries) via the REST and WebSocket APIs.
7
7
 
8
8
  import asyncio
9
9
  import logging
10
- from typing import Annotated, Any, Literal, cast, get_args
10
+ from typing import Annotated, Any, Literal, get_args
11
11
 
12
12
  from fastmcp.exceptions import ToolError
13
13
  from fastmcp.tools import tool
@@ -459,27 +459,18 @@ class IntegrationTools:
459
459
  include_diagnostics_bool = coerce_bool_param(
460
460
  include_diagnostics, "include_diagnostics", default=False
461
461
  )
462
- include_subentries_bool = cast(
463
- bool,
464
- coerce_bool_param(
465
- include_subentries, "include_subentries", default=False
466
- ),
462
+ include_subentries_bool = coerce_bool_param(
463
+ include_subentries, "include_subentries", default=False
467
464
  )
468
- include_subentry_schema_bool = cast(
469
- bool,
470
- coerce_bool_param(
471
- include_subentry_schema,
472
- "include_subentry_schema",
473
- default=False,
474
- ),
465
+ include_subentry_schema_bool = coerce_bool_param(
466
+ include_subentry_schema,
467
+ "include_subentry_schema",
468
+ default=False,
475
469
  )
476
- show_advanced_options_bool = cast(
477
- bool,
478
- coerce_bool_param(
479
- show_advanced_options,
480
- "show_advanced_options",
481
- default=False,
482
- ),
470
+ show_advanced_options_bool = coerce_bool_param(
471
+ show_advanced_options,
472
+ "show_advanced_options",
473
+ default=False,
483
474
  )
484
475
  exact_match_bool = coerce_bool_param(
485
476
  exact_match, "exact_match", default=True
@@ -1316,8 +1307,7 @@ class IntegrationTools:
1316
1307
  context={"helper_type": helper_type},
1317
1308
  )
1318
1309
 
1319
- # default=True guarantees a non-None return; cast for mypy.
1320
- wait_bool = cast(bool, coerce_bool_param(wait, "wait", default=True))
1310
+ wait_bool = coerce_bool_param(wait, "wait", default=True)
1321
1311
  warnings: list[str] = []
1322
1312
 
1323
1313
  # === Routing dispatch ===
@@ -30,6 +30,9 @@ from .util_helpers import (
30
30
  public_fields,
31
31
  result_fields_warning,
32
32
  )
33
+ from .util_helpers import (
34
+ project_entity_record as _project_entity,
35
+ )
33
36
 
34
37
  logger = logging.getLogger(__name__)
35
38
 
@@ -166,85 +169,6 @@ async def _exact_match_search(
166
169
  }
167
170
 
168
171
 
169
- def _project_entity(
170
- record: dict[str, Any],
171
- fields: list[str] | None,
172
- attribute_keys: list[str] | None,
173
- ) -> tuple[dict[str, Any], str | None]:
174
- """Apply optional field projection to a HA entity record.
175
-
176
- ``fields`` filters which top-level keys to keep (e.g. ["state", "attributes"]).
177
- ``attribute_keys`` further filters the ``attributes`` sub-dict.
178
- Both default None = full payload (no-op).
179
-
180
- Returns ``(projected_record, warning_string | None)``. *warning_string* is
181
- non-None when ``attribute_keys`` was specified, the original ``attributes``
182
- dict was non-empty, and the filter produced an empty result — i.e. the caller
183
- supplied only unknown attribute keys (typo guard). Callers should append the
184
- warning to the response ``warnings`` list so the user receives a diagnostic
185
- rather than a silently empty ``attributes: {}``.
186
-
187
- Both parameters are already parsed into ``list[str] | None`` — string/CSV inputs
188
- must be normalised at the call site via ``parse_string_list_param`` (see
189
- ``ha_get_state`` which parses once before the bulk loop to avoid re-parsing per
190
- entity record).
191
-
192
- Unlike ``project_fields``, this helper does not auto-retain ``success`` — entity
193
- records have no ``success`` field, so the asymmetry is intentional.
194
-
195
- Non-dict ``attributes`` handling: when ``attribute_keys`` is set but the
196
- record's ``attributes`` value is not a dict (``None``, a string, a list —
197
- rare from HA's state API but possible from malformed records, partial
198
- error payloads, or mocked fixtures), the key-set filter cannot be
199
- applied and the ``attributes`` value is returned unchanged. A
200
- ``warning``-level log line records the short-circuit so it is visible
201
- at default log levels. The bulk path shares this helper, so
202
- both single- and bulk-entity calls behave identically here. This is
203
- deliberately silent (no caller-facing warning) because malformed
204
- ``attributes`` is rare and the call still produces a usable record.
205
- """
206
- if not isinstance(record, dict):
207
- return (
208
- record,
209
- None,
210
- ) # non-dict (e.g. error path returning None) — skip projection
211
- if fields is not None:
212
- keep = set(fields)
213
- record = {k: v for k, v in record.items() if k in keep}
214
- attr_warn: str | None = None
215
- if attribute_keys is not None:
216
- attrs = record.get("attributes")
217
- if isinstance(attrs, dict):
218
- attr_keep = set(attribute_keys)
219
- filtered_attrs = {k: v for k, v in attrs.items() if k in attr_keep}
220
- # Typo guard: if the original had keys AND the caller specified at least
221
- # one key AND the filter yielded nothing, the caller likely mistyped an
222
- # attribute name. Return a diagnostic so the agent gets
223
- # "attributes came out empty — available: [...]" instead of silently
224
- # receiving ``attributes: {}``.
225
- # Exclude the attribute_keys=[] case — an empty list is an explicit
226
- # "keep nothing" request, not a typo.
227
- if attrs and attribute_keys and not filtered_attrs:
228
- available = sorted(attrs.keys())
229
- attr_warn = (
230
- f"attribute_keys {sorted(attribute_keys)!r} matched no attribute "
231
- f"keys — attributes came out empty. "
232
- f"Available keys: {available!r}"
233
- )
234
- record = {**record, "attributes": filtered_attrs}
235
- elif "attributes" in record:
236
- # ``attributes`` is present but not a dict — filter cannot apply.
237
- # Log at warning so the no-op is visible at default log levels
238
- # (this branch is exercised rarely; see docstring for rationale).
239
- logger.warning(
240
- "_project_entity: attribute_keys filter skipped — "
241
- "'attributes' is %s (expected dict) for record keys=%r",
242
- type(attrs).__name__,
243
- list(record.keys()),
244
- )
245
- return record, attr_warn
246
-
247
-
248
172
  def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
249
173
  """Register search and discovery tools with the MCP server."""
250
174
  smart_tools = kwargs.get("smart_tools")
@@ -1564,8 +1488,8 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1564
1488
  """
1565
1489
  # Parse search_types to handle JSON string input from MCP clients
1566
1490
  parsed_search_types = parse_string_list_param(search_types, "search_types")
1567
- include_config_bool = (
1568
- coerce_bool_param(include_config, "include_config", default=False) or False
1491
+ include_config_bool = coerce_bool_param(
1492
+ include_config, "include_config", default=False
1569
1493
  )
1570
1494
  exact_match_bool = coerce_bool_param(exact_match, "exact_match", default=True)
1571
1495
  try:
@@ -23,7 +23,14 @@ from .helpers import (
23
23
  raise_tool_error,
24
24
  register_tool_methods,
25
25
  )
26
- from .util_helpers import coerce_bool_param, parse_json_param, wait_for_state_change
26
+ from .util_helpers import (
27
+ coerce_bool_param,
28
+ compact_service_result,
29
+ parse_json_param,
30
+ parse_string_list_param,
31
+ project_entity_record,
32
+ wait_for_state_change,
33
+ )
27
34
 
28
35
 
29
36
  def _parse_json_dict_param(
@@ -215,6 +222,62 @@ class ServiceTools:
215
222
  f"Service executed but state verification failed: {e}"
216
223
  )
217
224
 
225
+ @staticmethod
226
+ def _project_service_result(
227
+ result: Any,
228
+ *,
229
+ entity_id: str | None,
230
+ verbose: bool,
231
+ fields: list[str] | None,
232
+ attribute_keys: list[str] | None,
233
+ ) -> tuple[Any, list[str]]:
234
+ """Apply compact / explicit projection to a service-call ``result``.
235
+
236
+ Issue #1446. Precedence:
237
+
238
+ - ``verbose=True``: bypass every transformation; return ``result`` as-is.
239
+ - Explicit ``fields`` or ``attribute_keys``: apply per-record projection
240
+ via ``project_entity_record`` to every record. No compaction; this is
241
+ the power-user path.
242
+ - Default: apply ``compact_service_result`` (filter to ``entity_id``
243
+ record when single string, drop top-level metadata + heavy lists).
244
+
245
+ Returns ``(projected, warnings)``. ``warnings`` collects per-record
246
+ typo-guard diagnostics from ``project_entity_record`` (e.g. all-empty
247
+ ``attribute_keys`` filter) — deduplicated so an N-record list with the
248
+ same typo doesn't emit N copies of the same warning.
249
+ """
250
+ if verbose:
251
+ return result, []
252
+ if fields is None and attribute_keys is None:
253
+ return compact_service_result(result, entity_id), []
254
+ if not isinstance(result, list):
255
+ return result, []
256
+ warnings: list[str] = []
257
+ # ``result_attribute_keys`` only takes effect when ``attributes`` is in
258
+ # the projected ``result_fields`` (or ``result_fields`` is None). Surface
259
+ # a warning rather than silently ignoring the parameter — mirrors
260
+ # ha_get_state's attribute_keys_no_effect handling.
261
+ if (
262
+ attribute_keys is not None
263
+ and fields is not None
264
+ and "attributes" not in fields
265
+ ):
266
+ warnings.append(
267
+ "result_attribute_keys was ignored because 'attributes' is not "
268
+ "in result_fields. Add 'attributes' to result_fields (or omit "
269
+ "result_fields) to apply result_attribute_keys."
270
+ )
271
+ projected: list[Any] = []
272
+ seen_warnings: set[str] = set()
273
+ for record in result:
274
+ new_record, warn = project_entity_record(record, fields, attribute_keys)
275
+ projected.append(new_record)
276
+ if warn and warn not in seen_warnings:
277
+ seen_warnings.add(warn)
278
+ warnings.append(warn)
279
+ return projected, warnings
280
+
218
281
  @tool(
219
282
  name="ha_call_service",
220
283
  tags={"Service & Device Control"},
@@ -229,6 +292,43 @@ class ServiceTools:
229
292
  data: str | dict[str, Any] | None = None,
230
293
  return_response: bool | str = False,
231
294
  wait: bool | str = True,
295
+ verbose: Annotated[
296
+ bool | str,
297
+ Field(
298
+ description=(
299
+ "Return HA's raw service response unchanged (default: False). "
300
+ "Use as an escape hatch when you need the full propagation "
301
+ "chain or raw attribute payload (debug / inspection). "
302
+ "WARNING: brings back token-bloat for nested-group targets — "
303
+ "prefer result_fields / result_attribute_keys for targeted control."
304
+ ),
305
+ ),
306
+ ] = False,
307
+ result_fields: Annotated[
308
+ str | list[str] | None,
309
+ Field(
310
+ default=None,
311
+ description=(
312
+ "Project each record in 'result' to only these top-level keys "
313
+ "(e.g. ['entity_id', 'state']). Mirrors ha_get_state's fields=. "
314
+ "Setting this DISABLES default compaction — no entity-id filter, "
315
+ "no metadata strip — and applies the explicit projection instead."
316
+ ),
317
+ ),
318
+ ] = None,
319
+ result_attribute_keys: Annotated[
320
+ str | list[str] | None,
321
+ Field(
322
+ default=None,
323
+ description=(
324
+ "Project each record's 'attributes' dict to only these keys "
325
+ "(e.g. ['brightness', 'rgb_color']). Mirrors ha_get_state's "
326
+ "attribute_keys=. Setting this DISABLES default compaction. "
327
+ "Requires 'attributes' to be present in result_fields (or "
328
+ "result_fields=None)."
329
+ ),
330
+ ),
331
+ ] = None,
232
332
  ) -> dict[str, Any]:
233
333
  """
234
334
  Execute Home Assistant services to control entities and trigger automations.
@@ -252,15 +352,15 @@ class ServiceTools:
252
352
  ha_call_service("homeassistant", "toggle", entity_id="switch.porch_light")
253
353
  ```
254
354
 
255
- **Parameters:**
256
- - **domain**: Service domain (light, climate, automation, etc.)
257
- - **service**: Service name (turn_on, set_temperature, trigger, etc.)
258
- - **entity_id**: Optional target entity. For some services (e.g., light.turn_off), omitting this targets all entities in the domain
259
- - **data**: Optional dict of service-specific parameters
260
- - **return_response**: Set to True for services that return data
261
- - **wait**: Wait for the entity state to change after the service call (default: True).
262
- Only applies to state-changing services on a single entity. Set to False for
263
- fire-and-forget calls, bulk operations, or services without observable state changes.
355
+ **Key behavior:**
356
+ - **wait** (default True): wait for the entity state to change before
357
+ returning. Only applies to state-changing services on a single entity.
358
+ - **Result compaction (issue #1446, default ON)**: ``result`` is trimmed
359
+ to the targeted entity's record (drops parent-group propagation) and
360
+ stripped of ``context`` / ``last_*`` metadata and heavy attribute
361
+ lists (``effect_list``, ``hue_scenes``). Escape hatches: ``verbose=True``
362
+ for the raw HA response, or ``result_fields`` / ``result_attribute_keys``
363
+ for explicit per-record projection (mirrors ``ha_get_state``).
264
364
 
265
365
  **For detailed service documentation, use ha_get_skill_guide.**
266
366
 
@@ -271,11 +371,30 @@ class ServiceTools:
271
371
  service_data = self._parse_service_data(data, entity_id)
272
372
 
273
373
  # Coerce return_response boolean parameter
274
- return_response_bool = (
275
- coerce_bool_param(return_response, "return_response", default=False)
276
- or False
374
+ return_response_bool = coerce_bool_param(
375
+ return_response, "return_response", default=False
277
376
  )
278
377
  wait_bool = coerce_bool_param(wait, "wait", default=True)
378
+ try:
379
+ verbose_bool = coerce_bool_param(verbose, "verbose", default=False)
380
+ except ValueError as e:
381
+ raise_tool_error(create_validation_error(str(e), parameter="verbose"))
382
+ try:
383
+ parsed_result_fields = parse_string_list_param(
384
+ result_fields, "result_fields", allow_csv=True
385
+ )
386
+ except ValueError as e:
387
+ raise_tool_error(
388
+ create_validation_error(str(e), parameter="result_fields")
389
+ )
390
+ try:
391
+ parsed_result_attribute_keys = parse_string_list_param(
392
+ result_attribute_keys, "result_attribute_keys", allow_csv=True
393
+ )
394
+ except ValueError as e:
395
+ raise_tool_error(
396
+ create_validation_error(str(e), parameter="result_attribute_keys")
397
+ )
279
398
 
280
399
  # Determine if we should wait for state change:
281
400
  # Only for state-changing services on a single entity, not for
@@ -296,15 +415,25 @@ class ServiceTools:
296
415
  domain, service, service_data, return_response=return_response_bool
297
416
  )
298
417
 
418
+ projected_result, projection_warnings = self._project_service_result(
419
+ result,
420
+ entity_id=entity_id,
421
+ verbose=verbose_bool,
422
+ fields=parsed_result_fields,
423
+ attribute_keys=parsed_result_attribute_keys,
424
+ )
425
+
299
426
  response: dict[str, Any] = {
300
427
  "success": True,
301
428
  "domain": domain,
302
429
  "service": service,
303
430
  "entity_id": entity_id,
304
431
  "parameters": data,
305
- "result": result,
432
+ "result": projected_result,
306
433
  "message": f"Successfully executed {domain}.{service}",
307
434
  }
435
+ if projection_warnings:
436
+ response.setdefault("warnings", []).extend(projection_warnings)
308
437
 
309
438
  # If return_response was requested, include the service_response key prominently
310
439
  if return_response_bool and isinstance(result, dict):
@@ -65,7 +65,11 @@ class SystemTools:
65
65
  @tool(
66
66
  name="ha_check_config",
67
67
  tags={"System"},
68
- annotations={"idempotentHint": True, "readOnlyHint": True, "title": "Check Configuration"},
68
+ annotations={
69
+ "idempotentHint": True,
70
+ "readOnlyHint": True,
71
+ "title": "Check Configuration",
72
+ },
69
73
  )
70
74
  @log_tool_usage
71
75
  async def ha_check_config(self) -> dict[str, Any]:
@@ -142,22 +146,24 @@ class SystemTools:
142
146
  instead, which reloads specific components without a full restart.
143
147
  """
144
148
  # Coerce boolean parameter that may come as string from XML-style calls
145
- confirm_bool = coerce_bool_param(confirm, "confirm", default=False) or False
149
+ confirm_bool = coerce_bool_param(confirm, "confirm", default=False)
146
150
 
147
151
  if not confirm_bool:
148
- raise_tool_error(create_error_response(
149
- ErrorCode.VALIDATION_INVALID_PARAMETER,
150
- "Restart not confirmed",
151
- details=(
152
- "You must set confirm=True to restart Home Assistant. "
153
- "This is a safety measure to prevent accidental restarts."
154
- ),
155
- suggestions=[
156
- "Run ha_check_config() first to validate configuration",
157
- "Call ha_restart(confirm=True) to proceed with restart",
158
- "Consider using ha_reload_core() for config-only changes",
159
- ],
160
- ))
152
+ raise_tool_error(
153
+ create_error_response(
154
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
155
+ "Restart not confirmed",
156
+ details=(
157
+ "You must set confirm=True to restart Home Assistant. "
158
+ "This is a safety measure to prevent accidental restarts."
159
+ ),
160
+ suggestions=[
161
+ "Run ha_check_config() first to validate configuration",
162
+ "Call ha_restart(confirm=True) to proceed with restart",
163
+ "Consider using ha_reload_core() for config-only changes",
164
+ ],
165
+ )
166
+ )
161
167
 
162
168
  restart_initiated = False
163
169
  try:
@@ -165,15 +171,17 @@ class SystemTools:
165
171
  config_result = await self._client.check_config()
166
172
  if config_result.get("result") != "valid":
167
173
  errors = config_result.get("errors") or []
168
- raise_tool_error(create_error_response(
169
- ErrorCode.CONFIG_INVALID,
170
- "Configuration is invalid - restart aborted",
171
- details=(
172
- "Home Assistant configuration has errors. "
173
- "Fix the errors before restarting."
174
- ),
175
- context={"config_errors": errors},
176
- ))
174
+ raise_tool_error(
175
+ create_error_response(
176
+ ErrorCode.CONFIG_INVALID,
177
+ "Configuration is invalid - restart aborted",
178
+ details=(
179
+ "Home Assistant configuration has errors. "
180
+ "Fix the errors before restarting."
181
+ ),
182
+ context={"config_errors": errors},
183
+ )
184
+ )
177
185
 
178
186
  # Call the restart service - mark as initiated before the call
179
187
  # as the connection may be closed before we get a response
@@ -199,8 +207,7 @@ class SystemTools:
199
207
  # Connection errors after restart initiated are expected
200
208
  # (HA closes connections during restart)
201
209
  if restart_initiated and any(
202
- pattern in error_msg.lower()
203
- for pattern in ("connect", "closed", "504")
210
+ pattern in error_msg.lower() for pattern in ("connect", "closed", "504")
204
211
  ):
205
212
  return {
206
213
  "success": True,
@@ -272,12 +279,17 @@ class SystemTools:
272
279
  target = target.lower().strip()
273
280
 
274
281
  if target not in RELOAD_TARGETS:
275
- raise_tool_error(create_error_response(
276
- ErrorCode.VALIDATION_INVALID_PARAMETER,
277
- f"Invalid reload target: {target}",
278
- context={"target": target, "valid_targets": list(RELOAD_TARGETS.keys())},
279
- suggestions=[f"Use one of: {', '.join(RELOAD_TARGETS.keys())}"],
280
- ))
282
+ raise_tool_error(
283
+ create_error_response(
284
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
285
+ f"Invalid reload target: {target}",
286
+ context={
287
+ "target": target,
288
+ "valid_targets": list(RELOAD_TARGETS.keys()),
289
+ },
290
+ suggestions=[f"Use one of: {', '.join(RELOAD_TARGETS.keys())}"],
291
+ )
292
+ )
281
293
 
282
294
  try:
283
295
  if target == "all":
@@ -313,11 +325,13 @@ class SystemTools:
313
325
  service_info = RELOAD_TARGETS[target]
314
326
  if service_info is None:
315
327
  # This shouldn't happen as we check for "all" above
316
- raise_tool_error(create_error_response(
317
- ErrorCode.INTERNAL_ERROR,
318
- f"Invalid target configuration for: {target}",
319
- context={"target": target},
320
- ))
328
+ raise_tool_error(
329
+ create_error_response(
330
+ ErrorCode.INTERNAL_ERROR,
331
+ f"Invalid target configuration for: {target}",
332
+ context={"target": target},
333
+ )
334
+ )
321
335
  domain, service = service_info
322
336
  await self._client.call_service(domain, service, {})
323
337
 
@@ -343,7 +357,11 @@ class SystemTools:
343
357
  @tool(
344
358
  name="ha_get_system_health",
345
359
  tags={"System", "Zigbee", "Z-Wave", "Integrations"},
346
- annotations={"idempotentHint": True, "readOnlyHint": True, "title": "Get System Health (incl. ZHA/Z-Wave/integration diagnostics)"},
360
+ annotations={
361
+ "idempotentHint": True,
362
+ "readOnlyHint": True,
363
+ "title": "Get System Health (incl. ZHA/Z-Wave/integration diagnostics)",
364
+ },
347
365
  )
348
366
  @log_tool_usage
349
367
  async def ha_get_system_health(
@@ -474,9 +492,7 @@ class SystemTools:
474
492
  ("zha_network", self._fetch_zha_network(ws_client, full=zha_full))
475
493
  )
476
494
  if want_zwave:
477
- sections.append(
478
- ("zwave_network", self._fetch_zwave_network(ws_client))
479
- )
495
+ sections.append(("zwave_network", self._fetch_zwave_network(ws_client)))
480
496
 
481
497
  if sections:
482
498
  gathered = await asyncio.gather(
@@ -641,25 +657,32 @@ class SystemTools:
641
657
  verify_ssl=self._client.verify_ssl,
642
658
  )
643
659
  if error or ws_client is None:
644
- raise_tool_error(error or create_error_response(
645
- ErrorCode.CONNECTION_FAILED,
646
- "Failed to connect to Home Assistant WebSocket",
647
- ))
660
+ raise_tool_error(
661
+ error
662
+ or create_error_response(
663
+ ErrorCode.CONNECTION_FAILED,
664
+ "Failed to connect to Home Assistant WebSocket",
665
+ )
666
+ )
648
667
 
649
668
  try:
650
669
  _, event_response = await ws_client.send_command_with_event(
651
670
  "system_health/info", wait_timeout=10.0
652
671
  )
653
672
  except TimeoutError:
654
- raise_tool_error(create_error_response(
655
- ErrorCode.SERVICE_CALL_FAILED,
656
- "Timeout waiting for system health data",
657
- ))
673
+ raise_tool_error(
674
+ create_error_response(
675
+ ErrorCode.SERVICE_CALL_FAILED,
676
+ "Timeout waiting for system health data",
677
+ )
678
+ )
658
679
  except Exception as e:
659
- raise_tool_error(create_error_response(
660
- ErrorCode.SERVICE_CALL_FAILED,
661
- str(e),
662
- ))
680
+ raise_tool_error(
681
+ create_error_response(
682
+ ErrorCode.SERVICE_CALL_FAILED,
683
+ str(e),
684
+ )
685
+ )
663
686
 
664
687
  health_info = event_response.get("event", {})
665
688
  component_count = len(health_info) if isinstance(health_info, dict) else 0
@@ -673,7 +696,6 @@ class SystemTools:
673
696
 
674
697
  return ws_client, result
675
698
 
676
-
677
699
  @staticmethod
678
700
  async def _fetch_repairs(
679
701
  ws_client: Any, *, include_dismissed: bool = False
@@ -703,9 +725,7 @@ class SystemTools:
703
725
  else:
704
726
  err = repairs_result.get("error") or {}
705
727
  err_msg = (
706
- err.get("message")
707
- if isinstance(err, dict)
708
- else str(err)
728
+ err.get("message") if isinstance(err, dict) else str(err)
709
729
  ) or "unknown error"
710
730
  logger.warning(
711
731
  "repairs/list_issues returned success=false: %s", err_msg
@@ -717,9 +737,7 @@ class SystemTools:
717
737
  return repairs
718
738
 
719
739
  @staticmethod
720
- async def _fetch_zha_network(
721
- ws_client: Any, *, full: bool
722
- ) -> dict[str, Any]:
740
+ async def _fetch_zha_network(ws_client: Any, *, full: bool) -> dict[str, Any]:
723
741
  """Fetch ZHA Zigbee network device data."""
724
742
  ZHA_SUMMARY_LIMIT = 50
725
743
  ZHA_FULL_LIMIT = 25