ha-mcp-dev 7.4.1.dev439__tar.gz → 7.4.1.dev440__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 (107) hide show
  1. {ha_mcp_dev-7.4.1.dev439/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev440}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/client/rest_client.py +217 -24
  4. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_utility.py +228 -33
  5. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  6. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/LICENSE +0 -0
  7. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/MANIFEST.in +0 -0
  8. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/README.md +0 -0
  9. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/setup.cfg +0 -0
  10. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/__init__.py +0 -0
  11. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/__main__.py +0 -0
  12. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/_pypi_marker +0 -0
  13. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/_version.py +0 -0
  14. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/auth/__init__.py +0 -0
  15. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/auth/consent_form.py +0 -0
  16. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/auth/provider.py +0 -0
  17. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/client/__init__.py +0 -0
  18. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/client/websocket_client.py +0 -0
  19. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/client/websocket_listener.py +0 -0
  20. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/config.py +0 -0
  21. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/errors.py +0 -0
  22. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/py.typed +0 -0
  23. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  24. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  25. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  26. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  27. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  28. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  29. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  30. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  31. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  32. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  33. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  34. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  35. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  36. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  37. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  38. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  39. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  40. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  41. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  42. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  43. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/server.py +0 -0
  44. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/settings_ui.py +0 -0
  45. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/smoke_test.py +0 -0
  46. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/__init__.py +0 -0
  47. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/backup.py +0 -0
  48. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  49. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/device_control.py +0 -0
  50. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/enhanced.py +0 -0
  51. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/helpers.py +0 -0
  52. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/reference_validator.py +0 -0
  53. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/registry.py +0 -0
  54. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/smart_search.py +0 -0
  55. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_addons.py +0 -0
  56. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_areas.py +0 -0
  57. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  58. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  59. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_calendar.py +0 -0
  60. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_camera.py +0 -0
  61. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_categories.py +0 -0
  62. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  63. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  64. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  65. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  66. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  67. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_energy.py +0 -0
  68. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_entities.py +0 -0
  69. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  70. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_groups.py +0 -0
  71. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_hacs.py +0 -0
  72. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_history.py +0 -0
  73. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_integrations.py +0 -0
  74. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_labels.py +0 -0
  75. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  76. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_registry.py +0 -0
  77. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_resources.py +0 -0
  78. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_search.py +0 -0
  79. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_service.py +0 -0
  80. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_services.py +0 -0
  81. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_system.py +0 -0
  82. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_todo.py +0 -0
  83. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_traces.py +0 -0
  84. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_updates.py +0 -0
  85. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  86. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  87. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_zones.py +0 -0
  88. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/util_helpers.py +0 -0
  89. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/transforms/__init__.py +0 -0
  90. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/transforms/categorized_search.py +0 -0
  91. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/__init__.py +0 -0
  92. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/config_hash.py +0 -0
  93. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/data_paths.py +0 -0
  94. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/domain_handlers.py +0 -0
  95. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  96. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  97. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/operation_manager.py +0 -0
  98. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/python_sandbox.py +0 -0
  99. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/usage_logger.py +0 -0
  100. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  101. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  102. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  103. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  104. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  105. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/tests/__init__.py +0 -0
  106. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/tests/test_constants.py +0 -0
  107. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/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.dev439
3
+ Version: 7.4.1.dev440
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.dev439"
7
+ version = "7.4.1.dev440"
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"
@@ -5,11 +5,13 @@ Home Assistant HTTP client with authentication and error handling.
5
5
  import asyncio
6
6
  import json
7
7
  import logging
8
+ import os
8
9
  import ssl
9
10
  from typing import Any
10
11
 
11
12
  import httpx
12
13
 
14
+ from .._version import is_running_in_addon
13
15
  from ..config import get_global_settings
14
16
 
15
17
 
@@ -26,6 +28,7 @@ def _is_ssl_error(exc: BaseException) -> bool:
26
28
  cur = cur.__cause__ or cur.__context__
27
29
  return False
28
30
 
31
+
29
32
  logger = logging.getLogger(__name__)
30
33
 
31
34
 
@@ -33,17 +36,14 @@ class HomeAssistantError(Exception):
33
36
  """Base exception for Home Assistant API errors."""
34
37
 
35
38
 
36
-
37
39
  class HomeAssistantConnectionError(HomeAssistantError):
38
40
  """Connection error to Home Assistant."""
39
41
 
40
42
 
41
-
42
43
  class HomeAssistantAuthError(HomeAssistantError):
43
44
  """Authentication error with Home Assistant."""
