ha-mcp-dev 7.0.0.dev274__tar.gz → 7.0.0.dev276__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 (95) hide show
  1. {ha_mcp_dev-7.0.0.dev274/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.0.0.dev276}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/auth/provider.py +1 -1
  4. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/client/rest_client.py +20 -0
  5. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/errors.py +7 -0
  6. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/server.py +2 -1
  7. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_filesystem.py +34 -86
  8. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_hacs.py +38 -115
  9. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_integrations.py +2 -19
  10. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_mcp_component.py +2 -12
  11. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  12. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/LICENSE +0 -0
  13. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/MANIFEST.in +0 -0
  14. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/README.md +0 -0
  15. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/setup.cfg +0 -0
  16. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/__init__.py +0 -0
  17. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/__main__.py +0 -0
  18. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/_pypi_marker +0 -0
  19. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/auth/__init__.py +0 -0
  20. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/auth/consent_form.py +0 -0
  21. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/client/__init__.py +0 -0
  22. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/client/websocket_client.py +0 -0
  23. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/client/websocket_listener.py +0 -0
  24. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/config.py +0 -0
  25. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/py.typed +0 -0
  26. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/card_types.json +0 -0
  27. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/dashboard_guide.md +0 -0
  28. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  29. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  30. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  31. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  32. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  33. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  34. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  35. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  36. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  37. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  38. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  39. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  40. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  41. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  42. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  43. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  44. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/smoke_test.py +0 -0
  45. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/__init__.py +0 -0
  46. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/backup.py +0 -0
  47. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  48. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/device_control.py +0 -0
  49. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/enhanced.py +0 -0
  50. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/helpers.py +0 -0
  51. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/registry.py +0 -0
  52. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/smart_search.py +0 -0
  53. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_addons.py +0 -0
  54. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_areas.py +0 -0
  55. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  56. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  57. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_calendar.py +0 -0
  58. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_camera.py +0 -0
  59. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  60. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  61. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  62. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  63. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_config_info.py +0 -0
  64. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  65. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_entities.py +0 -0
  66. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_groups.py +0 -0
  67. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_history.py +0 -0
  68. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_labels.py +0 -0
  69. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_registry.py +0 -0
  70. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_resources.py +0 -0
  71. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_search.py +0 -0
  72. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_service.py +0 -0
  73. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_services.py +0 -0
  74. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_system.py +0 -0
  75. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_todo.py +0 -0
  76. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_traces.py +0 -0
  77. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_updates.py +0 -0
  78. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_utility.py +0 -0
  79. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  80. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/tools_zones.py +0 -0
  81. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/tools/util_helpers.py +0 -0
  82. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/utils/__init__.py +0 -0
  83. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/utils/domain_handlers.py +0 -0
  84. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  85. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/utils/operation_manager.py +0 -0
  86. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/utils/python_sandbox.py +0 -0
  87. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp/utils/usage_logger.py +0 -0
  88. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  89. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  90. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  91. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  92. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  93. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/tests/__init__.py +0 -0
  94. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/tests/test_constants.py +0 -0
  95. {ha_mcp_dev-7.0.0.dev274 → ha_mcp_dev-7.0.0.dev276}/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.0.0.dev274
3
+ Version: 7.0.0.dev276
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.0.0.dev274"
7
+ version = "7.0.0.dev276"
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"
@@ -168,7 +168,7 @@ class HomeAssistantOAuthProvider(OAuthProvider):
168
168
  decoded = urlsafe_b64decode(token.encode()).decode()
169
169
  payload = json.loads(decoded)
170
170
 
171
- ha_token = payload.get("ha_token")
171
+ ha_token: str | None = payload.get("ha_token")
172
172
 
173
173
  if ha_token:
174
174
  return str(ha_token)
@@ -769,6 +769,26 @@ class HomeAssistantClient:
769
769
  )
770
770
  return found
771
771
 
772
+ async def delete_config_entry(self, entry_id: str) -> dict[str, Any]:
773
+ """Delete a config entry via REST API.
774
+
775
+ The WebSocket command ``config_entries/delete`` is not supported by
776
+ Home Assistant. The REST endpoint ``DELETE /api/config/config_entries/
777
+ entry/{entry_id}`` is the correct way to remove a config entry.
778
+
779
+ Args:
780
+ entry_id: Config entry ID to delete.
781
+
782
+ Returns:
783
+ Result dict with ``require_restart`` flag.
784
+
785
+ Raises:
786
+ HomeAssistantAPIError: If the entry is not found or the API
787
+ returns an error status.
788
+ """
789
+ logger.debug(f"Deleting config entry: {entry_id}")
790
+ return await self._request("DELETE", f"/config/config_entries/entry/{entry_id}")
791
+
772
792
  async def send_websocket_message(self, message: dict[str, Any]) -> dict[str, Any]:
773
793
  """Send message via WebSocket and wait for response.
774
794
 
@@ -78,6 +78,9 @@ class ErrorCode(StrEnum):
78
78
  RESOURCE_ALREADY_EXISTS = "RESOURCE_ALREADY_EXISTS"
79
79
  RESOURCE_LOCKED = "RESOURCE_LOCKED"
80
80
 
81
+ # Component errors
82
+ COMPONENT_NOT_INSTALLED = "COMPONENT_NOT_INSTALLED"
83
+
81
84
 
82
85
  # Default suggestions for common error codes
83
86
  DEFAULT_SUGGESTIONS: dict[ErrorCode, list[str]] = {
@@ -183,6 +186,10 @@ DEFAULT_SUGGESTIONS: dict[ErrorCode, list[str]] = {
183
186
  "Check Home Assistant MCP server logs",
184
187
  "Report this issue if it persists",
185
188
  ],
189
+ ErrorCode.COMPONENT_NOT_INSTALLED: [
190
+ "Install the required custom component via HACS",
191
+ "Restart Home Assistant after installation",
192
+ ],
186
193
  }
187
194
 
188
195
 
@@ -10,6 +10,7 @@ Implements lazy initialization pattern for improved startup time:
10
10
  from __future__ import annotations
11
11
 
12
12
  import logging
13
+ from collections.abc import Callable, Coroutine
13
14
  from pathlib import Path
14
15
  from typing import TYPE_CHECKING, Any, cast
15
16
 
@@ -388,7 +389,7 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
388
389
  # Use factory to capture ref_files in closure
389
390
  def _make_skill_handler(
390
391
  s_name: str, s_uri: str, files: list[dict[str, str]],
391
- ):
392
+ ) -> Callable[[], Coroutine[Any, Any, dict[str, Any]]]:
392
393
  async def handler() -> dict[str, Any]:
393
394
  return {
394
395
  "skill": s_name,
@@ -20,6 +20,7 @@ from typing import Annotated, Any
20
20
  from fastmcp.exceptions import ToolError
21
21
  from pydantic import Field
22
22
 
23
+ from ..errors import ErrorCode, create_error_response
23
24
  from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
24
25
  from .util_helpers import add_timezone_metadata, coerce_bool_param, coerce_int_param
25
26
 
@@ -55,23 +56,34 @@ def is_filesystem_tools_enabled() -> bool:
55
56
  return value in ("true", "1", "yes", "on")
56
57
 
57
58
 
58
- async def _check_mcp_tools_available(client: Any) -> tuple[bool, str | None]:
59
- """Check if the ha_mcp_tools custom component is available.
59
+ async def _is_mcp_tools_available(client: Any) -> bool:
60
+ """Return True if the ha_mcp_tools custom component is registered in HA services.
60
61
 
61
- Returns:
62
- Tuple of (is_available, error_message if not available)
62
+ Raises if the services API call fails — callers handle API errors via
63
+ their own exception_to_structured_error blocks.
63
64
  """
64
- try:
65
- # Check if the domain is in the list of services
66
- services = await client.get_services()
67
- if MCP_TOOLS_DOMAIN in services:
68
- return True, None
69
- return False, (
65
+ # HA /api/services returns a list of {"domain": str, "services": {...}} objects.
66
+ # This format has been stable since before HA 0.7 (the first public release).
67
+ services = await client.get_services()
68
+ return any(
69
+ isinstance(s, dict) and s.get("domain") == MCP_TOOLS_DOMAIN
70
+ for s in services
71
+ )
72
+
73
+
74
+ async def _assert_mcp_tools_available(client: Any) -> None:
75
+ """Raise ToolError if ha_mcp_tools is not available.
76
+
77
+ Must be called within a try block that handles API errors via
78
+ exception_to_structured_error, so connection failures are classified
79
+ correctly rather than masked as COMPONENT_NOT_INSTALLED.
80
+ """
81
+ if not await _is_mcp_tools_available(client):
82
+ raise_tool_error(create_error_response(
83
+ ErrorCode.COMPONENT_NOT_INSTALLED,
70
84
  f"The {MCP_TOOLS_DOMAIN} custom component is not installed. "
71
- "Use ha_install_mcp_tools() to install it via HACS, then restart Home Assistant."
72
- )
73
- except Exception as e:
74
- return False, f"Failed to check for {MCP_TOOLS_DOMAIN}: {str(e)}"
85
+ "Use ha_install_mcp_tools() to install it via HACS, then restart Home Assistant.",
86
+ ))
75
87
 
76
88
 
77
89
  def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
@@ -145,20 +157,7 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
145
157
  """
146
158
  try:
147
159
  # Check if custom component is available
148
- is_available, error_msg = await _check_mcp_tools_available(client)
149
- if not is_available:
150
- return await add_timezone_metadata(
151
- client,
152
- {
153
- "success": False,
154
- "error": error_msg,
155
- "error_code": "MCP_TOOLS_NOT_INSTALLED",
156
- "suggestions": [
157
- "Run ha_install_mcp_tools() to install the custom component",
158
- "Restart Home Assistant after installation",
159
- ],
160
- },
161
- )
160
+ await _assert_mcp_tools_available(client)
162
161
 
163
162
  # Build service data
164
163
  service_data: dict[str, Any] = {"path": path}
@@ -192,13 +191,10 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
192
191
  except ToolError:
193
192
  raise
194
193
  except Exception as e:
195
- error_response = exception_to_structured_error(
194
+ exception_to_structured_error(
196
195
  e,
197
196
  context={"tool": "ha_list_files", "path": path, "pattern": pattern},
198
- raise_error=False,
199
197
  )
200
- error_with_tz = await add_timezone_metadata(client, error_response)
201
- raise_tool_error(error_with_tz)
202
198
 
203
199
  @mcp.tool(
204
200
  annotations={
@@ -277,20 +273,7 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
277
273
  )
278
274
 
279
275
  # Check if custom component is available
280
- is_available, error_msg = await _check_mcp_tools_available(client)
281
- if not is_available:
282
- return await add_timezone_metadata(
283
- client,
284
- {
285
- "success": False,
286
- "error": error_msg,
287
- "error_code": "MCP_TOOLS_NOT_INSTALLED",
288
- "suggestions": [
289
- "Run ha_install_mcp_tools() to install the custom component",
290
- "Restart Home Assistant after installation",
291
- ],
292
- },
293
- )
276
+ await _assert_mcp_tools_available(client)
294
277
 
295
278
  # Build service data
296
279
  service_data: dict[str, Any] = {"path": path}
@@ -320,13 +303,10 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
320
303
  except ToolError:
321
304
  raise
322
305
  except Exception as e:
323
- error_response = exception_to_structured_error(
306
+ exception_to_structured_error(
324
307
  e,
325
308
  context={"tool": "ha_read_file", "path": path},
326
- raise_error=False,
327
309
  )
328
- error_with_tz = await add_timezone_metadata(client, error_response)
329
- raise_tool_error(error_with_tz)
330
310
 
331
311
  @mcp.tool(
332
312
  annotations={
@@ -420,20 +400,7 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
420
400
  create_dirs_bool = coerce_bool_param(create_dirs, "create_dirs", default=True)
421
401
 
422
402
  # Check if custom component is available
423
- is_available, error_msg = await _check_mcp_tools_available(client)
424
- if not is_available:
425
- return await add_timezone_metadata(
426
- client,
427
- {
428
- "success": False,
429
- "error": error_msg,
430
- "error_code": "MCP_TOOLS_NOT_INSTALLED",
431
- "suggestions": [
432
- "Run ha_install_mcp_tools() to install the custom component",
433
- "Restart Home Assistant after installation",
434
- ],
435
- },
436
- )
403
+ await _assert_mcp_tools_available(client)
437
404
 
438
405
  # Build service data
439
406
  service_data: dict[str, Any] = {
@@ -466,13 +433,10 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
466
433
  except ToolError:
467
434
  raise
468
435
  except Exception as e:
469
- error_response = exception_to_structured_error(
436
+ exception_to_structured_error(
470
437
  e,
471
438
  context={"tool": "ha_write_file", "path": path},
472
- raise_error=False,
473
439
  )
474
- error_with_tz = await add_timezone_metadata(client, error_response)
475
- raise_tool_error(error_with_tz)
476
440
 
477
441
  @mcp.tool(
478
442
  annotations={
@@ -556,20 +520,7 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
556
520
  )
557
521
 
558
522
  # Check if custom component is available
559
- is_available, error_msg = await _check_mcp_tools_available(client)
560
- if not is_available:
561
- return await add_timezone_metadata(
562
- client,
563
- {
564
- "success": False,
565
- "error": error_msg,
566
- "error_code": "MCP_TOOLS_NOT_INSTALLED",
567
- "suggestions": [
568
- "Run ha_install_mcp_tools() to install the custom component",
569
- "Restart Home Assistant after installation",
570
- ],
571
- },
572
- )
523
+ await _assert_mcp_tools_available(client)
573
524
 
574
525
  # Build service data
575
526
  service_data: dict[str, Any] = {"path": path}
@@ -597,10 +548,7 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
597
548
  except ToolError:
598
549
  raise
599
550
  except Exception as e:
600
- error_response = exception_to_structured_error(
551
+ exception_to_structured_error(
601
552
  e,
602
553
  context={"tool": "ha_delete_file", "path": path},
603
- raise_error=False,
604
554
  )
605
- error_with_tz = await add_timezone_metadata(client, error_response)
606
- raise_tool_error(error_with_tz)
@@ -11,6 +11,7 @@ from typing import Annotated, Any, Literal
11
11
  from fastmcp.exceptions import ToolError
12
12
  from pydantic import Field
13
13
 
14
+ from ..errors import ErrorCode, create_error_response
14
15
  from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
15
16
  from .util_helpers import add_timezone_metadata, coerce_int_param
16
17
 
@@ -32,29 +33,35 @@ CATEGORY_DISPLAY = {v: k for k, v in CATEGORY_MAP.items()}
32
33
  CATEGORY_DISPLAY["plugin"] = "lovelace" # Display as lovelace for users
33
34
 
34
35
 
35
- async def _check_hacs_available(client: Any) -> tuple[bool, str | None]:
36
- """
37
- Check if HACS is installed and available via WebSocket.
36
+ async def _is_hacs_available() -> bool:
37
+ """Return True if HACS is installed and responding via WebSocket.
38
38
 
39
- Returns:
40
- Tuple of (is_available, error_message)
39
+ Raises if the WebSocket connection fails — callers handle API errors via
40
+ their own exception_to_structured_error blocks.
41
41
  """
