ha-mcp-dev 7.2.0.dev330__py3-none-any.whl → 7.2.0.dev332__py3-none-any.whl

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.
ha_mcp/tools/backup.py CHANGED
@@ -51,7 +51,7 @@ def _get_backup_hint_text() -> str:
51
51
 
52
52
  async def _get_backup_password(
53
53
  ws_client: HomeAssistantWebSocketClient,
54
- ) -> tuple[str | None, dict[str, Any] | None]:
54
+ ) -> str:
55
55
  """
56
56
  Retrieve default backup password from Home Assistant configuration.
57
57
 
@@ -59,27 +59,30 @@ async def _get_backup_password(
59
59
  ws_client: Connected WebSocket client
60
60
 
61
61
  Returns:
62
- Tuple of (password, error_dict). If retrieval fails, password is None.
62
+ The backup password string.
63
+
64
+ Raises:
65
+ ToolError: If backup config cannot be retrieved or no password is configured.
63
66
  """
64
67
  backup_config = await ws_client.send_command("backup/config/info")
65
68
  if not backup_config.get("success"):
66
- return None, {
67
- "success": False,
68
- "error": "Failed to retrieve backup configuration",
69
- "details": backup_config,
70
- }
69
+ raise_tool_error(create_error_response(
70
+ ErrorCode.SERVICE_CALL_FAILED,
71
+ "Failed to retrieve backup configuration",
72
+ context={"details": backup_config},
73
+ ))
71
74
 
72
75
  config_data = backup_config.get("result", {}).get("config", {})
73
76
  default_password = config_data.get("create_backup", {}).get("password")
74
77
 
75
78
  if not default_password:
76
- return None, {
77
- "success": False,
78
- "error": "No default backup password configured in Home Assistant",
79
- "suggestion": "Configure automatic backups in Home Assistant settings to set a default password",
80
- }
79
+ raise_tool_error(create_error_response(
80
+ ErrorCode.SERVICE_CALL_FAILED,
81
+ "No default backup password configured in Home Assistant",
82
+ suggestions=["Configure automatic backups in Home Assistant settings to set a default password"],
83
+ ))
81
84
 
82
- return default_password, None
85
+ return cast(str, default_password)
83
86
 
84
87
 
85
88
  async def create_backup(
@@ -107,13 +110,8 @@ async def create_backup(
107
110
  ))
108
111
  ws_client = cast(HomeAssistantWebSocketClient, ws_client)
109
112
 
110
- # Get backup password
111
- password, error = await _get_backup_password(ws_client)
112
- if error:
113
- raise_tool_error(create_error_response(
114
- ErrorCode.SERVICE_CALL_FAILED,
115
- error.get("error", "Failed to retrieve backup password"),
116
- ))
113
+ # Get backup password (raises ToolError on failure)
114
+ password = await _get_backup_password(ws_client)
117
115
 
118
116
  # Generate backup name if not provided
119
117
  if not name:
