ha-mcp-dev 7.7.0.dev692__tar.gz → 7.7.0.dev694__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.dev692/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.7.0.dev694}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_addons.py +5 -1
  4. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_areas.py +4 -0
  5. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_config_helpers.py +10 -0
  6. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_energy.py +9 -3
  7. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_entities.py +4 -0
  8. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_groups.py +4 -0
  9. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_history.py +7 -0
  10. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_integrations.py +2 -0
  11. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_registry.py +2 -0
  12. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_search.py +9 -0
  13. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_service.py +8 -13
  14. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_services.py +3 -0
  15. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_system.py +4 -1
  16. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/validation_middleware.py +23 -5
  17. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  18. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/LICENSE +0 -0
  19. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/MANIFEST.in +0 -0
  20. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/README.md +0 -0
  21. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/setup.cfg +0 -0
  22. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/__init__.py +0 -0
  23. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/__main__.py +0 -0
  24. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/_pypi_marker +0 -0
  25. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/_version.py +0 -0
  26. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/auth/__init__.py +0 -0
  27. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/auth/consent_form.py +0 -0
  28. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/auth/provider.py +0 -0
  29. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/backup_manager.py +0 -0
  30. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/client/__init__.py +0 -0
  31. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/client/rest_client.py +0 -0
  32. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/client/supervisor_client.py +0 -0
  33. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/client/websocket_client.py +0 -0
  34. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/client/websocket_listener.py +0 -0
  35. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/config.py +0 -0
  36. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  37. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  38. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  39. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/errors.py +0 -0
  40. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/policy/__init__.py +0 -0
  41. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/policy/approval_queue.py +0 -0
  42. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/policy/evaluator.py +0 -0
  43. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/policy/handlers.py +0 -0
  44. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/policy/middleware.py +0 -0
  45. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/policy/model.py +0 -0
  46. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/policy/persistence.py +0 -0
  47. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/policy/value_sources.py +0 -0
  48. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/py.typed +0 -0
  49. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/read_only.py +0 -0
  50. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  51. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  52. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  53. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  54. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  55. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  56. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  57. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  58. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  59. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  60. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  61. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  62. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  63. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  64. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  65. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  66. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  67. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  68. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  69. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  70. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  71. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  72. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/server.py +0 -0
  73. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/settings.css +0 -0
  74. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/settings.js +0 -0
  75. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/settings_ui.py +0 -0
  76. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/smoke_test.py +0 -0
  77. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  78. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/__init__.py +0 -0
  79. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/auto_backup.py +0 -0
  80. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/backup.py +0 -0
  81. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  82. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  83. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/device_control.py +0 -0
  84. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/enhanced.py +0 -0
  85. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/helpers.py +0 -0
  86. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/reference_validator.py +0 -0
  87. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/registry.py +0 -0
  88. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  89. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  90. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  91. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  92. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  93. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  94. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  95. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  96. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  97. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  98. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  99. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_calendar.py +0 -0
  100. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_camera.py +0 -0
  101. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_categories.py +0 -0
  102. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_code.py +0 -0
  103. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  104. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  105. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  106. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  107. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  108. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  109. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_hacs.py +0 -0
  110. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_labels.py +0 -0
  111. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  112. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_resources.py +0 -0
  113. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_themes.py +0 -0
  114. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_todo.py +0 -0
  115. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_traces.py +0 -0
  116. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_updates.py +0 -0
  117. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_utility.py +0 -0
  118. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  119. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  120. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/tools_zones.py +0 -0
  121. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/tools/util_helpers.py +0 -0
  122. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/transforms/__init__.py +0 -0
  123. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/transforms/categorized_search.py +0 -0
  124. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  125. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/utils/__init__.py +0 -0
  126. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/utils/config_hash.py +0 -0
  127. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/utils/data_paths.py +0 -0
  128. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/utils/domain_handlers.py +0 -0
  129. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  130. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  131. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/utils/operation_manager.py +0 -0
  132. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/utils/python_sandbox.py +0 -0
  133. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/utils/skill_loader.py +0 -0
  134. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp/utils/usage_logger.py +0 -0
  135. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  136. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  137. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  138. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  139. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  140. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/tests/__init__.py +0 -0
  141. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/tests/test_constants.py +0 -0
  142. {ha_mcp_dev-7.7.0.dev692 → ha_mcp_dev-7.7.0.dev694}/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.dev692
