ha-mcp-dev 7.2.0.dev319__tar.gz → 7.2.0.dev321__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 (100) hide show
  1. {ha_mcp_dev-7.2.0.dev319/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.2.0.dev321}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/pyproject.toml +2 -1
  3. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_automations.py +58 -14
  4. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_helpers.py +43 -1
  5. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_scripts.py +27 -0
  6. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_yaml_config.py +2 -2
  7. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/util_helpers.py +69 -0
  8. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  9. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/LICENSE +0 -0
  10. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/MANIFEST.in +0 -0
  11. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/README.md +0 -0
  12. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/setup.cfg +0 -0
  13. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/__main__.py +0 -0
  15. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/_pypi_marker +0 -0
  16. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/auth/__init__.py +0 -0
  17. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/auth/consent_form.py +0 -0
  18. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/auth/provider.py +0 -0
  19. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/client/__init__.py +0 -0
  20. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/client/rest_client.py +0 -0
  21. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/client/websocket_client.py +0 -0
  22. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/client/websocket_listener.py +0 -0
  23. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/config.py +0 -0
  24. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/errors.py +0 -0
  25. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/py.typed +0 -0
  26. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  27. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  28. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  29. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  30. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  31. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  32. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  33. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  34. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  35. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  36. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  37. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  38. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  39. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  40. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  41. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  42. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  43. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  44. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  45. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  46. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/server.py +0 -0
  47. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/smoke_test.py +0 -0
  48. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/__init__.py +0 -0
  49. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/backup.py +0 -0
  50. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  51. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/device_control.py +0 -0
  52. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/enhanced.py +0 -0
  53. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/helpers.py +0 -0
  54. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/registry.py +0 -0
  55. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/smart_search.py +0 -0
  56. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_addons.py +0 -0
  57. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_areas.py +0 -0
  58. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  59. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  60. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_calendar.py +0 -0
  61. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_camera.py +0 -0
  62. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_categories.py +0 -0
  63. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  64. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  65. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_entities.py +0 -0
  66. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  67. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_groups.py +0 -0
  68. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_hacs.py +0 -0
  69. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_history.py +0 -0
  70. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_integrations.py +0 -0
  71. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_labels.py +0 -0
  72. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  73. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_registry.py +0 -0
  74. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_resources.py +0 -0
  75. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_search.py +0 -0
  76. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_service.py +0 -0
  77. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_services.py +0 -0
  78. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_system.py +0 -0
  79. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_todo.py +0 -0
  80. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_traces.py +0 -0
  81. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_updates.py +0 -0
  82. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_utility.py +0 -0
  83. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  84. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/tools/tools_zones.py +0 -0
  85. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/transforms/__init__.py +0 -0
  86. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/transforms/categorized_search.py +0 -0
  87. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/utils/__init__.py +0 -0
  88. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/utils/domain_handlers.py +0 -0
  89. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  90. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/utils/operation_manager.py +0 -0
  91. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/utils/python_sandbox.py +0 -0
  92. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp/utils/usage_logger.py +0 -0
  93. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  94. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  95. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  96. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  97. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  98. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/tests/__init__.py +0 -0
  99. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/tests/test_constants.py +0 -0
  100. {ha_mcp_dev-7.2.0.dev319 → ha_mcp_dev-7.2.0.dev321}/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.2.0.dev319
