ha-mcp-dev 7.2.0.dev332__py3-none-any.whl → 7.2.0.dev334__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/helpers.py CHANGED
@@ -7,6 +7,7 @@ Centralized utilities that can be shared across multiple tool implementations.
7
7
  import functools
8
8
  import json
9
9
  import logging
10
+ import sys
10
11
  import time
11
12
  from typing import Any, Literal, NoReturn, overload
12
13
 
@@ -230,6 +231,30 @@ def exception_to_structured_error(
230
231
  if suggestions and "error" in error_response and isinstance(error_response["error"], dict):
231
232
  error_response["error"]["suggestions"] = suggestions
232
233
 
234
+ # Append macOS-specific hints for connection failures (after all other processing
235
+ # so hints survive regardless of whether caller provided explicit suggestions)
236
+ if (
237
+ sys.platform == "darwin"
238
+ and "error" in error_response
239
+ and isinstance(error_response["error"], dict)
240
+ and error_response["error"].get("code")
241
+ in (ErrorCode.CONNECTION_FAILED, ErrorCode.CONNECTION_TIMEOUT)
242
+ ):
243
+ macos_hints = [
244
+ "macOS may block local network access for Claude Desktop subprocesses "
245
+ "(System Settings > Privacy & Security > Local Network)",
246
+ "Try an SSH tunnel: ssh -N -L 8123:localhost:8123 user@ha-server, "
247
+ "then use http://localhost:8123",
248
+ "Ensure you are using http:// (not https://) unless SSL/TLS is configured",
249
+ ]
250
+ # Handle both "suggestions" (plural, 2+ items) and "suggestion" (singular, 1 item)
251
+ existing = error_response["error"].get("suggestions") or []
252
+ if not existing:
253
+ single = error_response["error"].get("suggestion")
254
+ if single:
255
+ existing = [single]
256
+ error_response["error"]["suggestions"] = existing + macos_hints
257
+
233
258
  if raise_error:
234
259
  raise_tool_error(error_response)
235
260
 
@@ -4,6 +4,7 @@ Smart search tools for Home Assistant MCP server.
4
4
 
5
5
  import asyncio
6
6
  import logging
7
+ import os
7
8
  import random
8
9
  import time
9
10
  from typing import Any
@@ -23,11 +24,23 @@ BULK_REST_TIMEOUT = 5.0 # Timeout for bulk REST endpoint calls
23
24
  BULK_WEBSOCKET_TIMEOUT = 3.0 # Timeout for bulk WebSocket calls
24
25
  INDIVIDUAL_CONFIG_TIMEOUT = 5.0 # Timeout for individual config fetches
25
26
 
26
- # Time budgets for fallback individual fetching (in seconds)
27
- AUTOMATION_CONFIG_TIME_BUDGET = (
28
- 15.0 # Max time for fetching automation configs individually
29
- )
30
- SCRIPT_CONFIG_TIME_BUDGET = 10.0 # Max time for fetching script configs individually
27
+ # Time budgets for fallback individual fetching (in seconds).
28
+ # Configurable via env vars for instances with many automations/scripts.
29
+ def _env_float(key: str, default: float) -> float:
30
+ raw = os.environ.get(key)
31
+ if raw is None:
32
+ return default
33
+ try:
34
+ return float(raw)
35
+ except (ValueError, TypeError):
36
+ logger.warning(f"Invalid value for {key}={raw!r}, using default {default}")
37
+ return default
38
+
39
+ AUTOMATION_CONFIG_TIME_BUDGET = _env_float("HAMCP_AUTOMATION_CONFIG_TIME_BUDGET", 30.0)
40
+ SCRIPT_CONFIG_TIME_BUDGET = _env_float("HAMCP_SCRIPT_CONFIG_TIME_BUDGET", 20.0)
41
+
42
+ # Batch size for parallel individual config fetches (Attempt C fallback)
43
+ INDIVIDUAL_FETCH_BATCH_SIZE = 10
31
44
 
32
45
 
33
46
  def _simplify_states_summary(
@@ -903,39 +916,61 @@ class SmartSearchTools:
903
916
  f"Automation WebSocket bulk fetch ({ws_type}) failed: {e}"
904
917
  )
905
918
 
906
- # Attempt C: Individual REST calls with time budget (LAST RESORT)
907
- # Prioritize name-matched automations so we at least get their configs
919
+ # Attempt C: Parallel individual REST calls with time budget (LAST RESORT)
920
+ # Fetch configs in parallel batches (subject to time budget) — don't prioritize by name score.
921
+ # Name score is only used for result ranking, not fetch order, because
922
+ # deep_search's purpose is to find matches INSIDE configs (conditions/actions),
923
+ # not just by name. Prioritizing by name would skip the configs most likely
924
+ # to contain non-obvious matches. See #879.
908
925
  if not bulk_fetched:
909
926
  budget_start = time.perf_counter()
910
- sorted_by_score = sorted(
911
- name_scored, key=lambda x: x[2], reverse=True
912
- )
927
+ uids_to_fetch = [
928
+ uid
929
+ for _, _, _, uid in name_scored
930
+ if uid and uid not in all_automation_configs
931
+ ]
932
+ total_to_fetch = len(uids_to_fetch)
933
+ fetched_count = 0
934
+ failed_count = 0
913
935
 
914
- for (
915
- _entity_id,
916
- _friendly_name,
917
- _name_score,
918
- unique_id,
919
- ) in sorted_by_score:
920
- if (
921
- time.perf_counter() - budget_start
922
- > AUTOMATION_CONFIG_TIME_BUDGET
923
- ):
924
- break
925
- if not unique_id or unique_id in all_automation_configs:
926
- continue
936
+ async def _fetch_automation_config(uid: str) -> tuple[str, dict[str, Any] | None]:
927
937
  try:
928
938
  config = await asyncio.wait_for(
929
939
  self.client._request(
930
- "GET", f"/config/automation/config/{unique_id}"
940
+ "GET", f"/config/automation/config/{uid}"
931
941
  ),
932
942
  timeout=INDIVIDUAL_CONFIG_TIMEOUT,
933
943
  )
934
- all_automation_configs[unique_id] = config
944
+ return (uid, config)
935
945
  except Exception as e:
936
946
  logger.debug(
937
- f"Automation individual config fetch ({unique_id}) failed: {e}"
947
+ f"Automation individual config fetch ({uid}) failed: {e}"
938
948
  )
949
+ return (uid, None)
950
+
951
+ for i in range(0, len(uids_to_fetch), INDIVIDUAL_FETCH_BATCH_SIZE):
952
+ if (
953
+ time.perf_counter() - budget_start
954
+ > AUTOMATION_CONFIG_TIME_BUDGET
955
+ ):
956
+ skipped = total_to_fetch - fetched_count - failed_count
957
+ logger.warning(
958
+ f"Automation config fetch budget exhausted "
959
+ f"({AUTOMATION_CONFIG_TIME_BUDGET}s). "
960
+ f"Fetched {fetched_count}/{total_to_fetch} "
961
+ f"({failed_count} failed), skipped {skipped} automations."
962
+ )
963
+ break
964
+ batch = uids_to_fetch[i : i + INDIVIDUAL_FETCH_BATCH_SIZE]
965
+ batch_results = await asyncio.gather(
966
+ *[_fetch_automation_config(uid) for uid in batch],
967
+ )
968
+ for uid_result, config_result in batch_results:
969
+ if config_result is not None:
970
+ all_automation_configs[uid_result] = config_result
971
+ fetched_count += 1
972
+ else:
973
+ failed_count += 1
939
974
 
940
975
  # Phase 3: Score with whatever configs we have
941
976
  for entity_id, friendly_name, name_score, unique_id in name_scored:
@@ -1040,37 +1075,54 @@ class SmartSearchTools:
1040
1075
  f"Script WebSocket bulk fetch ({ws_type}) failed: {e}"
1041
1076
  )