44
45
 
45
46
 
46
-
47
47
  class HomeAssistantAPIError(HomeAssistantError):
48
48
  """API error from Home Assistant."""
49
49
 
@@ -125,7 +125,7 @@ class HomeAssistantClient:
125
125
 
126
126
  logger.info(f"Initialized Home Assistant client for {self.base_url}")
127
127
 
128
- async def __aenter__(self) -> 'HomeAssistantClient':
128
+ async def __aenter__(self) -> "HomeAssistantClient":
129
129
  """Async context manager entry."""
130
130
  return self
131
131
 
@@ -192,7 +192,9 @@ class HomeAssistantClient:
192
192
  except httpx.HTTPError as e:
193
193
  raise HomeAssistantConnectionError(f"HTTP error: {e}") from e
194
194
 
195
- async def _request(self, method: str, endpoint: str, **kwargs: Any) -> dict[str, Any]:
195
+ async def _request(
196
+ self, method: str, endpoint: str, **kwargs: Any
197
+ ) -> dict[str, Any]:
196
198
  """
197
199
  Make authenticated request to Home Assistant API and parse JSON body.
198
200
 
@@ -267,8 +269,11 @@ class HomeAssistantClient:
267
269
  return await self._request("POST", f"/states/{entity_id}", json=payload)
268
270
 
269
271
  async def call_service(
270
- self, domain: str, service: str, data: dict[str, Any] | None = None,
271
- return_response: bool = False
272
+ self,
273
+ domain: str,
274
+ service: str,
275
+ data: dict[str, Any] | None = None,
276
+ return_response: bool = False,
272
277
  ) -> list[dict[str, Any]] | dict[str, Any]:
273
278
  """
274
279
  Call Home Assistant service.
@@ -284,7 +289,9 @@ class HomeAssistantClient:
284
289
  Service response data - list of affected states normally, or dict with
285
290
  service response if return_response=True
286
291
  """
287
- logger.debug(f"Calling service {domain}.{service} (return_response={return_response})")
292
+ logger.debug(
293
+ f"Calling service {domain}.{service} (return_response={return_response})"
294
+ )
288
295
 
289
296
  payload = data or {}
290
297
 
@@ -294,7 +301,10 @@ class HomeAssistantClient:
294
301
  params["return_response"] = "true"
295
302
 
296
303
  result = await self._request(
297
- "POST", f"/services/{domain}/{service}", json=payload, params=params if params else None
304
+ "POST",
305
+ f"/services/{domain}/{service}",
306
+ json=payload,
307
+ params=params if params else None,
298
308
  )
299
309
 
300
310
  # When return_response is True, HA returns a dict with service_response key
@@ -366,7 +376,9 @@ class HomeAssistantClient:
366
376
  Returns:
367
377
  Logbook entries
368
378
  """
369
- logger.debug(f"Fetching logbook entries for entity: {entity_id}, start: {start_time}, end: {end_time}")
379
+ logger.debug(
380
+ f"Fetching logbook entries for entity: {entity_id}, start: {start_time}, end: {end_time}"
381
+ )
370
382
 
371
383
  # Build endpoint - start_time goes in URL path if provided
372
384
  if start_time:
@@ -428,27 +440,66 @@ class HomeAssistantClient:
428
440
  return await self._request("POST", "/config/core/check_config")
429
441
 
430
442
  async def get_error_log(self) -> str:
431
- """Get Home Assistant error log."""
432
- logger.debug("Fetching error log")
443
+ """Get Home Assistant error log.
444
+
445
+ Branch on ``is_running_in_addon()``: inside the add-on container,
446
+ HA Core's ``bootstrap.py`` sets ``err_log_path = None`` when the
447
+ ``SUPERVISOR`` env var is present, so ``hass.data[DATA_LOGGING]``
448
+ is never populated and the ``APIErrorLog`` view is not registered
449
+ — ``/api/error_log`` returns 404 by-design on HA OS / Supervised.
450
+ Route to ``_supervisor_logs_get("core")`` on this branch: same
451
+ content (HA Core's container log) via a different transport
452
+ (Supervisor REST). On non-addon installs keep the
453
+ ``/api/error_log`` proxy path.
454
+
455
+ Same root cause and fix shape as ``get_addon_logs`` — see #1116.
456
+
457
+ Raises:
458
+ HomeAssistantAuthError: 401, or empty ``SUPERVISOR_TOKEN`` on
459
+ the addon branch.
460
+ HomeAssistantAPIError: 403 (role too low — addon needs
461
+ ``hassio_role: manager``), 404, other non-2xx.
462
+ HomeAssistantConnectionError: Network, timeout, or transport
463
+ error.
464
+ """
465
+ if is_running_in_addon():
466
+ logger.debug("Fetching error log via Supervisor direct (core service)")
467
+ return await self._supervisor_logs_get("core")
468
+
469
+ logger.debug("Fetching error log via HA Core proxy")
433
470
  response = await self._request("GET", "/error_log")
