ha-mcp-dev 7.3.0.dev379__tar.gz → 7.3.0.dev380__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 (101) hide show
  1. {ha_mcp_dev-7.3.0.dev379/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.3.0.dev380}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/client/rest_client.py +65 -23
  4. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_utility.py +46 -47
  5. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  6. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/LICENSE +0 -0
  7. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/MANIFEST.in +0 -0
  8. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/README.md +0 -0
  9. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/setup.cfg +0 -0
  10. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/__init__.py +0 -0
  11. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/__main__.py +0 -0
  12. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/_pypi_marker +0 -0
  13. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/auth/__init__.py +0 -0
  14. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/auth/consent_form.py +0 -0
  15. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/auth/provider.py +0 -0
  16. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/client/__init__.py +0 -0
  17. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/client/websocket_client.py +0 -0
  18. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/client/websocket_listener.py +0 -0
  19. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/config.py +0 -0
  20. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/errors.py +0 -0
  21. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/py.typed +0 -0
  22. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  23. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  24. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  25. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  26. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  27. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  28. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  29. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  30. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  31. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  32. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  33. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  34. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  35. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  36. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  37. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  38. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  39. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  40. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  41. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  42. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/server.py +0 -0
  43. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/smoke_test.py +0 -0
  44. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/__init__.py +0 -0
  45. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/backup.py +0 -0
  46. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  47. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/device_control.py +0 -0
  48. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/enhanced.py +0 -0
  49. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/helpers.py +0 -0
  50. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/registry.py +0 -0
  51. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/smart_search.py +0 -0
  52. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_addons.py +0 -0
  53. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_areas.py +0 -0
  54. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  55. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  56. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_calendar.py +0 -0
  57. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_camera.py +0 -0
  58. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_categories.py +0 -0
  59. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  60. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  61. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  62. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  63. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  64. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_entities.py +0 -0
  65. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  66. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_groups.py +0 -0
  67. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_hacs.py +0 -0
  68. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_history.py +0 -0
  69. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_integrations.py +0 -0
  70. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_labels.py +0 -0
  71. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  72. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_registry.py +0 -0
  73. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_resources.py +0 -0
  74. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_search.py +0 -0
  75. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_service.py +0 -0
  76. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_services.py +0 -0
  77. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_system.py +0 -0
  78. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_todo.py +0 -0
  79. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_traces.py +0 -0
  80. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_updates.py +0 -0
  81. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  82. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  83. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/tools_zones.py +0 -0
  84. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/tools/util_helpers.py +0 -0
  85. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/transforms/__init__.py +0 -0
  86. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/transforms/categorized_search.py +0 -0
  87. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/utils/__init__.py +0 -0
  88. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/utils/config_hash.py +0 -0
  89. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/utils/domain_handlers.py +0 -0
  90. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  91. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/utils/operation_manager.py +0 -0
  92. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/utils/python_sandbox.py +0 -0
  93. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp/utils/usage_logger.py +0 -0
  94. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  95. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  96. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  97. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  98. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  99. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/tests/__init__.py +0 -0
  100. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/tests/test_constants.py +0 -0
  101. {ha_mcp_dev-7.3.0.dev379 → ha_mcp_dev-7.3.0.dev380}/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.3.0.dev379
3
+ Version: 7.3.0.dev380
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.3.0.dev379"
7
+ version = "7.3.0.dev380"
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"
@@ -97,50 +97,44 @@ class HomeAssistantClient:
97
97
  await self.httpx_client.aclose()
98
98
  logger.debug("Closed Home Assistant client")
99
99
 
100
- async def _request(self, method: str, endpoint: str, **kwargs: Any) -> dict[str, Any]:
101
- """
102
- Make authenticated request to Home Assistant API.
100
+ async def _raw_request(
101
+ self, method: str, endpoint: str, **kwargs: Any
102
+ ) -> httpx.Response:
103
+ """Authenticated request that returns the raw httpx.Response.
103
104
 
104
- Args:
105
- method: HTTP method (GET, POST, etc.)
106
- endpoint: API endpoint (without /api prefix)
107
- **kwargs: Additional arguments for httpx request
108
-
109
- Returns:
110
- Response data as dictionary
105
+ Handles auth, HTTP 4xx/5xx, and transport errors in one place.
106
+ Callers parse the body themselves (JSON via `_request`, text via
107
+ `get_addon_logs`, etc.).
111
108
 
112
109
  Raises:
