ha-mcp-dev 7.4.1.dev466__tar.gz → 7.4.1.dev468__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 (109) hide show
  1. {ha_mcp_dev-7.4.1.dev466/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev468}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/server.py +8 -0
  4. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_addons.py +10 -8
  5. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_config_dashboards.py +59 -22
  6. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  7. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/LICENSE +0 -0
  8. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/MANIFEST.in +0 -0
  9. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/README.md +0 -0
  10. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/setup.cfg +0 -0
  11. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/__init__.py +0 -0
  12. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/__main__.py +0 -0
  13. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/_pypi_marker +0 -0
  14. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/_version.py +0 -0
  15. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/auth/__init__.py +0 -0
  16. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/auth/consent_form.py +0 -0
  17. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/auth/provider.py +0 -0
  18. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/client/__init__.py +0 -0
  19. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/client/rest_client.py +0 -0
  20. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/client/websocket_client.py +0 -0
  21. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/client/websocket_listener.py +0 -0
  22. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/config.py +0 -0
  23. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/errors.py +0 -0
  24. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/py.typed +0 -0
  25. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  26. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  27. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  28. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  29. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  30. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  31. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  32. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  33. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  34. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  35. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  36. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  37. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  38. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  39. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  40. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  41. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  42. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  43. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  44. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  45. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/settings_ui.py +0 -0
  46. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/smoke_test.py +0 -0
  47. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/__init__.py +0 -0
  48. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/backup.py +0 -0
  49. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  50. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/device_control.py +0 -0
  51. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/enhanced.py +0 -0
  52. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/helpers.py +0 -0
  53. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/reference_validator.py +0 -0
  54. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/registry.py +0 -0
  55. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/smart_search.py +0 -0
  56. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_areas.py +0 -0
  57. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  58. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  59. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_calendar.py +0 -0
  60. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_camera.py +0 -0
  61. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_categories.py +0 -0
  62. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_code.py +0 -0
  63. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  64. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  65. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  66. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  67. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  68. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_energy.py +0 -0
  69. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_entities.py +0 -0
  70. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  71. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_groups.py +0 -0
  72. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_hacs.py +0 -0
  73. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_history.py +0 -0
  74. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_integrations.py +0 -0
  75. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_labels.py +0 -0
  76. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  77. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_registry.py +0 -0
  78. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_resources.py +0 -0
  79. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_search.py +0 -0
  80. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_service.py +0 -0
  81. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_services.py +0 -0
  82. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_system.py +0 -0
  83. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_todo.py +0 -0
  84. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_traces.py +0 -0
  85. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_updates.py +0 -0
  86. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_utility.py +0 -0
  87. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  88. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  89. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/tools_zones.py +0 -0
  90. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/tools/util_helpers.py +0 -0
  91. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/transforms/__init__.py +0 -0
  92. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/transforms/categorized_search.py +0 -0
  93. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/utils/__init__.py +0 -0
  94. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/utils/config_hash.py +0 -0
  95. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/utils/data_paths.py +0 -0
  96. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/utils/domain_handlers.py +0 -0
  97. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  98. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  99. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/utils/operation_manager.py +0 -0
  100. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/utils/python_sandbox.py +0 -0
  101. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp/utils/usage_logger.py +0 -0
  102. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  103. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  104. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  105. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  106. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  107. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/tests/__init__.py +0 -0
  108. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/tests/test_constants.py +0 -0
  109. {ha_mcp_dev-7.4.1.dev466 → ha_mcp_dev-7.4.1.dev468}/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.4.1.dev466
3
+ Version: 7.4.1.dev468
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.4.1.dev466"
7
+ version = "7.4.1.dev468"
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"
@@ -525,6 +525,14 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
525
525
  "binary_sensor command_line rest mqtt platform yaml-only "
526
526
  "config file modify add remove replace"
527
527
  ),
528
+ "ha_manage_addon": (
529
+ "manage addon add-on configure settings options port network boot "
530
+ "watchdog auto_update supervisor ingress proxy websocket api rest "
531
+ "esphome nodered node-red frigate mosquitto mqtt zigbee2mqtt zigbee "
532
+ "z-wave zwave appdaemon hacs studio code server file editor terminal "
533
+ "ssh samba grafana influxdb deconz motioneye compile validate upload "
534
+ "deploy firmware ota flash yaml device logs flows events stats"
535
+ ),
528
536
  }
529
537
 
530
538
  # Description overrides that REPLACE the original description for BM25.