42
- try:
43
- from ..client.websocket_client import get_websocket_client
44
- ws_client = await get_websocket_client()
42
+ from ..client.websocket_client import get_websocket_client
43
+ ws_client = await get_websocket_client()
44
+ response = await ws_client.send_command("hacs/info")
45
+ return bool(response.get("success"))
46
+
45
47
 
46
- # Try to get HACS info to verify it's installed
47
- response = await ws_client.send_command("hacs/info")
48
+ async def _assert_hacs_available() -> None:
49
+ """Raise ToolError if HACS is not available.
48
50
 
49
- if response.get("success"):
50
- return True, None
51
- else:
52
- return False, "HACS is installed but returned an error"
53
- except Exception as e:
54
- error_str = str(e).lower()
55
- if "unknown command" in error_str or "not found" in error_str:
56
- return False, "HACS is not installed or not loaded. Please install HACS from https://hacs.xyz/"
57
- return False, f"Failed to connect to HACS: {str(e)}"
51
+ Must be called within a try block that handles API errors via
52
+ exception_to_structured_error, so connection failures are classified
53
+ correctly rather than masked as COMPONENT_NOT_INSTALLED.
54
+ """
55
+ if not await _is_hacs_available():
56
+ raise_tool_error(create_error_response(
57
+ ErrorCode.COMPONENT_NOT_INSTALLED,
58
+ "HACS is not installed or not loaded.",
59
+ suggestions=[
60
+ "Install HACS from https://hacs.xyz/",
61
+ "Restart Home Assistant after HACS installation",
62
+ "Check Home Assistant logs for HACS errors",
63
+ ],
64
+ ))
58
65
 
