ha-mcp-dev 7.4.1.dev471__tar.gz → 7.4.1.dev473__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 (110) hide show
  1. {ha_mcp_dev-7.4.1.dev471/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev473}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/client/rest_client.py +10 -11
  4. ha_mcp_dev-7.4.1.dev473/src/ha_mcp/client/supervisor_client.py +88 -0
  5. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/settings_ui.py +5 -8
  6. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_bug_report.py +4 -8
  7. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_config_scenes.py +7 -10
  8. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/utils/python_sandbox.py +11 -0
  9. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  10. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  11. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/LICENSE +0 -0
  12. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/MANIFEST.in +0 -0
  13. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/README.md +0 -0
  14. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/setup.cfg +0 -0
  15. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/__init__.py +0 -0
  16. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/__main__.py +0 -0
  17. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/_pypi_marker +0 -0
  18. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/_version.py +0 -0
  19. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/auth/__init__.py +0 -0
  20. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/auth/consent_form.py +0 -0
  21. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/auth/provider.py +0 -0
  22. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/client/__init__.py +0 -0
  23. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/client/websocket_client.py +0 -0
  24. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/client/websocket_listener.py +0 -0
  25. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/config.py +0 -0
  26. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/errors.py +0 -0
  27. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/py.typed +0 -0
  28. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  29. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  30. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  31. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  32. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  33. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  34. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  35. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  36. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  37. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  38. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  39. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  40. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  41. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  42. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  43. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  44. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  45. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  46. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  47. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  48. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/server.py +0 -0
  49. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/smoke_test.py +0 -0
  50. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/__init__.py +0 -0
  51. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/backup.py +0 -0
  52. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  53. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/device_control.py +0 -0
  54. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/enhanced.py +0 -0
  55. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/helpers.py +0 -0
  56. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/reference_validator.py +0 -0
  57. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/registry.py +0 -0
  58. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/smart_search.py +0 -0
  59. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_addons.py +0 -0
  60. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_areas.py +0 -0
  61. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  62. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_calendar.py +0 -0
  63. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_camera.py +0 -0
  64. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_categories.py +0 -0
  65. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_code.py +0 -0
  66. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  67. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  68. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  69. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  70. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  71. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_energy.py +0 -0
  72. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_entities.py +0 -0
  73. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  74. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_groups.py +0 -0
  75. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_hacs.py +0 -0
  76. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_history.py +0 -0
  77. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_integrations.py +0 -0
  78. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_labels.py +0 -0
  79. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  80. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_registry.py +0 -0
  81. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_resources.py +0 -0
  82. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_search.py +0 -0
  83. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_service.py +0 -0
  84. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_services.py +0 -0
  85. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_system.py +0 -0
  86. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_todo.py +0 -0
  87. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_traces.py +0 -0
  88. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_updates.py +0 -0
  89. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_utility.py +0 -0
  90. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  91. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  92. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/tools_zones.py +0 -0
  93. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/tools/util_helpers.py +0 -0
  94. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/transforms/__init__.py +0 -0
  95. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/transforms/categorized_search.py +0 -0
  96. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/utils/__init__.py +0 -0
  97. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/utils/config_hash.py +0 -0
  98. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/utils/data_paths.py +0 -0
  99. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/utils/domain_handlers.py +0 -0
  100. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  101. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  102. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/utils/operation_manager.py +0 -0
  103. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp/utils/usage_logger.py +0 -0
  104. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  105. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  106. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  107. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  108. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/tests/__init__.py +0 -0
  109. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/tests/test_constants.py +0 -0
  110. {ha_mcp_dev-7.4.1.dev471 → ha_mcp_dev-7.4.1.dev473}/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.dev471
3
+ Version: 7.4.1.dev473
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.dev471"
7
+ version = "7.4.1.dev473"
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"
@@ -13,6 +13,7 @@ import httpx
13
13
 
14
14
  from .._version import get_supervisor_base_url, is_running_in_addon
15
15
  from ..config import get_global_settings
16
+ from .supervisor_client import make_supervisor_httpx_client
16
17
 
17
18
 
18
19
  def _is_ssl_error(exc: BaseException) -> bool:
