ha-mcp-dev 7.5.0.dev549__tar.gz → 7.5.0.dev551__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 (111) hide show
  1. {ha_mcp_dev-7.5.0.dev549/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev551}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/backup.py +3 -1
  4. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_addons.py +2 -2
  5. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_config_automations.py +59 -6
  6. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_energy.py +3 -3
  7. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_entities.py +27 -10
  8. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_integrations.py +4 -4
  9. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_search.py +1 -1
  10. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_service.py +6 -6
  11. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_system.py +7 -5
  12. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  13. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/LICENSE +0 -0
  14. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/MANIFEST.in +0 -0
  15. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/README.md +0 -0
  16. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/setup.cfg +0 -0
  17. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/__init__.py +0 -0
  18. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/__main__.py +0 -0
  19. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/_pypi_marker +0 -0
  20. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/_version.py +0 -0
  21. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/auth/__init__.py +0 -0
  22. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/auth/consent_form.py +0 -0
  23. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/auth/provider.py +0 -0
  24. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/client/__init__.py +0 -0
  25. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/client/rest_client.py +0 -0
  26. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/client/supervisor_client.py +0 -0
  27. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/client/websocket_client.py +0 -0
  28. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/client/websocket_listener.py +0 -0
  29. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/config.py +0 -0
  30. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/errors.py +0 -0
  31. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/py.typed +0 -0
  32. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  33. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  34. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  35. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  36. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  37. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  38. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  39. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  40. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  41. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  42. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  43. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  44. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  45. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  46. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  47. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  48. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  49. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  50. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  51. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  52. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/server.py +0 -0
  53. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/settings_ui.py +0 -0
  54. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/smoke_test.py +0 -0
  55. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/__init__.py +0 -0
  56. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  57. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/device_control.py +0 -0
  58. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/enhanced.py +0 -0
  59. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/helpers.py +0 -0
  60. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/reference_validator.py +0 -0
  61. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/registry.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/smart_search.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_areas.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_calendar.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_camera.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_categories.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_code.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_groups.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_hacs.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_history.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_labels.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_registry.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_resources.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_services.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_todo.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_traces.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_updates.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_utility.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/tools_zones.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/tools/util_helpers.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/transforms/__init__.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/transforms/categorized_search.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/utils/__init__.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/utils/config_hash.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/utils/data_paths.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/utils/domain_handlers.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/utils/operation_manager.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/utils/python_sandbox.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp/utils/usage_logger.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  105. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  106. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  107. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  108. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  109. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/tests/__init__.py +0 -0
  110. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/tests/test_constants.py +0 -0
  111. {ha_mcp_dev-7.5.0.dev549 → ha_mcp_dev-7.5.0.dev551}/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.dev549
3
+ Version: 7.5.0.dev551
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.dev549"
7
+ version = "7.5.0.dev551"
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"
@@ -450,7 +450,9 @@ async def restore_backup(
450
450
  "status": "Restore initiated - Home Assistant will restart",
451
451
  "safety_backup_id": safety_backup_id,
452
452
  "restore_database": restore_database,
453
- "warning": "Home Assistant is restarting. Connection will be temporarily lost.",
453
+ "warnings": [
454
+ "Home Assistant is restarting. Connection will be temporarily lost."
455
+ ],
454
456
  "note": "A safety backup was created before restore. You can restore from it if needed.",
455
457
  }
456
458
  else:
@@ -1272,7 +1272,7 @@ def _apply_array_ops(
1272
1272
  and inspectable
1273
1273
  and not any(field in it for it in inspectable)
1274
1274
  ):
1275
- entry["warning"] = (
1275
+ entry.setdefault("warnings", []).append(
1276
1276
  f"field {field!r} is not present on any item — "
1277
1277
  "check for a typo in the field name"
1278
1278
  )
@@ -2212,7 +2212,7 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
2212
2212
  "submitted_fields": submitted_fields,
2213
2213
  }
2214
2214
  if ignored_fields:
2215
- response["warning"] = (
2215
+ response.setdefault("warnings", []).append(
2216
2216
  f"{len(ignored_fields)} field(s) not in add-on schema were ignored "
2217
2217
  f"before write: {ignored_fields}. Use ha_get_addon(slug) to see the "
2218
2218
  "declared schema."
@@ -231,6 +231,30 @@ def _normalize_config_for_roundtrip(config: dict[str, Any]) -> dict[str, Any]:
231
231
  return cast(dict[str, Any], normalized)
232
232
 
233
233
 
234
+ def _strip_redundant_identifier_echo(
235
+ result: dict[str, Any],
236
+ *,
237
+ extra_excludes: tuple[str, ...] = (),
238
+ ) -> dict[str, Any]:
239
+ """Strip the redundant ``identifier`` echo from an upsert/delete response.
240
+
241
+ The canonical ``automation_id`` key (resolved entity_id, falling back to
242
+ input identifier or ``unique_id``) makes re-echoing the raw ``identifier``
243
+ redundant noise.
244
+
245
+ ``unique_id`` is intentionally retained — it's HA's internal identifier,
246
+ distinct from ``entity_id``/``automation_id``, and callers track it for
247
+ cleanup. Do not extend ``extra_excludes`` to ``"unique_id"``: that
248
+ regression broke E2E ``test_duplicate_automation_prevention`` at 5fe5338.
249
+
250
+ ``extra_excludes`` lets a call site drop additional internal keys the
251
+ spread shouldn't surface (e.g. ``"success"`` on the python_transform
252
+ branch, where the caller manages that key directly).
253
+ """
254
+ excluded = {"identifier", *extra_excludes}
255
+ return {k: v for k, v in result.items() if k not in excluded}
256
+
257
+
234
258
  class AutomationConfigTools:
235
259
  """Configuration management tools for Home Assistant automations."""
236
260
 
@@ -284,7 +308,9 @@ class AutomationConfigTools:
284
308
 
285
309
  The returned `config_hash` is stable across consecutive reads of an unchanged config — `compute_config_hash` documents the underlying contract.
286
310
 
287
- The returned `automation_id` is the resolved entity_id (canonical form, e.g. `automation.morning_routine`) when the registry lookup succeeds, falling back to the input `identifier` otherwise.
311
+ The returned `automation_id` is the resolved entity_id (canonical
312
+ form, e.g. `automation.morning_routine`) when the registry lookup
313
+ succeeds, falling back to the input `identifier` otherwise.
288
314
 
289
315
  EXAMPLES:
290
316
  - Get automation: ha_config_get_automation("automation.morning_routine")
@@ -406,6 +432,12 @@ class AutomationConfigTools:
406
432
  """
407
433
  Create or update a Home Assistant automation.
408
434
 
435
+ The returned `automation_id` is the resolved entity_id (canonical
436
+ form, e.g. `automation.morning_routine`) when entity registration
437
+ succeeds, falling back to the input `identifier` (update path) or
438
+ the generated `unique_id` from the upsert response (fresh create
439
+ when no identifier was passed).
440
+
409
441
  Before reaching for ``ha_config_set_automation``, consider whether a
410
442
  dedicated tool fits the use case better:
411
443
 
@@ -666,12 +698,15 @@ class AutomationConfigTools:
666
698
  response: dict[str, Any] = {
667
699
  "success": True,
668
700
  "action": "python_transform",
669
- "identifier": identifier,
701
+ "automation_id": (
702
+ entity_id or identifier or result.get("unique_id")
703
+ ),
670
704
  "config_hash": new_config_hash,
671
705
  "python_expression": python_transform,
672
706
  "message": f"Automation {identifier} updated via Python transform",
673
- # Merge upsert result, excluding "success" (we set it ourselves)
674
- **{k: v for k, v in result.items() if k != "success"},
707
+ **_strip_redundant_identifier_echo(
708
+ result, extra_excludes=("success",)
709
+ ),
675
710
  }
676
711
  if bp_warnings:
677
712
  response["best_practice_warnings"] = bp_warnings
@@ -761,9 +796,15 @@ class AutomationConfigTools:
761
796
 
762
797
  merge_validation_meta(result, validation_meta)
763
798
 
799
+ automation_id = entity_id or identifier or result.get("unique_id")
764
800
  return {
765
801
  "success": True,
766
- **result,
802
+ # automation_id omitted when all three fallbacks are falsy —
803
+ # the create path is unguarded by validate_identifier_not_empty,
804
+ # and surfacing automation_id=None would lie about resolvability.
805
+ # HA's upsert contract makes this branch unreachable in practice.
806
+ **({"automation_id": automation_id} if automation_id else {}),
807
+ **_strip_redundant_identifier_echo(result),
767
808
  }
768
809
 
769
810
  except ToolError:
@@ -1001,6 +1042,11 @@ class AutomationConfigTools:
1001
1042
  """
1002
1043
  Delete a Home Assistant automation.
1003
1044
 
1045
+ The returned `automation_id` is the resolved entity_id (canonical
1046
+ form, e.g. `automation.morning_routine`) when the registry lookup
1047
+ succeeded before the delete, falling back to the input
1048
+ `identifier` otherwise.
1049
+
1004
1050
  EXAMPLES:
1005
1051
  - Delete automation: ha_config_remove_automation("automation.old_automation")
1006
1052
  - Delete by unique_id: ha_config_remove_automation("my_unique_id")
@@ -1040,7 +1086,14 @@ class AutomationConfigTools:
1040
1086
  f"Deletion confirmed but removal verification failed: {e}"
1041
1087
  )
1042
1088
 
1043
- return {"success": True, "action": "delete", **result}
1089
+ return {
1090
+ "success": True,
1091
+ "action": "delete",
1092
+ "automation_id": (
1093
+ entity_id_for_wait or identifier or result.get("unique_id")
1094
+ ),
1095
+ **_strip_redundant_identifier_echo(result),
1096
+ }
1044
1097
  except ToolError:
1045
1098
  raise
1046
1099
  except Exception as e:
@@ -673,7 +673,7 @@ class EnergyTools:
673
673
  }
674
674
  if validate_warning is not None:
675
675
  response["partial"] = True
676
- response["warning"] = validate_warning
676
+ response.setdefault("warnings", []).append(validate_warning)
677
677
  return response
678
678
 
679
679
  except ToolError:
@@ -953,14 +953,14 @@ class EnergyTools:
953
953
  }
954
954
  if post_save_errors:
955
955
  response["post_save_validation_errors"] = post_save_errors
956
- response["warning"] = (
956
+ response.setdefault("warnings", []).append(
957
957
  f"Save succeeded, but the persisted config has "
958
958
  f"{len(post_save_errors)} validation error(s). Review "
959
959
  "and re-write if any relate to this change."
960
960
  )
961
961
  elif post_save_validate_error is not None:
962
962
  response["partial"] = True
963
- response["warning"] = (
963
+ response.setdefault("warnings", []).append(
964
964
  f"Save succeeded, but post-save energy/validate "
965
965
  f"failed: {post_save_validate_error}. The persisted "
966
966
  "config has not been re-validated."
@@ -376,16 +376,27 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
376
376
  _extract_ws_error(get_result),
377
377
  )
378
378
  device_rename_result = {
379
- "warning": "Entity registry lookup failed — could not determine device. Retry may succeed.",
379
+ "warnings": [
380
+ "Entity registry lookup failed — could not determine device. Retry may succeed."
381
+ ],
382
+ "lookup_failed": True,
380
383
  }
381
384
 
382
385
  device_id = (
383
386
  entity_entry.get("device_id") if not device_rename_result else None
384
387
  )
385
388
  if not device_id:
386
- device_rename_result = {
387
- "warning": "Entity has no associated device device rename skipped",
388
- }
389
+ # Only fire the "no device" warning when the registry lookup
390
+ # succeeded otherwise the L378 "lookup failed" warning
391
+ # already carries the more accurate signal, and a second
392
+ # "no associated device" claim would be unverified (we don't
393
+ # actually know what the registry says when the lookup failed).
394
+ if device_rename_result is None:
395
+ device_rename_result = {
396
+ "warnings": [
397
+ "Entity has no associated device — device rename skipped"
398
+ ],
399
+ }
389
400
  else:
390
401
  device_msg: dict[str, Any] = {
391
402
  "type": "config/device_registry/update",
@@ -397,7 +408,9 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
397
408
  device_rename_result = {"success": True, "device_id": device_id}
398
409
  else:
399
410
  device_rename_result = {
400
- "warning": f"Entity updated but device rename failed: {_extract_ws_error(device_result)}",
411
+ "warnings": [
412
+ f"Entity updated but device rename failed: {_extract_ws_error(device_result)}"
413
+ ],
401
414
  "device_id": device_id,
402
415
  }
403
416
 
@@ -511,7 +524,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
511
524
  # Include old_entity_id and rename warning when a rename was performed
512
525
  if new_entity_id is not None:
513
526
  response_data["old_entity_id"] = original_entity_id
514
- response_data["warning"] = (
527
+ response_data.setdefault("warnings", []).append(
515
528
  "Remember to update any automations, scripts, or dashboards "
516
529
  "that reference the old entity_id"
517
530
  )
@@ -521,10 +534,14 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
521
534
 
522
535
  if device_rename_result is not None:
523
536
  response_data["device_rename"] = device_rename_result
524
- # Only mark partial when device rename was attempted and failed
525
- # (not when entity simply has no device)
526
- if "warning" in device_rename_result and device_rename_result.get(
527
- "device_id"
537
+ # Mark partial when a device rename was requested but didn't complete
538
+ # for an operational reason: WS-call failure (device_id present + warnings)
539
+ # or upstream registry lookup failure (lookup_failed marker). Not partial
540
+ # when the entity simply has no device — that's a no-op, not an incomplete
541
+ # operation.
542
+ if device_rename_result.get("warnings") and (
543
+ device_rename_result.get("device_id")
544
+ or device_rename_result.get("lookup_failed")
528
545
  ):
529
546
  response_data["partial"] = True
530
547
 
@@ -1237,13 +1237,13 @@ class IntegrationTools:
1237
1237
  if res is not True
1238
1238
  ]
1239
1239
  if not_removed:
1240
- response["warning"] = (
1240
+ response.setdefault("warnings", []).append(
1241
1241
  f"Deletion confirmed but the following entities "
1242
1242
  f"are still present after the wait window: "
1243
1243
  f"{not_removed}"
1244
1244
  )
1245
1245
  if warnings:
1246
- response["warnings"] = warnings
1246
+ response.setdefault("warnings", []).extend(warnings)
1247
1247
  return response
1248
1248
 
1249
1249
  except ToolError:
@@ -1388,7 +1388,7 @@ class IntegrationTools:
1388
1388
  client, entity_id
1389
1389
  )
1390
1390
  if not removed:
1391
- response["warning"] = (
1391
+ response.setdefault("warnings", []).append(
1392
1392
  f"Deletion confirmed but {entity_id} "
1393
1393
  "is still present after the wait window."
1394
1394
  )
@@ -1531,7 +1531,7 @@ class IntegrationTools:
1531
1531
  client, entity_id
1532
1532
  )
1533
1533
  if not removed:
1534
- response["warning"] = (
1534
+ response.setdefault("warnings", []).append(
1535
1535
  f"Deletion confirmed but {entity_id} "
1536
1536
  "is still present after the wait window."
1537
1537
  )
@@ -736,7 +736,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
736
736
 
737
737
  # Add warning and partial flag if fallback was used
738
738
  if warning:
739
- result["warning"] = warning
739
+ result.setdefault("warnings", []).append(warning)
740
740
  result["partial"] = True
741
741
 
742
742
  return await add_timezone_metadata(client, result)
@@ -145,14 +145,14 @@ class ServiceTools:
145
145
  f"did not respond within the timeout period. The operation is likely "
146
146
  f"still running in the background."
147
147
  ),
148
- "warning": (
148
+ "warnings": [
149
149
  "Response timed out. This is normal for long-running services "
150
150
  f"like updates or firmware installs. Use ha_get_state('{entity_id}') "
151
151
  "to check the current status."
152
152
  if entity_id
153
153
  else "Response timed out. This is normal for long-running services. "
154
154
  "The service was dispatched and may still be executing."
155
- ),
155
+ ],
156
156
  }
157
157
 
158
158
  async def _capture_initial_state(self, entity_id: str | None) -> str | None:
@@ -186,11 +186,11 @@ class ServiceTools:
186
186
  if new_state:
187
187
  response["verified_state"] = new_state.get("state")
188
188
  else:
189
- response["warning"] = (
189
+ response.setdefault("warnings", []).append(
190
190
  "Service executed but state change could not be verified within timeout."
191
191
  )
192
192
  except Exception as e:
193
- response["warning"] = (
193
+ response.setdefault("warnings", []).append(
194
194
  f"Service executed but state verification failed: {e}"
195
195
  )
196
196
 
@@ -512,10 +512,10 @@ class ServiceTools:
512
512
  f"Event {event_type} was dispatched but Home Assistant "
513
513
  "did not respond within the timeout period."
514
514
  ),
515
- "warning": (
515
+ "warnings": [
516
516
  "Response timed out. The event was dispatched and may still "
517
517
  "have been delivered to subscribers."
518
- ),
518
+ ],
519
519
  }
520
520
  exception_to_structured_error(
521
521
  error,
@@ -186,10 +186,10 @@ class SystemTools:
186
186
  "Home Assistant restart initiated. "
187
187
  "The system will be unavailable for 1-5 minutes."
188
188
  ),
189
- "warning": (
189
+ "warnings": [
190
190
  "Connection will be lost during restart. "
191
191
  "Wait for Home Assistant to become available again."
192
- ),
192
+ ],
193
193
  }
194
194
 
195
195
  except ToolError:
@@ -208,7 +208,7 @@ class SystemTools:
208
208
  "Home Assistant restart initiated. "
209
209
  "Connection was closed as expected during restart."
210
210
  ),
211
- "warning": "Wait 1-5 minutes for Home Assistant to restart.",
211
+ "warnings": ["Wait 1-5 minutes for Home Assistant to restart."],
212
212
  }
213
213
 
214
214
  exception_to_structured_error(e)
@@ -299,12 +299,14 @@ class SystemTools:
299
299
  if "not found" not in error_msg.lower():
300
300
  errors.append(f"{reload_target}: {error_msg}")
301
301
 
302
- return {
302
+ response: dict[str, Any] = {
303
303
  "success": True,
304
304
  "message": f"Reloaded {len(results)} components",
305
305
  "reloaded": results,
306
- "warnings": errors if errors else None,
307
306
  }
307
+ if errors:
308
+ response["warnings"] = errors
309
+ return response
308
310
 
309
311
  else:
310
312
  # Reload specific component
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.5.0.dev549
3
+ Version: 7.5.0.dev551
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