1042
1077
 
1043
- # Attempt C: Individual fetch with budget
1078
+ # Attempt C: Parallel individual fetch with budget (see #879)
1044
1079
  if not script_bulk_fetched:
1045
1080
  budget_start = time.perf_counter()
1046
- sorted_scripts = sorted(
1047
- script_name_scored, key=lambda x: x[3], reverse=True
1048
- )
1049
- for (
1050
- _entity_id,
1051
- _friendly_name,
1052
- script_id,
1053
- _name_score,
1054
- ) in sorted_scripts:
1055
- if (
1056
- time.perf_counter() - budget_start
1057
- > SCRIPT_CONFIG_TIME_BUDGET
1058
- ):
1059
- break
1060
- if script_id in all_script_configs:
1061
- continue
1081
+ sids_to_fetch = [
1082
+ sid
1083
+ for _, _, sid, _ in script_name_scored
1084
+ if sid and sid not in all_script_configs
1085
+ ]
1086
+ total_to_fetch = len(sids_to_fetch)
1087
+ fetched_count = 0
1088
+ failed_count = 0
1089
+
1090
+ async def _fetch_script_config(sid: str) -> tuple[str, dict[str, Any] | None]:
1062
1091
  try:
1063
1092
  config_resp = await asyncio.wait_for(
1064
- self.client.get_script_config(script_id),
1093
+ self.client.get_script_config(sid),
1065
1094
  timeout=INDIVIDUAL_CONFIG_TIMEOUT,
1066
1095
  )