59
66
 
60
67
  def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
@@ -83,18 +90,7 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
83
90
  """
84
91
  try:
85
92
  # Check if HACS is available
86
- is_available, error_msg = await _check_hacs_available(client)
87
- if not is_available:
88
- return await add_timezone_metadata(client, {
89
- "success": False,
90
- "error": error_msg,
91
- "error_code": "HACS_NOT_AVAILABLE",
92
- "suggestions": [
93
- "Install HACS from https://hacs.xyz/",
94
- "Ensure Home Assistant has been restarted after HACS installation",
95
- "Check Home Assistant logs for HACS errors",
96
- ],
97
- })
93
+ await _assert_hacs_available()
98
94
 
99
95
  # Get HACS info via WebSocket
100
96
  from ..client.websocket_client import get_websocket_client
@@ -123,18 +119,15 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
123
119
  except ToolError:
124
120
  raise
125
121
  except Exception as e:
126
- error_response = exception_to_structured_error(
122
+ exception_to_structured_error(
127
123
  e,
128
124
  context={"tool": "ha_hacs_info"},
129
- raise_error=False,
130
125
  suggestions=[
131
126
  "Verify HACS is installed: https://hacs.xyz/",
132
127
  "Check Home Assistant connection",
133
128
  "Restart Home Assistant if HACS was recently installed",
134
129
  ],
135
130
  )
136
- error_with_tz = await add_timezone_metadata(client, error_response)
137
- raise_tool_error(error_with_tz)
138
131
 
139
132
  @mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["hacs", "search"], "title": "List HACS Installed"})
140
133
  @log_tool_usage
@@ -179,18 +172,7 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
179
172
  """
180
173
  try:
181
174
  # Check if HACS is available
182
- is_available, error_msg = await _check_hacs_available(client)
183
- if not is_available:
184
- return await add_timezone_metadata(client, {
185
- "success": False,
186
- "error": error_msg,
187
- "error_code": "HACS_NOT_AVAILABLE",
188
- "suggestions": [
189
- "Install HACS from https://hacs.xyz/",
190
- "Ensure Home Assistant has been restarted after HACS installation",
191
- "Check Home Assistant logs for HACS errors",
192
- ],
193
- })
175
+ await _assert_hacs_available()
194
176
 
195
177
  # Get installed repositories via WebSocket
196
178
  from ..client.websocket_client import get_websocket_client
@@ -244,18 +226,15 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
244
226
  except ToolError:
245
227
  raise
246
228
  except Exception as e:
247
- error_response = exception_to_structured_error(
229
+ exception_to_structured_error(
248
230
  e,
249
231
  context={"tool": "ha_hacs_list_installed", "category": category},
250
- raise_error=False,
251
232
  suggestions=[
252
233
  "Verify HACS is installed: https://hacs.xyz/",
253
234
  "Check category name is valid: integration, lovelace, theme, appdaemon, python_script",
254
235
  "Check Home Assistant connection",
255
236
  ],
256
237
  )
257
- error_with_tz = await add_timezone_metadata(client, error_response)
258
- raise_tool_error(error_with_tz)
259
238
 
260
239
  @mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["hacs", "search"], "title": "Search HACS Store"})
261
240
  @log_tool_usage
@@ -329,18 +308,7 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
329
308
  ) or 0
330
309
 
331
310
  # Check if HACS is available
332
- is_available, error_msg = await _check_hacs_available(client)
333
- if not is_available:
334
- return await add_timezone_metadata(client, {
335
- "success": False,
336
- "error": error_msg,
337
- "error_code": "HACS_NOT_AVAILABLE",
338
- "suggestions": [
339
- "Install HACS from https://hacs.xyz/",
340
- "Ensure Home Assistant has been restarted after HACS installation",
341
- "Check Home Assistant logs for HACS errors",
342
- ],
343
- })
311
+ await _assert_hacs_available()
344
312
 