3
+ Version: 7.7.0.dev694
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.dev692"
7
+ version = "7.7.0.dev694"
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"
@@ -41,7 +41,7 @@ from .helpers import (
41
41
  raise_tool_error,
42
42
  validate_identifier_not_empty,
43
43
  )
44
- from .util_helpers import ANSI_ESCAPE_RE
44
+ from .util_helpers import ANSI_ESCAPE_RE, JSON_STRING_COERCION
45
45
 
46
46
  logger = logging.getLogger(__name__)
47
47
 
@@ -2712,6 +2712,7 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
2712
2712
  ] = None,
2713
2713
  options: Annotated[
2714
2714
  dict[str, Any] | None,
2715
+ JSON_STRING_COERCION,
2715
2716
  Field(
2716
2717
  description="Config mode: Add-on configuration values (the 'Configuration' tab in the UI).",
2717
2718
  default=None,
@@ -2719,6 +2720,7 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
2719
2720
  ] = None,
2720
2721
  network: Annotated[
2721
2722
  dict[str, Any] | None,
2723
+ JSON_STRING_COERCION,
2722
2724
  Field(
2723
2725
  description="Config mode: Host port mappings (e.g., {'5800/tcp': 8081}).",
2724
2726
  default=None,
@@ -2747,6 +2749,7 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
2747
2749
  ] = None,
2748
2750
  array_patch: Annotated[
2749
2751
  dict[str, Any] | None,
2752
+ JSON_STRING_COERCION,
2750
2753
  Field(
2751
2754
  description=(
2752
2755
  "Array-patch mode: atomically GET a JSON array endpoint, "
@@ -2760,6 +2763,7 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
2760
2763
  ] = None,
2761
2764
  request_headers: Annotated[
2762
2765
  dict[str, str] | None,
2766
+ JSON_STRING_COERCION,
2763
2767
  Field(
2764
2768
  description=(
2765
2769
  "Proxy/array-patch mode: extra HTTP headers to send to the addon API. "
@@ -23,6 +23,7 @@ from .helpers import (
23
23
  validate_identifier_not_empty,
24
24
  )
25
25
  from .util_helpers import (
26
+ JSON_STRING_COERCION,
26
27
  parse_string_list_param,
27
28
  project_fields,
28
29
  project_records,
@@ -148,6 +149,7 @@ class AreaTools:
148
149
  self,
149
150
  fields: Annotated[
150
151
  str | list[str] | None,
152
+ JSON_STRING_COERCION,
151
153
  Field(
152
154
  default=None,
153
155
  description=(
@@ -162,6 +164,7 @@ class AreaTools:
162
164
  ] = None,
163
165
  area_fields: Annotated[
164
166
  str | list[str] | None,
167
+ JSON_STRING_COERCION,
165
168
  Field(
166
169
  default=None,
167
170
  description=(
@@ -446,6 +449,7 @@ class AreaTools:
446
449
  ] = None,
447
450
  aliases: Annotated[
448
451
  str | list[str] | None,
452
+ JSON_STRING_COERCION,
449
453
  Field(
450
454
  description="Alternative names for voice assistant recognition (e.g., ['lounge'], empty list to clear)",
451
455
  default=None,
@@ -3500,6 +3500,7 @@ class HelperConfigTools:
3500
3500
  ] = None,
3501
3501
  labels: Annotated[
3502
3502
  str | list[str] | None,
3503
+ JSON_STRING_COERCION,
3503
3504
  Field(description="Labels to categorize the helper", default=None),
3504
3505
  ] = None,
3505
3506
  min_value: Annotated[
@@ -3535,6 +3536,7 @@ class HelperConfigTools:
3535
3536
  ] = None,
3536
3537
  options: Annotated[
3537
3538
  str | list[str] | None,
3539
+ JSON_STRING_COERCION,
3538
3540
  Field(
3539
3541
  description="List of options for input_select (required for input_select)",
3540
3542
  default=None,
@@ -3582,6 +3584,7 @@ class HelperConfigTools:
3582
3584
  ] = None,
3583
3585
  monday: Annotated[
3584
3586
  list[dict[str, Any]] | None,
3587
+ JSON_STRING_COERCION,
3585
3588
  Field(
3586
3589
  description="Schedule time ranges for Monday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes (e.g. {'from': '07:00', 'to': '22:00', 'data': {'mode': 'comfort'}})",
3587
3590
  default=None,
@@ -3589,6 +3592,7 @@ class HelperConfigTools:
3589
3592
  ] = None,
3590
3593
  tuesday: Annotated[
3591
3594
  list[dict[str, Any]] | None,
3595
+ JSON_STRING_COERCION,
3592
3596
  Field(
3593
3597
  description="Schedule time ranges for Tuesday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes.",
3594
3598
  default=None,
@@ -3596,6 +3600,7 @@ class HelperConfigTools:
3596
3600
  ] = None,
3597
3601
  wednesday: Annotated[
3598
3602
  list[dict[str, Any]] | None,
3603
+ JSON_STRING_COERCION,
3599
3604
  Field(
3600
3605
  description="Schedule time ranges for Wednesday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes.",
3601
3606
  default=None,
@@ -3603,6 +3608,7 @@ class HelperConfigTools:
3603
3608
  ] = None,
3604
3609
  thursday: Annotated[
3605
3610
  list[dict[str, Any]] | None,
3611
+ JSON_STRING_COERCION,
3606
3612
  Field(
3607
3613
  description="Schedule time ranges for Thursday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes.",
3608
3614
  default=None,
@@ -3610,6 +3616,7 @@ class HelperConfigTools:
3610
3616
  ] = None,
3611
3617
  friday: Annotated[
3612
3618
  list[dict[str, Any]] | None,
3619
+ JSON_STRING_COERCION,
3613
3620
  Field(
3614
3621
  description="Schedule time ranges for Friday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes.",
3615
3622
  default=None,
@@ -3617,6 +3624,7 @@ class HelperConfigTools:
3617
3624
  ] = None,
3618
3625
  saturday: Annotated[
3619
3626
  list[dict[str, Any]] | None,
3627
+ JSON_STRING_COERCION,
3620
3628
  Field(
3621
3629
  description="Schedule time ranges for Saturday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes.",
3622
3630
  default=None,
@@ -3624,6 +3632,7 @@ class HelperConfigTools:
3624
3632
  ] = None,
3625
3633
  sunday: Annotated[
3626
3634
  list[dict[str, Any]] | None,
3635
+ JSON_STRING_COERCION,
3627
3636
  Field(
3628
3637
  description="Schedule time ranges for Sunday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes.",
3629
3638
  default=None,
@@ -3654,6 +3663,7 @@ class HelperConfigTools:
3654
3663
  ] = None,
3655
3664
  device_trackers: Annotated[
3656
3665
  list[str] | None,
3666
+ JSON_STRING_COERCION,
3657
3667
  Field(
3658
3668
  description="List of device_tracker entity IDs for person", default=None
3659
3669
  ),
@@ -43,6 +43,7 @@ from .helpers import (
43
43
  register_tool_methods,
44
44
  validate_identifier_not_empty,
45
45
  )
46
+ from .util_helpers import JSON_STRING_COERCION
46
47
 
47
48
  logger = logging.getLogger(__name__)
48
49
 
@@ -341,6 +342,7 @@ class EnergyTools:
341
342
  ],
342
343
  config: Annotated[
343
344
  dict[str, Any] | None,
345
+ JSON_STRING_COERCION,
344
346
  Field(
345
347
  description=(
346
348
  "Full prefs payload for mode='set'. Must contain the "
@@ -362,9 +364,12 @@ class EnergyTools:
362
364
  "Hash from a previous mode='get' call. REQUIRED for "
363
365
  "mode='set' unless dry_run=True. Two forms: str (full-"
364
366
  "blob lock) or dict (per-key lock, taken from the "
365
- "config_hash_per_key field of mode='get'). See the tool "
366
- "docstring for fail-closed semantics. Ignored by "
367
- "convenience modes."
367
+ "config_hash_per_key field of mode='get'). Pass the dict "
368
+ "form as a native object, NOT a JSON-encoded string a "
369
+ "stringified dict is treated as a full-blob token and will "
370
+ "report RESOURCE_LOCKED; clients that can only send strings "
371
+ "should use the str full-blob form. See the tool docstring "
372
+ "for fail-closed semantics. Ignored by convenience modes."
368
373
  ),
369
374
  default=None,
370
375
  ),
@@ -438,6 +443,7 @@ class EnergyTools:
438
443
  ] = False,
439
444
  source: Annotated[
440
445
  dict[str, Any] | None,
446
+ JSON_STRING_COERCION,
441
447
  Field(
442
448
  description=(
443
449
  "Single energy_sources entry for mode='add_source'. Must "
@@ -559,6 +559,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
559
559
  async def ha_set_entity(
560
560
  entity_id: Annotated[
561
561
  str | list[str],
562
+ JSON_STRING_COERCION,
562
563
  Field(
563
564
  description="Entity ID or list of entity IDs to update. Bulk operations (list) only support labels, expose_to, and categories parameters."
564
565
  ),
@@ -639,6 +640,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
639
640
  ] = None,
640
641
  aliases: Annotated[
641
642
  str | list[str] | None,
643
+ JSON_STRING_COERCION,
642
644
  Field(
643
645
  description="List of voice assistant aliases for the entity (replaces existing aliases). Single entity only.",
644
646
  default=None,
@@ -659,6 +661,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
659
661
  ] = None,
660
662
  labels: Annotated[
661
663
  str | list[str] | None,
664
+ JSON_STRING_COERCION,
662
665
  Field(
663
666
  description="List of label IDs for the entity. Behavior depends on label_operation parameter. Supports bulk operations.",
664
667
  default=None,
@@ -1117,6 +1120,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1117
1120
  async def ha_get_entity(
1118
1121
  entity_id: Annotated[
1119
1122
  str | list[str],
1123
+ JSON_STRING_COERCION,
1120
1124
  Field(
1121
1125
  description="Entity ID or list of entity IDs to retrieve (e.g., 'sensor.temperature' or ['light.living_room', 'switch.porch'])"
1122
1126
  ),
@@ -26,6 +26,7 @@ from .helpers import (
26
26
  validate_identifier_not_empty,
27
27
  )
28
28
  from .util_helpers import (
29
+ JSON_STRING_COERCION,
29
30
  wait_for_entity_registered,
30
31
  wait_for_entity_removed,
31
32
  )
@@ -223,6 +224,7 @@ class GroupTools:
223
224
  ],
224
225
  entities: Annotated[
225
226
  list[str] | None,
227
+ JSON_STRING_COERCION,
226
228
  Field(
227
229
  description="List of entity IDs for the group. Required when creating new group. When updating, replaces all entities (mutually exclusive with add_entities/remove_entities).",
228
230
  default=None,
@@ -251,6 +253,7 @@ class GroupTools:
251
253
  ] = None,
252
254
  add_entities: Annotated[
253
255
  list[str] | None,
256
+ JSON_STRING_COERCION,
254
257
  Field(
255
258
  description="Add these entities to an existing group (mutually exclusive with entities)",
256
259
  default=None,
@@ -258,6 +261,7 @@ class GroupTools:
258
261
  ] = None,
259
262
  remove_entities: Annotated[
260
263
  list[str] | None,
264
+ JSON_STRING_COERCION,
261
265
  Field(
262
266
  description="Remove these entities from an existing group (mutually exclusive with entities)",
263
267
  default=None,
@@ -30,6 +30,7 @@ from .helpers import (
30
30
  safe_progress,
31
31
  )
32
32
  from .util_helpers import (
33
+ JSON_STRING_COERCION,
33
34
  add_timezone_metadata,
34
35
  build_pagination_metadata,
35
36
  parse_string_list_param,
@@ -136,6 +137,7 @@ class HistoryTools:
136
137
  self,
137
138
  entity_ids: Annotated[
138
139
  str | list[str],
140
+ JSON_STRING_COERCION,
139
141
  Field(
140
142
  description="Entity ID(s) to query. Can be a single ID, comma-separated string, or JSON array."
141
143
  ),
@@ -206,6 +208,7 @@ class HistoryTools:
206
208
  ] = "day",
207
209
  statistic_types: Annotated[
208
210
  str | list[str] | None,
211
+ JSON_STRING_COERCION,
209
212
  Field(
210
213
  description='Statistics types: "mean", "min", "max", "sum", "state", "change". Default: all. Ignored when source="history"',
211
214
  default=None,
@@ -224,6 +227,7 @@ class HistoryTools:
224
227
  ] = "desc",
225
228
  fields: Annotated[
226
229
  str | list[str] | None,
230
+ JSON_STRING_COERCION,
227
231
  Field(
228
232
  default=None,
229
233
  description=(
@@ -425,6 +429,9 @@ def _parse_entity_ids(entity_ids: str | list[str]) -> list[str]:
425
429
  """Parse entity_ids parameter into a list of strings."""
426
430
  if isinstance(entity_ids, str):
427
431
  if entity_ids.startswith("["):
432
+ # Belt-and-suspenders: JSON_STRING_COERCION on the param already
433
+ # parses a JSON-array string to a list upstream, so a string reaching
434
+ # here is normally CSV/single. This branch stays as a fallback.
428
435
  parsed_ids = parse_string_list_param(entity_ids, "entity_ids")
429
436
  if parsed_ids is None:
430
437
  raise_tool_error(
@@ -33,6 +33,7 @@ from .tools_config_helpers import (
33
33
  _get_entities_for_config_entry,
34
34
  )
35
35
  from .util_helpers import (
36
+ JSON_STRING_COERCION,
36
37
  build_pagination_metadata,
37
38
  fetch_integration_diagnostics,
38
39
  get_logger_levels,
@@ -459,6 +460,7 @@ class IntegrationTools:
459
460
  ] = None,
460
461
  diagnostics_fields: Annotated[
461
462
  list[str] | str | None,
463
+ JSON_STRING_COERCION,
462
464
  Field(
463
465
  description=(
464
466
  "Optional list of top-level keys to keep from the diagnostics "
@@ -21,6 +21,7 @@ from .helpers import (
21
21
  validate_identifier_not_empty,
22
22
  )
23
23
  from .util_helpers import (
24
+ JSON_STRING_COERCION,
24
25
  build_pagination_metadata,
25
26
  parse_string_list_param,
26
27
  )
@@ -627,6 +628,7 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
627
628
  ] = None,
628
629
  labels: Annotated[
629
630
  str | list[str] | None,
631
+ JSON_STRING_COERCION,
630
632
  Field(
631
633
  description="Labels to assign to the device (replaces existing labels)",
632
634
  default=None,
@@ -18,6 +18,7 @@ from ..transforms.categorized_search import DEFAULT_PINNED_TOOLS
18
18
  from ..utils.fuzzy_search import apply_hidden_penalty
19
19
  from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
20
20
  from .util_helpers import (
21
+ JSON_STRING_COERCION,
21
22
  add_timezone_metadata,
22
23
  build_pagination_metadata,
23
24
  filter_active_repairs,
@@ -581,6 +582,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
581
582
  ] = None,
582
583
  search_types: Annotated[
583
584
  str | list[str] | None,
585
+ JSON_STRING_COERCION,
584
586
  Field(
585
587
  default=None,
586
588
  description=(
@@ -672,6 +674,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
672
674
  ] = None,
673
675
  result_fields: Annotated[
674
676
  str | list[str] | None,
677
+ JSON_STRING_COERCION,
675
678
  Field(
676
679
  default=None,
677
680
  description=(
@@ -682,6 +685,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
682
685
  ] = None,
683
686
  fields: Annotated[
684
687
  str | list[str] | None,
688
+ JSON_STRING_COERCION,
685
689
  Field(
686
690
  default=None,
687
691
  description=(
@@ -1810,6 +1814,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1810
1814
  ] = "minimal",
1811
1815
  domains: Annotated[
1812
1816
  str | list[str] | None,
1817
+ JSON_STRING_COERCION,
1813
1818
  Field(
1814
1819
  default=None,
1815
1820
  description=(
@@ -1878,6 +1883,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1878
1883
  ] = False,
1879
1884
  fields: Annotated[
1880
1885
  str | list[str] | None,
1886
+ JSON_STRING_COERCION,
1881
1887
  Field(
1882
1888
  default=None,
1883
1889
  description=(
@@ -2326,6 +2332,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2326
2332
  async def ha_get_state(
2327
2333
  entity_id: Annotated[
2328
2334
  str | list[str],
2335
+ JSON_STRING_COERCION,
2329
2336
  Field(
2330
2337
  description="Entity ID or list of entity IDs to retrieve state for "
2331
2338
  "(e.g., 'light.kitchen' or ['light.kitchen', 'sensor.temperature'])"
@@ -2333,6 +2340,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2333
2340
  ],
2334
2341
  fields: Annotated[
2335
2342
  str | list[str] | None,
2343
+ JSON_STRING_COERCION,
2336
2344
  Field(
2337
2345
  default=None,
2338
2346
  description=(
@@ -2346,6 +2354,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2346
2354
  ] = None,
2347
2355
  attribute_keys: Annotated[
2348
2356
  str | list[str] | None,
2357
+ JSON_STRING_COERCION,
2349
2358
  Field(
2350
2359
  default=None,
2351
2360
  description=(
@@ -315,6 +315,7 @@ class ServiceTools:
315
315
  ] = False,
316
316
  result_fields: Annotated[
317
317
  str | list[str] | None,
318
+ JSON_STRING_COERCION,
318
319
  Field(
319
320
  default=None,
320
321
  description=(
@@ -327,6 +328,7 @@ class ServiceTools:
327
328
  ] = None,
328
329
  result_attribute_keys: Annotated[
329
330
  str | list[str] | None,
331
+ JSON_STRING_COERCION,
330
332
  Field(
331
333
  default=None,
332
334
  description=(
@@ -522,6 +524,7 @@ class ServiceTools:
522
524
  self,
523
525
  operation_id: Annotated[
524
526
  str | list[str],
527
+ JSON_STRING_COERCION,
525
528
  Field(
526
529
  description=(
527
530
  "Single operation ID or list of operation IDs to check. "
@@ -544,23 +547,15 @@ class ServiceTools:
544
547
  For current entity states, use ha_get_state instead.
545
548
  """
546
549
  try:
547
- # Handle JSON string coercion (MCP clients may send '["op1","op2"]')
548
- resolved_id: str | list[str] = operation_id
549
- if isinstance(operation_id, str):
550
- try:
551
- parsed = parse_json_param(operation_id, "operation_id")
552
- if isinstance(parsed, list):
553
- resolved_id = [str(item) for item in parsed]
554
- except ValueError:
555
- pass # Plain string — treat as single operation ID
556
-
557
- if isinstance(resolved_id, list):
550
+ # JSON_STRING_COERCION turns a '["op1","op2"]' string into a list
551
+ # before the body runs, so operation_id is already the final shape.
552
+ if isinstance(operation_id, list):
558
553
  result = await self._device_tools.get_bulk_operation_status(
559
- operation_ids=resolved_id
554
+ operation_ids=operation_id
560
555
  )
561
556
  return cast(dict[str, Any], result)
562
557
  result = await self._device_tools.get_device_operation_status(
563
- operation_id=resolved_id, timeout_seconds=timeout_seconds
558
+ operation_id=operation_id, timeout_seconds=timeout_seconds
564
559
  )
565
560
  return cast(dict[str, Any], result)
566
561
  except ToolError:
@@ -20,6 +20,7 @@ from .helpers import (
20
20
  register_tool_methods,
21
21
  )
22
22
  from .util_helpers import (
23
+ JSON_STRING_COERCION,
23
24
  build_pagination_metadata,
24
25
  parse_string_list_param,
25
26
  project_fields,
@@ -78,6 +79,7 @@ class ServiceDiscoveryTools:
78
79
  ] = "summary",
79
80
  service_fields: Annotated[
80
81
  str | list[str] | None,
82
+ JSON_STRING_COERCION,
81
83
  Field(
82
84
  default=None,
83
85
  description=(
@@ -91,6 +93,7 @@ class ServiceDiscoveryTools:
91
93
  ] = None,
92
94
  fields: Annotated[
93
95
  str | list[str] | None,
96
+ JSON_STRING_COERCION,
94
97
  Field(
95
98
  default=None,
96
99
  description=(
@@ -25,6 +25,7 @@ from .helpers import (
25
25
  register_tool_methods,
26
26
  )
27
27
  from .util_helpers import (
28
+ JSON_STRING_COERCION,
28
29
  fetch_integration_diagnostics,
29
30
  filter_active_repairs,
30
31
  parse_diagnostics_fields,
@@ -323,7 +324,9 @@ class SystemTools:
323
324
  include_dismissed_repairs: bool | None = False,
324
325
  config_entry_id: str | None = None,
325
326
  device_id: str | None = None,
326
- diagnostics_fields: list[str] | str | None = None,
327
+ diagnostics_fields: Annotated[
328
+ list[str] | str | None, JSON_STRING_COERCION
329
+ ] = None,
327
330
  diagnostics_truncate_at_bytes: Annotated[int, Field(ge=1)] | None = None,
328
331
  diagnostics_data_path: str | None = None,
329
332
  diagnostics_data_offset: Annotated[int, Field(ge=0)] | None = 0,
@@ -43,15 +43,33 @@ class ValidationErrorMiddleware(Middleware):
43
43
  return await call_next(context)
44
44
  except PydanticValidationError as exc:
45
45
  errors = exc.errors(include_url=False)
46
- parts: list[str] = []
46
+ # Group by the real argument path. A union param like
47
+ # `str | list[str]` emits one error per arm with loc (param, "str"),
48
+ # (param, "list[str]"); without grouping the user saw `param.str` /
49
+ # `param.list[str]` instead of `param` (#1601). We keep the param
50
+ # name plus any numeric list indices (so a bad element still reports
51
+ # `monday.1`) but drop the non-numeric union-arm tags.
52
+ grouped: dict[str, list[Any]] = {}
47
53
  for err in errors:
48
- loc = err.get("loc", ())
49
- param = ".".join(str(p) for p in loc if p != "__root__")
50
- hint = _TYPE_HINTS.get(err["type"], err["msg"])
54
+ loc = [str(p) for p in err.get("loc", ()) if p != "__root__"]
55
+ if loc:
56
+ key = ".".join([loc[0], *(p for p in loc[1:] if p.isdigit())])
57
+ else:
58
+ key = ""
59
+ grouped.setdefault(key, []).append(err)
60
+
61
+ parts: list[str] = []
62
+ for param, errs in grouped.items():
63
+ # Prefer an actionable container hint when any arm produced one
64
+ # (dict_type/list_type); else fall back to the first raw message.
65
+ hint = next(
66
+ (_TYPE_HINTS[e["type"]] for e in errs if e["type"] in _TYPE_HINTS),
67
+ errs[0]["msg"],
68
+ )
51
69
  parts.append(f"`{param}`: {hint}" if param else hint)
52
70
  raise_tool_error(
53
71
  create_validation_error(
54
72
  "; ".join(parts) if parts else "Invalid argument types.",
55
- details=", ".join(err["type"] for err in errors),
73
+ details=", ".join(dict.fromkeys(err["type"] for err in errors)),
56
74
  )
57
75
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.7.0.dev692
3
+ Version: 7.7.0.dev694
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