3
+ Version: 7.2.0.dev321
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.2.0.dev319"
7
+ version = "7.2.0.dev321"
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"
@@ -175,6 +175,7 @@ dev = [
175
175
  "pytest-xdist>=3.8.0",
176
176
  "requests>=2.25.0",
177
177
  "lefthook>=1.10.0",
178
+ "ruamel.yaml>=0.18.0",
178
179
  "ruff>=0.12.12",
179
180
  "testcontainers>=4.13.0",
180
181
  "ast-grep-cli>=0.42.0",
@@ -24,7 +24,9 @@ from .best_practice_checker import (
24
24
  )
25
25
  from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
26
26
  from .util_helpers import (
27
+ apply_entity_category,
27
28
  coerce_bool_param,
29
+ fetch_entity_category,
28
30
  parse_json_param,
29
31
  wait_for_entity_registered,
30
32
  wait_for_entity_removed,
@@ -205,6 +207,27 @@ def _strip_empty_automation_fields(config: dict[str, Any]) -> dict[str, Any]:
205
207
  def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
206
208
  """Register Home Assistant automation configuration tools."""
207
209
 
210
+ async def _resolve_automation_entity_id(identifier: str) -> str | None:
211
+ """Resolve an automation identifier to its entity_id.
212
+
213
+ If identifier is already an entity_id (starts with "automation."),
214
+ returns it directly. Otherwise, searches states to find the entity
215
+ whose unique_id matches the identifier.
216
+ """
217
+ if identifier.startswith("automation."):
218
+ return identifier
219
+ try:
220
+ states = await client.get_states()
221
+ for state in states:
222
+ if (
223
+ state.get("entity_id", "").startswith("automation.")
224
+ and state.get("attributes", {}).get("id") == identifier
225
+ ):
226
+ return str(state["entity_id"])
227
+ except Exception as e:
228
+ logger.debug(f"Failed to resolve entity_id for automation {identifier}: {e}")
229
+ return None
230
+
208
231
  @mcp.tool(
209
232
  tags={"Automations"},
210
233
  annotations={
@@ -237,6 +260,14 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
237
260
  config_result = await client.get_automation_config(identifier)
238
261
  # Normalize config for round-trip compatibility (GET → SET)
239
262
  normalized_config = _normalize_config_for_roundtrip(config_result)
263
+
264
+ # Resolve entity_id and fetch category from entity registry
265
+ entity_id = await _resolve_automation_entity_id(identifier)
266
+ if entity_id:
267
+ cat_id = await fetch_entity_category(client, entity_id, "automation")
268
+ if cat_id:
269
+ normalized_config["category"] = cat_id
270
+
240
271
  return {
241
272
  "success": True,
242
273
  "action": "get",
@@ -296,6 +327,13 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
296
327
  default=None,
297
328
  ),
298
329
  ] = None,
330
+ category: Annotated[
331
+ str | None,
332
+ Field(
333
+ description="Category ID to assign to this automation. Use ha_config_get_category(scope='automation') to list available categories, or ha_config_set_category() to create one.",
334
+ default=None,
335
+ ),
336
+ ] = None,
299
337
  wait: Annotated[
300
338
  bool | str,
301
339
  Field(
@@ -327,6 +365,7 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
327
365
 
328
366
  OPTIONAL CONFIG FIELDS (Regular Automations):
329
367
  - description: Detailed description of the user's intent (RECOMMENDED: helps safely modify implementation later)
368
+ - category: Category ID for organization (use ha_config_get_category to list, ha_config_set_category to create)
330
369
  - condition: Additional conditions that must be met
331
370
  - mode: 'single' (default), 'restart', 'queued', 'parallel'
332
371
  - max: Maximum concurrent executions (for queued/parallel modes)
@@ -444,6 +483,11 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
444
483
 
445
484
  config_dict = cast(dict[str, Any], parsed_config)
446
485
 
486
+ # Extract category before sending to HA REST API (which rejects unknown keys).
487
+ # Parameter takes precedence over config dict value.
488
+ config_category = config_dict.pop("category", None)
489
+ effective_category = category if category is not None else config_category
490
+
447
491
  # Normalize field names (triggers -> trigger, actions -> action, etc.)
448
492
  config_dict = _normalize_automation_config(config_dict)
449
493
 
@@ -499,6 +543,9 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
499
543
  # Wait for automation to be queryable
500
544
  wait_bool = coerce_bool_param(wait, "wait", default=True)
501
545
  entity_id = result.get("entity_id")
546
+ # On updates, entity_id may not be in the result — derive from identifier
547
+ if not entity_id and identifier and identifier.startswith("automation."):
548
+ entity_id = identifier
502
549
  if wait_bool and entity_id:
503
550
  try:
504
551
  registered = await wait_for_entity_registered(client, entity_id)
@@ -507,6 +554,12 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
507
554
  except Exception as e:
508
555
  result["warning"] = f"Automation created but verification failed: {e}"
509
556
 
557
+ # Apply category to entity registry if provided
558
+ if effective_category and entity_id:
559
+ await apply_entity_category(
560
+ client, entity_id, effective_category, "automation", result, "automation"
561
+ )
562
+
510
563
  if bp_warnings:
511
564
  result["best_practice_warnings"] = bp_warnings
512
565
 
@@ -572,20 +625,11 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
572
625
  """
573
626
  try:
574
627
  # Resolve entity_id for wait verification (identifier may be a unique_id)
575
- entity_id_for_wait: str | None = None
576
- if identifier.startswith("automation."):
577
- entity_id_for_wait = identifier
578
- else:
579
- # Try to find entity_id by matching unique_id in automation states
580
- try:
581
- states = await client.get_states()
582
- for state in states:
583
- eid = state.get("entity_id", "")
584
- if eid.startswith("automation.") and state.get("attributes", {}).get("id") == identifier:
585
- entity_id_for_wait = eid
586
- break
587
- except Exception as e:
588
- logger.warning(f"Could not resolve unique_id '{identifier}' to entity_id: {e} — wait verification will be skipped")
628
+ entity_id_for_wait = await _resolve_automation_entity_id(identifier)
629
+ if not entity_id_for_wait:
630
+ logger.warning(
631
+ f"Could not resolve unique_id '{identifier}' to entity_id — wait verification will be skipped"
632
+ )
589
633
 
590
634
  result = await client.delete_automation_config(identifier)
591
635
 
@@ -16,6 +16,7 @@ from pydantic import Field
16
16
  from ..errors import ErrorCode, create_error_response
17
17
  from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
18
18
  from .util_helpers import (
19
+ apply_entity_category,
19
20
  coerce_bool_param,
20
21
  parse_string_list_param,
21
22
  wait_for_entity_registered,
@@ -405,6 +406,13 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
405
406
  default=None,
406
407
  ),
407
408
  ] = None,
409
+ category: Annotated[
410
+ str | None,
411
+ Field(
412
+ description="Category ID to assign to this helper. Use ha_config_get_category(scope='helpers') to list available categories, or ha_config_set_category() to create one.",
413
+ default=None,
414
+ ),
415
+ ] = None,
408
416
  wait: Annotated[
409
417
  bool | str,
410
418
  Field(
@@ -632,6 +640,9 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
632
640
  if result.get("success"):
633
641
  helper_data = result.get("result", {})
634
642
  entity_id = helper_data.get("entity_id")
643
+ # Some helper types don't return entity_id — derive from result id
644
+ if not entity_id and helper_data.get("id"):
645
+ entity_id = f"{helper_type}.{helper_data['id']}"
635
646
 
636
647
  # Wait for entity to be properly registered before proceeding
637
648
  wait_bool = coerce_bool_param(wait, "wait", default=True)
@@ -660,6 +671,18 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
660
671
  if update_result.get("success"):
661
672
  helper_data["area_id"] = area_id
662
673
  helper_data["labels"] = labels
674
+ else:
675
+ error_detail = update_result.get("error", {})
676
+ error_msg = error_detail.get("message", "Unknown error") if isinstance(error_detail, dict) else str(error_detail)
677
+ helper_data["warning"] = (
678
+ f"Helper created but entity registry update failed: {error_msg}"
679
+ )
680
+
681
+ # Apply category via shared helper (consistent with automations/scripts)
682
+ if category and entity_id:
683
+ await apply_entity_category(
684
+ client, entity_id, category, "helpers", helper_data, "helper"
685
+ )
663
686
 
664
687
  return {
665
688
  "success": True,
@@ -890,7 +913,20 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
890
913
  registry_update["area_id"] = area_id
891
914
  if labels:
892
915
  registry_update["labels"] = labels
893
- await client.send_websocket_message(registry_update)
916
+ reg_result = await client.send_websocket_message(registry_update)
917
+ if not reg_result.get("success"):
918
+ error_detail = reg_result.get("error", {})
919
+ error_msg = error_detail.get("message", "Unknown error") if isinstance(error_detail, dict) else str(error_detail)
920
+ logger.warning(f"Entity registry update failed for {entity_id}: {error_msg}")
921
+ updated_data["warning"] = (
922
+ f"Config updated but entity registry update failed: {error_msg}"
923
+ )
924
+
925
+ # Apply category via shared helper
926
+ if category:
927
+ await apply_entity_category(
928
+ client, entity_id, category, "helpers", updated_data, "helper"
929
+ )
894
930
 
895
931
  else:
896
932
  # Standard helpers: entity registry update only
@@ -919,6 +955,12 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
919
955
  context={"helper_type": helper_type, "entity_id": entity_id},
920
956
  ))
921
957
 
958
+ # Apply category via shared helper
959
+ if category:
960
+ await apply_entity_category(
961
+ client, entity_id, category, "helpers", updated_data, "helper"
962
+ )
963
+
922
964
  # Wait for entity to reflect the update
923
965
  wait_bool = coerce_bool_param(wait, "wait", default=True)
924
966
  response: dict[str, Any] = {
@@ -20,7 +20,9 @@ from .best_practice_checker import (
20
20
  )
21
21
  from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
22
22
  from .util_helpers import (
23
+ apply_entity_category,
23
24
  coerce_bool_param,
25
+ fetch_entity_category,
24
26
  parse_json_param,
25
27
  wait_for_entity_registered,
26
28
  wait_for_entity_removed,
@@ -82,6 +84,13 @@ def register_config_script_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
82
84
  """
83
85
  try:
84
86
  config_result = await client.get_script_config(script_id)
87
+
88
+ # Fetch category from entity registry (best-effort)
89
+ entity_id = f"script.{script_id}"
90
+ cat_id = await fetch_entity_category(client, entity_id, "script")
91
+ if cat_id:
92
+ config_result["category"] = cat_id
93
+
85
94
  return {
86
95
  "success": True,
87
96
  "action": "get",
@@ -119,6 +128,13 @@ def register_config_script_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
119
128
  description="Script configuration dictionary. Must include EITHER 'sequence' (for regular scripts) OR 'use_blueprint' (for blueprint-based scripts). Optional fields: 'alias', 'description', 'icon', 'mode', 'max', 'fields'"
120
129
  ),
121
130
  ],
131
+ category: Annotated[
132
+ str | None,
133
+ Field(
134
+ description="Category ID to assign to this script. Use ha_config_get_category(scope='script') to list available categories, or ha_config_set_category() to create one.",
135
+ default=None,
136
+ ),
137
+ ] = None,
122
138
  wait: Annotated[
123
139
  bool | str,
124
140
  Field(
@@ -259,6 +275,11 @@ def register_config_script_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
259
275
 
260
276
  config_dict = cast(dict[str, Any], parsed_config)
261
277
 
278
+ # Extract category before sending to HA REST API (which rejects unknown keys).
279
+ # Parameter takes precedence over config dict value.
280
+ config_category = config_dict.pop("category", None)
281
+ effective_category = category if category is not None else config_category
282
+
262
283
  # Validate required fields based on script type
263
284
  # Blueprint scripts only need use_blueprint, regular scripts need sequence
264
285
  if "use_blueprint" in config_dict:
@@ -289,6 +310,12 @@ def register_config_script_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
289
310
  except Exception as e:
290
311
  result["warning"] = f"Script created but verification failed: {e}"
291
312
 
313
+ # Apply category to entity registry if provided
314
+ if effective_category and entity_id:
315
+ await apply_entity_category(
316
+ client, entity_id, effective_category, "script", result, "script"
317
+ )
318
+
292
319
  if bp_warnings:
293
320
  result["best_practice_warnings"] = bp_warnings
294
321
 
@@ -123,8 +123,8 @@ def register_yaml_config_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
123
123
  a full HA restart ('restart_required'). Only template, mqtt, and
124
124
  group support reload ('reload_available' with 'reload_service').
125
125
 
126
- Note: YAML comments are not preserved after editing. The backup
127
- retains the original file with comments intact.
126
+ YAML comments and Home Assistant tags (!include, !secret, etc.)
127
+ are preserved through edits.
128
128
  """
129
129
  try:
130
130
  # Validate action
@@ -426,3 +426,72 @@ async def wait_for_state_change(
426
426
 
427
427
  logger.warning(f"Entity {entity_id} state did not change within {timeout}s")
428
428
  return None
429
+
430
+
431
+ async def fetch_entity_category(
432
+ client: Any, entity_id: str, scope: str
433
+ ) -> str | None:
434
+ """Fetch a category ID for an entity from the entity registry.
435
+
436
+ Args:
437
+ client: HomeAssistantClient instance
438
+ entity_id: Entity to look up (e.g., 'automation.morning_routine')
439
+ scope: Category scope (e.g., 'automation', 'script', 'helpers')
440
+
441
+ Returns:
442
+ Category ID string if set, None otherwise
443
+ """
444
+ try:
445
+ result = await client.send_websocket_message(
446
+ {"type": "config/entity_registry/get", "entity_id": entity_id}
447
+ )
448
+ if result.get("success"):
449
+ categories = result.get("result", {}).get("categories", {})
450
+ cat_id = categories.get(scope)
451
+ return str(cat_id) if cat_id is not None else None
452
+ except Exception as e:
453
+ logger.warning(f"Failed to fetch category for {entity_id}: {e}")
454
+ return None
455
+
456
+
457
+ async def apply_entity_category(
458
+ client: Any,
459
+ entity_id: str,
460
+ category: str,
461
+ scope: str,
462
+ result_dict: dict[str, Any],
463
+ entity_type: str = "entity",
464
+ ) -> None:
465
+ """Apply a category to an entity via the entity registry.
466
+
467
+ Updates result_dict in-place with 'category' on success or
468
+ 'category_warning' on failure.
469
+
470
+ Args:
471
+ client: HomeAssistantClient instance
472
+ entity_id: Entity to update
473
+ category: Category ID to assign
474
+ scope: Category scope (e.g., 'automation', 'script')
475
+ result_dict: Tool result dict to update with category status
476
+ entity_type: Human-readable type for warning messages
477
+ """
478
+ try:
479
+ ws_result = await client.send_websocket_message({
480
+ "type": "config/entity_registry/update",
481
+ "entity_id": entity_id,
482
+ "categories": {scope: category},
483
+ })
484
+ if ws_result.get("success"):
485
+ result_dict["category"] = category
486
+ else:
487
+ error_detail = ws_result.get("error", {})
488
+ error_msg = error_detail.get("message", "Unknown error") if isinstance(error_detail, dict) else str(error_detail)
489
+ logger.warning(f"Failed to set category for {entity_id}: {error_msg}")
490
+ result_dict["category_warning"] = (
491
+ f"{entity_type.capitalize()} saved but failed to set category: {error_msg}"
492
+ )
493
+ except Exception as e:
494
+ logger.warning(f"Failed to set category for {entity_id}: {e}")
495
+ result_dict["category_warning"] = (
496
+ f"{entity_type.capitalize()} saved but failed to set category: {e}"
497
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.2.0.dev319
3
+ Version: 7.2.0.dev321
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