1067
- all_script_configs[script_id] = config_resp.get(
1068
- "config", {}
1069
- )
1096
+ return (sid, config_resp.get("config", {}))
1070
1097
  except Exception as e:
1071
1098
  logger.debug(
1072
- f"Script individual config fetch ({script_id}) failed: {e}"
1099
+ f"Script individual config fetch ({sid}) failed: {e}"
1073
1100
  )
1101
+ return (sid, None)
1102
+
1103
+ for i in range(0, len(sids_to_fetch), INDIVIDUAL_FETCH_BATCH_SIZE):
1104
+ if (
1105
+ time.perf_counter() - budget_start
1106
+ > SCRIPT_CONFIG_TIME_BUDGET
1107
+ ):
1108
+ skipped = total_to_fetch - fetched_count - failed_count
1109
+ logger.warning(
1110
+ f"Script config fetch budget exhausted "
1111
+ f"({SCRIPT_CONFIG_TIME_BUDGET}s). "
1112
+ f"Fetched {fetched_count}/{total_to_fetch} "
1113
+ f"({failed_count} failed), skipped {skipped} scripts."
1114
+ )
1115
+ break
1116
+ batch = sids_to_fetch[i : i + INDIVIDUAL_FETCH_BATCH_SIZE]
1117
+ batch_results = await asyncio.gather(
1118
+ *[_fetch_script_config(sid) for sid in batch],
1119
+ )
1120
+ for sid_result, config_result in batch_results:
1121
+ if config_result is not None:
1122
+ all_script_configs[sid_result] = config_result
1123
+ fetched_count += 1
1124
+ else:
1125
+ failed_count += 1
1074
1126
 
1075
1127
  # Phase 3: Score scripts
1076
1128
  for (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.2.0.dev332
3
+ Version: 7.2.0.dev334
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
@@ -38,9 +38,9 @@ ha_mcp/tools/backup.py,sha256=QN0ZhLke1ItivS0ykJV74WAtsiirPduZfhIMXW4pfJc,18372
38
38
  ha_mcp/tools/best_practice_checker.py,sha256=wyeeB5L3wPFG11VX6acg7F0fz6ISbgK-5wrUhxntIPE,14576
39
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
- ha_mcp/tools/helpers.py,sha256=GveFEr0UPmnGwv18gHTkzPenrOOYtr8PT8EF8b4mmus,9504
41
+ ha_mcp/tools/helpers.py,sha256=GkSSdgfxRuQx3YoGfzl4U83hsDuUqN2g9maegZrsDmU,10725
42
42
  ha_mcp/tools/registry.py,sha256=LDU17zgJCgrlsv-yoBHm6TxAvkfnGoIdYsWfzLTmhGs,7718
43
- ha_mcp/tools/smart_search.py,sha256=cJzoa5bNcodLy8kBoyOA7syH-Xh9G7Oj-5dZgXqvwX0,62056
43
+ ha_mcp/tools/smart_search.py,sha256=P6kxcW2n-85M711EIt-1bB10HQ1Qwmu5iLBk_BOsd4I,65158
44
44
  ha_mcp/tools/tools_addons.py,sha256=gGHn_dYB9cDEReGfZB_cV5S1dSEzmsfCCzTHNQ2uS1A,39378
45
45
  ha_mcp/tools/tools_areas.py,sha256=KNCqVom1KD8TjH3cYCgWcDajEXUVPSaB7mUbJ5kL1Kc,18505
46
46
  ha_mcp/tools/tools_blueprints.py,sha256=y3m6uC_-EyUj5Onm5DIP3-CkqMhdxOe94KLmpXnx0Q0,13645
@@ -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.dev332.dist-info/licenses/LICENSE,sha256=7rJXXKBJWgJF8595wk-YTxwVTEi1kQaIqyy9dh5o_oY,1062
86
+ ha_mcp_dev-7.2.0.dev334.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.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,,
90
+ ha_mcp_dev-7.2.0.dev334.dist-info/METADATA,sha256=z21Te4mNY-Hd3XU56YR1H8E5IR30m8kMBF3qdcG6kWI,18573
91
+ ha_mcp_dev-7.2.0.dev334.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
92
+ ha_mcp_dev-7.2.0.dev334.dist-info/entry_points.txt,sha256=ckO8PIrfV4-YQEyjqgO8wIzcQiMFTTJNWKZLyRtFpms,292
93
+ ha_mcp_dev-7.2.0.dev334.dist-info/top_level.txt,sha256=cqJLEmgh4gQBKg_vBqj0ahS4DCg4J0qBXYgZCDQ2IWs,13
94
+ ha_mcp_dev-7.2.0.dev334.dist-info/RECORD,,