ha-mcp-dev 7.4.1.dev464__tar.gz → 7.4.1.dev466__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.dev464/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev466}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/_version.py +11 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/client/rest_client.py +15 -4
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/settings_ui.py +2 -2
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_bug_report.py +374 -56
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_utility.py +31 -1
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/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.dev466"
|
|
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"
|
|
@@ -56,3 +56,14 @@ def is_running_in_addon() -> bool:
|
|
|
56
56
|
users, who already see the dev/stable distinction in the HAOS add-on UI.
|
|
57
57
|
"""
|
|
58
58
|
return bool(os.environ.get("SUPERVISOR_TOKEN"))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_supervisor_base_url() -> str:
|
|
62
|
+
"""Return the base URL for direct Supervisor REST calls.
|
|
63
|
+
|
|
64
|
+
Defaults to ``http://supervisor`` (the in-addon Supervisor hostname). The
|
|
65
|
+
``SUPERVISOR_BASE_URL`` env var override exists so E2E tests can point the
|
|
66
|
+
direct-Supervisor httpx call sites at a local mock without /etc/hosts or
|
|
67
|
+
DNS hacks. Production add-ons leave it unset.
|
|
68
|
+
"""
|
|
69
|
+
return os.environ.get("SUPERVISOR_BASE_URL", "http://supervisor")
|
|
@@ -11,7 +11,7 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
import httpx
|
|
13
13
|
|
|
14
|
-
from .._version import is_running_in_addon
|
|
14
|
+
from .._version import get_supervisor_base_url, is_running_in_addon
|
|
15
15
|
from ..config import get_global_settings
|
|
16
16
|
|
|
17
17
|
|
|
@@ -41,7 +41,19 @@ class HomeAssistantConnectionError(HomeAssistantError):
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
class HomeAssistantAuthError(HomeAssistantError):
|
|
44
|
-
"""Authentication error with Home Assistant.
|
|
44
|
+
"""Authentication error with Home Assistant.
|
|
45
|
+
|
|
46
|
+
Sibling of ``HomeAssistantAPIError`` (not a subclass). The codebase has
|
|
47
|
+
18 ``except HomeAssistantAPIError`` sites (util_helpers polling,
|
|
48
|
+
tools_integrations registry lookups, etc.) that deliberately rely on
|
|
49
|
+
auth errors NOT matching so they can propagate to a paired
|
|
50
|
+
``except (HomeAssistantConnectionError, HomeAssistantAuthError): raise``
|
|
51
|
+
block. Subclassing AuthError under APIError silently swallowed those
|
|
52
|
+
auth errors as part of the local "this entity is not registered yet"
|
|
53
|
+
polling logic. Sites that specifically need to catch both must list
|
|
54
|
+
them explicitly (see ``_get_supervisor_log`` and
|
|
55
|
+
``_get_system_service_log`` in ``tools_utility.py``).
|
|
56
|
+
"""
|
|
45
57
|
|
|
46
58
|
|
|
47
59
|
class HomeAssistantAPIError(HomeAssistantError):
|
|
@@ -543,7 +555,7 @@ class HomeAssistantClient:
|
|
|
543
555
|
"(addon-mode gate fired but SUPERVISOR_TOKEN env var not set)"
|
|
544
556
|
)
|
|
545
557
|
|
|
546
|
-
url = f"
|
|
558
|
+
url = f"{get_supervisor_base_url()}/{path}/logs"
|
|
547
559
|
logger.debug("Fetching %s via Supervisor direct", url)
|
|
548
560
|
|
|
549
561
|
try:
|
|
@@ -1301,7 +1313,6 @@ class HomeAssistantClient:
|
|
|
1301
1313
|
) from e
|
|
1302
1314
|
raise
|
|
1303
1315
|
|
|
1304
|
-
|
|
1305
1316
|
async def resolve_scene_id(self, identifier: str) -> str:
|
|
1306
1317
|
"""
|
|
1307
1318
|
Resolve a scene identifier to its storage key via the entity registry.
|
|
@@ -19,7 +19,7 @@ import httpx
|
|
|
19
19
|
from starlette.requests import Request
|
|
20
20
|
from starlette.responses import HTMLResponse, JSONResponse
|
|
21
21
|
|
|
22
|
-
from ._version import is_running_in_addon
|
|
22
|
+
from ._version import get_supervisor_base_url, is_running_in_addon
|
|
23
23
|
from .errors import ErrorCode, create_error_response
|
|
24
24
|
from .transforms import DEFAULT_PINNED_TOOLS
|
|
25
25
|
from .utils.data_paths import get_data_dir
|
|
@@ -910,7 +910,7 @@ def register_settings_routes(
|
|
|
910
910
|
timeout=5.0, verify=server.settings.verify_ssl
|
|
911
911
|
) as client:
|
|
912
912
|
resp = await client.post(
|
|
913
|
-
"
|
|
913
|
+
f"{get_supervisor_base_url()}/addons/self/restart",
|
|
914
914
|
headers={"Authorization": f"Bearer {token}"},
|
|
915
915
|
)
|
|
916
916
|
except (httpx.ReadError, httpx.RemoteProtocolError):
|
|
@@ -15,11 +15,13 @@ from typing import Annotated, Any
|
|
|
15
15
|
from urllib.parse import quote_plus
|
|
16
16
|
|
|
17
17
|
import httpx
|
|
18
|
+
from fastmcp import Context
|
|
18
19
|
from pydantic import Field
|
|
19
20
|
|
|
20
21
|
from ha_mcp import __version__
|
|
21
22
|
|
|
22
|
-
from ..
|
|
23
|
+
from .._version import get_supervisor_base_url
|
|
24
|
+
from ..config import Settings, get_global_settings
|
|
23
25
|
from ..utils.usage_logger import (
|
|
24
26
|
AVG_LOG_ENTRIES_PER_TOOL,
|
|
25
27
|
get_recent_logs,
|
|
@@ -31,8 +33,12 @@ from .util_helpers import ANSI_ESCAPE_RE
|
|
|
31
33
|
logger = logging.getLogger(__name__)
|
|
32
34
|
|
|
33
35
|
# GitHub issue template URLs
|
|
34
|
-
RUNTIME_BUG_URL =
|
|
35
|
-
|
|
36
|
+
RUNTIME_BUG_URL = (
|
|
37
|
+
"https://github.com/homeassistant-ai/ha-mcp/issues/new?template=runtime_bug.yml"
|
|
38
|
+
)
|
|
39
|
+
AGENT_BEHAVIOR_URL = (
|
|
40
|
+
"https://github.com/homeassistant-ai/ha-mcp/issues/new?template=agent_behavior.yml"
|
|
41
|
+
)
|
|
36
42
|
|
|
37
43
|
# Max characters to include from addon container logs.
|
|
38
44
|
# 3000 chars ≈ 750 LLM tokens — keeps the tool response well below context budgets
|
|
@@ -104,6 +110,172 @@ def _detect_platform() -> dict[str, str]:
|
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
|
|
113
|
+
# Tool-surface-shaping toggles surfaced in bug reports. The set is small on
|
|
114
|
+
# purpose: only flags that materially change which tools the agent sees, since
|
|
115
|
+
# the same bug report behaves very differently depending on these. New
|
|
116
|
+
# tool-shaping toggles should be added here so triage doesn't have to ask.
|
|
117
|
+
_CONFIG_TOGGLE_FIELDS: tuple[str, ...] = (
|
|
118
|
+
"enable_websocket",
|
|
119
|
+
"enable_dashboard_partial_tools",
|
|
120
|
+
"enable_tool_search",
|
|
121
|
+
"tool_search_max_results",
|
|
122
|
+
"enable_yaml_config_editing",
|
|
123
|
+
"enable_code_mode",
|
|
124
|
+
"enabled_tool_modules",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _get_config_toggles(settings: Settings | None = None) -> dict[str, Any]:
|
|
129
|
+
"""Read tool-surface-shaping config toggles from Settings.
|
|
130
|
+
|
|
131
|
+
Defaults to the global settings singleton; tests can pass a fake Settings
|
|
132
|
+
instance instead. Returns an empty dict on any failure (Settings
|
|
133
|
+
construction, attribute coercion, list-field split) so a misconfigured
|
|
134
|
+
environment can't break the bug report path itself.
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
s = settings if settings is not None else get_global_settings()
|
|
138
|
+
|
|
139
|
+
toggles: dict[str, Any] = {}
|
|
140
|
+
for field in _CONFIG_TOGGLE_FIELDS:
|
|
141
|
+
value = getattr(s, field, None)
|
|
142
|
+
if value is None:
|
|
143
|
+
continue
|
|
144
|
+
toggles[field] = value
|
|
145
|
+
|
|
146
|
+
# Summarize list-shaped seeds as counts rather than dumping the full
|
|
147
|
+
# strings — they can be very long, and listing the exact tools the
|
|
148
|
+
# user disabled isn't useful for triage.
|
|
149
|
+
for list_field in ("disabled_tools", "pinned_tools"):
|
|
150
|
+
raw = getattr(s, list_field, "") or ""
|
|
151
|
+
count = len([item for item in raw.split(",") if item.strip()])
|
|
152
|
+
toggles[f"{list_field}_count"] = count
|
|
153
|
+
|
|
154
|
+
return toggles
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.warning(
|
|
157
|
+
"Failed to read settings for bug report toggles: %s (%s)",
|
|
158
|
+
e,
|
|
159
|
+
type(e).__name__,
|
|
160
|
+
)
|
|
161
|
+
return {}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _extract_client_info(ctx: Context | None) -> dict[str, str]:
|
|
165
|
+
"""Pull the connecting MCP client's self-identification off the request context.
|
|
166
|
+
|
|
167
|
+
The MCP ``initialize`` handshake carries a ``clientInfo`` Implementation
|
|
168
|
+
object (``name``/``version``/optional ``title``). FastMCP exposes the
|
|
169
|
+
underlying server session as ``ctx.session``; the MCP SDK's
|
|
170
|
+
``ServerSession`` keeps the parsed initialize params on ``client_params``.
|
|
171
|
+
The attribute name on the parsed Pydantic model is ``clientInfo`` in
|
|
172
|
+
``mcp`` 1.24.x (the version this project pins) — we also fall back to
|
|
173
|
+
``client_info`` to stay forward-compatible with SDK versions that switch
|
|
174
|
+
to snake_case.
|
|
175
|
+
|
|
176
|
+
Returns ``{"name": ..., "version": ..., "title": ...}``. ``name`` and
|
|
177
|
+
``version`` fall back to ``"unknown"`` when the client didn't send them;
|
|
178
|
+
``title`` falls back to the empty string so callers can distinguish "not
|
|
179
|
+
sent" from a real title without false-positive aside rendering.
|
|
180
|
+
|
|
181
|
+
Returns an empty dict if no context is available (tool invoked outside an
|
|
182
|
+
MCP request, e.g. unit tests) so the bug-report path stays robust. The
|
|
183
|
+
log level is intentionally INFO, not DEBUG: this catch is the only signal
|
|
184
|
+
we'd get if FastMCP/MCP SDK shape drifts in a future release, and silent
|
|
185
|
+
drift would hide a regression for months.
|
|
186
|
+
"""
|
|
187
|
+
if ctx is None:
|
|
188
|
+
return {}
|
|
189
|
+
try:
|
|
190
|
+
session = getattr(ctx, "session", None)
|
|
191
|
+
params = (
|
|
192
|
+
getattr(session, "client_params", None) if session is not None else None
|
|
193
|
+
)
|
|
194
|
+
if params is None:
|
|
195
|
+
return {}
|
|
196
|
+
# Try the camelCase attribute (mcp 1.24.x) first, then snake_case so
|
|
197
|
+
# we keep working if the SDK switches the alias direction.
|
|
198
|
+
client = getattr(params, "clientInfo", None) or getattr(
|
|
199
|
+
params, "client_info", None
|
|
200
|
+
)
|
|
201
|
+
if client is None:
|
|
202
|
+
return {}
|
|
203
|
+
return {
|
|
204
|
+
"name": getattr(client, "name", None) or "unknown",
|
|
205
|
+
"version": getattr(client, "version", None) or "unknown",
|
|
206
|
+
"title": getattr(client, "title", None) or "",
|
|
207
|
+
}
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.info(
|
|
210
|
+
"Failed to read MCP client info from context: %s (%s)",
|
|
211
|
+
e,
|
|
212
|
+
type(e).__name__,
|
|
213
|
+
)
|
|
214
|
+
return {}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _format_client_info_for_template(info: dict[str, str]) -> str:
|
|
218
|
+
"""Render the MCP client identification as a single human-readable line.
|
|
219
|
+
|
|
220
|
+
Falls back to ``unknown (client did not advertise itself)`` when no
|
|
221
|
+
client info was available — this happens for direct MCP clients that
|
|
222
|
+
skip the optional ``clientInfo`` field, or when the bug report tool
|
|
223
|
+
runs outside a live request. Phrasing is deliberately observable
|
|
224
|
+
rather than naming the underlying API field (which may be renamed).
|
|
225
|
+
"""
|
|
226
|
+
if not info:
|
|
227
|
+
return "unknown (client did not advertise itself)"
|
|
228
|
+
name = info.get("name") or "unknown"
|
|
229
|
+
version = info.get("version") or "unknown"
|
|
230
|
+
title = info.get("title") or ""
|
|
231
|
+
base = f"{name} {version}"
|
|
232
|
+
if title and title != name:
|
|
233
|
+
return f"{base} _(advertised title: {title})_"
|
|
234
|
+
return base
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _detect_mcp_transport() -> str:
|
|
238
|
+
"""Best-effort MCP transport detection.
|
|
239
|
+
|
|
240
|
+
Returns ``stdio`` / ``http`` / ``sse`` / ``unknown``. We can't observe the
|
|
241
|
+
transport perfectly from a tool call, so we look at the entrypoint name
|
|
242
|
+
and well-known env hints. The result is informational — the bug template
|
|
243
|
+
surfaces it as an auto-detect that the agent or user can override.
|
|
244
|
+
"""
|
|
245
|
+
# Entry-point script name (e.g. ``ha-mcp-web`` for HTTP, ``ha-mcp-sse``
|
|
246
|
+
# for SSE; pyproject.toml's [project.scripts] is the source of truth).
|
|
247
|
+
argv0 = (sys.argv[0] if sys.argv else "").lower()
|
|
248
|
+
basename = os.path.basename(argv0)
|
|
249
|
+
if basename.endswith("-web"):
|
|
250
|
+
return "http"
|
|
251
|
+
if basename.endswith("-sse"):
|
|
252
|
+
return "sse"
|
|
253
|
+
|
|
254
|
+
# Env hints set by HTTP wrappers / supervisors. ``streamable-http`` is the
|
|
255
|
+
# documented FastMCP variant; collapse it to ``http`` since the
|
|
256
|
+
# distinction doesn't change triage decisions.
|
|
257
|
+
transport_env = os.environ.get("FASTMCP_TRANSPORT", "").strip().lower()
|
|
258
|
+
if transport_env in {"http", "stdio", "sse"}:
|
|
259
|
+
return transport_env
|
|
260
|
+
if transport_env == "streamable-http":
|
|
261
|
+
return "http"
|
|
262
|
+
if os.environ.get("MCP_HTTP_PORT") or os.environ.get("FASTMCP_PORT"):
|
|
263
|
+
return "http"
|
|
264
|
+
|
|
265
|
+
# If stdin is piped (not a TTY), ha-mcp was launched by an MCP host on
|
|
266
|
+
# stdio. If it IS a TTY, this is a manual / interactive run with no
|
|
267
|
+
# other transport hints — fall through to ``unknown``.
|
|
268
|
+
try:
|
|
269
|
+
if not sys.stdin.isatty():
|
|
270
|
+
return "stdio"
|
|
271
|
+
except (AttributeError, OSError, ValueError):
|
|
272
|
+
# ``sys.stdin`` can be None or detached (pythonw, daemonized
|
|
273
|
+
# contexts, certain test harnesses). Treat as no signal.
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
return "unknown"
|
|
277
|
+
|
|
278
|
+
|
|
107
279
|
def _sanitize_log_text(text: str) -> str:
|
|
108
280
|
"""Best-effort secret scrubber for log text.
|
|
109
281
|
|
|
@@ -186,13 +358,11 @@ async def _fetch_addon_logs() -> str:
|
|
|
186
358
|
timeout=10.0, verify=get_global_settings().verify_ssl
|
|
187
359
|
) as http_client:
|
|
188
360
|
resp = await http_client.get(
|
|
189
|
-
"
|
|
361
|
+
f"{get_supervisor_base_url()}/addons/self/logs",
|
|
190
362
|
headers={"Authorization": f"Bearer {token}"},
|
|
191
363
|
)
|
|
192
364
|
if resp.status_code != 200:
|
|
193
|
-
logger.info(
|
|
194
|
-
"Addon log fetch returned HTTP %s", resp.status_code
|
|
195
|
-
)
|
|
365
|
+
logger.info("Addon log fetch returned HTTP %s", resp.status_code)
|
|
196
366
|
return ""
|
|
197
367
|
|
|
198
368
|
# Strip ANSI escape codes first, then sanitize, then truncate.
|
|
@@ -221,8 +391,8 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
221
391
|
annotations={
|
|
222
392
|
"idempotentHint": True,
|
|
223
393
|
"readOnlyHint": True,
|
|
224
|
-
"title": "Report Issue or Feedback"
|
|
225
|
-
}
|
|
394
|
+
"title": "Report Issue or Feedback",
|
|
395
|
+
},
|
|
226
396
|
)
|
|
227
397
|
@log_tool_usage
|
|
228
398
|
async def ha_report_issue(
|
|
@@ -240,6 +410,7 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
240
410
|
),
|
|
241
411
|
),
|
|
242
412
|
] = 10,
|
|
413
|
+
ctx: Context | None = None,
|
|
243
414
|
) -> dict[str, Any]:
|
|
244
415
|
"""
|
|
245
416
|
Collect diagnostic information for filing issue reports or feedback.
|
|
@@ -279,14 +450,20 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
279
450
|
empty string otherwise)
|
|
280
451
|
- `suggested_title`, `duplicate_check_urls`, `anonymization_guide`
|
|
281
452
|
"""
|
|
282
|
-
# Detect installation method and
|
|
453
|
+
# Detect installation method, platform, and runtime config.
|
|
283
454
|
install_method = _detect_installation_method()
|
|
284
455
|
platform_info = _detect_platform()
|
|
456
|
+
config_toggles = _get_config_toggles()
|
|
457
|
+
mcp_transport = _detect_mcp_transport()
|
|
458
|
+
client_info = _extract_client_info(ctx)
|
|
285
459
|
|
|
286
460
|
diagnostic_info: dict[str, Any] = {
|
|
287
461
|
"ha_mcp_version": __version__,
|
|
288
462
|
"installation_method": install_method,
|
|
289
463
|
"platform": platform_info,
|
|
464
|
+
"mcp_transport": mcp_transport,
|
|
465
|
+
"mcp_client_info": client_info,
|
|
466
|
+
"config_toggles": config_toggles,
|
|
290
467
|
"connection_status": "Unknown",
|
|
291
468
|
"home_assistant_version": "Unknown",
|
|
292
469
|
"entity_count": 0,
|
|
@@ -296,9 +473,7 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
296
473
|
try:
|
|
297
474
|
config = await client.get_config()
|
|
298
475
|
diagnostic_info["connection_status"] = "Connected"
|
|
299
|
-
diagnostic_info["home_assistant_version"] = config.get(
|
|
300
|
-
"version", "Unknown"
|
|
301
|
-
)
|
|
476
|
+
diagnostic_info["home_assistant_version"] = config.get("version", "Unknown")
|
|
302
477
|
diagnostic_info["location_name"] = config.get("location_name", "Unknown")
|
|
303
478
|
diagnostic_info["time_zone"] = config.get("time_zone", "Unknown")
|
|
304
479
|
except Exception as e:
|
|
@@ -336,7 +511,9 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
336
511
|
"",
|
|
337
512
|
f"ha-mcp Version: {diagnostic_info['ha_mcp_version']}",
|
|
338
513
|
f"Installation Method: {diagnostic_info['installation_method']}",
|
|
339
|
-
f"
|
|
514
|
+
f"MCP Transport: {mcp_transport}",
|
|
515
|
+
f"MCP Client: {_format_client_info_for_template(client_info)}",
|
|
516
|
+
f"Operating System: {platform_info['os']} {platform_info['os_release']} ({platform_info['architecture']})",
|
|
340
517
|
f"Python Version: {platform_info['python_version']}",
|
|
341
518
|
f"Home Assistant Version: {diagnostic_info['home_assistant_version']}",
|
|
342
519
|
f"Connection Status: {diagnostic_info['connection_status']}",
|
|
@@ -349,45 +526,70 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
349
526
|
if "time_zone" in diagnostic_info:
|
|
350
527
|
report_lines.append(f"Time Zone: {diagnostic_info['time_zone']}")
|
|
351
528
|
|
|
529
|
+
if config_toggles:
|
|
530
|
+
report_lines.extend(["", "=== ha-mcp Config Toggles ==="])
|
|
531
|
+
for key, value in config_toggles.items():
|
|
532
|
+
report_lines.append(f" {key}: {value}")
|
|
533
|
+
|
|
352
534
|
if startup_logs:
|
|
353
|
-
report_lines.extend(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
535
|
+
report_lines.extend(
|
|
536
|
+
[
|
|
537
|
+
"",
|
|
538
|
+
f"=== Startup Logs ({len(startup_logs)} entries) ===",
|
|
539
|
+
startup_log_summary,
|
|
540
|
+
]
|
|
541
|
+
)
|
|
358
542
|
|
|
359
543
|
if recent_logs:
|
|
360
|
-
report_lines.extend(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
544
|
+
report_lines.extend(
|
|
545
|
+
[
|
|
546
|
+
"",
|
|
547
|
+
f"=== Recent Tool Calls ({len(recent_logs)} entries) ===",
|
|
548
|
+
log_summary,
|
|
549
|
+
]
|
|
550
|
+
)
|
|
365
551
|
|
|
366
552
|
if addon_logs:
|
|
367
|
-
report_lines.extend(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
553
|
+
report_lines.extend(
|
|
554
|
+
[
|
|
555
|
+
"",
|
|
556
|
+
"=== Add-on Container Logs ===",
|
|
557
|
+
addon_logs,
|
|
558
|
+
]
|
|
559
|
+
)
|
|
372
560
|
|
|
373
561
|
formatted_report = "\n".join(report_lines)
|
|
374
562
|
|
|
563
|
+
# Generate suggested title up-front so it can be folded into the
|
|
564
|
+
# submission URLs as a `&title=` query param. This auto-fills the
|
|
565
|
+
# GitHub issue title field — without it, users routinely submit reports
|
|
566
|
+
# titled just "[BUG]".
|
|
567
|
+
suggested_title = _generate_bug_title(diagnostic_info, recent_logs)
|
|
568
|
+
title_query = quote_plus(suggested_title)
|
|
569
|
+
runtime_bug_submit_url = f"{RUNTIME_BUG_URL}&title={title_query}"
|
|
570
|
+
agent_behavior_submit_url = f"{AGENT_BEHAVIOR_URL}&title={title_query}"
|
|
571
|
+
|
|
375
572
|
# Generate BOTH templates
|
|
376
573
|
runtime_bug_template = _generate_runtime_bug_template(
|
|
377
|
-
diagnostic_info,
|
|
574
|
+
diagnostic_info,
|
|
575
|
+
log_summary,
|
|
576
|
+
startup_log_summary,
|
|
577
|
+
recent_logs,
|
|
578
|
+
startup_logs,
|
|
378
579
|
addon_logs=addon_logs,
|
|
580
|
+
submit_url=runtime_bug_submit_url,
|
|
379
581
|
)
|
|
380
582
|
|
|
381
583
|
agent_behavior_template = _generate_agent_behavior_template(
|
|
382
|
-
diagnostic_info,
|
|
584
|
+
diagnostic_info,
|
|
585
|
+
log_summary,
|
|
586
|
+
recent_logs,
|
|
587
|
+
submit_url=agent_behavior_submit_url,
|
|
383
588
|
)
|
|
384
589
|
|
|
385
590
|
# Anonymization instructions
|
|
386
591
|
anonymization_guide = _generate_anonymization_guide()
|
|
387
592
|
|
|
388
|
-
# Generate suggested title
|
|
389
|
-
suggested_title = _generate_bug_title(diagnostic_info, recent_logs)
|
|
390
|
-
|
|
391
593
|
# Generate search keywords and URLs for duplicate check
|
|
392
594
|
search_keywords = _generate_search_keywords(diagnostic_info, recent_logs)
|
|
393
595
|
duplicate_check_urls = [
|
|
@@ -408,12 +610,14 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
408
610
|
"agent_behavior_template": agent_behavior_template,
|
|
409
611
|
"anonymization_guide": anonymization_guide,
|
|
410
612
|
"suggested_title": suggested_title,
|
|
613
|
+
"runtime_bug_submit_url": runtime_bug_submit_url,
|
|
614
|
+
"agent_behavior_submit_url": agent_behavior_submit_url,
|
|
411
615
|
"duplicate_check_urls": duplicate_check_urls,
|
|
412
616
|
"instructions": (
|
|
413
617
|
"WORKFLOW FOR PRESENTING BUG REPORTS:\n\n"
|
|
414
618
|
"1. **Check for duplicates FIRST** (before presenting the template):\n"
|
|
415
619
|
" - Use the duplicate_check_urls to search for similar issues\n"
|
|
416
|
-
|
|
620
|
+
' - If gh CLI is available: use `gh issue list --search "keyword"`\n'
|
|
417
621
|
" - Otherwise: inform user to check the duplicate_check_urls\n"
|
|
418
622
|
" - If duplicates found, ask user if they want to comment on existing issue instead\n\n"
|
|
419
623
|
"2. **Determine which template to present**:\n"
|
|
@@ -435,12 +639,27 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
435
639
|
" d. Verify no tokens, passwords, or IPs are visible\n"
|
|
436
640
|
" e. Keep entity domains, error messages, and technical details\n"
|
|
437
641
|
" See anonymization_guide for full details.\n\n"
|
|
438
|
-
"4. **
|
|
439
|
-
"
|
|
642
|
+
"4. **Fill in the self-reported fields BEFORE presenting**:\n"
|
|
643
|
+
" - `**AI Model:**` — write your identity on this line (provider/family + the\n"
|
|
644
|
+
" most specific version you know, in whatever form you'd describe yourself).\n"
|
|
645
|
+
" Do not invent a version number. If you don't know it, say so or omit the\n"
|
|
646
|
+
" version. There are no options to pick from — just answer honestly.\n"
|
|
647
|
+
" - `**Triggering Prompt & Tool Call:** <fill in>` — the EXACT user message\n"
|
|
648
|
+
" and the tool call(s) that produced the bug, copy-pasted verbatim. Truncate\n"
|
|
649
|
+
" long inputs only after anonymization. This is the single most useful field\n"
|
|
650
|
+
" for triage — do not skip it.\n"
|
|
651
|
+
" `MCP Transport` and `MCP Client` are auto-detected by the server (the latter\n"
|
|
652
|
+
" from the MCP `initialize` handshake); leave both as-is unless they're clearly\n"
|
|
653
|
+
" wrong.\n\n"
|
|
654
|
+
"5. **Present the anonymized report to the user**:\n"
|
|
655
|
+
" a. Show the suggested_title (user can edit if needed) and tell them GitHub's\n"
|
|
656
|
+
" title field is now pre-filled via the submission URL — they don't need to\n"
|
|
657
|
+
" retype it.\n"
|
|
440
658
|
" b. Present the chosen ANONYMIZED template IN A MARKDOWN CODE BLOCK (```markdown...```) for easy copy/paste\n"
|
|
441
|
-
" c. PROMINENTLY display the submission URL at the top
|
|
442
|
-
|
|
443
|
-
|
|
659
|
+
" c. PROMINENTLY display the submission URL at the top — these include the\n"
|
|
660
|
+
" pre-filled title:\n"
|
|
661
|
+
" - Runtime bugs: see runtime_bug_submit_url\n"
|
|
662
|
+
" - Agent behavior: see agent_behavior_submit_url\n"
|
|
444
663
|
" d. Ask them to fill in the description sections\n"
|
|
445
664
|
" e. For HA add-on installs, the runtime bug template includes a collapsible '📦 Add-on Container Logs' section auto-filled from addon_logs — keep it as-is\n"
|
|
446
665
|
" f. Remind them to review for any remaining personal information before submitting\n\n"
|
|
@@ -449,6 +668,20 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
449
668
|
}
|
|
450
669
|
|
|
451
670
|
|
|
671
|
+
def _format_config_toggles_for_template(toggles: dict[str, Any]) -> str:
|
|
672
|
+
"""Render config toggle snapshot as a markdown bullet list.
|
|
673
|
+
|
|
674
|
+
Returns a placeholder line when no toggles were collected (e.g. Settings
|
|
675
|
+
construction failed) so the template stays consistent.
|
|
676
|
+
"""
|
|
677
|
+
if not toggles:
|
|
678
|
+
return "_(config toggles unavailable)_"
|
|
679
|
+
lines = []
|
|
680
|
+
for key, value in toggles.items():
|
|
681
|
+
lines.append(f"- **{key}:** `{value}`")
|
|
682
|
+
return "\n".join(lines)
|
|
683
|
+
|
|
684
|
+
|
|
452
685
|
def _format_logs_for_report(logs: list[dict[str, Any]]) -> str:
|
|
453
686
|
"""Format log entries for inclusion in a bug report."""
|
|
454
687
|
if not logs:
|
|
@@ -564,7 +797,9 @@ def _generate_search_keywords(
|
|
|
564
797
|
keywords = set()
|
|
565
798
|
|
|
566
799
|
# Find the most recent error from logs
|
|
567
|
-
last_error_log = next(
|
|
800
|
+
last_error_log = next(
|
|
801
|
+
(log for log in reversed(recent_logs) if log.get("error_message")), None
|
|
802
|
+
)
|
|
568
803
|
|
|
569
804
|
if last_error_log:
|
|
570
805
|
tool_name = last_error_log.get("tool_name")
|
|
@@ -602,6 +837,7 @@ def _generate_runtime_bug_template(
|
|
|
602
837
|
startup_logs: list[dict[str, Any]],
|
|
603
838
|
*,
|
|
604
839
|
addon_logs: str = "",
|
|
840
|
+
submit_url: str = RUNTIME_BUG_URL,
|
|
605
841
|
) -> str:
|
|
606
842
|
"""
|
|
607
843
|
Generate a runtime bug report template matching runtime_bug.md format.
|
|
@@ -610,10 +846,19 @@ def _generate_runtime_bug_template(
|
|
|
610
846
|
copy-paste without format conflicts.
|
|
611
847
|
"""
|
|
612
848
|
platform_info = diagnostic_info.get("platform", {})
|
|
849
|
+
config_toggles = diagnostic_info.get("config_toggles") or {}
|
|
850
|
+
mcp_transport = diagnostic_info.get("mcp_transport", "unknown")
|
|
851
|
+
client_info = diagnostic_info.get("mcp_client_info") or {}
|
|
613
852
|
|
|
614
853
|
# Extract error messages from recent logs
|
|
615
854
|
error_messages = _extract_error_messages(recent_logs)
|
|
616
|
-
error_section =
|
|
855
|
+
error_section = (
|
|
856
|
+
"\n".join(error_messages)
|
|
857
|
+
if error_messages
|
|
858
|
+
else "<!-- No errors detected in recent logs -->"
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
config_toggles_section = _format_config_toggles_for_template(config_toggles)
|
|
617
862
|
|
|
618
863
|
# Show startup logs section only if they exist
|
|
619
864
|
startup_section = ""
|
|
@@ -657,7 +902,9 @@ def _generate_runtime_bug_template(
|
|
|
657
902
|
> All environment info and logs below were collected automatically.
|
|
658
903
|
|
|
659
904
|
**Submit this report at:**
|
|
660
|
-
{
|
|
905
|
+
{submit_url}
|
|
906
|
+
|
|
907
|
+
(The submission link above pre-fills the issue title — you don't need to retype it.)
|
|
661
908
|
|
|
662
909
|
---
|
|
663
910
|
|
|
@@ -680,17 +927,49 @@ def _generate_runtime_bug_template(
|
|
|
680
927
|
<!-- What actually happened? -->
|
|
681
928
|
|
|
682
929
|
|
|
930
|
+
---
|
|
931
|
+
|
|
932
|
+
## 💬 Triggering Prompt & Tool Call
|
|
933
|
+
|
|
934
|
+
<!-- The calling AI agent fills this in. Paste, verbatim, the user message that
|
|
935
|
+
triggered this bug AND the tool call(s) that produced it. Truncate only
|
|
936
|
+
after anonymizing tokens / personal names. This is the highest-leverage
|
|
937
|
+
field for triage. -->
|
|
938
|
+
|
|
939
|
+
**User prompt:** <fill in>
|
|
940
|
+
|
|
941
|
+
**Tool call(s):**
|
|
942
|
+
```
|
|
943
|
+
<fill in — name + arguments + (truncated) response, e.g.:
|
|
944
|
+
ha_call_service(domain="light", service="turn_on", entity_id="light.example")
|
|
945
|
+
→ ToolError: Service not found
|
|
946
|
+
>
|
|
947
|
+
```
|
|
948
|
+
|
|
683
949
|
---
|
|
684
950
|
|
|
685
951
|
## 🔧 Environment
|
|
686
952
|
|
|
687
|
-
- **ha-mcp Version:** {diagnostic_info.get(
|
|
688
|
-
- **Installation Method:** {diagnostic_info.get(
|
|
689
|
-
- **
|
|
690
|
-
- **
|
|
691
|
-
- **
|
|
692
|
-
- **
|
|
693
|
-
- **
|
|
953
|
+
- **ha-mcp Version:** {diagnostic_info.get("ha_mcp_version", "Unknown")}
|
|
954
|
+
- **Installation Method:** {diagnostic_info.get("installation_method", "Unknown")}
|
|
955
|
+
- **MCP Transport:** {mcp_transport} _(auto-detected — correct if wrong)_
|
|
956
|
+
- **MCP Client:** {_format_client_info_for_template(client_info)} _(auto-detected from the MCP `initialize` handshake)_
|
|
957
|
+
- **AI Model:**
|
|
958
|
+
- **Operating System:** {platform_info.get("os", "Unknown")} {platform_info.get("os_release", "")} ({platform_info.get("architecture", "Unknown")})
|
|
959
|
+
- **Python Version:** {platform_info.get("python_version", "Unknown")}
|
|
960
|
+
- **Home Assistant Version:** {diagnostic_info.get("home_assistant_version", "Unknown")}
|
|
961
|
+
- **Connection Status:** {diagnostic_info.get("connection_status", "Unknown")}
|
|
962
|
+
- **Entity Count:** {diagnostic_info.get("entity_count", 0)}
|
|
963
|
+
|
|
964
|
+
---
|
|
965
|
+
|
|
966
|
+
## ⚙️ ha-mcp Configuration
|
|
967
|
+
|
|
968
|
+
These flags shape which tools the agent sees, so the same report can mean
|
|
969
|
+
different things depending on toggle state. Auto-collected from the running
|
|
970
|
+
server:
|
|
971
|
+
|
|
972
|
+
{config_toggles_section}
|
|
694
973
|
|
|
695
974
|
---
|
|
696
975
|
|
|
@@ -734,13 +1013,23 @@ def _generate_agent_behavior_template(
|
|
|
734
1013
|
diagnostic_info: dict[str, Any],
|
|
735
1014
|
log_summary: str,
|
|
736
1015
|
recent_logs: list[dict[str, Any]],
|
|
1016
|
+
*,
|
|
1017
|
+
submit_url: str = AGENT_BEHAVIOR_URL,
|
|
737
1018
|
) -> str:
|
|
738
1019
|
"""
|
|
739
1020
|
Generate an agent behavior feedback template matching agent_behavior_feedback.md format.
|
|
740
1021
|
|
|
741
1022
|
This template focuses on AI agent tool usage patterns and inefficiencies.
|
|
742
1023
|
"""
|
|
743
|
-
|
|
1024
|
+
config_toggles = diagnostic_info.get("config_toggles") or {}
|
|
1025
|
+
mcp_transport = diagnostic_info.get("mcp_transport", "unknown")
|
|
1026
|
+
client_info = diagnostic_info.get("mcp_client_info") or {}
|
|
1027
|
+
config_toggles_section = _format_config_toggles_for_template(config_toggles)
|
|
1028
|
+
|
|
1029
|
+
# _extract_error_messages and recent_logs are unused in the agent template;
|
|
1030
|
+
# tool sequence already lives in log_summary. Kept in the signature so
|
|
1031
|
+
# callers don't have to remember which template needs which arg.
|
|
1032
|
+
del recent_logs
|
|
744
1033
|
|
|
745
1034
|
return f"""## 🤖 Auto-Generated by `ha_report_issue` Tool
|
|
746
1035
|
|
|
@@ -748,7 +1037,9 @@ def _generate_agent_behavior_template(
|
|
|
748
1037
|
> Tool call history was collected automatically to help analyze agent behavior.
|
|
749
1038
|
|
|
750
1039
|
**Submit this feedback at:**
|
|
751
|
-
{
|
|
1040
|
+
{submit_url}
|
|
1041
|
+
|
|
1042
|
+
(The submission link above pre-fills the issue title — you don't need to retype it.)
|
|
752
1043
|
|
|
753
1044
|
---
|
|
754
1045
|
|
|
@@ -774,6 +1065,21 @@ def _generate_agent_behavior_template(
|
|
|
774
1065
|
<!-- Example: "I asked the agent to create an automation that..." -->
|
|
775
1066
|
|
|
776
1067
|
|
|
1068
|
+
---
|
|
1069
|
+
|
|
1070
|
+
## 💬 Triggering Prompt & Tool Call
|
|
1071
|
+
|
|
1072
|
+
<!-- The AI agent fills this in. Paste, verbatim, the user message that
|
|
1073
|
+
prompted the questionable behavior AND the tool call(s) the agent made
|
|
1074
|
+
in response. Truncate only after anonymizing tokens / personal names. -->
|
|
1075
|
+
|
|
1076
|
+
**User prompt:** <fill in>
|
|
1077
|
+
|
|
1078
|
+
**Tool call(s) the agent chose:**
|
|
1079
|
+
```
|
|
1080
|
+
<fill in — name + arguments + (truncated) response>
|
|
1081
|
+
```
|
|
1082
|
+
|
|
777
1083
|
---
|
|
778
1084
|
|
|
779
1085
|
## 🔧 Tool Calls Made (Auto-Filled)
|
|
@@ -806,11 +1112,23 @@ def _generate_agent_behavior_template(
|
|
|
806
1112
|
|
|
807
1113
|
---
|
|
808
1114
|
|
|
809
|
-
## 📊 Environment
|
|
1115
|
+
## 📊 Environment
|
|
1116
|
+
|
|
1117
|
+
- **ha-mcp Version:** {diagnostic_info.get("ha_mcp_version", "Unknown")}
|
|
1118
|
+
- **Installation Method:** {diagnostic_info.get("installation_method", "Unknown")}
|
|
1119
|
+
- **MCP Transport:** {mcp_transport} _(auto-detected — correct if wrong)_
|
|
1120
|
+
- **MCP Client:** {_format_client_info_for_template(client_info)} _(auto-detected from the MCP `initialize` handshake)_
|
|
1121
|
+
- **AI Model:**
|
|
1122
|
+
- **Home Assistant Version:** {diagnostic_info.get("home_assistant_version", "Unknown")}
|
|
1123
|
+
|
|
1124
|
+
---
|
|
1125
|
+
|
|
1126
|
+
## ⚙️ ha-mcp Configuration
|
|
1127
|
+
|
|
1128
|
+
These flags shape which tools the agent sees, so the same behavior may be
|
|
1129
|
+
expected vs. surprising depending on toggle state:
|
|
810
1130
|
|
|
811
|
-
|
|
812
|
-
- **AI Client:** (Claude Desktop / Claude Code / Other)
|
|
813
|
-
- **Home Assistant Version:** {diagnostic_info.get('home_assistant_version', 'Unknown')}
|
|
1131
|
+
{config_toggles_section}
|
|
814
1132
|
|
|
815
1133
|
---
|
|
816
1134
|
|
|
@@ -12,7 +12,11 @@ from typing import Any, Literal
|
|
|
12
12
|
|
|
13
13
|
from fastmcp.exceptions import ToolError
|
|
14
14
|
|
|
15
|
-
from ..client.rest_client import
|
|
15
|
+
from ..client.rest_client import (
|
|
16
|
+
HomeAssistantAPIError,
|
|
17
|
+
HomeAssistantAuthError,
|
|
18
|
+
HomeAssistantConnectionError,
|
|
19
|
+
)
|
|
16
20
|
from ..errors import ErrorCode, create_error_response
|
|
17
21
|
from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
|
|
18
22
|
from .util_helpers import (
|
|
@@ -755,6 +759,19 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
755
759
|
|
|
756
760
|
except ToolError:
|
|
757
761
|
raise
|
|
762
|
+
except HomeAssistantAuthError as e:
|
|
763
|
+
# Listed before HomeAssistantAPIError because AuthError is a sibling,
|
|
764
|
+
# not a subclass — without this explicit clause the 401 from
|
|
765
|
+
# _supervisor_logs_get propagates raw to FastMCP and surfaces
|
|
766
|
+
# without a structured `code` field.
|
|
767
|
+
exception_to_structured_error(
|
|
768
|
+
e,
|
|
769
|
+
context={"source": "supervisor", "slug": slug},
|
|
770
|
+
suggestions=[
|
|
771
|
+
"Verify SUPERVISOR_TOKEN is set correctly inside the add-on",
|
|
772
|
+
"Reinstall the add-on if the token may have rotated",
|
|
773
|
+
],
|
|
774
|
+
)
|
|
758
775
|
except HomeAssistantAPIError as e:
|
|
759
776
|
status = getattr(e, "status_code", None)
|
|
760
777
|
if status == 400:
|
|
@@ -857,6 +874,19 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
857
874
|
|
|
858
875
|
except ToolError:
|
|
859
876
|
raise
|
|
877
|
+
except HomeAssistantAuthError as e:
|
|
878
|
+
# Listed before HomeAssistantAPIError because AuthError is a sibling,
|
|
879
|
+
# not a subclass — without this explicit clause the 401 from
|
|
880
|
+
# _supervisor_logs_get propagates raw to FastMCP and surfaces
|
|
881
|
+
# without a structured `code` field.
|
|
882
|
+
exception_to_structured_error(
|
|
883
|
+
e,
|
|
884
|
+
context={"source": "system_service", "slug": service},
|
|
885
|
+
suggestions=[
|
|
886
|
+
"Verify SUPERVISOR_TOKEN is set correctly inside the add-on",
|
|
887
|
+
"Reinstall the add-on if the token may have rotated",
|
|
888
|
+
],
|
|
889
|
+
)
|
|
860
890
|
except HomeAssistantAPIError as e:
|
|
861
891
|
status = getattr(e, "status_code", None)
|
|
862
892
|
if status == 403:
|
|
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.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/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
|
{ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/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.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/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.dev464 → ha_mcp_dev-7.4.1.dev466}/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.dev464 → ha_mcp_dev-7.4.1.dev466}/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
|
{ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/kill_signal_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/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
|