@@ -283,12 +281,15 @@ async def restore_backup(
283
281
  safety_backup_name = f"PreRestore_Safety_{now.strftime('%Y-%m-%d_%H:%M:%S')}"
284
282
 
285
283
  # Get backup password
286
- password, error = await _get_backup_password(ws_client)
287
- if error:
284
+ try:
285
+ password = await _get_backup_password(ws_client)
286
+ except ToolError:
288
287
  # Password error - log warning but continue (restore might still work)
289
288
  logger.warning("No default password - proceeding without safety backup")
289
+ password = None
290
290
  safety_backup_id = None
291
- else:
291
+
292
+ if password is not None:
292
293
  safety_backup = await ws_client.send_command(
293
294
  "backup/generate",
294
295
  name=safety_backup_name,
@@ -421,38 +421,40 @@ class DeviceControlTools:
421
421
  }
422
422
 
423
423
  elif operation.status.value == "failed":
424
- return {
425
- "operation_id": operation_id,
426
- "status": "failed",
427
- "success": False,
428
- "entity_id": operation.entity_id,
429
- "action": operation.action,
430
- "error": operation.error_message,
431
- "duration_ms": operation.duration_ms,
432
- "suggestions": [
424
+ raise_tool_error(create_error_response(
425
+ ErrorCode.SERVICE_CALL_FAILED,
426
+ operation.error_message or "Device operation failed",
427
+ context={
428
+ "operation_id": operation_id,
429
+ "entity_id": operation.entity_id,
430
+ "action": operation.action,
431
+ "duration_ms": operation.duration_ms,
432
+ },
433
+ suggestions=[
433
434
  "Check if device is available and responding",
434
435
  "Verify device supports the requested action",
435
436
  "Check Home Assistant logs for error details",
436
437
  "Try a simpler action like toggle",
437
438
  ],
438
- }
439
+ ))
439
440
 
440
441
  elif operation.status.value == "timeout":
441
- return {
442
- "operation_id": operation_id,
443
- "status": "timeout",
444
- "success": False,
445
- "entity_id": operation.entity_id,
446
- "action": operation.action,
447
- "error": f"Operation timed out after {operation.timeout_ms}ms",
448
- "elapsed_ms": operation.elapsed_ms,
449
- "suggestions": [
442
+ raise_tool_error(create_error_response(
443
+ ErrorCode.TIMEOUT_OPERATION,
444
+ f"Operation timed out after {operation.timeout_ms}ms",
445
+ context={
446
+ "operation_id": operation_id,
447
+ "entity_id": operation.entity_id,
448
+ "action": operation.action,
449
+ "elapsed_ms": operation.elapsed_ms,
450
+ },
451
+ suggestions=[
450
452
  "Device may be slow to respond or offline",
451
453
  "Check device connectivity",
452
454
  "Try increasing timeout for slow devices",
453
455
  "Verify device is powered on",
454
456
  ],
455
- }
457
+ ))
456
458
 
457
459
  else: # pending
458
460
  return {
@@ -240,21 +240,20 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
240
240
  else str(error)
241
241
  )
242
242
  failed = dict.fromkeys(assistants, should_expose)
243
- response: dict[str, Any] = {
244
- "success": False,
245
- "error": {
246
- "code": ErrorCode.SERVICE_CALL_FAILED.value,
247
- "message": f"Exposure failed: {error_msg}",
248
- "suggestion": "Check Home Assistant connection and entity availability",
249
- },
243
+ context: dict[str, Any] = {
250
244
  "entity_id": entity_id,
251
245
  "exposure_succeeded": succeeded,
252
246
  "exposure_failed": failed,
253
247
  }
254
248
  if has_registry_updates:
255
- response["partial"] = True
256
- response["entity_entry"] = _format_entity_entry(entity_entry)
257
- return response
249
+ context["partial"] = True
250
+ context["entity_entry"] = _format_entity_entry(entity_entry)
251
+ raise_tool_error(create_error_response(
252
+ ErrorCode.SERVICE_CALL_FAILED,
253
+ f"Exposure failed: {error_msg}",
254
+ context=context,
255
+ suggestions=["Check Home Assistant connection and entity availability"],
256
+ ))
258
257
 
259
258
  # Track successful exposures
260
259
  for a in assistants:
@@ -272,21 +271,15 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
272
271
  if get_result.get("success"):
273
272
  entity_entry = get_result.get("result", {})
274
273
  else:
275
- # Return plain dict so caller can inspect exposure_succeeded
276
- return {
277
- "success": False,
278
- "error": {
279
- "code": ErrorCode.ENTITY_NOT_FOUND.value,
280
- "message": f"Entity '{entity_id}' not found in registry after applying exposure changes",
281
- "suggestion": "Use ha_search_entities() to verify the entity exists",
282
- },
283
- "entity_id": entity_id,
284
- "suggestions": [
274
+ raise_tool_error(create_error_response(
275
+ ErrorCode.ENTITY_NOT_FOUND,
276
+ f"Entity '{entity_id}' not found in registry after applying exposure changes",
277
+ context={"entity_id": entity_id, "exposure_succeeded": exposure_result},
278
+ suggestions=[
285
279
  "Verify the entity_id exists using ha_search_entities()",
286
280
  "The entity's exposure settings were likely changed, but its current state could not be confirmed.",
287
281
  ],
288
- "exposure_succeeded": exposure_result,
289
- }
282
+ ))
290
283
 
291
284
  response_data: dict[str, Any] = {
292
285
  "success": True,
@@ -831,15 +824,10 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
831
824
  if isinstance(error, dict)
832
825
  else str(error)
833
826
  )
834
- return {
835
- "success": False,
836
- "entity_id": eid,
837
- "error": error_msg,
838
- }
827
+ raise ValueError(error_msg)
839
828
 
840
829
  entry = result.get("result", {})
841
830
  return {
842
- "success": True,
843
831
  "entity_id": entry.get("entity_id"),
844
832
  "name": entry.get("name"),
845
833
  "original_name": entry.get("original_name"),
@@ -861,21 +849,13 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
861
849
  if not is_bulk:
862
850
  eid = entity_ids[0]
863
851
  logger.info(f"Getting entity registry entry for {eid}")
864
- result = await _fetch_entity(eid)
865
-
866
- if result.get("success"):
867
- return {
868
- "success": True,
869
- "entity_id": eid,
870
- "entity_entry": {
871
- k: v for k, v in result.items() if k not in ("success",)
872
- },
873
- }
874
- else:
852
+ try:
853
+ result = await _fetch_entity(eid)
854
+ except ValueError as e:
875
855
  raise_tool_error(
876
856
  create_error_response(
877
857
  ErrorCode.SERVICE_CALL_FAILED,
878
- f"Entity not found: {result.get('error', 'Unknown error')}",
858
+ f"Entity not found: {e}",
879
859
  context={"entity_id": eid},
880
860
  suggestions=[
881
861
  "Use ha_search_entities() to find valid entity IDs",
@@ -883,6 +863,11 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
883
863
  ],
884
864
  )
885
865
  )
866
+ return {
867
+ "success": True,
868
+ "entity_id": eid,
869
+ "entity_entry": result,
870
+ }
886
871
 
887
872
  # Bulk case - fetch all entities
888
873
  logger.info(
@@ -904,18 +889,8 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
904
889
  "error": str(fetch_result),
905
890
  }
906
891
  )
907
- continue
908
- if fetch_result.get("success"):
909
- entity_entries.append(
910
- {k: v for k, v in fetch_result.items() if k not in ("success",)}
911
- )
912
892
  else:
913
- errors.append(
914
- {
915
- "entity_id": eid,
916
- "error": fetch_result.get("error", "Unknown error"),
917
- }
918
- )
893
+ entity_entries.append(fetch_result)
919
894
 
920
895
  response: dict[str, Any] = {
921
896
  "success": True,
@@ -13,7 +13,7 @@ from pydantic import Field
13
13
 
14
14
  from ..errors import ErrorCode, create_error_response
15
15
  from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
16
- from .util_helpers import add_timezone_metadata, coerce_int_param
16
+ from .util_helpers import add_timezone_metadata, coerce_bool_param, coerce_int_param
17
17
 
18
18
  logger = logging.getLogger(__name__)
19
19
 
@@ -33,220 +33,85 @@ CATEGORY_DISPLAY = {v: k for k, v in CATEGORY_MAP.items()}
33
33
  CATEGORY_DISPLAY["plugin"] = "lovelace" # Display as lovelace for users
34
34
 
35
35
 
36
- async def _is_hacs_available() -> bool:
37
- """Return True if HACS is installed and responding via WebSocket.
38
-
39
- Raises if the WebSocket connection fails — callers handle API errors via
40
- their own exception_to_structured_error blocks.
41
- """
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
-
47
-
48
36
  async def _assert_hacs_available() -> None:
49
- """Raise ToolError if HACS is not available.
37
+ """Raise ToolError if HACS is not installed or not responding.
38
+
39
+ Distinguishes "unknown command" (HACS not installed) from other failures
40
+ (HACS installed but broken) so the error message is accurate.
50
41
 
51
42
  Must be called within a try block that handles API errors via
52
43
  exception_to_structured_error, so connection failures are classified
53
44
  correctly rather than masked as COMPONENT_NOT_INSTALLED.
54
45
  """
55
- if not await _is_hacs_available():
56
- raise_tool_error(create_error_response(
46
+ from ..client.websocket_client import get_websocket_client
47
+
48
+ ws_client = await get_websocket_client()
49
+ response = await ws_client.send_command("hacs/info")
50
+ if response.get("success"):
51
+ return
52
+
53
+ error = response.get("error", {})
54
+ error_code = error.get("code") if isinstance(error, dict) else None
55
+ error_message = (
56
+ error.get("message", "") if isinstance(error, dict) else str(error)
57
+ )
58
+
59
+ # "unknown_command" means HACS is not installed at all
60
+ if error_code == "unknown_command" or "unknown command" in error_message.lower():
61
+ raise_tool_error(
62
+ create_error_response(
63
+ ErrorCode.COMPONENT_NOT_INSTALLED,
64
+ "HACS is not installed.",
65
+ suggestions=[
66
+ "Install HACS from https://hacs.xyz/",
67
+ "Restart Home Assistant after HACS installation",
68
+ ],
69
+ )
70
+ )
71
+
72
+ # HACS is installed but not responding correctly
73
+ raise_tool_error(
74
+ create_error_response(
57
75
  ErrorCode.COMPONENT_NOT_INSTALLED,
58
- "HACS is not installed or not loaded.",
76
+ f"HACS is installed but not responding: {error_message or 'unknown error'}",
59
77
  suggestions=[
60
- "Install HACS from https://hacs.xyz/",
61
- "Restart Home Assistant after HACS installation",
78
+ "Restart Home Assistant",
62
79
  "Check Home Assistant logs for HACS errors",
80
+ "Verify HACS is up to date",
63
81
  ],
64
- ))
82
+ )
83
+ )
65
84
 
66
85
 
67
86
  def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
68
87
  """Register HACS integration tools with the MCP server."""
69
88
 
70
- @mcp.tool(tags={"HACS"}, annotations={"idempotentHint": True, "readOnlyHint": True, "title": "Get HACS Info"})
71
- @log_tool_usage
72
- async def ha_hacs_info() -> dict[str, Any]:
73
- """Get HACS status, version, and enabled categories.
74
-
75
- Returns information about the HACS installation including:
76
- - Version number
77
- - Enabled categories (integration, lovelace, theme, etc.)
78
- - Stage (running, startup, etc.)
79
- - Lovelace mode
80
- - Disabled reason (if any)
81
-
82
- This is useful for validating that HACS is installed and operational
83
- before using other HACS tools.
84
-
85
- **HACS Installation:**
86
- If HACS is not installed, visit https://hacs.xyz/ for installation instructions.
87
-
88
- Returns:
89
- Dictionary with HACS status information or error if HACS is not available.
90
- """
91
- try:
92
- # Check if HACS is available
93
- await _assert_hacs_available()
94
-
95
- # Get HACS info via WebSocket
96
- from ..client.websocket_client import get_websocket_client
97
- ws_client = await get_websocket_client()
98
- response = await ws_client.send_command("hacs/info")
99
-
100
- if not response.get("success"):
101
- exception_to_structured_error(
102
- Exception(f"HACS info request failed: {response}"),
103
- context={"command": "hacs/info"},
104
- raise_error=True,
105
- )
106
-
107
- result = response.get("result", {})
108
-
109
- return await add_timezone_metadata(client, {
110
- "success": True,
111
- "version": result.get("version"),
112
- "categories": result.get("categories", []),
113
- "stage": result.get("stage"),
114
- "lovelace_mode": result.get("lovelace_mode"),
115
- "disabled_reason": result.get("disabled_reason"),
116
- "data": result,
117
- })
118
-
119
- except ToolError:
120
- raise
121
- except Exception as e:
122
- exception_to_structured_error(
123
- e,
124
- context={"tool": "ha_hacs_info"},
125
- suggestions=[
126
- "Verify HACS is installed: https://hacs.xyz/",
127
- "Check Home Assistant connection",
128
- "Restart Home Assistant if HACS was recently installed",
129
- ],
130
- )
131
-
132
- @mcp.tool(tags={"HACS"}, annotations={"idempotentHint": True, "readOnlyHint": True, "title": "List HACS Installed"})
133
- @log_tool_usage
134
- async def ha_hacs_list_installed(
135
- category: Annotated[
136
- Literal["integration", "lovelace", "theme", "appdaemon", "python_script"] | None,
137
- Field(
138
- default=None,
139
- description=(
140
- "Filter by category: 'integration', 'lovelace', 'theme', "
141
- "'appdaemon', or 'python_script'. Use None for all categories."
142
- ),
143
- ),
144
- ] = None,
145
- ) -> dict[str, Any]:
146
- """List installed HACS repositories with focused, small response.
147
-
148
- **DASHBOARD TIP:** Use `category="lovelace"` to discover installed custom cards
149
- for use with `ha_config_set_dashboard()`.
150
-
151
- Returns a list of installed repositories with key information:
152
- - name: Repository name
153
- - full_name: Full GitHub repository name (owner/repo)
154
- - category: Type of repository (integration, lovelace, theme, etc.)
155
- - installed_version: Currently installed version
156
- - available_version: Latest available version
157
- - pending_update: Whether an update is available
158
- - description: Repository description
159
-
160
- **Categories:**
161
- - `integration`: Custom integrations and components
162
- - `lovelace`: Custom dashboard cards and panels
163
- - `theme`: Custom themes for the UI
164
- - `appdaemon`: AppDaemon apps
165
- - `python_script`: Python scripts
166
-
167
- Args:
168
- category: Filter results by category (default: all categories)
169
-
170
- Returns:
171
- List of installed HACS repositories or error if HACS is not available.
172
- """
173
- try:
174
- # Check if HACS is available
175
- await _assert_hacs_available()
176
-
177
- # Get installed repositories via WebSocket
178
- from ..client.websocket_client import get_websocket_client
179
- ws_client = await get_websocket_client()
180
-
181
- # Build command parameters - map user-friendly category to HACS internal name
182
- kwargs_cmd: dict[str, Any] = {}
183
- if category:
184
- hacs_category = CATEGORY_MAP.get(category, category)
185
- kwargs_cmd["categories"] = [hacs_category]
186
-
187
- response = await ws_client.send_command("hacs/repositories/list", **kwargs_cmd)
188
-
189
- if not response.get("success"):
190
- exception_to_structured_error(
191
- Exception(f"HACS repositories list request failed: {response}"),
192
- context={"command": "hacs/repositories/list", "category": category},
193
- raise_error=True,
194
- )
195
-
196
- repositories = response.get("result", [])
197
-
198
- # Filter to only installed repositories and extract key info
199
- installed = []
200
- for repo in repositories:
201
- if repo.get("installed", False):
202
- # Map HACS internal category back to user-friendly name
203
- repo_category = repo.get("category", "")
204
- display_category = CATEGORY_DISPLAY.get(repo_category, repo_category)
205
- installed.append({
206
- "name": repo.get("name"),
207
- "full_name": repo.get("full_name"),
208
- "category": display_category,
209
- "id": repo.get("id"), # Include numeric ID for repository_info
210
- "installed_version": repo.get("installed_version"),
211
- "available_version": repo.get("available_version"),
212
- "pending_update": repo.get("pending_upgrade", False),
213
- "description": repo.get("description"),
214
- "authors": repo.get("authors", []),
215
- "domain": repo.get("domain"), # For integrations
216
- "stars": repo.get("stars", 0),
217
- })
218
-
219
- return await add_timezone_metadata(client, {
220
- "success": True,
221
- "category_filter": category,
222
- "total_installed": len(installed),
223
- "repositories": installed,
224
- })
225
-
226
- except ToolError:
227
- raise
228
- except Exception as e:
229
- exception_to_structured_error(
230
- e,
231
- context={"tool": "ha_hacs_list_installed", "category": category},
232
- suggestions=[
233
- "Verify HACS is installed: https://hacs.xyz/",
234
- "Check category name is valid: integration, lovelace, theme, appdaemon, python_script",
235
- "Check Home Assistant connection",
236
- ],
237
- )
238
-
239
- @mcp.tool(tags={"HACS"}, annotations={"idempotentHint": True, "readOnlyHint": True, "title": "Search HACS Store"})
89
+ @mcp.tool(
90
+ tags={"HACS"},
91
+ annotations={
92
+ "idempotentHint": True,
93
+ "readOnlyHint": True,
94
+ "title": "Search HACS Store",
95
+ },
96
+ )
240
97
  @log_tool_usage
241
98
  async def ha_hacs_search(
242
- query: str,
99
+ query: str = "",
243
100
  category: Annotated[
244
- Literal["integration", "lovelace", "theme", "appdaemon", "python_script"] | None,
101
+ Literal["integration", "lovelace", "theme", "appdaemon", "python_script"]
102
+ | None,
245
103
  Field(
246
104
  default=None,
247
105
  description="Filter by category (optional)",
248
106
  ),
249
107
  ] = None,
108
+ installed_only: Annotated[
109
+ bool | str,
110
+ Field(
111
+ default=False,
112
+ description="Only return installed repositories (default: False)",
113
+ ),
114
+ ] = False,
250
115
  max_results: Annotated[
251
116
  int | str,
252
117
  Field(
@@ -262,56 +127,55 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
262
127
  ),
263
128
  ] = 0,
264
129
  ) -> dict[str, Any]:
265
- """Search HACS store for repositories by keyword with pagination.
130
+ """Search HACS store for repositories, or list installed repositories.
266
131
 
267
- Searches the HACS store for repositories matching the query string.
268
- Returns repository information including stars, downloads, and descriptions.
132
+ **Search mode** (default): Searches by keyword across name, description, and authors.
133
+ **Browse mode** (no query, `installed_only=False`): Returns all HACS store repos
134
+ sorted alphabetically, paginated by `max_results` and `offset`.
135
+ **Installed mode** (`installed_only=True`): Lists installed repos (no query needed).
269
136
 
270
- **Use Cases:**
137
+ **DASHBOARD TIP:** Use `installed_only=True, category="lovelace"` to discover
138
+ installed custom cards for use with `ha_config_set_dashboard()`.
139
+
140
+ **Examples:**
271
141
  - Find custom cards: `ha_hacs_search("mushroom", category="lovelace")`
272
142
  - Find integrations: `ha_hacs_search("nest", category="integration")`
273
- - Browse themes: `ha_hacs_search("dark", category="theme")`
274
-
275
- Results include:
276
- - name: Repository name
277
- - full_name: Full GitHub repository name
278
- - description: Repository description
279
- - category: Type of repository
280
- - stars: GitHub stars count
281
- - downloads: Number of HACS installations
282
- - authors: Repository authors
283
- - installed: Whether already installed
143
+ - List installed: `ha_hacs_search(installed_only=True)`
144
+ - Installed by category: `ha_hacs_search(installed_only=True, category="lovelace")`
284
145
 
285
146
  Args:
286
- query: Search query (repository name, description, author)
147
+ query: Search query (repository name, description, author). Empty string with
148
+ installed_only=True lists all installed repos.
287
149
  category: Filter by category (optional)
150
+ installed_only: Only return installed repositories (default: False)
288
151
  max_results: Maximum results to return (default: 10, max: 100)
289
152
  offset: Number of results to skip for pagination (default: 0)
290
-
291
- Returns:
292
- Search results from HACS store or error if HACS is not available.
293
153
  """
294
154
  try:
295
- # Coerce max_results and offset to int
155
+ # Coerce parameters
156
+ installed_only_bool = coerce_bool_param(
157
+ installed_only, "installed_only", default=False
158
+ )
296
159
  max_results_int = coerce_int_param(
297
160
  max_results,
298
161
  "max_results",
299
162
  default=10,
300
163
  min_value=1,
301
164
  max_value=100,
302
- ) or 10
165
+ )
303
166
  offset_int = coerce_int_param(
304
167
  offset,
305
168
  "offset",
306
169
  default=0,
307
170
  min_value=0,
308
- ) or 0
171
+ )
309
172
 
310
173
  # Check if HACS is available
311
174
  await _assert_hacs_available()
312
175
 
313
176
  # Get all repositories via WebSocket
314
177
  from ..client.websocket_client import get_websocket_client
178
+
315
179
  ws_client = await get_websocket_client()
316
180
 
317
181
  # Build command parameters - map user-friendly category to HACS internal name
@@ -320,22 +184,31 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
320
184
  hacs_category = CATEGORY_MAP.get(category, category)
321
185
  kwargs_cmd["categories"] = [hacs_category]
322
186
 
323
- response = await ws_client.send_command("hacs/repositories/list", **kwargs_cmd)
187
+ response = await ws_client.send_command(
188
+ "hacs/repositories/list", **kwargs_cmd
189
+ )
324
190
 
325
191
  if not response.get("success"):
326
192
  exception_to_structured_error(
327
193
  Exception(f"HACS search request failed: {response}"),
328
- context={"command": "hacs/repositories/list", "query": query, "category": category},
194
+ context={
195
+ "command": "hacs/repositories/list",
196
+ "query": query,
197
+ "category": category,
198
+ },
329
199
  raise_error=True,
330
200
  )
331
201
 
332
202
  all_repositories = response.get("result", [])
333
203
 
334
- # Simple search: filter by query string in name, description, or authors
335
204
  query_lower = query.lower().strip()
336
205
  matches = []
337
206
 
338
207
  for repo in all_repositories:
208
+ # Filter to installed only when requested
209
+ if installed_only_bool and not repo.get("installed", False):
210
+ continue
211
+
339
212
  # Handle None values safely
340
213
  name = (repo.get("name") or "").lower()
341
214
  description = (repo.get("description") or "").lower()
@@ -343,60 +216,83 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
343
216
  authors_list = repo.get("authors") or []
344
217
  authors = " ".join(authors_list).lower()
345
218
 
346
- # Calculate relevance score
347
- score = 0
348
- if query_lower in name:
349
- score += 100
350
- if query_lower in full_name:
351
- score += 50
352
- if query_lower in description:
353
- score += 30
354
- if query_lower in authors:
355
- score += 20
356
-
357
- if score > 0:
358
- # Map HACS internal category back to user-friendly name
359
- repo_category = repo.get("category", "")
360
- display_category = CATEGORY_DISPLAY.get(repo_category, repo_category)
361
- matches.append({
362
- "name": repo.get("name"),
363
- "full_name": repo.get("full_name"),
364
- "description": repo.get("description"),
365
- "category": display_category,
366
- "id": repo.get("id"), # Include numeric ID for repository_info
367
- "stars": repo.get("stars", 0),
368
- "downloads": repo.get("downloads", 0),
369
- "authors": authors_list,
370
- "installed": repo.get("installed", False),
371
- "installed_version": repo.get("installed_version") if repo.get("installed") else None,
372
- "available_version": repo.get("available_version"),
373
- "score": score,
374
- })
375
-
376
- # Sort by score (descending) and apply offset + limit
377
- matches.sort(key=lambda x: x["score"], reverse=True)
378
- limited_matches = matches[offset_int:offset_int + max_results_int]
219
+ # Calculate relevance score (all repos match when query is empty)
220
+ if query_lower:
221
+ score = 0
222
+ if query_lower in name:
223
+ score += 100
224
+ if query_lower in full_name:
225
+ score += 50
226
+ if query_lower in description:
227
+ score += 30
228
+ if query_lower in authors:
229
+ score += 20
230
+ if score == 0:
231
+ continue
232
+ else:
233
+ score = 0 # No scoring when listing all
234
+
235
+ # Map HACS internal category back to user-friendly name
236
+ repo_category = repo.get("category", "")
237
+ display_category = CATEGORY_DISPLAY.get(repo_category, repo_category)
238
+ entry: dict[str, Any] = {
239
+ "name": repo.get("name"),
240
+ "full_name": repo.get("full_name"),
241
+ "description": repo.get("description"),
242
+ "category": display_category,
243
+ "id": repo.get("id"),
244
+ "stars": repo.get("stars", 0),
245
+ "downloads": repo.get("downloads", 0),
246
+ "authors": authors_list,
247
+ "installed": repo.get("installed", False),
248
+ "installed_version": repo.get("installed_version")
249
+ if repo.get("installed")
250
+ else None,
251
+ "available_version": repo.get("available_version"),
252
+ }
253
+ if query_lower:
254
+ entry["score"] = score
255
+ if repo.get("installed"):
256
+ entry["pending_update"] = repo.get("pending_upgrade", False)
257
+ entry["domain"] = repo.get("domain")
258
+ matches.append(entry)
259
+
260
+ # Sort by score (descending) when searching, by name when listing
261
+ if query_lower:
262
+ matches.sort(key=lambda x: x.get("score", 0), reverse=True)
263
+ else:
264
+ matches.sort(key=lambda x: (x.get("name") or "").lower())
265
+
266
+ limited_matches = matches[offset_int : offset_int + max_results_int]
379
267
  has_more = (offset_int + len(limited_matches)) < len(matches)
380
268
 
381
- return await add_timezone_metadata(client, {
382
- "success": True,
383
- "query": query,
384
- "category_filter": category,
385
- "total_matches": len(matches),
386
- "offset": offset_int,
387
- "limit": max_results_int,
388
- "count": len(limited_matches),
389
- "has_more": has_more,
390
- "next_offset": offset_int + max_results_int if has_more else None,
391
- "results": limited_matches,
392
- })
269
+ return await add_timezone_metadata(
270
+ client,
271
+ {
272
+ "success": True,
273
+ "query": query if query_lower else None,
274
+ "category_filter": category,
275
+ "installed_only": installed_only_bool,
276
+ "total_matches": len(matches),
277
+ "offset": offset_int,
278
+ "limit": max_results_int,
279
+ "count": len(limited_matches),
280
+ "has_more": has_more,
281
+ "next_offset": offset_int + max_results_int if has_more else None,
282
+ "results": limited_matches,
283
+ },
284
+ )
393
285
 
394
286
  except ToolError:
395
287
  raise
396
288
  except Exception as e:
397
289
  exception_to_structured_error(
398
290
  e,
399
- context={"tool": "ha_hacs_search", "query": query, "category": category},
291
+ context={
292
+ "tool": "ha_hacs_search",
293
+ "query": query,
294
+ "category": category,
295
+ },
400
296
  suggestions=[
401
297
  "Verify HACS is installed: https://hacs.xyz/",
402
298
  "Try a simpler search query",
@@ -404,7 +300,14 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
404
300
  ],
405
301
  )
406
302
 
407
- @mcp.tool(tags={"HACS"}, annotations={"idempotentHint": True, "readOnlyHint": True, "title": "Get HACS Repository Info"})
303
+ @mcp.tool(
304
+ tags={"HACS"},
305
+ annotations={
306
+ "idempotentHint": True,
307
+ "readOnlyHint": True,
308
+ "title": "Get HACS Repository Info",
309
+ },
310
+ )
408
311
  @log_tool_usage
409
312
  async def ha_hacs_repository_info(repository_id: str) -> dict[str, Any]:
410
313
  """Get detailed repository information including README and documentation.
@@ -423,7 +326,7 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
423
326
  - Find theme customization options
424
327
 
425
328
  **Note:** The repository_id is the numeric ID from HACS, not the GitHub path.
426
- Use `ha_hacs_list_installed()` or `ha_hacs_search()` to find the numeric ID.
329
+ Use `ha_hacs_search()` to find the numeric ID.
427
330
 
428
331
  Args:
429
332
  repository_id: Repository numeric ID (e.g., "441028036") or GitHub path (e.g., "dvd-dev/hilo")
@@ -436,6 +339,7 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
436
339
  await _assert_hacs_available()
437
340
 
438
341
  from ..client.websocket_client import get_websocket_client
342
+
439
343
  ws_client = await get_websocket_client()
440
344
 
441
345
  # If repository_id contains a slash, it's a GitHub path - need to look up numeric ID
@@ -450,58 +354,70 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
450
354
  actual_id = str(repo.get("id"))
451
355
  break
452
356
  else:
453
- return await add_timezone_metadata(client, {
454
- "success": False,
455
- "error": f"Repository '{repository_id}' not found in HACS",
456
- "error_code": "REPOSITORY_NOT_FOUND",
457
- "suggestions": [
458
- "Use ha_hacs_search() to find the repository",
459
- "Check the repository name is correct (case-insensitive)",
460
- "The repository may need to be added to HACS first",
461
- ],
462
- })
357
+ raise_tool_error(
358
+ create_error_response(
359
+ ErrorCode.RESOURCE_NOT_FOUND,
360
+ f"Repository '{repository_id}' not found in HACS",
361
+ suggestions=[
362
+ "Use ha_hacs_search() to find the repository",
363
+ "Check the repository name is correct (case-insensitive)",
364
+ "The repository may need to be added to HACS first",
365
+ ],
366
+ )
367
+ )
463
368
 
464
369
  # Get repository info via WebSocket using numeric ID
465
- response = await ws_client.send_command("hacs/repository/info", repository_id=actual_id)
370
+ response = await ws_client.send_command(
371
+ "hacs/repository/info", repository_id=actual_id
372
+ )
466
373
 
467
374
  if not response.get("success"):
468
375
  exception_to_structured_error(
469
376
  Exception(f"HACS repository info request failed: {response}"),
470
- context={"command": "hacs/repository/info", "repository_id": repository_id},
377
+ context={
378
+ "command": "hacs/repository/info",
379
+ "repository_id": repository_id,
380
+ },
471
381
  raise_error=True,
472
382
  )
473
383
 
474
384
  result = response.get("result", {})
475
385
 
476
386
  # Extract and structure the most useful information
477
- return await add_timezone_metadata(client, {
478
- "success": True,
479
- "repository_id": repository_id,
480
- "name": result.get("name"),
481
- "full_name": result.get("full_name"),
482
- "description": result.get("description"),
483
- "category": result.get("category"),
484
- "authors": result.get("authors", []),
485
- "domain": result.get("domain"), # For integrations
486
- "installed": result.get("installed", False),
487
- "installed_version": result.get("installed_version"),
488
- "available_version": result.get("available_version"),
489
- "pending_update": result.get("pending_upgrade", False),
490
- "stars": result.get("stars", 0),
491
- "downloads": result.get("downloads", 0),
492
- "topics": result.get("topics", []),
493
- "releases": result.get("releases", []),
494
- "default_branch": result.get("default_branch"),
495
- "readme": result.get("readme"), # Full README content
496
- "data": result, # Full response for advanced use
497
- })
387
+ return await add_timezone_metadata(
388
+ client,
389
+ {
390
+ "success": True,
391
+ "repository_id": repository_id,
392
+ "name": result.get("name"),
393
+ "full_name": result.get("full_name"),
394
+ "description": result.get("description"),
395
+ "category": result.get("category"),
396
+ "authors": result.get("authors", []),
397
+ "domain": result.get("domain"), # For integrations
398
+ "installed": result.get("installed", False),
399
+ "installed_version": result.get("installed_version"),
400
+ "available_version": result.get("available_version"),
401
+ "pending_update": result.get("pending_upgrade", False),
402
+ "stars": result.get("stars", 0),
403
+ "downloads": result.get("downloads", 0),
404
+ "topics": result.get("topics", []),
405
+ "releases": result.get("releases", []),
406
+ "default_branch": result.get("default_branch"),
407
+ "readme": result.get("readme"), # Full README content
408
+ "data": result, # Full response for advanced use
409
+ },
410
+ )
498
411
 
499
412
  except ToolError:
500
413
  raise
501
414
  except Exception as e:
502
415
  exception_to_structured_error(
503
416
  e,
504
- context={"tool": "ha_hacs_repository_info", "repository_id": repository_id},
417
+ context={
418
+ "tool": "ha_hacs_repository_info",
419
+ "repository_id": repository_id,
420
+ },
505
421
  suggestions=[
506
422
  "Verify HACS is installed: https://hacs.xyz/",
507
423
  "Check repository ID format (e.g., 'hacs/integration' or 'owner/repo')",
@@ -509,7 +425,10 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
509
425
  ],
510
426
  )
511
427
 
512
- @mcp.tool(tags={"HACS"}, annotations={"destructiveHint": True, "title": "Add HACS Repository"})
428
+ @mcp.tool(
429
+ tags={"HACS"},
430
+ annotations={"destructiveHint": True, "title": "Add HACS Repository"},
431
+ )
513
432
  @log_tool_usage
514
433
  async def ha_hacs_add_repository(
515
434
  repository: str,
@@ -559,18 +478,20 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
559
478
 
560
479
  # Validate repository format
561
480
  if "/" not in repository:
562
- return await add_timezone_metadata(client, {
563
- "success": False,
564
- "error": "Invalid repository format. Must be 'owner/repo'",
565
- "error_code": "INVALID_REPOSITORY_FORMAT",
566
- "suggestions": [
567
- "Use format: 'owner/repo' (e.g., 'hacs/integration')",
568
- "Check the repository exists on GitHub",
569
- ],
570
- })
481
+ raise_tool_error(
482
+ create_error_response(
483
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
484
+ "Invalid repository format. Must be 'owner/repo'",
485
+ suggestions=[
486
+ "Use format: 'owner/repo' (e.g., 'hacs/integration')",
487
+ "Check the repository exists on GitHub",
488
+ ],
489
+ )
490
+ )
571
491
 
572
492
  # Add repository via WebSocket
573
493
  from ..client.websocket_client import get_websocket_client
494
+
574
495
  ws_client = await get_websocket_client()
575
496
 
576
497
  # Map user-friendly category to HACS internal name
@@ -595,14 +516,17 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
595
516
 
596
517
  result = response.get("result", {})
597
518
 
598
- return await add_timezone_metadata(client, {
599
- "success": True,
600
- "repository": repository,
601
- "category": category,
602
- "repository_id": result.get("id"),
603
- "message": f"Successfully added {repository} to HACS",
604
- "data": result,
605
- })
519
+ return await add_timezone_metadata(
520
+ client,
521
+ {
522
+ "success": True,
523
+ "repository": repository,
524
+ "category": category,
525
+ "repository_id": result.get("id"),
526
+ "message": f"Successfully added {repository} to HACS",
527
+ "data": result,
528
+ },
529
+ )
606
530
 
607
531
  except ToolError:
608
532
  raise
@@ -623,7 +547,13 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
623
547
  ],
624
548
  )
625
549
 
626
- @mcp.tool(tags={"HACS"}, annotations={"destructiveHint": True, "title": "Download/Install HACS Repository"})
550
+ @mcp.tool(
551
+ tags={"HACS"},
552
+ annotations={
553
+ "destructiveHint": True,
554
+ "title": "Download/Install HACS Repository",
555
+ },
556
+ )
627
557
  @log_tool_usage
628
558
  async def ha_hacs_download(
629
559
  repository_id: str,
@@ -642,7 +572,7 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
642
572
 
643
573
  **Prerequisites:**
644
574
  - The repository must already be in HACS (either from the default store or added via `ha_hacs_add_repository`)
645
- - Use `ha_hacs_search()` or `ha_hacs_list_installed()` to find the repository ID
575
+ - Use `ha_hacs_search()` to find the repository ID
646
576
 
647
577
  **Examples:**
648
578
  ```python
@@ -671,6 +601,7 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
671
601
  await _assert_hacs_available()
672
602
 
673
603
  from ..client.websocket_client import get_websocket_client
604
+
674
605
  ws_client = await get_websocket_client()
675
606
 
676
607
  # If repository_id contains a slash, it's a GitHub path - need to look up numeric ID
@@ -687,16 +618,17 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
687
618
  repo_name = repo.get("name") or repository_id
688
619
  break
689
620
  else:
690
- return await add_timezone_metadata(client, {
691
- "success": False,
692
- "error": f"Repository '{repository_id}' not found in HACS",
693
- "error_code": "REPOSITORY_NOT_FOUND",
694
- "suggestions": [
695
- "Use ha_hacs_add_repository() to add the repository first",
696
- "Use ha_hacs_search() to find available repositories",
697
- "Check the repository name is correct (case-insensitive)",
698
- ],
699
- })
621
+ raise_tool_error(
622
+ create_error_response(
623
+ ErrorCode.RESOURCE_NOT_FOUND,
624
+ f"Repository '{repository_id}' not found in HACS",
625
+ suggestions=[
626
+ "Use ha_hacs_add_repository() to add the repository first",
627
+ "Use ha_hacs_search() to find available repositories",
628
+ "Check the repository name is correct (case-insensitive)",
629
+ ],
630
+ )
631
+ )
700
632
 
701
633
  # Build download command parameters
702
634
  download_kwargs: dict[str, Any] = {"repository": actual_id}
@@ -704,7 +636,9 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
704
636
  download_kwargs["version"] = version
705
637
 
706
638
  # Download/install the repository
707
- response = await ws_client.send_command("hacs/repository/download", **download_kwargs)
639
+ response = await ws_client.send_command(
640
+ "hacs/repository/download", **download_kwargs
641
+ )
708
642
 
709
643
  if not response.get("success"):
710
644
  exception_to_structured_error(
@@ -719,15 +653,19 @@ def register_hacs_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
719
653
 
720
654
  result = response.get("result", {})
721
655
 
722
- return await add_timezone_metadata(client, {
723
- "success": True,
724
- "repository_id": actual_id,
725
- "repository": repo_name,
726
- "version": version or "latest",
727
- "message": f"Successfully installed {repo_name}" + (f" version {version}" if version else ""),
728
- "note": "For integrations, restart Home Assistant to activate. For Lovelace cards, clear browser cache.",
729
- "data": result,
730
- })
656
+ return await add_timezone_metadata(
657
+ client,
658
+ {
659
+ "success": True,
660
+ "repository_id": actual_id,
661
+ "repository": repo_name,
662
+ "version": version or "latest",
663
+ "message": f"Successfully installed {repo_name}"
664
+ + (f" version {version}" if version else ""),
665
+ "note": "For integrations, restart Home Assistant to activate. For Lovelace cards, clear browser cache.",
666
+ "data": result,
667
+ },
668
+ )
731
669
 
732
670
  except ToolError:
733
671
  raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.2.0.dev330
3
+ Version: 7.2.0.dev332
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
@@ -37,7 +37,7 @@ Dynamic: license-file
37
37
  <!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
38
38
 
39
39
  <p align="center">
40
- <img src="https://img.shields.io/badge/tools-93-blue" alt="95+ Tools">
40
+ <img src="https://img.shields.io/badge/tools-91-blue" alt="95+ Tools">
41
41
  <a href="https://github.com/homeassistant-ai/ha-mcp/releases"><img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release"></a>
42
42
  <a href="https://github.com/homeassistant-ai/ha-mcp/actions/workflows/e2e-tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests"></a>
43
43
  <a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
@@ -160,7 +160,7 @@ Spend less time configuring, more time enjoying your smart home.
160
160
  <details>
161
161
  <!-- TOOLS_TABLE_START -->
162
162
 
163
- <summary><b>Complete Tool List (93 tools)</b></summary>
163
+ <summary><b>Complete Tool List (91 tools)</b></summary>
164
164
 
165
165
  | Category | Tools |
166
166
  |----------|-------|
@@ -175,7 +175,7 @@ Spend less time configuring, more time enjoying your smart home.
175
175
  | **Entity Registry** | `ha_get_entity_exposure`, `ha_get_entity`, `ha_remove_entity`, `ha_set_entity` |
176
176
  | **Files** | `ha_delete_file`, `ha_list_files`, `ha_read_file`, `ha_write_file` |
177
177
  | **Groups** | `ha_config_list_groups`, `ha_config_remove_group`, `ha_config_set_group` |
178
- | **HACS** | `ha_hacs_add_repository`, `ha_hacs_download`, `ha_hacs_info`, `ha_hacs_list_installed`, `ha_hacs_repository_info`, `ha_hacs_search` |
178
+ | **HACS** | `ha_hacs_add_repository`, `ha_hacs_download`, `ha_hacs_repository_info`, `ha_hacs_search` |
179
179
  | **Helper Entities** | `ha_config_list_helpers`, `ha_config_remove_helper`, `ha_config_set_helper`, `ha_get_helper_schema`, `ha_set_config_entry_helper` |
180
180
  | **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs`, `ha_get_statistics` |
181
181
  | **Integrations** | `ha_delete_config_entry`, `ha_get_integration`, `ha_set_integration_enabled` |
@@ -34,9 +34,9 @@ ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/h
34
34
  ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md,sha256=RVkY-J-ZrBDDD3INHwbbkkPocukDgNg8eOuApswRlqk,8014
35
35
  ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md,sha256=rrcM8KrJ0hZw1jd4PPsVnE4rTFnsA1uA7SgltVjewkw,17856
36
36
  ha_mcp/tools/__init__.py,sha256=79ML0aicKZ5WJQn47eTPaeQntJZe1Mzt30E31_v6tXU,334
37
- ha_mcp/tools/backup.py,sha256=uFtzT9qM3FXt07egRbxs31uTB7-ZtfAVWPacoy-9FyQ,18395
37
+ ha_mcp/tools/backup.py,sha256=QN0ZhLke1ItivS0ykJV74WAtsiirPduZfhIMXW4pfJc,18372
38
38
  ha_mcp/tools/best_practice_checker.py,sha256=wyeeB5L3wPFG11VX6acg7F0fz6ISbgK-5wrUhxntIPE,14576
39
- ha_mcp/tools/device_control.py,sha256=4PoeMyV7L1C1DirDpVxgpgjyCcaI-I2bsJJCGFzgGWA,28444
39
+ ha_mcp/tools/device_control.py,sha256=8vI7LqcbNeJi-j4pA0GCUMOKPy8mD90IDYbEAy38_ok,28586
40
40
  ha_mcp/tools/enhanced.py,sha256=plvrTJmuAJ-55M0yznEq1Vv5TFDl_2FgegTYK7RaLC8,6668
41
41
  ha_mcp/tools/helpers.py,sha256=GveFEr0UPmnGwv18gHTkzPenrOOYtr8PT8EF8b4mmus,9504
42
42
  ha_mcp/tools/registry.py,sha256=LDU17zgJCgrlsv-yoBHm6TxAvkfnGoIdYsWfzLTmhGs,7718
@@ -53,10 +53,10 @@ ha_mcp/tools/tools_config_dashboards.py,sha256=3eQKQI-x7ESEBpVdIztp6YCZhWGl26Plj
53
53
  ha_mcp/tools/tools_config_entry_flow.py,sha256=AlDGCQxCWZPRTEyee3ak-KeDzlQM83AFNYAi-D7tS4I,21065
54
54
  ha_mcp/tools/tools_config_helpers.py,sha256=7CHDyCBsKkArZRDBf9hsNlszTGcW3bqIBOyyucBXahg,55094
55
55
  ha_mcp/tools/tools_config_scripts.py,sha256=92CJmlx5u2IQCrIPXnqIjpvDLvAbmAmWQ4EY3oGLA_A,16218
56
- ha_mcp/tools/tools_entities.py,sha256=aPDqXCO5EidqyIU6OTtF-X7aqNyVimwvhWm1oOpHL28,43542
56
+ ha_mcp/tools/tools_entities.py,sha256=EgnR95p8SZatCKW4oFyhbDiIVvMte81qycZuJi_sVl4,42469
57
57
  ha_mcp/tools/tools_filesystem.py,sha256=-ZEOPD7Q4_4ULoxg2Cye8vJkdCUESHUhn1-bUHi_1_g,17846
58
58
  ha_mcp/tools/tools_groups.py,sha256=OQe3BHD8L86IAov-B6tDBkEJZIFq5p8_VmpkbfYKJ0k,14283
59
- ha_mcp/tools/tools_hacs.py,sha256=HwpkyC5j88y_EyeLxMmBO0rmCLf0R2f6_IdQHmcqpzc,31425
59
+ ha_mcp/tools/tools_hacs.py,sha256=YHDwb2YzBbJLajNrfsQC-UpRjGNbQ9jOmd2lzB0aGjk,27168
60
60
  ha_mcp/tools/tools_history.py,sha256=C6a83UBaTasY-hurS_-GP00T4Ty4g43Y31B3UWLb2Sk,29568
61
61
  ha_mcp/tools/tools_integrations.py,sha256=U50nh-B99fTnYFH1TawL-a1174nQX-W01lJL5ApC5ns,17351
62
62
  ha_mcp/tools/tools_labels.py,sha256=o9ZCHY786j80zHNzn7XMTzVjdZJqeYOYC-EtS1Xhews,9868
@@ -83,12 +83,12 @@ ha_mcp/utils/fuzzy_search.py,sha256=bvT1wnGVVb2q2a4GtAnXK4uSLRU8wfGZBeGVf6CQhR0,
83
83
  ha_mcp/utils/operation_manager.py,sha256=1ETI_L2TFNhnJUUJwtuH4R0s6ZP3_rscIOfdehYSmkU,14266
84
84
  ha_mcp/utils/python_sandbox.py,sha256=mVBrBR1caQksXso3voUw2YlqY2OQJDXkt3EAZpasE0M,7488
85
85
  ha_mcp/utils/usage_logger.py,sha256=ZXbr3vHV2WT7IozrEnuNCulKt3wLXDUJI1dxaBVq0kQ,9294
86
- ha_mcp_dev-7.2.0.dev330.dist-info/licenses/LICENSE,sha256=7rJXXKBJWgJF8595wk-YTxwVTEi1kQaIqyy9dh5o_oY,1062
86
+ ha_mcp_dev-7.2.0.dev332.dist-info/licenses/LICENSE,sha256=7rJXXKBJWgJF8595wk-YTxwVTEi1kQaIqyy9dh5o_oY,1062
87
87
  tests/__init__.py,sha256=YRpec-ZFYCJ48oD_7ZcNY7dB8avoTWOrZICjaM-BYJ0,39
88
88
  tests/test_constants.py,sha256=F14Pf5QMzG77RhsecaNWWaEL-B_8ykHJLIvVMcJxT8M,609
89
89
  tests/test_env_manager.py,sha256=wEYSfwmkga9IPanzVkSo03fsY77KVw71zJG5S7Kkdr8,12045
90
- ha_mcp_dev-7.2.0.dev330.dist-info/METADATA,sha256=EEzdQTDCouW4QKr5HCLcZokBrqntFeJOdRe6cAOZ8XI,18615
91
- ha_mcp_dev-7.2.0.dev330.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
92
- ha_mcp_dev-7.2.0.dev330.dist-info/entry_points.txt,sha256=ckO8PIrfV4-YQEyjqgO8wIzcQiMFTTJNWKZLyRtFpms,292
93
- ha_mcp_dev-7.2.0.dev330.dist-info/top_level.txt,sha256=cqJLEmgh4gQBKg_vBqj0ahS4DCg4J0qBXYgZCDQ2IWs,13
94
- ha_mcp_dev-7.2.0.dev330.dist-info/RECORD,,
90
+ ha_mcp_dev-7.2.0.dev332.dist-info/METADATA,sha256=IapaC2pCw7BgQNDcf5JIyR65aCI3yHH1rcAVAvFMOSM,18573
91
+ ha_mcp_dev-7.2.0.dev332.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
92
+ ha_mcp_dev-7.2.0.dev332.dist-info/entry_points.txt,sha256=ckO8PIrfV4-YQEyjqgO8wIzcQiMFTTJNWKZLyRtFpms,292
93
+ ha_mcp_dev-7.2.0.dev332.dist-info/top_level.txt,sha256=cqJLEmgh4gQBKg_vBqj0ahS4DCg4J0qBXYgZCDQ2IWs,13
94
+ ha_mcp_dev-7.2.0.dev332.dist-info/RECORD,,