345
313
  # Get all repositories via WebSocket
346
314
  from ..client.websocket_client import get_websocket_client
@@ -426,18 +394,15 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
426
394
  except ToolError:
427
395
  raise
428
396
  except Exception as e:
429
- error_response = exception_to_structured_error(
397
+ exception_to_structured_error(
430
398
  e,
431
399
  context={"tool": "ha_hacs_search", "query": query, "category": category},
432
- raise_error=False,
433
400
  suggestions=[
434
401
  "Verify HACS is installed: https://hacs.xyz/",
435
402
  "Try a simpler search query",
436
403
  "Check category name is valid: integration, lovelace, theme, appdaemon, python_script",
437
404
  ],
438
405
  )
439
- error_with_tz = await add_timezone_metadata(client, error_response)
440
- raise_tool_error(error_with_tz)
441
406
 
442
407
  @mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["hacs", "info"], "title": "Get HACS Repository Info"})
443
408
  @log_tool_usage
@@ -468,18 +433,7 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
468
433
  """
469
434
  try:
470
435
  # Check if HACS is available
471
- is_available, error_msg = await _check_hacs_available(client)
472
- if not is_available:
473
- return await add_timezone_metadata(client, {
474
- "success": False,
475
- "error": error_msg,
476
- "error_code": "HACS_NOT_AVAILABLE",
477
- "suggestions": [
478
- "Install HACS from https://hacs.xyz/",
479
- "Ensure Home Assistant has been restarted after HACS installation",
480
- "Check Home Assistant logs for HACS errors",
481
- ],
482
- })
436
+ await _assert_hacs_available()
483
437
 
484
438
  from ..client.websocket_client import get_websocket_client
485
439
  ws_client = await get_websocket_client()
@@ -545,18 +499,15 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
545
499
  except ToolError:
546
500
  raise
547
501
  except Exception as e:
548
- error_response = exception_to_structured_error(
502
+ exception_to_structured_error(
549
503
  e,
550
504
  context={"tool": "ha_hacs_repository_info", "repository_id": repository_id},
551
- raise_error=False,
552
505
  suggestions=[
553
506
  "Verify HACS is installed: https://hacs.xyz/",
554
507
  "Check repository ID format (e.g., 'hacs/integration' or 'owner/repo')",
555
508
  "Use ha_hacs_search() to find the correct repository ID",
556
509
  ],
557
510
  )
558
- error_with_tz = await add_timezone_metadata(client, error_response)
559
- raise_tool_error(error_with_tz)
560
511
 
561
512
  @mcp.tool(annotations={"destructiveHint": True, "tags": ["hacs", "management"], "title": "Add HACS Repository"})
562
513
  @log_tool_usage
@@ -604,18 +555,7 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
604
555
  """
605
556
  try:
606
557
  # Check if HACS is available
607
- is_available, error_msg = await _check_hacs_available(client)
608
- if not is_available:
609
- return await add_timezone_metadata(client, {
610
- "success": False,
611
- "error": error_msg,
612
- "error_code": "HACS_NOT_AVAILABLE",
613
- "suggestions": [
614
- "Install HACS from https://hacs.xyz/",
615
- "Ensure Home Assistant has been restarted after HACS installation",
616
- "Check Home Assistant logs for HACS errors",
617
- ],
618
- })
558
+ await _assert_hacs_available()
619
559
 
620
560
  # Validate repository format
621
561
  if "/" not in repository:
@@ -667,14 +607,13 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
667
607
  except ToolError:
668
608
  raise
669
609
  except Exception as e:
670
- error_response = exception_to_structured_error(
610
+ exception_to_structured_error(
671
611
  e,
672
612
  context={
673
613
  "tool": "ha_hacs_add_repository",
674
614
  "repository": repository,
675
615
  "category": category,
676
616
  },
677
- raise_error=False,
678
617
  suggestions=[
679
618
  "Verify HACS is installed: https://hacs.xyz/",
680
619
  "Check repository format: 'owner/repo'",
@@ -683,8 +622,6 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
683
622
  "Check repository follows HACS guidelines: https://hacs.xyz/docs/publish/start",
684
623
  ],
685
624
  )
686
- error_with_tz = await add_timezone_metadata(client, error_response)
687
- raise_tool_error(error_with_tz)
688
625
 
689
626
  @mcp.tool(annotations={"destructiveHint": True, "tags": ["hacs", "management"], "title": "Download/Install HACS Repository"})
690
627
  @log_tool_usage
@@ -731,18 +668,7 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
731
668
  """
732
669
  try:
733
670
  # Check if HACS is available
734
- is_available, error_msg = await _check_hacs_available(client)
735
- if not is_available:
736
- return await add_timezone_metadata(client, {
737
- "success": False,
738
- "error": error_msg,
739
- "error_code": "HACS_NOT_AVAILABLE",
740
- "suggestions": [
741
- "Install HACS from https://hacs.xyz/",
742
- "Ensure Home Assistant has been restarted after HACS installation",
743
- "Check Home Assistant logs for HACS errors",
744
- ],
745
- })
671
+ await _assert_hacs_available()
746
672
 
747
673
  from ..client.websocket_client import get_websocket_client
748
674
  ws_client = await get_websocket_client()
@@ -806,14 +732,13 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
806
732
  except ToolError:
807
733
  raise
808
734
  except Exception as e:
809
- error_response = exception_to_structured_error(
735
+ exception_to_structured_error(
810
736
  e,
811
737
  context={
812
738
  "tool": "ha_hacs_download",
813
739
  "repository_id": repository_id,
814
740
  "version": version,
815
741
  },
816
- raise_error=False,
817
742
  suggestions=[
818
743
  "Verify HACS is installed: https://hacs.xyz/",
819
744
  "Check repository ID is valid (use ha_hacs_search() to find it)",
@@ -821,5 +746,3 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
821
746
  "Check version format (e.g., 'v1.2.3' or '1.2.3')",
822
747
  ],
823
748
  )
824
- error_with_tz = await add_timezone_metadata(client, error_response)
825
- raise_tool_error(error_with_tz)
@@ -353,25 +353,8 @@ def register_integration_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
353
353
  },
354
354
  ))
355
355
 
356
- message = {
357
- "type": "config_entries/delete",
358
- "entry_id": entry_id,
359
- }
360
-
361
- result = await client.send_websocket_message(message)
362
-
363
- if not result.get("success"):
364
- error_msg = result.get("error", {})
365
- if isinstance(error_msg, dict):
366
- error_msg = error_msg.get("message", str(error_msg))
367
- raise_tool_error(create_error_response(
368
- ErrorCode.SERVICE_CALL_FAILED,
369
- f"Failed to delete config entry: {error_msg}",
370
- context={"entry_id": entry_id},
371
- ))
372
-
373
- # Get result info
374
- require_restart = result.get("result", {}).get("require_restart", False)
356
+ result = await client.delete_config_entry(entry_id)
357
+ require_restart = result.get("require_restart", False)
375
358
 
376
359
  return {
377
360
  "success": True,
@@ -52,7 +52,7 @@ def register_mcp_component_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
52
52
  logger.info("MCP tools installer enabled via feature flag")
53
53
 
54
54
  # Import HACS helpers - we depend on HACS functionality
55
- from .tools_hacs import CATEGORY_MAP, _check_hacs_available
55
+ from .tools_hacs import CATEGORY_MAP, _assert_hacs_available
56
56
 
57
57
  @mcp.tool(
58
58
  annotations={
@@ -98,17 +98,7 @@ def register_mcp_component_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
98
98
  """
99
99
  try:
100
100
  # Check if HACS is available
101
- is_available, error_msg = await _check_hacs_available(client)
102
- if not is_available:
103
- raise_tool_error(create_error_response(
104
- ErrorCode.SERVICE_CALL_FAILED,
105
- error_msg or "HACS is not available",
106
- suggestions=[
107
- "Install HACS from https://hacs.xyz/",
108
- "Ensure Home Assistant has been restarted after HACS installation",
109
- "Check Home Assistant logs for HACS errors",
110
- ],
111
- ))
101
+ await _assert_hacs_available()
112
102
 
113
103
  from ..client.websocket_client import get_websocket_client
114
104
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.0.0.dev274
3
+ Version: 7.0.0.dev276
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