113
- HomeAssistantConnectionError: Connection failed
114
- HomeAssistantAuthError: Authentication failed
115
- HomeAssistantAPIError: API error
110
+ HomeAssistantAuthError: 401 response.
111
+ HomeAssistantAPIError: Non-2xx response (with status_code and
112
+ response_data set from JSON body when possible).
113
+ HomeAssistantConnectionError: Network, timeout, or transport error.
116
114
  """
117
115
  try:
118
116
  response = await self.httpx_client.request(method, endpoint, **kwargs)
119
117
 
120
- # Handle authentication errors
121
118
  if response.status_code == 401:
122
119
  raise HomeAssistantAuthError("Invalid authentication token")
123
120
 
124
- # Handle other HTTP errors
125
121
  if response.status_code >= 400:
126
122
  try:
127
123
  error_data = response.json()
128
124
  except Exception:
129
125
  error_data = {"message": response.text}
130
126
 
127
+ message = error_data.get("message")
128
+ if not message or not message.strip():
129
+ message = response.reason_phrase or "<empty body>"
130
+
131
131
  raise HomeAssistantAPIError(
132
- f"API error: {response.status_code} - {error_data.get('message', 'Unknown error')}",
132
+ f"API error: {response.status_code} - {message}",
133
133
  status_code=response.status_code,
134
134
  response_data=error_data,
135
135
  )
136
136
 
137
- # Parse JSON response
138
- try:
139
- result: dict[str, Any] = response.json()
140
- return result
141
- except json.JSONDecodeError:
142
- # Some endpoints return empty responses
143
- return {}
137
+ return response
144
138
 
145
139
  except httpx.ConnectError as e:
146
140
  raise HomeAssistantConnectionError(
@@ -151,6 +145,31 @@ class HomeAssistantClient:
151
145
  except httpx.HTTPError as e:
152
146
  raise HomeAssistantConnectionError(f"HTTP error: {e}") from e
153
147
 
148
+ async def _request(self, method: str, endpoint: str, **kwargs: Any) -> dict[str, Any]:
149
+ """
150
+ Make authenticated request to Home Assistant API and parse JSON body.
151
+
152
+ Args:
153
+ method: HTTP method (GET, POST, etc.)
154
+ endpoint: API endpoint (without /api prefix)
155
+ **kwargs: Additional arguments for httpx request
156
+
157
+ Returns:
158
+ Response data as dictionary
159
+
160
+ Raises:
161
+ HomeAssistantConnectionError: Connection failed
162
+ HomeAssistantAuthError: Authentication failed
163
+ HomeAssistantAPIError: API error
164
+ """
165
+ response = await self._raw_request(method, endpoint, **kwargs)
166
+ try:
167
+ result: dict[str, Any] = response.json()
168
+ return result
169
+ except json.JSONDecodeError:
170
+ # Some endpoints return empty responses
171
+ return {}
172
+
154
173
  async def get_config(self) -> dict[str, Any]:
155
174
  """Get Home Assistant configuration."""
156
175
  logger.debug("Fetching Home Assistant configuration")
@@ -367,6 +386,29 @@ class HomeAssistantClient:
367
386
  response = await self._request("GET", "/error_log")
368
387
  return response if isinstance(response, str) else str(response)
369
388
 
389
+ async def get_addon_logs(self, slug: str) -> str:
390
+ """Fetch an add-on's container logs via HA Core's Supervisor REST proxy.
391
+
392
+ Uses `/api/hassio/addons/{slug}/logs`, which HA Core proxies to
393
+ Supervisor and returns as `text/plain`. This avoids the
394
+ `supervisor/api` websocket path that tries to JSON-decode the text
395
+ body and always fails (see #950).
396
+
397
+ Raises:
398
+ HomeAssistantAuthError: 401 from HA Core.
399
+ HomeAssistantAPIError: Non-2xx response (e.g. 404 unknown slug,
400
+ 400 addon not installed). `status_code` is set so callers
401
+ can map to specific suggestions.
402
+ HomeAssistantConnectionError: Network, timeout, or transport error.
403
+ """
404
+ logger.debug(f"Fetching addon logs for slug={slug}")
405
+ response = await self._raw_request(
406
+ "GET",
407
+ f"/hassio/addons/{slug}/logs",
408
+ headers={"Accept": "text/plain"},
409
+ )
410
+ return response.text
411
+
370
412
  async def test_connection(self) -> tuple[bool, str | None]:
371
413
  """
372
414
  Test connection to Home Assistant.
@@ -182,7 +182,7 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
182
182
  "The 'slug' parameter is required for source='supervisor'",
183
183
  suggestions=[
184
184
  "Provide the add-on slug, e.g. slug='core_mosquitto'",
185
- "Use ha_list_addons() to find available add-on slugs",
185
+ "Use ha_get_addon() to list installed add-on slugs",
186
186
  ],
187
187
  )
188
188
  )
@@ -515,53 +515,17 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
515
515
  limit: int | str | None = None,
516
516
  search: str | None = None,
517
517
  ) -> dict[str, Any]:
518
- """Fetch add-on container logs via the Supervisor API."""
518
+ """Fetch add-on container logs via HA Core's Supervisor REST proxy.
519
+
520
+ Routes through `/api/hassio/addons/{slug}/logs` (returned as
521
+ text/plain) instead of the `supervisor/api` websocket path, which
522
+ always fails because HA Core's proxy tries to JSON-decode the
523
+ text body. See #950.
524
+ """
519
525
  effective_limit = _coerce_limit(limit, default=DEFAULT_LOG_LIMIT, suggestion_example="100")
520
526
 
521
527
  try:
522
- result = await client.send_websocket_message(
523
- {
524
- "type": "supervisor/api",
525
- "endpoint": f"/addons/{slug}/logs",
526
- "method": "get",
527
- }
528
- )
529
-
530
- if not result.get("success"):
531
- error_msg = str(result.get("error", ""))
532
- suggestions = [
533
- f"Verify add-on slug '{slug}' is correct",
534
- "Use ha_list_addons() to find available add-on slugs",
535
- ]
536
- if "not_found" in error_msg.lower() or "unknown" in error_msg.lower():
537
- suggestions.insert(
538
- 0,
539
- "Supervisor API not available - requires HA OS or Supervised install",
540
- )
541
- raise_tool_error(
542
- create_error_response(
543
- ErrorCode.SERVICE_CALL_FAILED,
544
- result.get(
545
- "error", f"Failed to retrieve logs for add-on '{slug}'"
546
- ),
547
- context={"slug": slug},
548
- suggestions=suggestions,
549
- )
550
- )
551
-
552
- # Result may be a string (log text) or dict with result key
553
- log_text = result.get("result", "")
554
- if isinstance(log_text, dict):
555
- if "data" in log_text:
556
- log_text = log_text["data"]
557
- else:
558
- logger.warning(
559
- "Supervisor log for '%s' returned unexpected dict structure",
560
- slug,
561
- )
562
- log_text = ""
563
- if not isinstance(log_text, str):
564
- log_text = str(log_text)
528
+ log_text = await client.get_addon_logs(slug)
565
529
 
566
530
  lines = log_text.splitlines() if log_text else []
567
531
 
@@ -594,9 +558,43 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
594
558
 
595
559
  except ToolError:
596
560
  raise
561
+ except HomeAssistantAPIError as e:
562
+ status = getattr(e, "status_code", None)
563
+ if status == 400:
564
+ # Supervisor-side rejection — not caller validation. The default
565
+ # `exception_to_structured_error` path would map 400 →
566
+ # VALIDATION_INVALID_PARAMETER, which reads as "caller passed
567
+ # bad input"; a downstream proxy rejection is better modelled
568
+ # as SERVICE_CALL_FAILED.
569
+ raise_tool_error(
570
+ create_error_response(
571
+ ErrorCode.SERVICE_CALL_FAILED,
572
+ str(e),
573
+ context={"source": "supervisor", "slug": slug},
574
+ suggestions=[
575
+ f"Supervisor rejected the request for '{slug}' — "
576
+ "verify slug format or that the add-on is installed "
577
+ "and running",
578
+ "Use ha_get_addon() to list installed add-on slugs",
579
+ "Ensure Supervisor is available (HA OS or Supervised install)",
580
+ ],
581
+ )
582
+ )
583
+ if status == 404:
584
+ first_suggestion = f"Add-on '{slug}' not found or not installed"
585
+ else:
586
+ first_suggestion = f"Verify add-on slug '{slug}' is correct"
587
+ exception_to_structured_error(
588
+ e,
589
+ context={"source": "supervisor", "slug": slug},
590
+ suggestions=[
591
+ first_suggestion,
592
+ "Use ha_get_addon() to list installed add-on slugs",
593
+ "Ensure Supervisor is available (HA OS or Supervised install)",
594
+ ],
595
+ )
597
596
  except (
598
597
  HomeAssistantConnectionError,
599
- HomeAssistantAPIError,
600
598
  TimeoutError,
601
599
  OSError,
602
600
  ) as e:
@@ -604,8 +602,9 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
604
602
  e,
605
603
  context={"source": "supervisor", "slug": slug},
606
604
  suggestions=[
605
+ "Check Home Assistant connection",
607
606
  f"Verify add-on slug '{slug}' is correct",
608
- "Use ha_list_addons() to find available add-on slugs",
607
+ "Use ha_get_addon() to list installed add-on slugs",
609
608
  "Ensure Supervisor is available (HA OS or Supervised install)",
610
609
  ],
611
610
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.3.0.dev379
3
+ Version: 7.3.0.dev380
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