434
471
  return response if isinstance(response, str) else str(response)
435
472
 
436
473
  async def get_addon_logs(self, slug: str) -> str:
437
- """Fetch an add-on's container logs via HA Core's Supervisor REST proxy.
474
+ """Fetch an add-on's container logs.
475
+
476
+ Branch on ``is_running_in_addon()`` (which keys off ``SUPERVISOR_TOKEN``
477
+ in env): inside the add-on container goes directly to the Supervisor
478
+ REST API at ``http://supervisor/addons/{slug}/logs`` with the
479
+ Supervisor token. The HA Core proxy at
480
+ ``/api/hassio/addons/{slug}/logs`` rejects this token+path combination
481
+ on current HA Core releases (see #1116) — the direct path bypasses
482
+ HA Core entirely and is the documented Supervisor contract.
438
483
 
439
- Uses `/api/hassio/addons/{slug}/logs`, which HA Core proxies to
440
- Supervisor and returns as `text/plain`. This avoids the
441
- `supervisor/api` websocket path that tries to JSON-decode the text
442
- body and always fails (see #950).
484
+ On non-addon installs (Docker, pyinstaller, pip pointing at a normal
485
+ HA URL), falls back to the HA Core proxy path. That path requires an
486
+ admin LLA but works fine when not invoked from the add-on container.
487
+
488
+ Both branches return ``text/plain`` log content.
443
489
 
444
490
  Raises:
445
- HomeAssistantAuthError: 401 from HA Core.
446
- HomeAssistantAPIError: Non-2xx response (e.g. 404 unknown slug,
447
- 400 addon not installed). `status_code` is set so callers
448
- can map to specific suggestions.
491
+ HomeAssistantAuthError: 401 response, or ``SUPERVISOR_TOKEN`` empty
492
+ at call time on the addon branch.
493
+ HomeAssistantAPIError: 403 (role too low addon needs hassio_role
494
+ ``manager``), 404 (unknown slug), or other non-2xx. The
495
+ ``status_code`` attribute lets callers map to specific
496
+ suggestions.
449
497
  HomeAssistantConnectionError: Network, timeout, or transport error.
450
498
  """
451
- logger.debug(f"Fetching addon logs for slug={slug}")
499
+ if is_running_in_addon():
500
+ return await self._get_addon_logs_via_supervisor(slug)
501
+
502
+ logger.debug(f"Fetching addon logs for slug={slug} via HA Core proxy")
452
503
  response = await self._raw_request(
453
504
  "GET",
454
505
  f"/hassio/addons/{slug}/logs",
@@ -456,6 +507,146 @@ class HomeAssistantClient:
456
507
  )
457
508
  return response.text
458
509
 
510
+ async def _supervisor_logs_get(self, path: str) -> str:
511
+ """Fetch ``text/plain`` logs from a Supervisor REST endpoint.
512
+
513
+ ``path`` is everything between ``http://supervisor/`` and ``/logs``:
514
+
515
+ - ``"addons/<slug>"`` for add-on container logs
516
+ - ``"<service>"`` (where service ∈ {supervisor, host, core, dns, audio,
517
+ multicast, observer}) for system-service logs
518
+
519
+ Bypasses ``HomeAssistantClient.httpx_client`` because the Supervisor
520
+ endpoint uses a different base URL (``http://supervisor``) and a
521
+ different token (``SUPERVISOR_TOKEN``) than HA Core REST. Both
522
+ endpoints require the addon's ``hassio_role`` to be ``manager`` (not
523
+ ``default``); a ``default`` role gets a 403 here — see #1116.
524
+
525
+ Raises:
526
+ HomeAssistantAuthError: ``SUPERVISOR_TOKEN`` absent at call time,
527
+ or 401 from Supervisor.
528
+ HomeAssistantAPIError: 403 (role too low — distinct branch with
529
+ role hint), 404, other 4xx/5xx. Tries to parse Supervisor's
530
+ ``{"result":"error","message":"..."}`` JSON envelope before
531
+ falling back to text body / reason phrase / placeholder.
532
+ HomeAssistantConnectionError: Timeout or transport error, with
533
+ distinct messages so callers can tell them apart.
534
+ """
535
+ token = os.environ.get("SUPERVISOR_TOKEN", "")
536
+ if not token:
537
+ # The is_running_in_addon() gate already keys off SUPERVISOR_TOKEN
538
+ # being truthy, so a direct caller landing here without one is a
539
+ # detection/config mismatch — fail-fast with a distinct message
540
+ # so operators don't read it as "token rejected".
541
+ raise HomeAssistantAuthError(
542
+ f"Supervisor token absent at call time for /{path}/logs "
543
+ "(addon-mode gate fired but SUPERVISOR_TOKEN env var not set)"
544
+ )
545
+
546
+ url = f"http://supervisor/{path}/logs"
547
+ logger.debug("Fetching %s via Supervisor direct", url)
548
+
549
+ try:
550
+ async with httpx.AsyncClient(
551
+ timeout=httpx.Timeout(self.timeout),
552
+ # `verify` is a no-op for plain http://supervisor, but kept
553
+ # for symmetry with the other two direct-Supervisor httpx
554
+ # clients (#1128 establishes the 3-site convention).
555
+ verify=self.verify_ssl,
556
+ ) as client:
557
+ response = await client.get(
558
+ url,
559
+ headers={
560
+ "Authorization": f"Bearer {token}",
561
+ "Accept": "text/plain",
562
+ },
563
+ )
564
+ except httpx.TimeoutException as e:
565
+ raise HomeAssistantConnectionError(
566
+ f"Timeout fetching /{path}/logs from Supervisor: {e}"
567
+ ) from e
568
+ except httpx.HTTPError as e:
569
+ raise HomeAssistantConnectionError(
570
+ f"Transport error fetching /{path}/logs from Supervisor: {e}"
571
+ ) from e
572
+
573
+ if response.status_code == 401:
574
+ raise HomeAssistantAuthError(f"Invalid Supervisor token for /{path}/logs")
575
+ if response.status_code == 403:
576
+ # Distinct from 401: token is valid but addon's hassio_role isn't
577
+ # high enough. Most-likely cause for this exact endpoint at the
578
+ # time #1116 surfaced (default → manager bump in addon config.yaml
579
+ # is the same-PR companion fix).
580
+ logger.warning(
581
+ "Supervisor returned 403 for /%s/logs — addon hassio_role may "
582
+ "be too low (need 'manager')",
583
+ path,
584
+ )
585
+ raise HomeAssistantAPIError(
586
+ f"Supervisor forbids /{path}/logs (403) — addon's hassio_role "
587
+ "may be 'default'; need 'manager' or higher",
588
+ status_code=403,
589
+ response_data={"path": path},
590
+ )
591
+ if response.status_code >= 400:
592
+ text_body = response.text
593
+ # Supervisor returns {"result":"error","message":"..."} JSON on
594
+ # some 4xx paths. Try parsing that first so the user sees the
595
+ # human message instead of a JSON blob; then fall back to the
596
+ # text body, then reason_phrase, then a placeholder.
597
+ message = ""
598
+ try:
599
+ envelope = json.loads(text_body) if text_body else None
600
+ if isinstance(envelope, dict):
601
+ msg = envelope.get("message")
602
+ if isinstance(msg, str) and msg:
603
+ message = msg
604
+ except json.JSONDecodeError:
605
+ pass
606
+ if not message:
607
+ message = text_body.strip() or response.reason_phrase or "<empty body>"
608
+ logger.warning(
609
+ "Supervisor returned %s for /%s/logs: %s",
610
+ response.status_code,
611
+ path,
612
+ message,
613
+ )
614
+ raise HomeAssistantAPIError(
615
+ f"API error: {response.status_code} - {message}",
616
+ status_code=response.status_code,
617
+ response_data={"message": text_body, "path": path},
618
+ )
619
+ return response.text
620
+
621
+ async def _get_addon_logs_via_supervisor(self, slug: str) -> str:
622
+ """Fetch add-on container logs directly from Supervisor's REST API.
623
+
624
+ Distinct from ``tools_bug_report._fetch_addon_logs``: that helper is
625
+ hardcoded to ``/addons/self/logs`` and silently swallows failures
626
+ (it's an aux-data fetch for bug reports, fine to skip on error). This
627
+ helper takes arbitrary slugs and surfaces failures as exceptions
628
+ because callers (``ha_get_logs(source="supervisor", slug=...)``) need
629
+ them. Both endpoints require ``hassio_role: manager``.
630
+
631
+ Delegates to ``_supervisor_logs_get`` so error handling stays in
632
+ lockstep with ``_get_system_service_logs``.
633
+ """
634
+ return await self._supervisor_logs_get(f"addons/{slug}")
635
+
636
+ async def _get_system_service_logs(self, service: str) -> str:
637
+ """Fetch HA system-service logs directly from Supervisor's REST API.
638
+
639
+ Hits ``http://supervisor/{service}/logs``. ``service`` must be one of
640
+ the seven Supervisor-managed services: ``supervisor``, ``host``,
641
+ ``core``, ``dns``, ``audio``, ``multicast``, ``observer``. Caller is
642
+ responsible for validating ``service`` against the allowed set; this
643
+ helper does no validation and will raise ``HomeAssistantAPIError`` on
644
+ any unknown path (404 from Supervisor).
645
+
646
+ Requires ``hassio_role: manager`` like the addon-logs path.
647
+ """
648
+ return await self._supervisor_logs_get(service)
649
+
459
650
  async def test_connection(self) -> tuple[bool, str | None]:
460
651
  """
461
652
  Test connection to Home Assistant.
@@ -920,7 +1111,9 @@ class HomeAssistantClient:
920
1111
  await asyncio.sleep(retry_delay)
921
1112
  continue
922
1113
  else:
923
- logger.error(f"WebSocket 403 error after {max_retries} attempts: {error_str}")
1114
+ logger.error(
1115
+ f"WebSocket 403 error after {max_retries} attempts: {error_str}"
1116
+ )
924
1117
  return {
925
1118
  "success": False,
926
1119
  "error": f"WebSocket request blocked (403 Forbidden): {error_str}",
@@ -1018,7 +1211,7 @@ class HomeAssistantClient:
1018
1211
  except Exception:
1019
1212
  logger.debug(
1020
1213
  f"Entity registry lookup failed for {entity_id}, using bare id: {bare_id}",
1021
- exc_info=True # Log full traceback for better debugging
1214
+ exc_info=True, # Log full traceback for better debugging
1022
1215
  )
1023
1216
  return bare_id
1024
1217
 
@@ -26,7 +26,24 @@ logger = logging.getLogger(__name__)
26
26
 
27
27
  # Fields to keep in compact logbook mode (strips attribute dictionaries
28
28
  # and other bulky fields that can cause context exhaustion — see #683)
29
- COMPACT_LOGBOOK_FIELDS = {"when", "entity_id", "state", "name", "message", "domain", "context_id", "source"}
29
+ COMPACT_LOGBOOK_FIELDS = {
30
+ "when",
31
+ "entity_id",
32
+ "state",
33
+ "name",
34
+ "message",
35
+ "domain",
36
+ "context_id",
37
+ "source",
38
+ }
39
+
40
+
41
+ # Supervisor-managed system services exposed via /<slug>/logs. Stable set
42
+ # in HA Core; if Supervisor adds e.g. /cli/logs in a future release, extend
43
+ # here. See #1116.
44
+ SYSTEM_SERVICE_SLUGS = frozenset(
45
+ {"supervisor", "host", "core", "dns", "audio", "multicast", "observer"}
46
+ )
30
47
 
31
48
 
32
49
  def _compact_logbook_entries(entries: list[Any]) -> list[dict[str, Any]]:
@@ -57,15 +74,24 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
57
74
  ) -> int:
58
75
  """Coerce and validate a limit parameter, raising a structured tool error on failure."""
59
76
  try:
60
- return coerce_int_param(limit, param_name="limit", default=default, min_value=1, max_value=MAX_LIMIT)
77
+ return coerce_int_param(
78
+ limit,
79
+ param_name="limit",
80
+ default=default,
81
+ min_value=1,
82
+ max_value=MAX_LIMIT,
83
+ )
61
84
  except ValueError as e:
62
85
  raise_tool_error(
63
86
  create_error_response(
64
87
  ErrorCode.VALIDATION_INVALID_PARAMETER,
65
88
  str(e),
66
- suggestions=[f"Provide limit as an integer (e.g., {suggestion_example})"],
89
+ suggestions=[
90
+ f"Provide limit as an integer (e.g., {suggestion_example})"
91
+ ],
67
92
  )
68
93
  )
94
+
69
95
  # Regex to match log level at the start of a log line
70
96
  _LOG_LEVEL_RE = re.compile(
71
97
  r"(?:^|\s)(DEBUG|INFO|WARNING|ERROR|CRITICAL)(?:\s|:|\])", re.IGNORECASE
@@ -80,11 +106,18 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
80
106
  "idempotentHint": True,
81
107
  "readOnlyHint": True,
82
108
  "title": "Get Logs",
83
- }
109
+ },
84
110
  )
85
111
  @log_tool_usage
86
112
  async def ha_get_logs(
87
- source: Literal["logbook", "system", "error_log", "supervisor", "logger"] = "logbook",
113
+ source: Literal[
114
+ "logbook",
115
+ "system",
116
+ "error_log",
117
+ "supervisor",
118
+ "system_service",
119
+ "logger",
120
+ ] = "logbook",
88
121
  # Shared parameters
89
122
  limit: int | str | None = None,
90
123
  search: str | None = None,
@@ -96,7 +129,7 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
96
129
  compact: bool | str = True,
97
130
  # System/error_log-specific
98
131
  level: str | None = None,
99
- # Supervisor-specific
132
+ # Supervisor + system_service-specific (different namespaces — see below)
100
133
  slug: str | None = None,
101
134
  ) -> dict[str, Any]:
102
135
  """
@@ -106,13 +139,19 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
106
139
  - "logbook" (default): Entity state change history with pagination
107
140
  - "system": Structured system log entries (errors, warnings) via system_log/list
108
141
  - "error_log": Raw home-assistant.log text
109
- - "supervisor": Add-on container logs (requires slug parameter)
142
+ - "supervisor": Add-on container logs (requires slug = add-on slug)
143
+ - "system_service": HA-Supervisor-managed system service logs (requires
144
+ slug ∈ {supervisor, host, core, dns, audio, multicast, observer})
110
145
  - "logger": Effective log level per integration via logger/log_info (confirms logger.set_level changes took effect)
111
146
 
112
147
  **Shared params:** limit, search (keyword filter on entries/lines; matches integration domain for source='logger')
113
148
  **Logbook params:** hours_back, entity_id, end_time, offset, compact (default True — strips attribute dicts to save context)
114
149
  **System/error_log params:** level (ERROR, WARNING, INFO, DEBUG)
115
- **Supervisor params:** slug (add-on slug, e.g. "core_mosquitto")
150
+ **Supervisor params:** slug = add-on slug, e.g. "core_mosquitto" (use
151
+ ha_get_addon() to list installed slugs)
152
+ **System-service params:** slug = service name. The slug "supervisor"
153
+ here means the Supervisor service's own logs, NOT an add-on with
154
+ that name — the source param disambiguates.
116
155
  """
117
156
 
118
157
  # Validate level if provided
@@ -131,16 +170,28 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
131
170
  # Collect warnings about source-incompatible parameters
132
171
  warnings: list[str] = []
133
172
  if source != "logbook" and any(p is not None for p in [entity_id, end_time]):
134
- ignored = [p for p, v in [("entity_id", entity_id), ("end_time", end_time)] if v is not None]
173
+ ignored = [
174
+ p
175
+ for p, v in [("entity_id", entity_id), ("end_time", end_time)]
176
+ if v is not None
177
+ ]
135
178
  warnings.append(
136
179
  f"Parameters {', '.join(ignored)} only apply to source='logbook'; "
137
180
  f"ignored for source='{source}'"
138
181
  )
139
- if source in ("logbook", "logger", "supervisor") and level is not None:
182
+ if (
183
+ source in ("logbook", "logger", "supervisor", "system_service")
184
+ and level is not None
185
+ ):
140
186
  warnings.append(
141
187
  "Parameter 'level' only applies to source='system' or 'error_log'; "
142
188
  f"ignored for source='{source}'"
143
189
  )
190
+ if source not in ("supervisor", "system_service") and slug is not None:
191
+ warnings.append(
192
+ "Parameter 'slug' only applies to source='supervisor' or "
193
+ f"'system_service'; ignored for source='{source}'"
194
+ )
144
195
 
145
196
  # --- source="logbook" ---
146
197
  if source == "logbook":
@@ -186,6 +237,41 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
186
237
  result["warnings"] = warnings
187
238
  return result
188
239
 
240
+ # --- source="system_service" ---
241
+ if source == "system_service":
242
+ if not slug:
243
+ raise_tool_error(
244
+ create_error_response(
245
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
246
+ "The 'slug' parameter is required for source='system_service'",
247
+ suggestions=[
248
+ "Provide a service name, e.g. slug='supervisor' "
249
+ f"(allowed: {', '.join(sorted(SYSTEM_SERVICE_SLUGS))})",
250
+ ],
251
+ )
252
+ )
253
+ if slug not in SYSTEM_SERVICE_SLUGS:
254
+ raise_tool_error(
255
+ create_error_response(
256
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
257
+ f"Invalid system_service slug '{slug}'. Must be one of: "
258
+ f"{', '.join(sorted(SYSTEM_SERVICE_SLUGS))}",
259
+ suggestions=[
260
+ "Pick a valid service name (e.g. 'supervisor', 'host')",
261
+ "For add-on container logs use source='supervisor' with "
262
+ "the add-on slug instead",
263
+ ],
264
+ )
265
+ )
266
+ result = await _get_system_service_log(
267
+ service=slug,
268
+ limit=limit,
269
+ search=search,
270
+ )
271
+ if warnings:
272
+ result["warnings"] = warnings
273
+ return result
274
+
189
275
  # --- source="supervisor" ---
190
276
  # source == "supervisor" (Literal type guarantees this)
191
277
  if not slug:
@@ -462,7 +548,9 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
462
548
  level: str | None = None,
463
549
  ) -> dict[str, Any]:
464
550
  """Fetch raw error log text from home-assistant.log."""
465
- effective_limit = _coerce_limit(limit, default=DEFAULT_LOG_LIMIT, suggestion_example="100")
551
+ effective_limit = _coerce_limit(
552
+ limit, default=DEFAULT_LOG_LIMIT, suggestion_example="100"
553
+ )
466
554
 
467
555
  try:
468
556
  raw_log = await client.get_error_log()
@@ -572,7 +660,8 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
572
660
  if search:
573
661
  search_lower = search.lower()
574
662
  loggers = [
575
- entry for entry in loggers
663
+ entry
664
+ for entry in loggers
576
665
  if search_lower in entry["domain"].lower()
577
666
  ]
578
667
  filters_applied["search"] = search
@@ -619,14 +708,18 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
619
708
  limit: int | str | None = None,
620
709
  search: str | None = None,
621
710
  ) -> dict[str, Any]:
622
- """Fetch add-on container logs via HA Core's Supervisor REST proxy.
623
-
624
- Routes through `/api/hassio/addons/{slug}/logs` (returned as
625
- text/plain) instead of the `supervisor/api` websocket path, which
626
- always fails because HA Core's proxy tries to JSON-decode the
627
- text body. See #950.
711
+ """Fetch add-on container logs.
712
+
713
+ Delegates to ``HomeAssistantClient.get_addon_logs`` which branches on
714
+ ``is_running_in_addon()``: inside the add-on container hits Supervisor
715
+ directly at ``http://supervisor/addons/<slug>/logs`` (the HA-Core
716
+ proxy at ``/api/hassio/addons/<slug>/logs`` rejects the Supervisor
717
+ token there — see #1116); on non-addon installs falls back to the
718
+ HA-Core proxy. Both paths return ``text/plain``.
628
719
  """
629
- effective_limit = _coerce_limit(limit, default=DEFAULT_LOG_LIMIT, suggestion_example="100")
720
+ effective_limit = _coerce_limit(
721
+ limit, default=DEFAULT_LOG_LIMIT, suggestion_example="100"
722
+ )
630
723
 
631
724
  try:
632
725
  log_text = await client.get_addon_logs(slug)
@@ -713,13 +806,111 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
713
806
  ],
714
807
  )
715
808
 
809
+ # ---- System-service log source ----
810
+
811
+ async def _get_system_service_log(
812
+ service: str,
813
+ limit: int | str | None = None,
814
+ search: str | None = None,
815
+ ) -> dict[str, Any]:
816
+ """Fetch HA system-service logs from Supervisor's per-service endpoint.
817
+
818
+ ``service`` ∈ {supervisor, host, core, dns, audio, multicast, observer}.
819
+ Caller (``ha_get_logs(source='system_service')``) validates against
820
+ ``SYSTEM_SERVICE_SLUGS`` before dispatch. Hits
821
+ ``http://supervisor/<service>/logs`` directly via
822
+ ``HomeAssistantClient._get_system_service_logs`` — same direct-Supervisor
823
+ path #1116's add-on fix uses, just with a different URL prefix.
824
+ Requires ``hassio_role: manager`` in the addon manifest.
825
+ """
826
+ effective_limit = _coerce_limit(
827
+ limit, default=DEFAULT_LOG_LIMIT, suggestion_example="100"
828
+ )
829
+
830
+ try:
831
+ log_text = await client._get_system_service_logs(service)
832
+
833
+ lines = log_text.splitlines() if log_text else []
834
+
835
+ filters_applied: dict[str, str] = {}
836
+ if search:
837
+ search_lower = search.lower()
838
+ lines = [ln for ln in lines if search_lower in ln.lower()]
839
+ filters_applied["search"] = search
840
+
841
+ total_lines = len(lines)
842
+ lines = lines[-effective_limit:]
843
+
844
+ data: dict[str, Any] = {
845
+ "success": True,
846
+ "source": "system_service",
847
+ "slug": service,
848
+ "log": "\n".join(lines),
849
+ "total_lines": total_lines,
850
+ "returned_lines": len(lines),
851
+ "limit": effective_limit,
852
+ }
853
+ if filters_applied:
854
+ data["filters_applied"] = filters_applied
855
+
856
+ return data
857
+
858
+ except ToolError:
859
+ raise
860
+ except HomeAssistantAPIError as e:
861
+ status = getattr(e, "status_code", None)
862
+ if status == 403:
863
+ # Same role-too-low cause as the addon-logs branch.
864
+ exception_to_structured_error(
865
+ e,
866
+ context={"source": "system_service", "slug": service},
867
+ suggestions=[
868
+ "Addon's hassio_role must be 'manager' or higher to "
869
+ "read /<service>/logs",
870
+ "Verify the addon was reinstalled after the role bump "
871
+ "took effect",
872
+ ],
873
+ )
874
+ if status == 404:
875
+ exception_to_structured_error(
876
+ e,
877
+ context={"source": "system_service", "slug": service},
878
+ suggestions=[
879
+ f"Service '{service}' not found at "
880
+ f"http://supervisor/{service}/logs — Supervisor may "
881
+ "not expose it on this HA OS version",
882
+ f"Allowed services: {', '.join(sorted(SYSTEM_SERVICE_SLUGS))}",
883
+ ],
884
+ )
885
+ exception_to_structured_error(
886
+ e,
887
+ context={"source": "system_service", "slug": service},
888
+ suggestions=[
889
+ f"Supervisor returned an error for /{service}/logs",
890
+ "Ensure Supervisor is available (HA OS or Supervised install)",
891
+ ],
892
+ )
893
+ except (
894
+ HomeAssistantConnectionError,
895
+ TimeoutError,
896
+ OSError,
897
+ ) as e:
898
+ exception_to_structured_error(
899
+ e,
900
+ context={"source": "system_service", "slug": service},
901
+ suggestions=[
902
+ "Check Home Assistant connection",
903
+ "Ensure Supervisor is available (HA OS or Supervised install)",
904
+ ],
905
+ )
906
+
716
907
  @mcp.tool(
717
908
  tags={"Utilities"},
718
909
  annotations={
719
910
  "idempotentHint": True,
720
911
  "readOnlyHint": True,
721
- "title": "Evaluate Template"
722
- }
912
+ "title": "Evaluate Template",
913
+ },
723
914
  )
724
915
  @log_tool_usage
725
916
  async def ha_eval_template(
@@ -901,18 +1092,22 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
901
1092
  }
902
1093
  else:
903
1094
  error_info = result.get("error", "Unknown error occurred")
904
- raise_tool_error(create_error_response(
905
- ErrorCode.SERVICE_CALL_FAILED,
906
- str(error_info) if not isinstance(error_info, str) else error_info,
907
- context={"template": template, "request_id": request_id},
908
- suggestions=[
909
- "Check template syntax - ensure proper Jinja2 formatting",
910
- "Verify entity_ids exist using ha_get_state()",
911
- "Use default values: {{ states('sensor.temp') | float(0) }}",
912
- "Check for typos in function names and entity references",
913
- "Test simpler templates first to isolate issues",
914
- ],
915
- ))
1095
+ raise_tool_error(
1096
+ create_error_response(
1097
+ ErrorCode.SERVICE_CALL_FAILED,
1098
+ str(error_info)
1099
+ if not isinstance(error_info, str)
1100
+ else error_info,
1101
+ context={"template": template, "request_id": request_id},
1102
+ suggestions=[
1103
+ "Check template syntax - ensure proper Jinja2 formatting",
1104
+ "Verify entity_ids exist using ha_get_state()",
1105
+ "Use default values: {{ states('sensor.temp') | float(0) }}",
1106
+ "Check for typos in function names and entity references",
1107
+ "Test simpler templates first to isolate issues",
1108
+ ],
1109
+ )
1110
+ )
916
1111
 
917
1112
  except ToolError:
918
1113
  raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.4.1.dev439
3
+ Version: 7.4.1.dev440
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