@@ -507,7 +507,7 @@ async def get_addon_info(client: HomeAssistantClient, slug: str) -> dict[str, An
507
507
 
508
508
  Args:
509
509
  client: Home Assistant REST client
510
- slug: Add-on slug (e.g., "a0d7b954_nodered")
510
+ slug: Add-on slug (e.g., "<prefix>_nodered")
511
511
 
512
512
  Returns:
513
513
  Dictionary with add-on details including ingress info, state, options, etc.
@@ -740,7 +740,7 @@ async def _call_addon_ws(
740
740
 
741
741
  Args:
742
742
  client: Home Assistant REST client
743
- slug: Add-on slug (e.g., "5c53de3b_esphome")
743
+ slug: Add-on slug (e.g., "<prefix>_esphome")
744
744
  path: WebSocket endpoint path (e.g., "/compile", "/validate")
745
745
  body: Message to send after connecting (JSON-encoded if dict, raw if string)
746
746
  timeout: Max seconds to wait for messages (default 60)
@@ -1081,7 +1081,7 @@ async def _call_addon_api(
1081
1081
 
1082
1082
  Args:
1083
1083
  client: Home Assistant REST client
1084
- slug: Add-on slug (e.g., "a0d7b954_nodered")
1084
+ slug: Add-on slug (e.g., "<prefix>_nodered")
1085
1085
  path: API path relative to add-on root (e.g., "/flows")
1086
1086
  method: HTTP method (GET, POST, PUT, DELETE, PATCH)
1087
1087
  body: Request body for POST/PUT/PATCH
@@ -1349,8 +1349,9 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
1349
1349
  slug: Annotated[
1350
1350
  str | None,
1351
1351
  Field(
1352
- description="Add-on slug for detailed info (e.g., 'a0d7b954_nodered'). "
1353
- "Omit to list all add-ons.",
1352
+ description="Add-on slug for detailed info (e.g., '<prefix>_nodered'). "
1353
+ "Slug prefixes vary by add-on repository — omit to list all add-ons "
1354
+ "and discover the actual installed slug.",
1354
1355
  default=None,
1355
1356
  ),
1356
1357
  ] = None,
@@ -1402,7 +1403,7 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
1402
1403
 
1403
1404
  **Example Usage:**
1404
1405
  - List installed add-ons: ha_get_addon()
1405
- - Get Node-RED details: ha_get_addon(slug="a0d7b954_nodered")
1406
+ - Get Node-RED details: ha_get_addon(slug="<prefix>_nodered")
1406
1407
  - List with resource usage: ha_get_addon(include_stats=True)
1407
1408
  - List available add-ons: ha_get_addon(source="available")
1408
1409
  - Search for MQTT: ha_get_addon(source="available", query="mqtt")
@@ -1448,8 +1449,9 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
1448
1449
  slug: Annotated[
1449
1450
  str,
1450
1451
  Field(
1451
- description="Add-on slug (e.g., 'a0d7b954_nodered', 'ccab4aaf_frigate'). "
1452
- "Use ha_get_addon() to find installed add-on slugs.",
1452
+ description="Add-on slug (e.g., '<prefix>_nodered', '<prefix>_frigate'). "
1453
+ "Slug prefixes vary by add-on repository — call ha_get_addon() "
1454
+ "to discover the actual installed slug.",
1453
1455
  ),
1454
1456
  ],
1455
1457
  path: Annotated[
@@ -265,22 +265,37 @@ def _should_lazy_resolve(error_msg: str) -> bool:
265
265
  return _LAZY_RESOLVE_TRIGGER in error_msg
266
266
 
267
267
 
268
- async def _resolve_dashboard(client: Any, identifier: str) -> dict[str, str] | None:
268
+ async def _resolve_dashboard(
269
+ client: Any, identifier: str
270
+ ) -> tuple[dict[str, str] | None, list[dict[str, Any]] | None]:
269
271
  """Resolve a dashboard identifier (url_path or internal id) to both forms.
270
272
 
271
- Calls ``lovelace/dashboards/list`` and returns
272
- ``{"url_path": ..., "id": ...}`` when the identifier matches either field
273
- on a registry entry that has both fields populated; otherwise returns
274
- ``None``. Always pays the round-trip when called.
273
+ Calls ``lovelace/dashboards/list`` and returns a 2-tuple
274
+ ``(match, dashboards)``:
275
275
 
276
- Two call sites:
276
+ - ``match`` is ``{"url_path": ..., "id": ...}`` when the identifier
277
+ matches either field on a registry entry that has both fields
278
+ populated; otherwise ``None``.
279
+ - ``dashboards`` is the raw list as returned by HA when the
280
+ response shape is recognised (dict-with-``result`` or bare list);
281
+ ``None`` when the shape was unexpected and a warning was logged.
282
+
283
+ Returning ``dashboards`` alongside ``match`` lets callers reuse the
284
+ list for follow-on checks (existence, id lookup) instead of paying
285
+ a second ``lovelace/dashboards/list`` round-trip.
286
+
287
+ Three call sites:
277
288
  - **Lazy fallback** (``_lazy_resolve_and_retry``): only invoked after
278
289
  ``lovelace/config`` rejected the identifier with
279
290
  ``_LAZY_RESOLVE_TRIGGER`` — the round-trip is gated by the caller.
291
+ Discards ``dashboards``.
280
292
  - **Eager pre-resolve** (``ha_config_set_dashboard``): invoked before
281
293
  hyphen validation so callers may pass either form; gated on a
282
294
  cheap heuristic ("no hyphen, not 'lovelace'") rather than an error
283
- from HA.
295
+ from HA. Reuses ``dashboards`` for the existence-check below.
296
+ - **Delete** (``ha_config_delete_dashboard``): resolves either form
297
+ to the registry id before issuing the delete. Discards
298
+ ``dashboards``.
284
299
  """
285
300
  result = await client.send_websocket_message({"type": "lovelace/dashboards/list"})
286
301
  if isinstance(result, dict) and "result" in result:
@@ -297,7 +312,7 @@ async def _resolve_dashboard(client: Any, identifier: str) -> dict[str, str] | N
297
312
  "treating as no-match",
298
313
  type(result).__name__,
299
314
  )
300
- return None
315
+ return None, None
301
316
 
302
317
  for d in dashboards:
303
318
  if d.get("id") == identifier or d.get("url_path") == identifier:
@@ -309,8 +324,8 @@ async def _resolve_dashboard(client: Any, identifier: str) -> dict[str, str] | N
309
324
  # would be silently used by callers (e.g.
310
325
  # ``delete_dashboard`` would forward ``resolved_id=""``).
311
326
  continue
312
- return {"url_path": url_path, "id": entry_id}
313
- return None
327
+ return {"url_path": url_path, "id": entry_id}, dashboards
328
+ return None, dashboards
314
329
 
315
330
 
316
331
  @overload
@@ -376,7 +391,7 @@ async def _lazy_resolve_and_retry(
376
391
  return url_path, response
377
392
 
378
393
  try:
379
- resolved = await _resolve_dashboard(client, url_path)
394
+ resolved, _ = await _resolve_dashboard(client, url_path)
380
395
  except Exception as resolver_exc:
381
396
  # Resolver itself raised (timeout, network blip, etc.). Don't let
382
397
  # this exception escape and replace the original HA error with
@@ -941,12 +956,18 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
941
956
  # ``resolved_from`` on the success response so callers can
942
957
  # detect this redirect.
943
958
  pre_resolved_from: str | None = None
959
+ # When the pre-resolver fires and finds a match, ``_resolve_dashboard``
960
+ # has already fetched ``lovelace/dashboards/list``. Capture that list
961
+ # so the existence-check site below can reuse it instead of paying
962
+ # a second round-trip.
963
+ pre_fetched_dashboards: list[dict[str, Any]] | None = None
944
964
  if "-" not in url_path and url_path != "lovelace":
945
- resolved = await _resolve_dashboard(client, url_path)
965
+ resolved, dashboards = await _resolve_dashboard(client, url_path)
946
966
  if resolved is not None and resolved["url_path"]:
947
967
  original_url_path = url_path
948
968
  url_path = resolved["url_path"]
949
969
  pre_resolved_from = original_url_path
970
+ pre_fetched_dashboards = dashboards
950
971
  logger.info(
951
972
  "ha_config_set_dashboard pre-resolver mapped %r -> %r",
952
973
  original_url_path,
@@ -1129,16 +1150,32 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1129
1150
  transform_result["resolved_from"] = pre_resolved_from
1130
1151
  return transform_result
1131
1152
 
1132
- # Check if dashboard exists
1133
- result = await client.send_websocket_message(
1134
- {"type": "lovelace/dashboards/list"}
1135
- )
1136
- if isinstance(result, dict) and "result" in result:
1137
- existing_dashboards = result["result"]
1138
- elif isinstance(result, list):
1139
- existing_dashboards = result
1153
+ # Check if dashboard exists. When the pre-resolver fired
1154
+ # and matched (internal-id branch), reuse its already-fetched
1155
+ # ``lovelace/dashboards/list`` response to skip a redundant
1156
+ # round-trip — the matched dashboard is guaranteed present in
1157
+ # that list.
1158
+ if pre_fetched_dashboards is not None:
1159
+ existing_dashboards = pre_fetched_dashboards
1140
1160
  else:
1141
- existing_dashboards = []
1161
+ result = await client.send_websocket_message(
1162
+ {"type": "lovelace/dashboards/list"}
1163
+ )
1164
+ if isinstance(result, dict) and "result" in result:
1165
+ existing_dashboards = result["result"]
1166
+ elif isinstance(result, list):
1167
+ existing_dashboards = result
1168
+ else:
1169
+ # Mirror the warning emitted by ``_resolve_dashboard`` on
1170
+ # the same response-shape failure, so a future HA shape
1171
+ # change shows up at every fetch site rather than going
1172
+ # silent on this one.
1173
+ logger.warning(
1174
+ "lovelace/dashboards/list returned an unexpected shape "
1175
+ "(type=%s); treating as no-match",
1176
+ type(result).__name__,
1177
+ )
1178
+ existing_dashboards = []
1142
1179
  dashboard_exists = any(
1143
1180
  d.get("url_path") == url_path for d in existing_dashboards
1144
1181
  )
@@ -1407,7 +1444,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1407
1444
  Note: The default dashboard cannot be deleted via this method.
1408
1445
  """
1409
1446
  try:
1410
- resolved = await _resolve_dashboard(client, url_path)
1447
+ resolved, _ = await _resolve_dashboard(client, url_path)
1411
1448
  if resolved is None:
1412
1449
  raise_tool_error(
1413
1450
  create_resource_not_found_error(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.4.1.dev466
3
+ Version: 7.4.1.dev468
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