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 +25 -0
- ha_mcp/tools/smart_search.py +100 -48
- {ha_mcp_dev-7.2.0.dev332.dist-info → ha_mcp_dev-7.2.0.dev334.dist-info}/METADATA +1 -1
- {ha_mcp_dev-7.2.0.dev332.dist-info → ha_mcp_dev-7.2.0.dev334.dist-info}/RECORD +8 -8
- {ha_mcp_dev-7.2.0.dev332.dist-info → ha_mcp_dev-7.2.0.dev334.dist-info}/WHEEL +0 -0
- {ha_mcp_dev-7.2.0.dev332.dist-info → ha_mcp_dev-7.2.0.dev334.dist-info}/entry_points.txt +0 -0
- {ha_mcp_dev-7.2.0.dev332.dist-info → ha_mcp_dev-7.2.0.dev334.dist-info}/licenses/LICENSE +0 -0
- {ha_mcp_dev-7.2.0.dev332.dist-info → ha_mcp_dev-7.2.0.dev334.dist-info}/top_level.txt +0 -0
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
|
|
ha_mcp/tools/smart_search.py
CHANGED
|
@@ -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
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
30
|
-
|
|
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:
|
|
907
|
-
#
|
|
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
|
-
|
|
911
|
-
|
|
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
|
-
|
|
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/{
|
|
940
|
+
"GET", f"/config/automation/config/{uid}"
|
|
931
941
|
),
|
|
932
942
|
timeout=INDIVIDUAL_CONFIG_TIMEOUT,
|
|
933
943
|
)
|
|
934
|
-
|
|
944
|
+
return (uid, config)
|
|
935
945
|
except Exception as e:
|
|
936
946
|
logger.debug(
|
|
937
|
-
f"Automation individual config fetch ({
|
|
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:
|
|
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
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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(
|
|
1093
|
+
self.client.get_script_config(sid),
|
|
1065
1094
|
timeout=INDIVIDUAL_CONFIG_TIMEOUT,
|
|
1066
1095
|
)
|
|
1067
|
-
|
|
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 ({
|
|
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 (
|
|
@@ -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=
|
|
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=
|
|
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.
|
|
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.
|
|
91
|
-
ha_mcp_dev-7.2.0.
|
|
92
|
-
ha_mcp_dev-7.2.0.
|
|
93
|
-
ha_mcp_dev-7.2.0.
|
|
94
|
-
ha_mcp_dev-7.2.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|