ha-mcp-dev 7.3.0.dev399__tar.gz → 7.3.0.dev400__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 (104) hide show
  1. {ha_mcp_dev-7.3.0.dev399/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.3.0.dev400}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_areas.py +8 -1
  4. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_config_automations.py +54 -7
  5. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_config_helpers.py +3 -3
  6. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_config_scripts.py +8 -7
  7. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/util_helpers.py +7 -0
  8. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/transforms/categorized_search.py +41 -4
  9. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  10. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/LICENSE +0 -0
  11. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/MANIFEST.in +0 -0
  12. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/README.md +0 -0
  13. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/setup.cfg +0 -0
  14. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/__init__.py +0 -0
  15. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/__main__.py +0 -0
  16. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/_pypi_marker +0 -0
  17. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/_version.py +0 -0
  18. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/auth/__init__.py +0 -0
  19. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/auth/consent_form.py +0 -0
  20. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/auth/provider.py +0 -0
  21. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/client/__init__.py +0 -0
  22. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/client/rest_client.py +0 -0
  23. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/client/websocket_client.py +0 -0
  24. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/client/websocket_listener.py +0 -0
  25. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/config.py +0 -0
  26. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/errors.py +0 -0
  27. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/py.typed +0 -0
  28. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  29. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  30. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  31. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  32. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  33. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  34. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  35. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  36. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  37. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  38. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  39. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  40. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  41. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  42. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  43. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  44. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  45. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  46. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  47. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  48. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/server.py +0 -0
  49. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/smoke_test.py +0 -0
  50. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/__init__.py +0 -0
  51. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/backup.py +0 -0
  52. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  53. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/device_control.py +0 -0
  54. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/enhanced.py +0 -0
  55. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/helpers.py +0 -0
  56. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/reference_validator.py +0 -0
  57. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/registry.py +0 -0
  58. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/smart_search.py +0 -0
  59. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_addons.py +0 -0
  60. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  61. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  62. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_calendar.py +0 -0
  63. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_camera.py +0 -0
  64. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_categories.py +0 -0
  65. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  66. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  67. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_energy.py +0 -0
  68. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_entities.py +0 -0
  69. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  70. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_groups.py +0 -0
  71. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_hacs.py +0 -0
  72. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_history.py +0 -0
  73. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_integrations.py +0 -0
  74. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_labels.py +0 -0
  75. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  76. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_registry.py +0 -0
  77. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_resources.py +0 -0
  78. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_search.py +0 -0
  79. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_service.py +0 -0
  80. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_services.py +0 -0
  81. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_system.py +0 -0
  82. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_todo.py +0 -0
  83. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_traces.py +0 -0
  84. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_updates.py +0 -0
  85. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_utility.py +0 -0
  86. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  87. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  88. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_zones.py +0 -0
  89. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/transforms/__init__.py +0 -0
  90. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/__init__.py +0 -0
  91. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/config_hash.py +0 -0
  92. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/domain_handlers.py +0 -0
  93. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  94. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/operation_manager.py +0 -0
  95. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/python_sandbox.py +0 -0
  96. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/usage_logger.py +0 -0
  97. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  98. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  99. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  100. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  101. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  102. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/tests/__init__.py +0 -0
  103. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/tests/test_constants.py +0 -0
  104. {ha_mcp_dev-7.3.0.dev399 → ha_mcp_dev-7.3.0.dev400}/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.3.0.dev399
3
+ Version: 7.3.0.dev400
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.3.0.dev399"
7
+ version = "7.3.0.dev400"
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"
@@ -222,8 +222,15 @@ class AreaTools:
222
222
  """
223
223
  Create or update a Home Assistant area (room).
224
224
 
225
- Provide name only to create a new area. Provide area_id to update existing.
226
225
  Areas organize entities by physical location for room-based control.
226
+
227
+ Create: provide name only.
228
+ Update: provide area_id (from ha_config_list_areas) plus any fields to change.
229
+
230
+ EXAMPLES:
231
+ ha_config_set_area(name="Kitchen")
232
+ ha_config_set_area(name="Living Room", icon="mdi:sofa")
233
+ ha_config_set_area(area_id="kitchen", name="Kitchen Renamed", floor_id="ground_floor")
227
234
  """
228
235
  try:
229
236
  # Parse aliases if provided as string
