ha-mcp-dev 7.5.0.dev570__tar.gz → 7.5.0.dev571__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 (113) hide show
  1. {ha_mcp_dev-7.5.0.dev570/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev571}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_categories.py +39 -3
  4. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_automations.py +72 -1
  5. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_dashboards.py +11 -2
  6. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_scripts.py +189 -30
  7. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_labels.py +93 -36
  8. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_registry.py +16 -4
  9. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_zones.py +113 -50
  10. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  11. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/LICENSE +0 -0
  12. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/MANIFEST.in +0 -0
  13. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/README.md +0 -0
  14. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/setup.cfg +0 -0
  15. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/__init__.py +0 -0
  16. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/__main__.py +0 -0
  17. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/_pypi_marker +0 -0
  18. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/_version.py +0 -0
  19. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/auth/__init__.py +0 -0
  20. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/auth/consent_form.py +0 -0
  21. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/auth/provider.py +0 -0
  22. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/__init__.py +0 -0
  23. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/rest_client.py +0 -0
  24. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/supervisor_client.py +0 -0
  25. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/websocket_client.py +0 -0
  26. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/client/websocket_listener.py +0 -0
  27. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/config.py +0 -0
  28. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/errors.py +0 -0
  29. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/py.typed +0 -0
  30. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  31. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  32. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  33. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  34. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  35. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  36. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  37. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  38. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  39. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  40. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  41. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  42. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  43. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  44. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  45. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  46. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  47. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  48. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  49. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  50. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  51. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  52. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/server.py +0 -0
  53. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/settings_ui.py +0 -0
  54. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/smoke_test.py +0 -0
  55. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/__init__.py +0 -0
  56. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/backup.py +0 -0
  57. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  58. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/device_control.py +0 -0
  59. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/enhanced.py +0 -0
  60. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/helpers.py +0 -0
  61. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/reference_validator.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/registry.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/smart_search.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_addons.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_areas.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_calendar.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_camera.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_code.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_energy.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_entities.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_groups.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_hacs.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_history.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_integrations.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_resources.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_search.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_service.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_services.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_system.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_todo.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_traces.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_updates.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_utility.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/tools/util_helpers.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/transforms/__init__.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/transforms/categorized_search.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/__init__.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/config_hash.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/data_paths.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/domain_handlers.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/operation_manager.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/python_sandbox.py +0 -0
  105. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp/utils/usage_logger.py +0 -0
  106. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  107. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  108. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  109. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  110. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  111. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/tests/__init__.py +0 -0
  112. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/tests/test_constants.py +0 -0
  113. {ha_mcp_dev-7.5.0.dev570 → ha_mcp_dev-7.5.0.dev571}/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.dev570
3
+ Version: 7.5.0.dev571
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.dev570"
7
+ version = "7.5.0.dev571"
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"
@@ -36,7 +36,11 @@ class CategoryTools:
36
36
  @tool(
37
37
  name="ha_config_get_category",
38
38
  tags={"Labels & Categories"},
39
- annotations={"idempotentHint": True, "readOnlyHint": True, "title": "Get Category"},
39
+ annotations={
40
+ "idempotentHint": True,
41
+ "readOnlyHint": True,
42
+ "title": "Get Category",
43
+ },
40
44
  )
41
45
  @log_tool_usage