@@ -555,23 +556,21 @@ class HomeAssistantClient:
555
556
  "(addon-mode gate fired but SUPERVISOR_TOKEN env var not set)"
556
557
  )
557
558
 
558
- url = f"{get_supervisor_base_url()}/{path}/logs"
559
- logger.debug("Fetching %s via Supervisor direct", url)
559
+ relative_path = f"/{path}/logs"
560
+ logger.debug(
561
+ "Fetching %s%s via Supervisor direct",
562
+ get_supervisor_base_url(),
563
+ relative_path,
564
+ )
560
565
 
561
566
  try:
562
- async with httpx.AsyncClient(
567
+ async with make_supervisor_httpx_client(
563
568
  timeout=httpx.Timeout(self.timeout),
564
- # `verify` is a no-op for plain http://supervisor, but kept
565
- # for symmetry with the other two direct-Supervisor httpx
566
- # clients (#1128 establishes the 3-site convention).
567
569
  verify=self.verify_ssl,
568
570
  ) as client:
569
571
  response = await client.get(
570
- url,
571
- headers={
572
- "Authorization": f"Bearer {token}",
573
- "Accept": "text/plain",
574
- },
572
+ relative_path,
573
+ headers={"Accept": "text/plain"},
575
574
  )
576
575
  except httpx.TimeoutException as e:
577
576
  raise HomeAssistantConnectionError(
@@ -0,0 +1,88 @@
1
+ """Shared factory for direct-Supervisor httpx clients.
2
+
3
+ Three call sites in the codebase talk directly to the Home Assistant
4
+ Supervisor REST API at ``http://supervisor`` rather than through
5
+ ``HomeAssistantClient.httpx_client`` (which is bound to HA Core, not the
6
+ Supervisor — different base URL, different token, different role gate):
7
+
8
+ - :meth:`ha_mcp.client.rest_client.HomeAssistantClient._supervisor_logs_get`
9
+ — fetches addon and system-service logs
10
+ - :func:`ha_mcp.tools.tools_bug_report._fetch_addon_logs` — bundles ha-mcp's
11
+ own addon logs into a bug-report payload
12
+ - :func:`ha_mcp.settings_ui._restart_addon` — POSTs ``/addons/self/restart``
13
+ from the settings UI
14
+
15
+ All three share the same boilerplate (base URL, ``Authorization: Bearer
16
+ ${SUPERVISOR_TOKEN}`` header), so this module supplies a single factory and
17
+ keeps the three sites consistent.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ import ssl
24
+
25
+ import httpx
26
+
27
+ from .._version import get_supervisor_base_url
28
+
29
+ __all__ = ["make_supervisor_httpx_client"]
30
+
31
+
32
+ def make_supervisor_httpx_client(
33
+ *,
34
+ timeout: float | httpx.Timeout,
35
+ verify: bool | str | ssl.SSLContext,
36
+ ) -> httpx.AsyncClient:
37
+ """Construct an ``httpx.AsyncClient`` pre-configured for the Supervisor REST API.
38
+
39
+ Args:
40
+ timeout: Per-request timeout. Accepts either a plain ``float``
41
+ (seconds, applied to all phases) or a full :class:`httpx.Timeout`
42
+ for finer-grained control.
43
+ verify: TLS verify policy. A no-op for the default
44
+ ``http://supervisor`` base URL (plain HTTP — no TLS to verify),
45
+ but kept as a parameter because :func:`get_supervisor_base_url`
46
+ honours ``SUPERVISOR_BASE_URL`` env-var overrides that may be
47
+ HTTPS in non-add-on test rigs. The full httpx ``verify`` surface
48
+ (``bool``, CA-bundle path, or :class:`ssl.SSLContext`) is
49
+ accepted and forwarded verbatim.
50
+
51
+ Returns:
52
+ A new :class:`httpx.AsyncClient` bound to the Supervisor base URL
53
+ with ``Authorization: Bearer ${SUPERVISOR_TOKEN}`` preset. Callers
54
+ pass relative paths (``/addons/self/logs``) to ``client.get/post``;
55
+ ``base_url`` joins them onto the Supervisor host.
56
+
57
+ Raises:
58
+ RuntimeError: ``SUPERVISOR_TOKEN`` is unset or empty in the
59
+ environment. Each call site has its own absent-token policy
60
+ (a rich :class:`HomeAssistantAuthError`, a silent ``""``
61
+ return, or a 400 ``JSONResponse``) that does not share a
62
+ common shape, so the factory cannot translate. Detecting the
63
+ absence at construction time prevents a malformed
64
+ ``Authorization: Bearer `` header from being read as a token
65
+ rejection by Supervisor, which would mask the missing-env-var
66
+ root cause.
67
+
68
+ Note:
69
+ ``SUPERVISOR_TOKEN`` is read from env at construction time and
70
+ baked into the constructed client's ``Authorization`` header.
71
+ Reusing a single client across token rotations would not pick up
72
+ the new value — short-lived ``async with`` callers are unaffected,
73
+ but a future long-lived caller would need to discard and re-create.
74
+ """
75
+ token = os.environ.get("SUPERVISOR_TOKEN", "")
76
+ if not token:
77
+ raise RuntimeError(
78
+ "SUPERVISOR_TOKEN is not set; "
79
+ "make_supervisor_httpx_client cannot construct an "
80
+ "authenticated client. Callers must verify the token is "
81
+ "present before invoking the factory."
82
+ )
83
+ return httpx.AsyncClient(
84
+ base_url=get_supervisor_base_url(),
85
+ timeout=timeout,
86
+ verify=verify,
87
+ headers={"Authorization": f"Bearer {token}"},
88
+ )
@@ -19,7 +19,8 @@ import httpx
19
19
  from starlette.requests import Request
20
20
  from starlette.responses import HTMLResponse, JSONResponse
21
21
 
22
- from ._version import get_supervisor_base_url, is_running_in_addon
22
+ from ._version import is_running_in_addon
23
+ from .client.supervisor_client import make_supervisor_httpx_client
23
24
  from .errors import ErrorCode, create_error_response
24
25
  from .transforms import DEFAULT_PINNED_TOOLS
25
26
  from .utils.data_paths import get_data_dir
@@ -893,8 +894,7 @@ def register_settings_routes(
893
894
  )
894
895
 
895
896
  async def _restart_addon(_: Request) -> JSONResponse:
896
- token = os.environ.get("SUPERVISOR_TOKEN")
897
- if not token:
897
+ if not os.environ.get("SUPERVISOR_TOKEN"):
898
898
  return JSONResponse(
899
899
  create_error_response(
900
900
  ErrorCode.CONFIG_VALIDATION_FAILED,
@@ -906,13 +906,10 @@ def register_settings_routes(
906
906
  # Short timeout — the supervisor kills our process during restart so
907
907
  # the connection will drop. A connection drop is actually success.
908
908
  try:
909
- async with httpx.AsyncClient(
909
+ async with make_supervisor_httpx_client(
910
910
  timeout=5.0, verify=server.settings.verify_ssl
911
911
  ) as client:
912
- resp = await client.post(
913
- f"{get_supervisor_base_url()}/addons/self/restart",
914
- headers={"Authorization": f"Bearer {token}"},
915
- )
912
+ resp = await client.post("/addons/self/restart")
916
913
  except (httpx.ReadError, httpx.RemoteProtocolError):
917
914
  # Connection dropped mid-request — restart is happening.
918
915
  # `ConnectError` is deliberately NOT in this tuple: it fires
@@ -20,7 +20,7 @@ from pydantic import Field
20
20
 
21
21
  from ha_mcp import __version__
22
22
 
23
- from .._version import get_supervisor_base_url
23
+ from ..client.supervisor_client import make_supervisor_httpx_client
24
24
  from ..config import Settings, get_global_settings
25
25
  from ..utils.usage_logger import (
26
26
  AVG_LOG_ENTRIES_PER_TOOL,
@@ -349,18 +349,14 @@ async def _fetch_addon_logs() -> str:
349
349
  """
350
350
  # Redundant with the caller's `install_method == "addon"` gate, but kept
351
351
  # as a defensive guard for any direct callers added later.
352
- token = os.environ.get("SUPERVISOR_TOKEN", "")
353
- if not token:
352
+ if not os.environ.get("SUPERVISOR_TOKEN"):
354
353
  return ""
355
354
 
356
355
  try:
357
- async with httpx.AsyncClient(
356
+ async with make_supervisor_httpx_client(
358
357
  timeout=10.0, verify=get_global_settings().verify_ssl
359
358
  ) as http_client:
360
- resp = await http_client.get(
361
- f"{get_supervisor_base_url()}/addons/self/logs",
362
- headers={"Authorization": f"Bearer {token}"},
363
- )
359
+ resp = await http_client.get("/addons/self/logs")
364
360
  if resp.status_code != 200:
365
361
  logger.info("Addon log fetch returned HTTP %s", resp.status_code)
366
362
  return ""
@@ -23,6 +23,7 @@ from ..errors import ErrorCode, create_error_response
23
23
  from ..utils.config_hash import compute_config_hash
24
24
  from ..utils.python_sandbox import (
25
25
  PythonSandboxError,
26
+ format_sandbox_error,
26
27
  get_security_documentation,
27
28
  safe_execute,
28
29
  )
@@ -572,16 +573,12 @@ class ConfigSceneTools:
572
573
  try:
573
574
  transformed_config = safe_execute(python_transform, actual_config)
574
575
  except PythonSandboxError as e:
576
+ message, suggestions = format_sandbox_error(e, python_transform)
575
577
  raise_tool_error(
576
578
  create_error_response(
577
579
  ErrorCode.VALIDATION_FAILED,
578
- str(e),
579
- suggestions=[
580
- "Check expression syntax",
581
- "Ensure only allowed operations are used",
582
- "See tool description for allowed operations",
583
- f"Expression: {python_transform[:100]}{'...' if len(python_transform) > 100 else ''}",
584
- ],
580
+ message,
581
+ suggestions=suggestions,
585
582
  context={
586
583
  "action": "python_transform",
587
584
  "scene_id": resolved_id,
@@ -656,9 +653,9 @@ class ConfigSceneTools:
656
653
  # is the in-place equivalent of ``del`` — normalise both
657
654
  # by checking against ``(None, resolved_id)``, so only an
658
655
  # explicit non-None mismatch triggers the reject.
659
- if (
660
- "id" in transformed_config
661
- and transformed_config["id"] not in (None, resolved_id)
656
+ if "id" in transformed_config and transformed_config["id"] not in (
657
+ None,
658
+ resolved_id,
662
659
  ):
663
660
  raise_tool_error(
664
661
  create_error_response(
@@ -472,6 +472,17 @@ def format_sandbox_error(
472
472
  "See tool description for allowed operations",
473
473
  f"Expression: {preview}",
474
474
  ]
475
+ # Agents commonly emit ``\"`` (intending a JSON-style quote escape)
476
+ # inside transforms. Outside Python string literals this is a
477
+ # line-continuation token followed by an unexpected character.
478
+ # Pinpoint the actual mistake so the agent fixes it on the first
479
+ # retry instead of guessing at "syntax".
480
+ if "line continuation character" in str(error):
481
+ suggestions.insert(
482
+ 0,
483
+ 'Remove the leading backslash before quotes — write "foo" '
484
+ 'not \\"foo\\" in the Python source.',
485
+ )
475
486
  if variable_name != "config":
476
487
  suggestions = [
477
488
  f"Operate on the `{variable_name}` variable (in-place or reassign)",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.4.1.dev471
3
+ Version: 7.4.1.dev473
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
@@ -17,6 +17,7 @@ src/ha_mcp/auth/consent_form.py
17
17
  src/ha_mcp/auth/provider.py
18
18
  src/ha_mcp/client/__init__.py
19
19
  src/ha_mcp/client/rest_client.py
20
+ src/ha_mcp/client/supervisor_client.py
20
21
  src/ha_mcp/client/websocket_client.py
21
22
  src/ha_mcp/client/websocket_listener.py
22
23
  src/ha_mcp/resources/skills-vendor/AGENTS.md