ha-mcp-dev 7.4.1.dev413__tar.gz → 7.4.1.dev415__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.
- {ha_mcp_dev-7.4.1.dev413/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev415}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_config_dashboards.py +273 -63
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_search.py +5 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.4.1.
|
|
7
|
+
version = "7.4.1.dev415"
|
|
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"
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
@@ -7,7 +7,7 @@ This module provides tools for managing dashboard metadata and content.
|
|
|
7
7
|
import json
|
|
8
8
|
import logging
|
|
9
9
|
import re
|
|
10
|
-
from typing import Annotated, Any, cast
|
|
10
|
+
from typing import Annotated, Any, cast, overload
|
|
11
11
|
|
|
12
12
|
from fastmcp.exceptions import ToolError
|
|
13
13
|
from pydantic import Field
|
|
@@ -25,7 +25,6 @@ from .util_helpers import parse_json_param
|
|
|
25
25
|
logger = logging.getLogger(__name__)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
|
|
29
28
|
async def _verify_config_unchanged(
|
|
30
29
|
client: Any,
|
|
31
30
|
url_path: str,
|
|
@@ -247,6 +246,160 @@ def _card_matches(
|
|
|
247
246
|
return True
|
|
248
247
|
|
|
249
248
|
|
|
249
|
+
# Substring in WS error message that signals the dashboard identifier was not
|
|
250
|
+
# accepted by lovelace/config (e.g., caller passed an internal id where url_path
|
|
251
|
+
# is expected). Used to gate the lazy resolver fallback in get/set tools.
|
|
252
|
+
#
|
|
253
|
+
# Source: homeassistant/components/lovelace/websocket.py, _handle_errors —
|
|
254
|
+
# emits f"Unknown config specified: {url_path}" paired with structured
|
|
255
|
+
# error.code "config_not_found". The websocket client currently surfaces only
|
|
256
|
+
# the message string, so substring matching is the only signal available at
|
|
257
|
+
# the tool layer. If HA reformats this string, the lazy fallback regresses
|
|
258
|
+
# silently to never firing — re-verify with major HA upgrades.
|
|
259
|
+
_LAZY_RESOLVE_TRIGGER = "Unknown config specified"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _should_lazy_resolve(error_msg: str) -> bool:
|
|
263
|
+
"""Return True if a WS error message indicates the identifier needs resolving."""
|
|
264
|
+
return _LAZY_RESOLVE_TRIGGER in error_msg
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def _resolve_dashboard(client: Any, identifier: str) -> dict[str, str] | None:
|
|
268
|
+
"""Resolve a dashboard identifier (url_path or internal id) to both forms.
|
|
269
|
+
|
|
270
|
+
Calls ``lovelace/dashboards/list`` and returns
|
|
271
|
+
``{"url_path": ..., "id": ...}`` when the identifier matches either field
|
|
272
|
+
on a registry entry that has both fields populated; otherwise returns
|
|
273
|
+
``None``. Always pays the round-trip when called.
|
|
274
|
+
|
|
275
|
+
Two call sites:
|
|
276
|
+
- **Lazy fallback** (``_lazy_resolve_and_retry``): only invoked after
|
|
277
|
+
``lovelace/config`` rejected the identifier with
|
|
278
|
+
``_LAZY_RESOLVE_TRIGGER`` — the round-trip is gated by the caller.
|
|
279
|
+
- **Eager pre-resolve** (``ha_config_set_dashboard``): invoked before
|
|
280
|
+
hyphen validation so callers may pass either form; gated on a
|
|
281
|
+
cheap heuristic ("no hyphen, not 'lovelace'") rather than an error
|
|
282
|
+
from HA.
|
|
283
|
+
"""
|
|
284
|
+
result = await client.send_websocket_message({"type": "lovelace/dashboards/list"})
|
|
285
|
+
if isinstance(result, dict) and "result" in result:
|
|
286
|
+
dashboards = result["result"]
|
|
287
|
+
elif isinstance(result, list):
|
|
288
|
+
dashboards = result
|
|
289
|
+
else:
|
|
290
|
+
# Neither dict-with-result nor list — either HA returned an error
|
|
291
|
+
# envelope (unknown shape) or the response format changed.
|
|
292
|
+
# Surface a warning so the next response-shape change isn't a
|
|
293
|
+
# silent "always no match" regression.
|
|
294
|
+
logger.warning(
|
|
295
|
+
"lovelace/dashboards/list returned an unexpected shape (type=%s); "
|
|
296
|
+
"treating as no-match",
|
|
297
|
+
type(result).__name__,
|
|
298
|
+
)
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
for d in dashboards:
|
|
302
|
+
if d.get("id") == identifier or d.get("url_path") == identifier:
|
|
303
|
+
url_path = d.get("url_path") or ""
|
|
304
|
+
entry_id = d.get("id") or ""
|
|
305
|
+
if not url_path or not entry_id:
|
|
306
|
+
# Malformed registry entry — neither form is safe to
|
|
307
|
+
# forward. Skip rather than return empty strings that
|
|
308
|
+
# would be silently used by callers (e.g.
|
|
309
|
+
# ``delete_dashboard`` would forward ``resolved_id=""``).
|
|
310
|
+
continue
|
|
311
|
+
return {"url_path": url_path, "id": entry_id}
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@overload
|
|
316
|
+
async def _lazy_resolve_and_retry(
|
|
317
|
+
client: Any,
|
|
318
|
+
url_path: str,
|
|
319
|
+
ws_data: dict[str, Any],
|
|
320
|
+
response: Any,
|
|
321
|
+
) -> tuple[str, Any]: ...
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@overload
|
|
325
|
+
async def _lazy_resolve_and_retry(
|
|
326
|
+
client: Any,
|
|
327
|
+
url_path: None,
|
|
328
|
+
ws_data: dict[str, Any],
|
|
329
|
+
response: Any,
|
|
330
|
+
) -> tuple[None, Any]: ...
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
async def _lazy_resolve_and_retry(
|
|
334
|
+
client: Any,
|
|
335
|
+
url_path: str | None,
|
|
336
|
+
ws_data: dict[str, Any],
|
|
337
|
+
response: Any,
|
|
338
|
+
) -> tuple[str | None, Any]:
|
|
339
|
+
"""Trigger-gated lazy resolve + single retry of a lovelace/config call.
|
|
340
|
+
|
|
341
|
+
If `response` indicates HA rejected the identifier with the
|
|
342
|
+
_LAZY_RESOLVE_TRIGGER substring, resolves `url_path` via
|
|
343
|
+
lovelace/dashboards/list and retries the WS call with the canonical
|
|
344
|
+
url_path. Returns the (possibly updated) url_path and the
|
|
345
|
+
(possibly retried) response so the caller can chain naturally:
|
|
346
|
+
|
|
347
|
+
url_path, response = await _lazy_resolve_and_retry(
|
|
348
|
+
client, url_path, ws_data, response
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
No-op when:
|
|
352
|
+
- the response is not a failure (success=True or non-dict),
|
|
353
|
+
- ``url_path`` is empty,
|
|
354
|
+
- the error message does not contain ``_LAZY_RESOLVE_TRIGGER``
|
|
355
|
+
(the substring miss),
|
|
356
|
+
- the resolver finds no match,
|
|
357
|
+
- or the resolver itself raises (logged at WARNING).
|
|
358
|
+
|
|
359
|
+
In every no-op case the original ``response`` is returned unchanged
|
|
360
|
+
so the caller's existing error-handling path runs against the real
|
|
361
|
+
HA error rather than a synthetic "resolver failed" one.
|
|
362
|
+
|
|
363
|
+
The caller's `ws_data` dict is never mutated: when a retry is needed,
|
|
364
|
+
a shallow copy is made and the canonical `url_path` written into the
|
|
365
|
+
copy before the retry call.
|
|
366
|
+
"""
|
|
367
|
+
if not (isinstance(response, dict) and not response.get("success", True)):
|
|
368
|
+
return url_path, response
|
|
369
|
+
if not url_path:
|
|
370
|
+
return url_path, response
|
|
371
|
+
|
|
372
|
+
err = response.get("error", {})
|
|
373
|
+
err_msg = err.get("message", str(err)) if isinstance(err, dict) else str(err)
|
|
374
|
+
if not _should_lazy_resolve(err_msg):
|
|
375
|
+
return url_path, response
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
resolved = await _resolve_dashboard(client, url_path)
|
|
379
|
+
except Exception as resolver_exc:
|
|
380
|
+
# Resolver itself raised (timeout, network blip, etc.). Don't let
|
|
381
|
+
# this exception escape and replace the original HA error with
|
|
382
|
+
# one about the resolver — fall through with the original
|
|
383
|
+
# response so the caller surfaces the actual "Unknown config
|
|
384
|
+
# specified" error.
|
|
385
|
+
logger.warning(
|
|
386
|
+
"Lazy resolver failed for url_path=%r: %s; "
|
|
387
|
+
"falling through to original error",
|
|
388
|
+
url_path,
|
|
389
|
+
resolver_exc,
|
|
390
|
+
)
|
|
391
|
+
return url_path, response
|
|
392
|
+
|
|
393
|
+
if resolved is None or not resolved["url_path"]:
|
|
394
|
+
return url_path, response
|
|
395
|
+
|
|
396
|
+
url_path = resolved["url_path"]
|
|
397
|
+
retry_data = dict(ws_data)
|
|
398
|
+
retry_data["url_path"] = url_path
|
|
399
|
+
response = await client.send_websocket_message(retry_data)
|
|
400
|
+
return url_path, response
|
|
401
|
+
|
|
402
|
+
|
|
250
403
|
def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
251
404
|
"""Register Home Assistant dashboard configuration tools."""
|
|
252
405
|
|
|
@@ -255,8 +408,8 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
255
408
|
annotations={
|
|
256
409
|
"idempotentHint": True,
|
|
257
410
|
"readOnlyHint": True,
|
|
258
|
-
"title": "Get Dashboard"
|
|
259
|
-
}
|
|
411
|
+
"title": "Get Dashboard",
|
|
412
|
+
},
|
|
260
413
|
)
|
|
261
414
|
@log_tool_usage
|
|
262
415
|
async def ha_config_get_dashboard(
|
|
@@ -276,7 +429,10 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
276
429
|
),
|
|
277
430
|
] = False,
|
|
278
431
|
force_reload: Annotated[
|
|
279
|
-
bool,
|
|
432
|
+
bool,
|
|
433
|
+
Field(
|
|
434
|
+
description="Force reload from storage (bypass cache). Not applicable in search mode (search always uses force=True for fresh results)."
|
|
435
|
+
),
|
|
280
436
|
] = False,
|
|
281
437
|
entity_id: Annotated[
|
|
282
438
|
str | None,
|
|
@@ -347,7 +503,9 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
347
503
|
|
|
348
504
|
Note: YAML-mode dashboards (defined in configuration.yaml) are not included in list.
|
|
349
505
|
"""
|
|
350
|
-
search_mode =
|
|
506
|
+
search_mode = (
|
|
507
|
+
entity_id is not None or card_type is not None or heading is not None
|
|
508
|
+
)
|
|
351
509
|
try:
|
|
352
510
|
# List mode
|
|
353
511
|
if list_only:
|
|
@@ -371,11 +529,31 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
371
529
|
# Search mode — find cards, badges, or header cards
|
|
372
530
|
if search_mode:
|
|
373
531
|
get_data: dict[str, Any] = {"type": "lovelace/config", "force": True}
|
|
374
|
-
|
|
375
|
-
|
|
532
|
+
effective_url_path: str | None = (
|
|
533
|
+
url_path if url_path and url_path != "default" else None
|
|
534
|
+
)
|
|
535
|
+
if effective_url_path is not None:
|
|
536
|
+
get_data["url_path"] = effective_url_path
|
|
376
537
|
|
|
377
538
|
response = await client.send_websocket_message(get_data)
|
|
378
539
|
|
|
540
|
+
# Lazy resolver fallback: same gate as get-mode. If the
|
|
541
|
+
# caller passed an internal id where url_path is expected,
|
|
542
|
+
# HA rejects with the trigger substring; resolve and retry
|
|
543
|
+
# once. (set_dashboard handles this via an eager pre-resolver
|
|
544
|
+
# before the hyphen check, so it has no equivalent fallback
|
|
545
|
+
# here.)
|
|
546
|
+
search_resolved_from: str | None = None
|
|
547
|
+
if effective_url_path is not None:
|
|
548
|
+
new_url_path, response = await _lazy_resolve_and_retry(
|
|
549
|
+
client, effective_url_path, get_data, response
|
|
550
|
+
)
|
|
551
|
+
if new_url_path != effective_url_path:
|
|
552
|
+
# Surface the original caller-passed identifier so
|
|
553
|
+
# the caller can see their input was canonicalized.
|
|
554
|
+
search_resolved_from = url_path
|
|
555
|
+
url_path = new_url_path
|
|
556
|
+
|
|
379
557
|
if isinstance(response, dict) and not response.get("success", True):
|
|
380
558
|
error_msg = response.get("error", {})
|
|
381
559
|
if isinstance(error_msg, dict):
|
|
@@ -428,7 +606,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
428
606
|
|
|
429
607
|
config_hash: str | None = compute_config_hash(config)
|
|
430
608
|
|
|
431
|
-
|
|
609
|
+
search_result: dict[str, Any] = {
|
|
432
610
|
"success": True,
|
|
433
611
|
"action": "find_card",
|
|
434
612
|
"url_path": url_path,
|
|
@@ -447,6 +625,9 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
447
625
|
else "No matches found. Try broader search criteria."
|
|
448
626
|
),
|
|
449
627
|
}
|
|
628
|
+
if search_resolved_from is not None:
|
|
629
|
+
search_result["resolved_from"] = search_resolved_from
|
|
630
|
+
return search_result
|
|
450
631
|
|
|
451
632
|
# Get mode - build WebSocket message
|
|
452
633
|
data: dict[str, Any] = {"type": "lovelace/config", "force": force_reload}
|
|
@@ -456,7 +637,16 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
456
637
|
|
|
457
638
|
response = await client.send_websocket_message(data)
|
|
458
639
|
|
|
459
|
-
#
|
|
640
|
+
# Lazy resolver fallback: if HA rejects the identifier as unknown,
|
|
641
|
+
# resolve it via lovelace/dashboards/list and retry once. The
|
|
642
|
+
# round-trip is only paid when the caller passed an internal
|
|
643
|
+
# dashboard id (or another non-url_path form) HA does not accept.
|
|
644
|
+
original_url_path = url_path
|
|
645
|
+
url_path, response = await _lazy_resolve_and_retry(
|
|
646
|
+
client, url_path, data, response
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
# Check if request failed (after potential retry)
|
|
460
650
|
if isinstance(response, dict) and not response.get("success", True):
|
|
461
651
|
error_msg = response.get("error", {})
|
|
462
652
|
if isinstance(error_msg, dict):
|
|
@@ -485,7 +675,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
485
675
|
# Calculate config size for progressive disclosure hint
|
|
486
676
|
config_size = len(json.dumps(config)) if isinstance(config, dict) else 0
|
|
487
677
|
|
|
488
|
-
|
|
678
|
+
get_result: dict[str, Any] = {
|
|
489
679
|
"success": True,
|
|
490
680
|
"action": "get",
|
|
491
681
|
"url_path": url_path,
|
|
@@ -493,17 +683,23 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
493
683
|
"config_hash": config_hash,
|
|
494
684
|
"config_size_bytes": config_size,
|
|
495
685
|
}
|
|
686
|
+
# Surface the original caller-passed identifier when the lazy
|
|
687
|
+
# resolver canonicalised it (parity with delete_dashboard's
|
|
688
|
+
# resolved_id field). Caller can use this to detect that their
|
|
689
|
+
# input was an internal id rather than a url_path.
|
|
690
|
+
if original_url_path is not None and original_url_path != url_path:
|
|
691
|
+
get_result["resolved_from"] = original_url_path
|
|
496
692
|
|
|
497
693
|
# Add hint for large configs (progressive disclosure) - 10KB ≈ 2-3k tokens
|
|
498
694
|
if config_size >= 10000:
|
|
499
|
-
|
|
695
|
+
get_result["hint"] = (
|
|
500
696
|
f"Large config ({config_size:,} bytes). For edits, use "
|
|
501
697
|
"ha_config_get_dashboard(entity_id=...) to find card positions, "
|
|
502
698
|
"then ha_config_set_dashboard(python_transform=...) "
|
|
503
699
|
"instead of full config replacement."
|
|
504
700
|
)
|
|
505
701
|
|
|
506
|
-
return
|
|
702
|
+
return get_result
|
|
507
703
|
except ToolError:
|
|
508
704
|
raise
|
|
509
705
|
except Exception as e:
|
|
@@ -544,10 +740,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
544
740
|
|
|
545
741
|
@mcp.tool(
|
|
546
742
|
tags={"Dashboards"},
|
|
547
|
-
annotations={
|
|
548
|
-
"destructiveHint": True,
|
|
549
|
-
"title": "Create or Update Dashboard"
|
|
550
|
-
}
|
|
743
|
+
annotations={"destructiveHint": True, "title": "Create or Update Dashboard"},
|
|
551
744
|
)
|
|
552
745
|
@log_tool_usage
|
|
553
746
|
async def ha_config_set_dashboard(
|
|
@@ -729,6 +922,34 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
729
922
|
if url_path == "default":
|
|
730
923
|
url_path = "lovelace"
|
|
731
924
|
|
|
925
|
+
# Pre-resolve internal dashboard ID to url_path form before the
|
|
926
|
+
# hyphen check below, so callers may pass either form. Only fires
|
|
927
|
+
# when the identifier looks like an internal id (no hyphen, not
|
|
928
|
+
# the built-in "lovelace") and matches a known dashboard.
|
|
929
|
+
#
|
|
930
|
+
# Caveat: if a caller passes a hyphenless identifier intending
|
|
931
|
+
# to *create* a new dashboard, but it happens to match an
|
|
932
|
+
# existing dashboard's id, the rewrite silently re-targets the
|
|
933
|
+
# operation onto that existing dashboard. Pre-PR they'd have
|
|
934
|
+
# hit the hyphen-validation error and known their input was
|
|
935
|
+
# invalid; now the create-vs-update distinction depends on
|
|
936
|
+
# whether the registry happens to contain a matching id.
|
|
937
|
+
# We log the rewrite and surface the original identifier as
|
|
938
|
+
# ``resolved_from`` on the success response so callers can
|
|
939
|
+
# detect this redirect.
|
|
940
|
+
pre_resolved_from: str | None = None
|
|
941
|
+
if "-" not in url_path and url_path != "lovelace":
|
|
942
|
+
resolved = await _resolve_dashboard(client, url_path)
|
|
943
|
+
if resolved is not None and resolved["url_path"]:
|
|
944
|
+
original_url_path = url_path
|
|
945
|
+
url_path = resolved["url_path"]
|
|
946
|
+
pre_resolved_from = original_url_path
|
|
947
|
+
logger.info(
|
|
948
|
+
"ha_config_set_dashboard pre-resolver mapped %r -> %r",
|
|
949
|
+
original_url_path,
|
|
950
|
+
url_path,
|
|
951
|
+
)
|
|
952
|
+
|
|
732
953
|
# Validate url_path contains hyphen for new dashboards
|
|
733
954
|
# The built-in "lovelace" dashboard is exempt since it already exists
|
|
734
955
|
if "-" not in url_path and url_path != "lovelace":
|
|
@@ -897,7 +1118,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
897
1118
|
# Compute new hash for potential chaining
|
|
898
1119
|
new_config_hash = compute_config_hash(transformed_config)
|
|
899
1120
|
|
|
900
|
-
|
|
1121
|
+
transform_result: dict[str, Any] = {
|
|
901
1122
|
"success": True,
|
|
902
1123
|
"action": "python_transform",
|
|
903
1124
|
"url_path": url_path,
|
|
@@ -905,6 +1126,9 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
905
1126
|
"python_expression": python_transform,
|
|
906
1127
|
"message": f"Dashboard {url_path} updated via Python transform",
|
|
907
1128
|
}
|
|
1129
|
+
if pre_resolved_from is not None:
|
|
1130
|
+
transform_result["resolved_from"] = pre_resolved_from
|
|
1131
|
+
return transform_result
|
|
908
1132
|
|
|
909
1133
|
# Check if dashboard exists
|
|
910
1134
|
result = await client.send_websocket_message(
|
|
@@ -1130,6 +1354,12 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1130
1354
|
|
|
1131
1355
|
if hint:
|
|
1132
1356
|
result_dict["hint"] = hint
|
|
1357
|
+
if pre_resolved_from is not None:
|
|
1358
|
+
# Caller passed an internal id; pre-resolver mapped it to
|
|
1359
|
+
# the canonical url_path. Surface the original so a caller
|
|
1360
|
+
# who *intended* to create a new dashboard can detect that
|
|
1361
|
+
# an existing dashboard was updated instead.
|
|
1362
|
+
result_dict["resolved_from"] = pre_resolved_from
|
|
1133
1363
|
|
|
1134
1364
|
return result_dict
|
|
1135
1365
|
|
|
@@ -1150,17 +1380,15 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1150
1380
|
|
|
1151
1381
|
@mcp.tool(
|
|
1152
1382
|
tags={"Dashboards"},
|
|
1153
|
-
annotations={
|
|
1154
|
-
"destructiveHint": True,
|
|
1155
|
-
"title": "Delete Dashboard"
|
|
1156
|
-
}
|
|
1383
|
+
annotations={"destructiveHint": True, "title": "Delete Dashboard"},
|
|
1157
1384
|
)
|
|
1158
1385
|
@log_tool_usage
|
|
1159
1386
|
async def ha_config_delete_dashboard(
|
|
1160
|
-
|
|
1387
|
+
url_path: Annotated[
|
|
1161
1388
|
str,
|
|
1162
1389
|
Field(
|
|
1163
|
-
description="Dashboard
|
|
1390
|
+
description="Dashboard URL path or internal ID to delete "
|
|
1391
|
+
"(e.g., 'my-dashboard' or 'my_dashboard'). Both forms are accepted."
|
|
1164
1392
|
),
|
|
1165
1393
|
],
|
|
1166
1394
|
) -> dict[str, Any]:
|
|
@@ -1170,8 +1398,9 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1170
1398
|
WARNING: This permanently deletes the dashboard and all its configuration.
|
|
1171
1399
|
Cannot be undone. Does not work on YAML-mode dashboards.
|
|
1172
1400
|
|
|
1173
|
-
Accepts either the internal dashboard ID
|
|
1174
|
-
|
|
1401
|
+
Accepts either the URL path or the internal dashboard ID. HA internal IDs
|
|
1402
|
+
may differ from url_path (e.g. hyphens → underscores); the tool resolves
|
|
1403
|
+
either form to the actual registry ID before deletion.
|
|
1175
1404
|
|
|
1176
1405
|
EXAMPLES:
|
|
1177
1406
|
- Delete dashboard: ha_config_delete_dashboard("mobile-dashboard")
|
|
@@ -1179,37 +1408,19 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1179
1408
|
Note: The default dashboard cannot be deleted via this method.
|
|
1180
1409
|
"""
|
|
1181
1410
|
try:
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
resolved_id = None
|
|
1196
|
-
for d in dashboards:
|
|
1197
|
-
if d.get("id") == dashboard_id:
|
|
1198
|
-
resolved_id = d["id"]
|
|
1199
|
-
break
|
|
1200
|
-
if d.get("url_path") == dashboard_id:
|
|
1201
|
-
resolved_id = d["id"]
|
|
1202
|
-
break
|
|
1203
|
-
|
|
1204
|
-
if resolved_id is None:
|
|
1205
|
-
raise_tool_error(create_resource_not_found_error(
|
|
1206
|
-
"Dashboard",
|
|
1207
|
-
dashboard_id,
|
|
1208
|
-
details=(
|
|
1209
|
-
f"No dashboard found with ID or URL path '{dashboard_id}'. "
|
|
1210
|
-
"Use ha_config_get_dashboard(list_only=True) to see available dashboards."
|
|
1211
|
-
),
|
|
1212
|
-
))
|
|
1411
|
+
resolved = await _resolve_dashboard(client, url_path)
|
|
1412
|
+
if resolved is None:
|
|
1413
|
+
raise_tool_error(
|
|
1414
|
+
create_resource_not_found_error(
|
|
1415
|
+
"Dashboard",
|
|
1416
|
+
url_path,
|
|
1417
|
+
details=(
|
|
1418
|
+
f"No dashboard found with URL path or internal ID '{url_path}'. "
|
|
1419
|
+
"Use ha_config_get_dashboard(list_only=True) to see available dashboards."
|
|
1420
|
+
),
|
|
1421
|
+
)
|
|
1422
|
+
)
|
|
1423
|
+
resolved_id = resolved["id"]
|
|
1213
1424
|
|
|
1214
1425
|
response = await client.send_websocket_message(
|
|
1215
1426
|
{"type": "lovelace/dashboards/delete", "dashboard_id": resolved_id}
|
|
@@ -1233,7 +1444,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1233
1444
|
return {
|
|
1234
1445
|
"success": True,
|
|
1235
1446
|
"action": "delete",
|
|
1236
|
-
"
|
|
1447
|
+
"url_path": url_path,
|
|
1237
1448
|
"message": "Dashboard already deleted or does not exist",
|
|
1238
1449
|
}
|
|
1239
1450
|
|
|
@@ -1248,7 +1459,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1248
1459
|
"Use ha_config_get_dashboard(list_only=True) to see available dashboards",
|
|
1249
1460
|
"Cannot delete YAML-mode or default dashboard",
|
|
1250
1461
|
],
|
|
1251
|
-
context={"action": "delete", "
|
|
1462
|
+
context={"action": "delete", "url_path": url_path},
|
|
1252
1463
|
)
|
|
1253
1464
|
)
|
|
1254
1465
|
|
|
@@ -1256,10 +1467,10 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1256
1467
|
result: dict[str, Any] = {
|
|
1257
1468
|
"success": True,
|
|
1258
1469
|
"action": "delete",
|
|
1259
|
-
"
|
|
1470
|
+
"url_path": url_path,
|
|
1260
1471
|
"message": "Dashboard deleted successfully",
|
|
1261
1472
|
}
|
|
1262
|
-
if resolved_id !=
|
|
1473
|
+
if resolved_id != url_path:
|
|
1263
1474
|
result["resolved_id"] = resolved_id
|
|
1264
1475
|
return result
|
|
1265
1476
|
except ToolError:
|
|
@@ -1268,7 +1479,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1268
1479
|
logger.error(f"Error deleting dashboard: {e}")
|
|
1269
1480
|
exception_to_structured_error(
|
|
1270
1481
|
e,
|
|
1271
|
-
context={"action": "delete", "
|
|
1482
|
+
context={"action": "delete", "url_path": url_path},
|
|
1272
1483
|
suggestions=[
|
|
1273
1484
|
"Verify dashboard exists and is storage-mode",
|
|
1274
1485
|
"Check that you have admin permissions",
|
|
@@ -1286,4 +1497,3 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1286
1497
|
# - ha_config_set_dashboard_resource: Create/update resources (inline code or URL)
|
|
1287
1498
|
# - ha_config_delete_dashboard_resource: Delete resources
|
|
1288
1499
|
# =========================================================================
|
|
1289
|
-
|
|
@@ -666,6 +666,11 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
666
666
|
"safe_mode": config.get("safe_mode", False),
|
|
667
667
|
"internal_url": config.get("internal_url"),
|
|
668
668
|
"external_url": config.get("external_url"),
|
|
669
|
+
# No default: distinguish HA-not-exposing-the-key (None)
|
|
670
|
+
# from empty-allowlist ([]) — security-relevant for agents.
|
|
671
|
+
"allowlist_external_dirs": config.get(
|
|
672
|
+
"allowlist_external_dirs"
|
|
673
|
+
),
|
|
669
674
|
}
|
|
670
675
|
)
|
|
671
676
|
result["system_info"] = system_info
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/resources/skills-vendor/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev413 → ha_mcp_dev-7.4.1.dev415}/src/ha_mcp_dev.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|