42
46
  async def ha_config_get_category(
@@ -133,7 +137,7 @@ class CategoryTools:
133
137
  available_ids = [cat.get("category_id") for cat in categories[:10]]
134
138
  raise_tool_error(
135
139
  create_error_response(
136
- ErrorCode.ENTITY_NOT_FOUND,
140
+ ErrorCode.RESOURCE_NOT_FOUND,
137
141
  f"Category not found: {category_id}",
138
142
  context={
139
143
  "category_id": category_id,
@@ -247,6 +251,22 @@ class CategoryTools:
247
251
  "message": f"Successfully {action_past} category: {name}",
248
252
  }
249
253
  else:
254
+ error_str = str(result.get("error", "")).lower()
255
+ if "not found" in error_str or "doesn't exist" in error_str:
256
+ raise_tool_error(
257
+ create_error_response(
258
+ ErrorCode.RESOURCE_NOT_FOUND,
259
+ f"Category not found: {category_id}",
260
+ context={
261
+ "name": name,
262
+ "scope": scope,
263
+ "category_id": category_id,
264
+ },
265
+ suggestions=[
266
+ f"Use ha_config_get_category('{scope}') without category_id to see all categories",
267
+ ],
268
+ )
269
+ )
250
270
  raise_tool_error(
251
271
  create_error_response(
252
272
  ErrorCode.SERVICE_CALL_FAILED,
@@ -276,7 +296,11 @@ class CategoryTools:
276
296
  @tool(
277
297
  name="ha_config_remove_category",
278
298
  tags={"Labels & Categories"},
279
- annotations={"destructiveHint": True, "idempotentHint": True, "title": "Remove Category"},
299
+ annotations={
300
+ "destructiveHint": True,
301
+ "idempotentHint": True,
302
+ "title": "Remove Category",
303
+ },
280
304
  )
281
305
  @log_tool_usage
282
306
  async def ha_config_remove_category(
@@ -333,6 +357,18 @@ class CategoryTools:
333
357
  "message": f"Successfully deleted category: {category_id}",
334
358
  }
335
359
  else:
360
+ error_str = str(result.get("error", "")).lower()
361
+ if "not found" in error_str or "doesn't exist" in error_str:
362
+ raise_tool_error(
363
+ create_error_response(
364
+ ErrorCode.RESOURCE_NOT_FOUND,
365
+ f"Category not found: {category_id}",
366
+ context={"category_id": category_id, "scope": scope},
367
+ suggestions=[
368
+ f"Use ha_config_get_category('{scope}') without category_id to see all categories",
369
+ ],
370
+ )
371
+ )
336
372
  raise_tool_error(
337
373
  create_error_response(
338
374
  ErrorCode.SERVICE_CALL_FAILED,
@@ -13,6 +13,7 @@ from fastmcp.tools import tool
13
13
  from pydantic import Field
14
14
 
15
15
  from ..client.rest_client import (
16
+ HomeAssistantAPIError,
16
17
  HomeAssistantAuthError,
17
18
  HomeAssistantConnectionError,
18
19
  )
@@ -864,6 +865,13 @@ class AutomationConfigTools:
864
865
  except ToolError:
865
866
  raise
866
867
  except Exception as e:
868
+ # 404 during update only — create (identifier=None) never hits this branch.
869
+ if (
870
+ identifier
871
+ and isinstance(e, HomeAssistantAPIError)
872
+ and e.status_code == 404
873
+ ):
874
+ await self._raise_automation_not_found(identifier)
867
875
  suggestions = [
868
876
  "Check automation configuration format",
869
877
  "Ensure required fields: alias, trigger, action",
@@ -882,6 +890,57 @@ class AutomationConfigTools:
882
890
  suggestions=suggestions,
883
891
  )
884
892
 
893
+ async def _list_automation_entity_ids(self) -> list[str]:
894
+ """Best-effort list of automation entity_ids (up to 10) from the entity registry.
895
+
896
+ Used to populate ``available_automation_ids`` in RESOURCE_NOT_FOUND
897
+ error context. Returns an empty list on any failure — caller treats
898
+ absence as "no IDs to report" rather than failing the structured
899
+ error raise. The 10-entry cap lives here (not at the callers) so a
900
+ new call site can't accidentally bloat the error payload.
901
+ """
902
+ try:
903
+ result = await self._client.send_websocket_message(
904
+ {"type": "config/entity_registry/list"}
905
+ )
906
+ except Exception as e:
907
+ logger.debug("Failed to list automation entity_ids from registry: %s", e)
908
+ return []
909
+ entries = result.get("result", []) if isinstance(result, dict) else result
910
+ if not isinstance(entries, list):
911
+ return []
912
+ return [
913
+ entry["entity_id"]
914
+ for entry in entries
915
+ if isinstance(entry, dict)
916
+ and isinstance(entry.get("entity_id"), str)
917
+ and entry["entity_id"].startswith("automation.")
918
+ ][:10]
919
+
920
+ async def _raise_automation_not_found(self, identifier: str) -> None:
921
+ """Raise a structured RESOURCE_NOT_FOUND ToolError for a missing automation.
922
+
923
+ Single source of truth for the 404→RESOURCE_NOT_FOUND mapping used
924
+ by both the GET path (``_get_automation_config_internal``) and the
925
+ mutation paths (``ha_config_set_automation`` update branch,
926
+ ``ha_config_remove_automation``). Populates
927
+ ``available_automation_ids`` (up to 10) from the entity registry.
928
+ """
929
+ available_ids = await self._list_automation_entity_ids()
930
+ raise_tool_error(
931
+ create_error_response(
932
+ ErrorCode.RESOURCE_NOT_FOUND,
933
+ f"Automation not found: {identifier}",
934
+ context={
935
+ "automation_id": identifier,
936
+ "available_automation_ids": available_ids,
937
+ },
938
+ suggestions=[
939
+ "Use ha_search_entities(domain_filter='automation') to find existing automations"
940
+ ],
941
+ )
942
+ )
943
+
885
944
  async def _get_automation_config_internal(
886
945
  self, identifier: str
887
946
  ) -> tuple[dict[str, Any], str]:
@@ -889,8 +948,18 @@ class AutomationConfigTools:
889
948
 
890
949
  Returns (normalized_config, config_hash) tuple.
891
950
  Used internally by _fetch_and_verify_hash and ha_config_get_automation.
951
+
952
+ Raises a structured ``RESOURCE_NOT_FOUND`` ToolError via
953
+ ``_raise_automation_not_found`` when the REST client returns 404.
954
+ Other ``HomeAssistantAPIError`` instances propagate unchanged to
955
+ caller exception handlers (``exception_to_structured_error`` route).
892
956
  """
893
- config_result = await self._client.get_automation_config(identifier)
957
+ try:
958
+ config_result = await self._client.get_automation_config(identifier)
959
+ except HomeAssistantAPIError as e:
960
+ if e.status_code == 404:
961
+ await self._raise_automation_not_found(identifier)
962
+ raise
894
963
  normalized_config = _normalize_config_for_roundtrip(config_result)
895
964
  config_hash_value = compute_config_hash(normalized_config)
896
965
  return normalized_config, config_hash_value
@@ -1169,6 +1238,8 @@ class AutomationConfigTools:
1169
1238
  except ToolError:
1170
1239
  raise
1171
1240
  except Exception as e:
1241
+ if isinstance(e, HomeAssistantAPIError) and e.status_code == 404:
1242
+ await self._raise_automation_not_found(identifier)
1172
1243
  exception_to_structured_error(
1173
1244
  e,
1174
1245
  context={"identifier": identifier, "action": "delete"},
@@ -1530,8 +1530,13 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1530
1530
  ],
1531
1531
  context={"action": "delete"},
1532
1532
  )
1533
- resolved, _ = await _resolve_dashboard(client, url_path)
1533
+ resolved, dashboards = await _resolve_dashboard(client, url_path)
1534
1534
  if resolved is None:
1535
+ available_ids = [
1536
+ d.get("url_path")
1537
+ for d in (dashboards or [])[:10]
1538
+ if d.get("url_path")
1539
+ ]
1535
1540
  raise_tool_error(
1536
1541
  create_error_response(
1537
1542
  ErrorCode.RESOURCE_NOT_FOUND,
@@ -1541,7 +1546,11 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1541
1546
  "Use ha_config_get_dashboard(list_only=True) to see available dashboards",
1542
1547
  "YAML-mode and default dashboards are not deletable via this tool",
1543
1548
  ],
1544
- context={"action": "delete", "url_path": url_path},
1549
+ context={
1550
+ "action": "delete",
1551
+ "url_path": url_path,
1552
+ "available_dashboard_ids": available_ids,
1553
+ },
1545
1554
  )
1546
1555
  )
1547
1556
  resolved_id = resolved["id"]
@@ -13,6 +13,7 @@ from fastmcp.tools import tool
13
13
  from pydantic import Field
14
14
 
15
15
  from ..client.rest_client import (
16
+ HomeAssistantAPIError,
16
17
  HomeAssistantAuthError,
17
18
  HomeAssistantConnectionError,
18
19
  )
@@ -90,7 +91,10 @@ class ConfigScriptTools:
90
91
  async def ha_config_get_script(
91
92
  self,
92
93
  script_id: Annotated[
93
- str, Field(description="Script identifier (e.g., 'morning_routine')")
94
+ str,
95
+ Field(
96
+ description="Script identifier — bare storage key ('morning_routine') or entity_id form ('script.morning_routine'); a leading 'script.' prefix is stripped before lookup."
97
+ ),
94
98
  ],
95
99
  ) -> dict[str, Any]:
96
100
  """
@@ -100,15 +104,28 @@ class ConfigScriptTools:
100
104
 
101
105
  The returned `config_hash` is stable across consecutive reads of an unchanged config — `compute_config_hash` documents the underlying contract.
102
106
 
103
- The returned `script_id` is the canonical storage key resolved by the REST client (matching what `ha_config_set_script` / `ha_config_remove_script` expect), falling back to the input identifier on the rare path where the REST envelope omits it.
107
+ The returned `script_id` is the canonical bare storage key resolved by the REST client (matching what `ha_config_set_script` / `ha_config_remove_script` expect), falling back to the input identifier on the rare path where the REST envelope omits it. A leading `script.` prefix on the input is stripped before lookup — behavioral parity with `ha_config_get_automation` (mechanism differs: automations resolve via state lookup; scripts strip the prefix).
104
108
 
105
109
  EXAMPLES:
106
- - Get script: ha_config_get_script("morning_routine")
107
- - Get script: ha_config_get_script("backup_script")
110
+ - Get script (bare form): ha_config_get_script("morning_routine")
111
+ - Get script (entity_id form): ha_config_get_script("script.morning_routine")
108
112
 
109
113
  For detailed script configuration help, use ha_get_skill_guide.
110
114
  """
111
115
  try:
116
+ # Strip BEFORE validate so a bare ``"script."`` (empty after
117
+ # strip) is rejected as ``VALIDATION_INVALID_PARAMETER`` rather
118
+ # than slipping through validate (non-empty pre-strip) and
119
+ # 404-ing at ``get_script_config("")``. Accept entity_id form
120
+ # (``script.foo``) and bare storage key (``foo``) — behavioral
121
+ # parity with ``ha_config_get_automation`` (mechanism differs:
122
+ # automations resolve via state lookup; scripts strip the
123
+ # prefix). ``_raise_script_not_found`` suggests
124
+ # ``ha_search_entities(domain_filter='script')`` which returns
125
+ # entity_ids; without this strip, feeding that output back into
126
+ # the GET tool fails and reseeds the wrong-tool spiral that
127
+ # #1297 closes.
128
+ script_id = script_id.removeprefix("script.")
112
129
  # Empty/whitespace script_id would propagate to
113
130
  # ``get_script_config`` and surface as a misleading
114
131
  # ``RESOURCE_NOT_FOUND``. Extension of the #1312
@@ -122,7 +139,7 @@ class ConfigScriptTools:
122
139
  "Use ha_search_entities(domain_filter='script') to list scripts",
123
140
  ],
124
141
  )
125
- config_result = await self._client.get_script_config(script_id)
142
+ config_result = await self._fetch_script_config_envelope(script_id)
126
143
  # Extract actual script config body and compute hash before category injection
127
144
  actual_config = config_result.get("config", config_result)
128
145
  config_hash_value = compute_config_hash(actual_config)
@@ -170,6 +187,76 @@ class ConfigScriptTools:
170
187
  ],
171
188
  )
172
189
 
190
+ async def _list_script_entity_ids(self) -> list[str]:
191
+ """Best-effort list of bare script IDs (up to 10) from the entity registry.
192
+
193
+ Returns the bare storage keys (e.g. ``morning_routine``), stripping
194
+ the ``script.`` entity_id prefix — ``ha_config_get_script`` /
195
+ ``ha_config_set_script`` / ``ha_config_remove_script`` all take the
196
+ bare form, so the entity_id prefix would force callers to strip it
197
+ before retry. Returns an empty list on any failure — caller treats
198
+ absence as "no IDs to report" rather than failing the structured
199
+ error raise. The 10-entry cap lives here (not at the callers) so a
200
+ new call site can't accidentally bloat the error payload.
201
+ """
202
+ try:
203
+ result = await self._client.send_websocket_message(
204
+ {"type": "config/entity_registry/list"}
205
+ )
206
+ except Exception as e:
207
+ logger.debug("Failed to list script entity_ids from registry: %s", e)
208
+ return []
209
+ entries = result.get("result", []) if isinstance(result, dict) else result
210
+ if not isinstance(entries, list):
211
+ return []
212
+ return [
213
+ entry["entity_id"][len("script.") :]
214
+ for entry in entries
215
+ if isinstance(entry, dict)
216
+ and isinstance(entry.get("entity_id"), str)
217
+ and entry["entity_id"].startswith("script.")
218
+ ][:10]
219
+
220
+ async def _raise_script_not_found(self, script_id: str) -> None:
221
+ """Raise a structured RESOURCE_NOT_FOUND ToolError for a missing script.
222
+
223
+ Single source of truth for the 404→RESOURCE_NOT_FOUND mapping used
224
+ by the GET path (``_fetch_script_config_envelope``) and the
225
+ mutation paths (``ha_config_set_script`` update branch,
226
+ ``ha_config_remove_script``). Populates ``available_script_ids``
227
+ (up to 10 bare IDs) from the entity registry.
228
+ """
229
+ available_ids = await self._list_script_entity_ids()
230
+ raise_tool_error(
231
+ create_error_response(
232
+ ErrorCode.RESOURCE_NOT_FOUND,
233
+ f"Script not found: {script_id}",
234
+ context={
235
+ "script_id": script_id,
236
+ "available_script_ids": available_ids,
237
+ },
238
+ suggestions=[
239
+ "Use ha_search_entities(domain_filter='script') to find existing scripts"
240
+ ],
241
+ )
242
+ )
243
+
244
+ async def _fetch_script_config_envelope(self, script_id: str) -> dict[str, Any]:
245
+ """Fetch the raw REST envelope, mapping 404 to RESOURCE_NOT_FOUND.
246
+
247
+ Returns the dict envelope from ``rest_client.get_script_config``
248
+ (``success``/``script_id``/``config`` keys). Raises a structured
249
+ ``RESOURCE_NOT_FOUND`` ToolError via ``_raise_script_not_found`` on
250
+ 404. Other ``HomeAssistantAPIError`` instances propagate unchanged
251
+ to caller exception handlers.
252
+ """
253
+ try:
254
+ return cast(dict[str, Any], await self._client.get_script_config(script_id))
255
+ except HomeAssistantAPIError as e:
256
+ if e.status_code == 404:
257
+ await self._raise_script_not_found(script_id)
258
+ raise
259
+
173
260
  async def _get_script_config_internal(
174
261
  self, script_id: str
175
262
  ) -> tuple[dict[str, Any], str]:
@@ -178,8 +265,11 @@ class ConfigScriptTools:
178
265
  Returns (actual_config, config_hash) tuple where actual_config is
179
266
  the inner script body (not the REST wrapper).
180
267
  Used internally by _fetch_and_verify_hash and ha_config_get_script.
268
+
269
+ 404 responses from the REST client are mapped to a structured
270
+ ``RESOURCE_NOT_FOUND`` ToolError via ``_fetch_script_config_envelope``.
181
271
  """
182
- config_result = await self._client.get_script_config(script_id)
272
+ config_result = await self._fetch_script_config_envelope(script_id)
183
273
  actual_config = config_result.get("config", config_result)
184
274
  config_hash_value = compute_config_hash(actual_config)
185
275
  return actual_config, config_hash_value
@@ -223,19 +313,29 @@ class ConfigScriptTools:
223
313
  try:
224
314
  parsed_config = parse_json_param(config, "config")
225
315
  except ValueError as e:
226
- raise_tool_error(create_error_response(
227
- ErrorCode.VALIDATION_INVALID_JSON,
228
- f"Invalid config parameter: {e}",
229
- context={"script_id": script_id, "provided_config_type": type(config).__name__},
230
- ))
316
+ raise_tool_error(
317
+ create_error_response(
318
+ ErrorCode.VALIDATION_INVALID_JSON,
319
+ f"Invalid config parameter: {e}",
320
+ context={
321
+ "script_id": script_id,
322
+ "provided_config_type": type(config).__name__,
323
+ },
324
+ )
325
+ )
231
326
 
232
327
  # Ensure config is a dict
233
328
  if parsed_config is None or not isinstance(parsed_config, dict):
234
- raise_tool_error(create_error_response(
235
- ErrorCode.VALIDATION_INVALID_PARAMETER,
236
- "Config parameter must be a JSON object",
237
- context={"script_id": script_id, "provided_type": type(parsed_config).__name__},
238
- ))
329
+ raise_tool_error(
330
+ create_error_response(
331
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
332
+ "Config parameter must be a JSON object",
333
+ context={
334
+ "script_id": script_id,
335
+ "provided_type": type(parsed_config).__name__,
336
+ },
337
+ )
338
+ )
239
339
 
240
340
  config_dict = cast(dict[str, Any], parsed_config)
241
341
 
@@ -250,11 +350,16 @@ class ConfigScriptTools:
250
350
  # Strip empty sequence array that would override blueprint
251
351
  config_dict = _strip_empty_script_fields(config_dict)
252
352
  elif "sequence" not in config_dict:
253
- raise_tool_error(create_error_response(
254
- ErrorCode.VALIDATION_MISSING_PARAMETER,
255
- "config must include either 'sequence' field (for regular scripts) or 'use_blueprint' field (for blueprint-based scripts)",
256
- context={"script_id": script_id, "required_fields": ["sequence OR use_blueprint"]},
257
- ))
353
+ raise_tool_error(
354
+ create_error_response(
355
+ ErrorCode.VALIDATION_MISSING_PARAMETER,
356
+ "config must include either 'sequence' field (for regular scripts) or 'use_blueprint' field (for blueprint-based scripts)",
357
+ context={
358
+ "script_id": script_id,
359
+ "required_fields": ["sequence OR use_blueprint"],
360
+ },
361
+ )
362
+ )
258
363
 
259
364
  return config_dict, effective_category
260
365
 
@@ -270,7 +375,10 @@ class ConfigScriptTools:
270
375
  async def ha_config_set_script(
271
376
  self,
272
377
  script_id: Annotated[
273
- str, Field(description="Script identifier (e.g., 'morning_routine')")
378
+ str,
379
+ Field(
380
+ description="Script identifier — bare storage key ('morning_routine') or entity_id form ('script.morning_routine'); a leading 'script.' prefix is stripped before lookup."
381
+ ),
274
382
  ],
275
383
  config: Annotated[
276
384
  str | dict[str, Any] | None,
@@ -447,6 +555,18 @@ class ConfigScriptTools:
447
555
  """
448
556
  bp_warnings: list[str] = []
449
557
  try:
558
+ # Strip BEFORE validate so a bare ``"script."`` (empty after
559
+ # strip) is rejected as ``VALIDATION_INVALID_PARAMETER`` rather
560
+ # than slipping through validate (non-empty pre-strip) and
561
+ # writing a phantom ``script.foo`` storage key — HA keys writes
562
+ # by the literal ``script_id``, so passing ``"script.foo"``
563
+ # unchanged makes the row invisible to a later
564
+ # ``ha_config_get_script("foo")``. Behavioral parity with
565
+ # ``ha_config_get_script`` so an agent that received an
566
+ # entity_id (``script.foo``) from
567
+ # ``ha_search_entities(domain_filter='script')`` can update it
568
+ # without a manual prefix-strip step.
569
+ script_id = script_id.removeprefix("script.")
450
570
  # ``script_id`` is required (always non-None). Reject empty/
451
571
  # whitespace up-front so the caller gets a structured parameter
452
572
  # error instead of a misleading ``RESOURCE_NOT_FOUND`` from
@@ -488,7 +608,10 @@ class ConfigScriptTools:
488
608
  "Call ha_config_get_script() first",
489
609
  "Use the config_hash from that response",
490
610
  ],
491
- context={"action": "python_transform", "script_id": script_id},
611
+ context={
612
+ "action": "python_transform",
613
+ "script_id": script_id,
614
+ },
492
615
  )
493
616
  )
494
617
 
@@ -507,12 +630,18 @@ class ConfigScriptTools:
507
630
  ErrorCode.VALIDATION_FAILED,
508
631
  message,
509
632
  suggestions=suggestions,
510
- context={"action": "python_transform", "script_id": script_id},
633
+ context={
634
+ "action": "python_transform",
635
+ "script_id": script_id,
636
+ },
511
637
  )
512
638
  )
513
639
 
514
640
  # Validate transformed config
515
- if "sequence" not in transformed_config and "use_blueprint" not in transformed_config:
641
+ if (
642
+ "sequence" not in transformed_config
643
+ and "use_blueprint" not in transformed_config
644
+ ):
516
645
  raise_tool_error(
517
646
  create_error_response(
518
647
  ErrorCode.VALIDATION_FAILED,
@@ -521,7 +650,10 @@ class ConfigScriptTools:
521
650
  "The transform may have removed required fields",
522
651
  "Ensure the config still has a 'sequence' or 'use_blueprint' key",
523
652
  ],
524
- context={"action": "python_transform", "script_id": script_id},
653
+ context={
654
+ "action": "python_transform",
655
+ "script_id": script_id,
656
+ },
525
657
  )
526
658
  )
527
659
  bp_warnings = _check_best_practices(transformed_config)
@@ -562,7 +694,9 @@ class ConfigScriptTools:
562
694
  )
563
695
 
564
696
  config_dict, effective_category = self._validate_script_config(
565
- config, script_id, category,
697
+ config,
698
+ script_id,
699
+ category,
566
700
  )
567
701
 
568
702
  # Optional hash check for full config updates
@@ -586,7 +720,9 @@ class ConfigScriptTools:
586
720
  entity_id = f"script.{script_id}"
587
721
  if wait_bool:
588
722
  try:
589
- registered = await wait_for_entity_registered(self._client, entity_id)
723
+ registered = await wait_for_entity_registered(
724
+ self._client, entity_id
725
+ )
590
726
  if not registered:
591
727
  result.setdefault("warnings", []).append(
592
728
  f"Script saved but {entity_id} not yet queryable. "
@@ -600,7 +736,12 @@ class ConfigScriptTools:
600
736
  # Apply category to entity registry if provided
601
737
  if effective_category and entity_id:
602
738
  await apply_entity_category(
603
- self._client, entity_id, effective_category, "script", result, "script"
739
+ self._client,
740
+ entity_id,
741
+ effective_category,
742
+ "script",
743
+ result,
744
+ "script",
604
745
  )
605
746
 
606
747
  if bp_warnings:
@@ -629,6 +770,11 @@ class ConfigScriptTools:
629
770
  "Config had best-practice issues that may be related: "
630
771
  + "; ".join(bp_warnings)
631
772
  )
773
+ # 404 during update only — the create path raises on its own when
774
+ # the upsert hits an unknown identifier server-side. The bare
775
+ # script_id form is what callers pass and what the registry stores.
776
+ if isinstance(e, HomeAssistantAPIError) and e.status_code == 404:
777
+ await self._raise_script_not_found(script_id)
632
778
  exception_to_structured_error(
633
779
  e,
634
780
  context={"script_id": script_id},
@@ -648,7 +794,10 @@ class ConfigScriptTools:
648
794
  async def ha_config_remove_script(
649
795
  self,
650
796
  script_id: Annotated[
651
- str, Field(description="Script identifier to delete (e.g., 'old_script')")
797
+ str,
798
+ Field(
799
+ description="Script identifier to delete — bare storage key ('old_script') or entity_id form ('script.old_script'); a leading 'script.' prefix is stripped before lookup."
800
+ ),
652
801
  ],
653
802
  wait: Annotated[
654
803
  bool | str,
@@ -675,6 +824,14 @@ class ConfigScriptTools:
675
824
  **WARNING:** Deleting a script that is used by automations may cause those automations to fail.
676
825
  """
677
826
  try:
827
+ # Strip BEFORE validate so a bare ``"script."`` (empty after
828
+ # strip) is rejected as ``VALIDATION_INVALID_PARAMETER`` rather
829
+ # than slipping through validate (non-empty pre-strip) and
830
+ # producing a ``script.script.foo`` entity_id for the
831
+ # ``wait_for_entity_removed`` watcher below — that mis-formed
832
+ # entity_id never registers so the watcher times out on a
833
+ # phantom. Behavioral parity with ``ha_config_get_script``.
834
+ script_id = script_id.removeprefix("script.")
678
835
  # Empty/whitespace would surface as a misleading HA delete-failure.
679
836
  validate_identifier_not_empty(
680
837
  script_id,
@@ -705,6 +862,8 @@ class ConfigScriptTools:
705
862
  except ToolError:
706
863
  raise
707
864
  except Exception as e:
865
+ if isinstance(e, HomeAssistantAPIError) and e.status_code == 404:
866
+ await self._raise_script_not_found(script_id)
708
867
  exception_to_structured_error(
709
868
  e,
710
869
  context={"script_id": script_id},