@@ -41,6 +41,7 @@ from .reference_validator import validate_config_references
41
41
  from .util_helpers import (
42
42
  apply_entity_category,
43
43
  coerce_bool_param,
44
+ coerce_to_list,
44
45
  fetch_entity_category,
45
46
  merge_validation_meta,
46
47
  parse_json_param,
@@ -413,7 +414,7 @@ class AutomationConfigTools:
413
414
  BASIC EXAMPLES:
414
415
 
415
416
  Simple time-based automation:
416
- ha_config_set_automation({
417
+ ha_config_set_automation(config={
417
418
  "alias": "Morning Lights",
418
419
  "description": "Turn on bedroom lights at 7 AM to help wake up",
419
420
  "trigger": [{"platform": "time", "at": "07:00:00"}],
@@ -421,7 +422,7 @@ class AutomationConfigTools:
421
422
  })
422
423
 
423
424
  Motion-activated lighting with condition:
424
- ha_config_set_automation({
425
+ ha_config_set_automation(config={
425
426
  "alias": "Motion Light",
426
427
  "trigger": [{"platform": "state", "entity_id": "binary_sensor.motion", "to": "on"}],
427
428
  "condition": [{"condition": "sun", "after": "sunset"}],
@@ -449,7 +450,7 @@ class AutomationConfigTools:
449
450
  BLUEPRINT AUTOMATION EXAMPLES:
450
451
 
451
452
  Create automation from blueprint:
452
- ha_config_set_automation({
453
+ ha_config_set_automation(config={
453
454
  "alias": "Motion Light Kitchen",
454
455
  "use_blueprint": {
455
456
  "path": "homeassistant/motion_light.yaml",
@@ -757,10 +758,14 @@ class AutomationConfigTools:
757
758
  try:
758
759
  parsed_config = parse_json_param(config, "config")
759
760
  except ValueError as e:
760
- raise_tool_error(create_validation_error(
761
- f"Invalid config parameter: {e}",
762
- parameter="config",
763
- invalid_json=True,
761
+ raise_tool_error(create_error_response(
762
+ code=ErrorCode.VALIDATION_INVALID_JSON,
763
+ message=f"Invalid config parameter: {e}",
764
+ suggestions=[
765
+ "Pass 'config' as a dict, not a JSON string, to avoid escaping issues.",
766
+ "Check for JSON syntax errors: unquoted keys, trailing commas, or invalid escape sequences.",
767
+ ],
768
+ context={"parameter": "config"},
764
769
  ))
765
770
 
766
771
  if parsed_config is None or not isinstance(parsed_config, dict):
@@ -788,12 +793,54 @@ class AutomationConfigTools:
788
793
 
789
794
  missing_fields = [f for f in required_fields if f not in config_dict]
790
795
  if missing_fields:
796
+ # If the caller supplied a 'sequence' key, the config looks like a
797
+ # script — point them at ha_config_set_script instead of the generic
798
+ # missing-fields error.
799
+ if "sequence" in config_dict and (
800
+ "trigger" in missing_fields or "action" in missing_fields
801
+ ):
802
+ context: dict[str, Any] = {"missing_fields": missing_fields}
803
+ if identifier:
804
+ context["identifier"] = identifier
805
+ raise_tool_error(create_error_response(
806
+ code=ErrorCode.CONFIG_MISSING_REQUIRED_FIELDS,
807
+ message=f"Missing required fields: {', '.join(missing_fields)}",
808
+ details=(
809
+ "Config contains 'sequence', which belongs to scripts. "
810
+ "Automations use 'trigger' and 'action'; scripts use 'sequence'."
811
+ ),
812
+ suggestions=[
813
+ "Did you mean ha_config_set_script? Scripts use 'sequence' directly.",
814
+ "For an automation, replace 'sequence' with 'action' and add a 'trigger'.",
815
+ ],
816
+ context=context,
817
+ ))
791
818
  raise_tool_error(create_config_error(
792
819
  f"Missing required fields: {', '.join(missing_fields)}",
793
820
  identifier=identifier,
794
821
  missing_fields=missing_fields,
795
822
  ))
796
823
 
824
+ # HA accepts conditions with 'platform' (trigger syntax) but then crashes
825
+ # with an unhelpful 500 rather than a 400 validation error.
826
+ for idx, cond in enumerate(coerce_to_list(config_dict.get("condition"))):
827
+ if not isinstance(cond, dict):
828
+ continue
829
+ if "platform" in cond and "condition" not in cond:
830
+ raise_tool_error(create_error_response(
831
+ code=ErrorCode.VALIDATION_INVALID_PARAMETER,
832
+ message=(
833
+ f"Condition at index {idx} uses 'platform' (trigger syntax). "
834
+ "Conditions use 'condition', not 'platform'."
835
+ ),
836
+ suggestions=[
837
+ f"Replace 'platform' with 'condition': "
838
+ f"{{'condition': '{cond['platform']}', ...}}",
839
+ "Triggers use 'platform'; conditions use 'condition'.",
840
+ ],
841
+ context={"condition_index": idx, "found_key": "platform"},
842
+ ))
843
+
797
844
  # Prevent duplicate creation when config contains an existing automation id
798
845
  if identifier is None and "id" in config_dict:
799
846
  existing_id = config_dict["id"]
@@ -801,16 +801,16 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
801
801
 
802
802
  EXAMPLES (menu-based types + tod, where first-call payload is non-obvious):
803
803
  - template sensor:
804
- ha_config_set_helper("template", "Room Temp",
804
+ ha_config_set_helper(helper_type="template", name="Room Temp",
805
805
  config={"next_step_id": "sensor",
806
806
  "state": "{{ states('sensor.x')|float }}",
807
807
  "unit_of_measurement": "°C"})
808
808
  - group (light):
809
- ha_config_set_helper("group", "Kitchen Lights",
809
+ ha_config_set_helper(helper_type="group", name="Kitchen Lights",
810
810
  config={"group_type": "light",
811
811
  "entities": ["light.a", "light.b"]})
812
812
  - tod (time-of-day indicator, cross-midnight OK):
813
- ha_config_set_helper("tod", "Quiet Hours",
813
+ ha_config_set_helper(helper_type="tod", name="Quiet Hours",
814
814
  config={"after_time": "22:00:00", "before_time": "07:00:00"})
815
815
 
816
816
  For complex schemas and per-type parameter details, use ha_get_helper_schema.
@@ -313,19 +313,20 @@ class ConfigScriptTools:
313
313
  - max: Maximum concurrent executions (for queued/parallel modes)
314
314
  - fields: Input parameters for the script
315
315
 
316
- IMPORTANT: The 'config' parameter must be passed as a proper dictionary/object.
316
+ SCRIPTS vs AUTOMATIONS: Scripts use 'sequence', NOT 'trigger' or 'action'.
317
+ If you need trigger-based execution, use ha_config_set_automation instead.
317
318
 
318
319
  EXAMPLES:
319
320
 
320
321
  Create basic delay script:
321
- ha_config_set_script("wait_script", {
322
+ ha_config_set_script(script_id="wait_script", config={
322
323
  "sequence": [{"delay": {"seconds": 5}}],
323
324
  "alias": "Wait 5 Seconds",
324
325
  "description": "Simple delay script"
325
326
  })
326
327
 
327
328
  Create service call script:
328
- ha_config_set_script("blink_light", {
329
+ ha_config_set_script(script_id="blink_light", config={
329
330
  "sequence": [
330
331
  {"service": "light.turn_on", "target": {"entity_id": "light.living_room"}},
331
332
  {"delay": {"seconds": 2}},
@@ -336,7 +337,7 @@ class ConfigScriptTools:
336
337
  })
337
338
 
338
339
  Create script with parameters:
339
- ha_config_set_script("backup_script", {
340
+ ha_config_set_script(script_id="backup_script", config={
340
341
  "alias": "Backup with Reference",
341
342
  "description": "Create backup with optional reference parameter",
342
343
  "fields": {
@@ -360,7 +361,7 @@ class ConfigScriptTools:
360
361
  })
361
362
 
362
363
  Update script:
363
- ha_config_set_script("morning_routine", {
364
+ ha_config_set_script(script_id="morning_routine", config={
364
365
  "sequence": [
365
366
  {"service": "light.turn_on", "target": {"area_id": "bedroom"}},
366
367
  {"service": "climate.set_temperature", "target": {"entity_id": "climate.bedroom"}, "data": {"temperature": 22}}
@@ -369,7 +370,7 @@ class ConfigScriptTools:
369
370
  })
370
371
 
371
372
  Create blueprint-based script:
372
- ha_config_set_script("notification_script", {
373
+ ha_config_set_script(script_id="notification_script", config={
373
374
  "alias": "My Notification Script",
374
375
  "use_blueprint": {
375
376
  "path": "notification_script.yaml",
@@ -381,7 +382,7 @@ class ConfigScriptTools:
381
382
  })
382
383
 
383
384
  Update blueprint script inputs:
384
- ha_config_set_script("notification_script", {
385
+ ha_config_set_script(script_id="notification_script", config={
385
386
  "alias": "My Notification Script",
386
387
  "use_blueprint": {
387
388
  "path": "notification_script.yaml",
@@ -532,6 +532,13 @@ async def apply_entity_category(
532
532
  )
533
533
 
534
534
 
535
+ def coerce_to_list(value: Any) -> list[Any]:
536
+ """Return value as a list: list → as-is, dict/other → [value], None/falsy → []."""
537
+ if isinstance(value, list):
538
+ return value
539
+ return [value] if value else []
540
+
541
+
535
542
  def merge_validation_meta(
536
543
  result: dict[str, Any], validation_meta: dict[str, Any]
537
544
  ) -> None:
@@ -111,19 +111,22 @@ def _build_proxy_descriptions(search_tool_name: str) -> dict[str, str]:
111
111
  "read": (
112
112
  f"Execute a read-only tool discovered via {search_tool_name}. "
113
113
  f"Safe — does not modify any data or state.\n"
114
- f"{_PROXY_PARAMS_SUFFIX}"
114
+ f"{_PROXY_PARAMS_SUFFIX}\n"
115
+ f'EXAMPLE: ha_call_read_tool(name="ha_get_history", arguments={{"entity_ids": "light.x", "start_time": "24h"}})'
115
116
  ),
116
117
  "write": (
117
118
  f"Execute a write tool discovered via {search_tool_name}. "
118
119
  f"Creates or updates data. Use for any tool that modifies "
119
120
  f"state but does not delete/remove resources.\n"
120
- f"{_PROXY_PARAMS_SUFFIX}"
121
+ f"{_PROXY_PARAMS_SUFFIX}\n"
122
+ f'EXAMPLE: ha_call_write_tool(name="ha_config_set_area", arguments={{"name": "Kitchen"}})'
121
123
  ),
122
124
  "delete": (
123
125
  f"Execute a delete/remove tool discovered via {search_tool_name}. "
124
126
  f"Permanently removes data. Use for tools that delete or "
125
127
  f"remove resources (areas, automations, devices, etc.).\n"
126
- f"{_PROXY_PARAMS_SUFFIX}"
128
+ f"{_PROXY_PARAMS_SUFFIX}\n"
129
+ f'EXAMPLE: ha_call_delete_tool(name="ha_config_remove_area", arguments={{"area_id": "old_area"}})'
127
130
  ),
128
131
  }
129
132
 
@@ -254,13 +257,47 @@ class CategorizedSearchTransform(BM25SearchTransform):
254
257
  async def categorized_call(
255
258
  name: Annotated[str, "The name of the tool to call"],
256
259
  arguments: Annotated[
257
- dict[str, Any] | None, "Arguments to pass to the tool"
260
+ dict[str, Any] | str | None, "Arguments to pass to the tool"
258
261
  ] = None,
259
262
  ctx: Context = None, # type: ignore[assignment]
260
263
  ) -> Any:
261
264
  # Rebuild category cache if catalog has changed
262
265
  await transform._rebuild_category_cache(ctx)
263
266
 
267
+ # Tolerate `arguments` passed as a JSON string — small models
268
+ # sometimes serialize it before sending. Parse once up front so
269
+ # downstream logic can assume a dict (or None).
270
+ if isinstance(arguments, str):
271
+ try:
272
+ parsed = json.loads(arguments)
273
+ except json.JSONDecodeError as e:
274
+ raise ToolError(json.dumps(create_error_response(
275
+ code=ErrorCode.VALIDATION_INVALID_JSON,
276
+ message=f"'arguments' is a string but not valid JSON: {e}",
277
+ suggestions=[
278
+ "Pass 'arguments' as an object, not a JSON string.",
279
+ ],
280
+ context={"proxy_used": proxy_name, "tool_name": name},
281
+ ))) from e
282
+ if not isinstance(parsed, dict):
283
+ raise ToolError(json.dumps(create_error_response(
284
+ code=ErrorCode.VALIDATION_INVALID_PARAMETER,
285
+ message=(
286
+ "'arguments' must be a JSON object "
287
+ f"(got {type(parsed).__name__})."
288
+ ),
289
+ suggestions=[
290
+ "Pass 'arguments' as an object (dict), not a list or scalar.",
291
+ ],
292
+ context={"proxy_used": proxy_name, "tool_name": name},
293
+ )))
294
+ logger.warning(
295
+ "Proxy %s received 'arguments' as a JSON string for tool %s — parsed as fallback",
296
+ proxy_name,
297
+ name,
298
+ )
299
+ arguments = parsed
300
+
264
301
  # Determine which category set to check
265
302
  if category == "read":
266
303
  allowed = transform._read_tools
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.3.0.dev399
3
+ Version: 7.3.0.dev400
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