ha-mcp-dev 7.6.0.dev628__tar.gz → 7.6.0.dev630__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 (126) hide show
  1. {ha_mcp_dev-7.6.0.dev628/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.6.0.dev630}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/client/websocket_client.py +25 -1
  4. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/settings_ui.py +59 -10
  5. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/helpers.py +49 -22
  6. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_code.py +11 -0
  7. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  8. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/LICENSE +0 -0
  9. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/MANIFEST.in +0 -0
  10. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/README.md +0 -0
  11. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/setup.cfg +0 -0
  12. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/__init__.py +0 -0
  13. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/__main__.py +0 -0
  14. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/_pypi_marker +0 -0
  15. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/_version.py +0 -0
  16. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/auth/__init__.py +0 -0
  17. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/auth/consent_form.py +0 -0
  18. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/auth/provider.py +0 -0
  19. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/backup_manager.py +0 -0
  20. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/client/__init__.py +0 -0
  21. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/client/rest_client.py +0 -0
  22. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/client/supervisor_client.py +0 -0
  23. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/client/websocket_listener.py +0 -0
  24. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/config.py +0 -0
  25. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/errors.py +0 -0
  26. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/__init__.py +0 -0
  27. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/approval_queue.py +0 -0
  28. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/evaluator.py +0 -0
  29. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/handlers.py +0 -0
  30. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/middleware.py +0 -0
  31. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/model.py +0 -0
  32. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/persistence.py +0 -0
  33. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/value_sources.py +0 -0
  34. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/py.typed +0 -0
  35. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  36. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  37. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  38. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  39. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  40. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  41. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  42. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  43. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  44. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  45. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  46. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  47. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  48. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  49. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  50. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  51. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  52. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  53. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  54. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  55. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  56. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  57. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/server.py +0 -0
  58. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/smoke_test.py +0 -0
  59. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  60. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/__init__.py +0 -0
  61. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/auto_backup.py +0 -0
  62. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/backup.py +0 -0
  63. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  64. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/device_control.py +0 -0
  65. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/enhanced.py +0 -0
  66. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/reference_validator.py +0 -0
  67. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/registry.py +0 -0
  68. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/smart_search.py +0 -0
  69. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_addons.py +0 -0
  70. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_areas.py +0 -0
  71. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  72. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  73. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_calendar.py +0 -0
  74. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_camera.py +0 -0
  75. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_categories.py +0 -0
  76. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  77. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  78. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  79. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  80. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  81. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  82. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_energy.py +0 -0
  83. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_entities.py +0 -0
  84. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  85. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_groups.py +0 -0
  86. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_hacs.py +0 -0
  87. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_history.py +0 -0
  88. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_integrations.py +0 -0
  89. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_labels.py +0 -0
  90. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  91. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_registry.py +0 -0
  92. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_resources.py +0 -0
  93. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_search.py +0 -0
  94. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_service.py +0 -0
  95. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_services.py +0 -0
  96. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_system.py +0 -0
  97. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_todo.py +0 -0
  98. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_traces.py +0 -0
  99. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_updates.py +0 -0
  100. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_utility.py +0 -0
  101. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  102. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  103. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_zones.py +0 -0
  104. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/util_helpers.py +0 -0
  105. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/validation_middleware.py +0 -0
  106. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/transforms/__init__.py +0 -0
  107. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/transforms/categorized_search.py +0 -0
  108. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  109. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/__init__.py +0 -0
  110. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/config_hash.py +0 -0
  111. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/data_paths.py +0 -0
  112. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/domain_handlers.py +0 -0
  113. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  114. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  115. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/operation_manager.py +0 -0
  116. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/python_sandbox.py +0 -0
  117. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/skill_loader.py +0 -0
  118. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/usage_logger.py +0 -0
  119. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  120. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  121. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  122. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  123. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  124. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/tests/__init__.py +0 -0
  125. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/tests/test_constants.py +0 -0
  126. {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/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.6.0.dev628
3
+ Version: 7.6.0.dev630
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.6.0.dev628"
7
+ version = "7.6.0.dev630"
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"
@@ -235,6 +235,10 @@ class HomeAssistantWebSocketClient:
235
235
  self._send_lock: asyncio.Lock | None = None
236
236
  self._lock_loop: asyncio.AbstractEventLoop | None = None
237
237
  self._state = WebSocketConnectionState()
238
+ # Reason the most recent connect() attempt failed (exception text),
239
+ # or None. Surfaced by callers so the agent sees *why* a WebSocket
240
+ # connection failed instead of an opaque "Failed to connect" string.
241
+ self._last_connect_error: str | None = None
238
242
 
239
243
  # Parse URL to get WebSocket endpoint
240
244
  parsed = urlparse(self.base_url)
@@ -259,6 +263,7 @@ class HomeAssistantWebSocketClient:
259
263
  try:
260
264
  logger.info(f"Connecting to Home Assistant WebSocket: {self.ws_url}")
261
265
  self._state.reset_connection()
266
+ self._last_connect_error = None
262
267
 
263
268
  # Only configure an SSLContext for wss://; ws:// (Supervisor
264
269
  # proxy) doesn't use TLS and gets ssl=None.
@@ -326,6 +331,7 @@ class HomeAssistantWebSocketClient:
326
331
  return True
327
332
 
328
333
  except Exception as e:
334
+ self._last_connect_error = f"{type(e).__name__}: {e}"
329
335
  if _is_ssl_error(e) and self.verify_ssl:
330
336
  logger.error(
331
337
  "WebSocket TLS verification failed for %s: %s. "
@@ -925,6 +931,17 @@ class HomeAssistantWebSocketClient:
925
931
  """Check if WebSocket is connected and authenticated."""
926
932
  return self._state.is_ready
927
933
 
934
+ @property
935
+ def last_connect_error(self) -> str | None:
936
+ """Reason the most recent ``connect()`` attempt failed, or ``None``.
937
+
938
+ Captured from the underlying exception (e.g. an auth timeout, a
939
+ handshake HTTP/TLS error, or "Did not receive auth_required") so
940
+ callers can surface *why* the connection failed instead of an
941
+ opaque "Failed to connect to Home Assistant WebSocket".
942
+ """
943
+ return self._last_connect_error
944
+
928
945
 
929
946
  MAX_POOL_SIZE = 50
930
947
 
@@ -1059,7 +1076,14 @@ class WebSocketManager:
1059
1076
 
1060
1077
  connected = await client.connect()
1061
1078
  if not connected:
1062
- raise Exception("Failed to connect to Home Assistant WebSocket")
1079
+ reason = client.last_connect_error
1080
+ # Append only an actual string reason; the isinstance guard
1081
+ # keeps a non-str (e.g. a MagicMock in tests) from polluting
1082
+ # the message with a repr.
1083
+ detail = f": {reason}" if isinstance(reason, str) else ""
1084
+ raise Exception(
1085
+ "Failed to connect to Home Assistant WebSocket" + detail
1086
+ )
1063
1087
 
1064
1088
  self._clients[key] = client
1065
1089
  self._last_used[key] = time.monotonic()
@@ -602,6 +602,10 @@ _SETTINGS_HTML = (
602
602
  <meta charset="UTF-8">
603
603
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
604
604
  <title>HA-MCP Tool Settings</title>
605
+ <!-- Empty data URI: tells the browser "no favicon" so it never requests
606
+ /favicon.ico (which would 404 and log a console error, since the
607
+ settings server serves no such asset in any deployment mode). -->
608
+ <link rel="icon" href="data:,">
605
609
  <style>
606
610
  :root {
607
611
  --bg: #1c1c1e; --surface: #2c2c2e; --surface-hover: #3a3a3c;
@@ -2548,9 +2552,36 @@ function renderCodeModeSubRows(parentEl, masterOn, codeModeOn) {
2548
2552
 
2549
2553
  const info = document.createElement('div');
2550
2554
  info.className = 'feature-info';
2551
- const lockedNote = f.origin === 'env'
2552
- ? `<div class="feature-locked-note">Set via env var <code>${escapeHtml(f.env_var)}</code> unset it to edit here.</div>`
2553
- : '';
2555
+ // code_mode_saved_tools_path needs honest, field-specific copy:
2556
+ // - add-on mode: hardcoded by start.py (setdefault to /data); not
2557
+ // Supervisor-managed and absent from the addon schema, so it
2558
+ // genuinely can't be changed — don't imply a lever exists.
2559
+ // - standalone with the env var set: the "unset it" hint IS
2560
+ // actionable (the operator controls the env var), so keep it.
2561
+ // - standalone with no path: a blank path disables persistence —
2562
+ // warn that saved tools live in memory only.
2563
+ // Other env-locked code-mode sub-fields keep the shared helper.
2564
+ let lockedNote = '';
2565
+ if (f.field === 'code_mode_saved_tools_path') {
2566
+ if (IS_ADDON_MODE) {
2567
+ lockedNote =
2568
+ '<div class="feature-locked-note">Hardcoded to ' +
2569
+ '<code>/data/saved_tools.json</code> in add-on mode and cannot ' +
2570
+ 'be changed (fixed here so saved tools survive add-on updates).' +
2571
+ '</div>';
2572
+ } else if (f.origin === 'env') {
2573
+ lockedNote =
2574
+ `<div class="feature-locked-note">${envLockedNoteHtml(f.env_var, f.field)}</div>`;
2575
+ } else if (!f.value) {
2576
+ lockedNote =
2577
+ '<div class="feature-locked-note">If blank, custom tools are kept ' +
2578
+ 'in memory only and lost on restart. Set a path on persistent ' +
2579
+ 'storage to keep them.</div>';
2580
+ }
2581
+ } else if (f.origin === 'env') {
2582
+ lockedNote =
2583
+ `<div class="feature-locked-note">${envLockedNoteHtml(f.env_var, f.field)}</div>`;
2584
+ }
2554
2585
  info.innerHTML =
2555
2586
  `<div class="feature-name">${escapeHtml(meta.label)}</div>` +
2556
2587
  `<div class="feature-help">${escapeHtml(meta.help)}</div>` +
@@ -5373,16 +5404,34 @@ def build_settings_handlers(
5373
5404
  if is_addon_synced:
5374
5405
  addon_writes_present = True
5375
5406
  elif os.environ.get(env_name) is not None:
5407
+ # Add-on mode has no env-var surface for non-schema keys
5408
+ # (e.g. CODE_MODE_SAVED_TOOLS_PATH, set by start.py), so
5409
+ # "unset it to edit here" is unactionable there — give
5410
+ # add-on-aware copy instead of implying a lever exists.
5411
+ if addon_mode:
5412
+ message = (
5413
+ f"{fname!r} is fixed by the add-on runtime and "
5414
+ "cannot be changed from the web UI."
5415
+ )
5416
+ suggestions = [
5417
+ "This value is baked into the add-on and is not "
5418
+ "exposed as an editable setting.",
5419
+ ]
5420
+ else:
5421
+ message = (
5422
+ f"{fname!r} is set via {env_name} env var — "
5423
+ "unset it to edit here."
5424
+ )
5425
+ suggestions = [
5426
+ f"Unset the {env_name} environment variable (or "
5427
+ "remove it from your Docker config), then restart "
5428
+ "to edit this setting from the UI.",
5429
+ ]
5376
5430
  return JSONResponse(
5377
5431
  create_error_response(
5378
5432
  ErrorCode.VALIDATION_INVALID_PARAMETER,
5379
- f"{fname!r} is set via {env_name} env var — "
5380
- "unset it to edit here.",
5381
- suggestions=[
5382
- f"Unset the {env_name} environment variable (or "
5383
- "remove it from your addon/Docker config), then "
5384
- "restart to edit this setting from the UI.",
5385
- ],
5433
+ message,
5434
+ suggestions=suggestions,
5386
5435
  context={"env_var": env_name},
5387
5436
  ),
5388
5437
  status_code=409,
@@ -182,14 +182,19 @@ async def get_connected_ws_client(
182
182
  ws_client = HomeAssistantWebSocketClient(base_url, token, verify_ssl=verify_ssl)
183
183
  connected = await ws_client.connect()
184
184
  if not connected:
185
+ reason = ws_client.last_connect_error
186
+ details = (
187
+ reason
188
+ if isinstance(reason, str)
189
+ else "WebSocket connection could not be established"
190
+ )
185
191
  return None, create_connection_error(
186
192
  "Failed to connect to Home Assistant WebSocket",
187
- details="WebSocket connection could not be established",
193
+ details=details,
188
194
  )
189
195
  return ws_client, None
190
196
 
191
197
 
192
-
193
198
  def _classify_api_status(
194
199
  error: HomeAssistantAPIError,
195
200
  error_msg: str,
@@ -202,13 +207,17 @@ def _classify_api_status(
202
207
  if entity_id:
203
208
  result = create_entity_not_found_error(entity_id, details=error_msg)
204
209
  else:
205
- result = create_error_response(ErrorCode.RESOURCE_NOT_FOUND, error_msg, context=context)
210
+ result = create_error_response(
211
+ ErrorCode.RESOURCE_NOT_FOUND, error_msg, context=context
212
+ )
206
213
  case 401 | 403:
207
214
  result = create_auth_error(error_msg, context=context)
208
215
  case 400:
209
216
  result = create_validation_error(error_msg, context=context)
210
217
  case _:
211
- result = create_error_response(ErrorCode.SERVICE_CALL_FAILED, error_msg, context=context)
218
+ result = create_error_response(
219
+ ErrorCode.SERVICE_CALL_FAILED, error_msg, context=context
220
+ )
212
221
  return result
213
222
 
214
223
 
@@ -244,7 +253,9 @@ def _classify_exception(
244
253
  case TimeoutError():
245
254
  operation = context.get("operation", "request") if context else "request"
246
255
  timeout_seconds = context.get("timeout_seconds", 30) if context else 30
247
- result = create_timeout_error(operation, timeout_seconds, details=error_msg, context=context)
256
+ result = create_timeout_error(
257
+ operation, timeout_seconds, details=error_msg, context=context
258
+ )
248
259
  case ValueError():
249
260
  result = create_validation_error(error_msg, context=context)
250
261
 
@@ -276,7 +287,9 @@ def _classify_by_message(
276
287
  "unknown type",
277
288
  )
278
289
  )
279
- or re.search(r"expected (?:a |str|int|bool|dict|list|float|type|one of)", error_str)
290
+ or re.search(
291
+ r"expected (?:a |str|int|bool|dict|list|float|type|one of)", error_str
292
+ )
280
293
  ):
281
294
  # Supervisor schema validation: vol.Invalid message arriving as a
282
295
  # HomeAssistantCommandError via HA Core's hassio WS bridge. The
@@ -298,20 +311,27 @@ def _classify_by_message(
298
311
  if entity_id:
299
312
  result = create_entity_not_found_error(entity_id, details=error_msg)
300
313
  else:
301
- result = create_error_response(ErrorCode.RESOURCE_NOT_FOUND, error_msg, context=context)
314
+ result = create_error_response(
315
+ ErrorCode.RESOURCE_NOT_FOUND, error_msg, context=context
316
+ )
302
317
  elif "timeout" in error_str:
303
- result = create_timeout_error("operation", 30, details=error_msg, context=context)
318
+ result = create_timeout_error(
319
+ "operation", 30, details=error_msg, context=context
320
+ )
304
321
  elif "connection" in error_str or "connect" in error_str:
305
322
  result = create_connection_error(error_msg, context=context)
306
- elif any(
307
- phrase in error_str
308
- for phrase in (
309
- "unauthorized",
310
- "authentication",
311
- "invalid token",
312
- "access denied",
323
+ elif (
324
+ any(
325
+ phrase in error_str
326
+ for phrase in (
327
+ "unauthorized",
328
+ "authentication",
329
+ "invalid token",
330
+ "access denied",
331
+ )
313
332
  )
314
- ) or "401" in error_str:
333
+ or "401" in error_str
334
+ ):
315
335
  result = create_auth_error(error_msg, context=context)
316
336
  elif error_str.startswith("command failed:"):
317
337
  # HomeAssistantCommandError fallback: WS ``success=False`` with a
@@ -319,10 +339,15 @@ def _classify_by_message(
319
339
  # known failure mode (the WS command itself failed), not an
320
340
  # unexpected internal error — route to SERVICE_CALL_FAILED,
321
341
  # mirroring the 4xx fallback in _classify_api_status.
322
- result = create_error_response(ErrorCode.SERVICE_CALL_FAILED, error_msg, context=context)
342
+ result = create_error_response(
343
+ ErrorCode.SERVICE_CALL_FAILED, error_msg, context=context
344
+ )
323
345
  else:
324
346
  result = create_error_response(
325
- ErrorCode.INTERNAL_ERROR, "An unexpected error occurred", details=error_msg, context=context
347
+ ErrorCode.INTERNAL_ERROR,
348
+ "An unexpected error occurred",
349
+ details=error_msg,
350
+ context=context,
326
351
  )
327
352
  return result
328
353
 
@@ -418,7 +443,11 @@ def exception_to_structured_error(
418
443
  ):
419
444
  logger.exception("Unclassified exception: %s", error_msg)
420
445
 
421
- if suggestions and "error" in error_response and isinstance(error_response["error"], dict):
446
+ if (
447
+ suggestions
448
+ and "error" in error_response
449
+ and isinstance(error_response["error"], dict)
450
+ ):
422
451
  # Set both `suggestion` (singular, first item) and `suggestions`
423
452
  # (plural, full list). create_error_response (errors.py) sets the
424
453
  # singular key; existing tests for exception_to_structured_error
@@ -535,6 +564,4 @@ def register_tool_methods(mcp: Any, instance: Any) -> None:
535
564
  mcp.add_tool(method)
536
565
  count += 1
537
566
  if count == 0:
538
- logger.warning(
539
- f"No @tool-decorated methods found on {type(instance).__name__}"
540
- )
567
+ logger.warning(f"No @tool-decorated methods found on {type(instance).__name__}")
@@ -1290,5 +1290,16 @@ def register_code_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1290
1290
  "in-memory cache. Check operator logs for the "
1291
1291
  "underlying I/O error."
1292
1292
  )
1293
+ elif not settings.code_mode_saved_tools_path:
1294
+ # Blank path = persistence disabled: the in-memory save
1295
+ # succeeded but is lost on restart. Surface a warning so
1296
+ # the agent doesn't assume the entry is durable — mirrors
1297
+ # the write-failure branch above.
1298
+ response["data"]["save_warning"] = (
1299
+ f"save_as={save_as!r} is kept in memory only — "
1300
+ "code_mode_saved_tools_path is unset, so custom tools "
1301
+ "are not persisted and are lost on restart. Set a path "
1302
+ "on persistent storage to keep them."
1303
+ )
1293
1304
 
1294
1305
  return response
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.6.0.dev628
3
+ Version: 7.6.0.dev630
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