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.
Files changed (109) hide show
  1. {ha_mcp_dev-7.4.1.dev464/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev466}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/_version.py +11 -0
  4. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/client/rest_client.py +15 -4
  5. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/settings_ui.py +2 -2
  6. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_bug_report.py +374 -56
  7. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_utility.py +31 -1
  8. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  9. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/LICENSE +0 -0
  10. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/MANIFEST.in +0 -0
  11. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/README.md +0 -0
  12. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/setup.cfg +0 -0
  13. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/__main__.py +0 -0
  15. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/_pypi_marker +0 -0
  16. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/auth/__init__.py +0 -0
  17. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/auth/consent_form.py +0 -0
  18. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/auth/provider.py +0 -0
  19. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/client/__init__.py +0 -0
  20. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/client/websocket_client.py +0 -0
  21. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/client/websocket_listener.py +0 -0
  22. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/config.py +0 -0
  23. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/errors.py +0 -0
  24. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/py.typed +0 -0
  25. {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
  26. {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
  27. {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
  28. {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
  29. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  30. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  31. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  32. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  33. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  34. {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
  35. {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
  36. {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
  37. {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
  38. {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
  39. {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
  40. {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
  41. {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
  42. {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
  43. {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
  44. {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
  45. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/server.py +0 -0
  46. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/smoke_test.py +0 -0
  47. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/__init__.py +0 -0
  48. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/backup.py +0 -0
  49. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  50. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/device_control.py +0 -0
  51. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/enhanced.py +0 -0
  52. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/helpers.py +0 -0
  53. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/reference_validator.py +0 -0
  54. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/registry.py +0 -0
  55. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/smart_search.py +0 -0
  56. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_addons.py +0 -0
  57. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_areas.py +0 -0
  58. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  59. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_calendar.py +0 -0
  60. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_camera.py +0 -0
  61. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_categories.py +0 -0
  62. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_code.py +0 -0
  63. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  64. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  65. {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
  66. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  67. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  68. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  69. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_energy.py +0 -0
  70. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_entities.py +0 -0
  71. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  72. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_groups.py +0 -0
  73. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_hacs.py +0 -0
  74. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_history.py +0 -0
  75. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_integrations.py +0 -0
  76. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_labels.py +0 -0
  77. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  78. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_registry.py +0 -0
  79. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_resources.py +0 -0
  80. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_search.py +0 -0
  81. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_service.py +0 -0
  82. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_services.py +0 -0
  83. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_system.py +0 -0
  84. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_todo.py +0 -0
  85. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_traces.py +0 -0
  86. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_updates.py +0 -0
  87. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  88. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  89. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/tools_zones.py +0 -0
  90. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/tools/util_helpers.py +0 -0
  91. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/transforms/__init__.py +0 -0
  92. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/transforms/categorized_search.py +0 -0
  93. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/__init__.py +0 -0
  94. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/config_hash.py +0 -0
  95. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/data_paths.py +0 -0
  96. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/domain_handlers.py +0 -0
  97. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  98. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  99. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/operation_manager.py +0 -0
  100. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/python_sandbox.py +0 -0
  101. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp/utils/usage_logger.py +0 -0
  102. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  103. {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
  104. {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
  105. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  106. {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
  107. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/tests/__init__.py +0 -0
  108. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/tests/test_constants.py +0 -0
  109. {ha_mcp_dev-7.4.1.dev464 → ha_mcp_dev-7.4.1.dev466}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.4.1.dev464
3
+ Version: 7.4.1.dev466
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -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.dev464"
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"http://supervisor/{path}/logs"
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
- "http://supervisor/addons/self/restart",
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 ..config import get_global_settings
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 = "https://github.com/homeassistant-ai/ha-mcp/issues/new?template=runtime_bug.yml"
35
- AGENT_BEHAVIOR_URL = "https://github.com/homeassistant-ai/ha-mcp/issues/new?template=agent_behavior.yml"
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
- "http://supervisor/addons/self/logs",
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 platform
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"Platform: {platform_info['os']} {platform_info['os_release']} ({platform_info['architecture']})",
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
- f"=== Startup Logs ({len(startup_logs)} entries) ===",
356
- startup_log_summary,
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
- f"=== Recent Tool Calls ({len(recent_logs)} entries) ===",
363
- log_summary,
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
- "=== Add-on Container Logs ===",
370
- addon_logs,
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, log_summary, startup_log_summary, recent_logs, startup_logs,
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, log_summary, recent_logs
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
- " - If gh CLI is available: use `gh issue list --search \"keyword\"`\n"
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. **Present the anonymized report to the user**:\n"
439
- " a. Show the suggested_title (user can edit if needed)\n"
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:\n"
442
- f" - Runtime bugs: {RUNTIME_BUG_URL}\n"
443
- f" - Agent behavior: {AGENT_BEHAVIOR_URL}\n"
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((log for log in reversed(recent_logs) if log.get("error_message")), None)
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 = "\n".join(error_messages) if error_messages else "<!-- No errors detected in recent logs -->"
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
- {RUNTIME_BUG_URL}
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('ha_mcp_version', 'Unknown')}
688
- - **Installation Method:** {diagnostic_info.get('installation_method', 'Unknown')}
689
- - **Platform:** {platform_info.get('os', 'Unknown')} {platform_info.get('os_release', '')} ({platform_info.get('architecture', 'Unknown')})
690
- - **Python Version:** {platform_info.get('python_version', 'Unknown')}
691
- - **Home Assistant Version:** {diagnostic_info.get('home_assistant_version', 'Unknown')}
692
- - **Connection Status:** {diagnostic_info.get('connection_status', 'Unknown')}
693
- - **Entity Count:** {diagnostic_info.get('entity_count', 0)}
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
- platform_info = diagnostic_info.get("platform", {})
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
- {AGENT_BEHAVIOR_URL}
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 (Optional)
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
- - **ha-mcp Version:** {diagnostic_info.get('ha_mcp_version', 'Unknown')}
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 HomeAssistantAPIError, HomeAssistantConnectionError
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.4.1.dev464
3
+ Version: 7.4.1.dev466
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT