ha-mcp-dev 7.3.0.dev398__tar.gz → 7.3.0.dev400__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 (104) hide show
  1. {ha_mcp_dev-7.3.0.dev398/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.3.0.dev400}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/__init__.py +3 -1
  4. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/__main__.py +24 -2
  5. ha_mcp_dev-7.3.0.dev400/src/ha_mcp/_version.py +58 -0
  6. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/config.py +4 -6
  7. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_areas.py +8 -1
  8. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_config_automations.py +54 -7
  9. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_config_helpers.py +3 -3
  10. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_config_scripts.py +8 -7
  11. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/util_helpers.py +7 -0
  12. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/transforms/categorized_search.py +41 -4
  13. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  14. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  15. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/LICENSE +0 -0
  16. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/MANIFEST.in +0 -0
  17. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/README.md +0 -0
  18. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/setup.cfg +0 -0
  19. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/_pypi_marker +0 -0
  20. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/auth/__init__.py +0 -0
  21. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/auth/consent_form.py +0 -0
  22. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/auth/provider.py +0 -0
  23. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/client/__init__.py +0 -0
  24. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/client/rest_client.py +0 -0
  25. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/client/websocket_client.py +0 -0
  26. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/client/websocket_listener.py +0 -0
  27. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/errors.py +0 -0
  28. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/py.typed +0 -0
  29. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  30. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  31. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  32. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  33. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  34. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  35. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  36. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  37. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  38. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  39. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  40. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  41. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  42. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  43. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  44. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  45. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  46. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  47. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  48. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  49. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/server.py +0 -0
  50. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/smoke_test.py +0 -0
  51. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/__init__.py +0 -0
  52. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/backup.py +0 -0
  53. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  54. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/device_control.py +0 -0
  55. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/enhanced.py +0 -0
  56. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/helpers.py +0 -0
  57. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/reference_validator.py +0 -0
  58. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/registry.py +0 -0
  59. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/smart_search.py +0 -0
  60. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_addons.py +0 -0
  61. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  62. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  63. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_calendar.py +0 -0
  64. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_camera.py +0 -0
  65. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_categories.py +0 -0
  66. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  67. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  68. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_energy.py +0 -0
  69. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_entities.py +0 -0
  70. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  71. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_groups.py +0 -0
  72. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_hacs.py +0 -0
  73. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_history.py +0 -0
  74. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_integrations.py +0 -0
  75. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_labels.py +0 -0
  76. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  77. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_registry.py +0 -0
  78. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_resources.py +0 -0
  79. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_search.py +0 -0
  80. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_service.py +0 -0
  81. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_services.py +0 -0
  82. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_system.py +0 -0
  83. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_todo.py +0 -0
  84. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_traces.py +0 -0
  85. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_updates.py +0 -0
  86. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_utility.py +0 -0
  87. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  88. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  89. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/tools/tools_zones.py +0 -0
  90. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/transforms/__init__.py +0 -0
  91. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/__init__.py +0 -0
  92. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/config_hash.py +0 -0
  93. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/domain_handlers.py +0 -0
  94. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  95. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/operation_manager.py +0 -0
  96. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/python_sandbox.py +0 -0
  97. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp/utils/usage_logger.py +0 -0
  98. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  99. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  100. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  101. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  102. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/tests/__init__.py +0 -0
  103. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/tests/test_constants.py +0 -0
  104. {ha_mcp_dev-7.3.0.dev398 → ha_mcp_dev-7.3.0.dev400}/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.3.0.dev398
3
+ Version: 7.3.0.dev400
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.3.0.dev398"
7
+ version = "7.3.0.dev400"
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"
@@ -5,7 +5,9 @@ A Model Context Protocol server that provides complete control over Home Assista
5
5
  through REST API and WebSocket integration with 20+ enhanced tools.
6
6
  """
7
7
 
8
- __version__ = "7.3.0"
8
+ from ._version import get_version
9
+
10
+ __version__ = get_version()
9
11
  __author__ = "Julien"
10
12
  __license__ = "MIT"
11
13
 
@@ -283,6 +283,7 @@ def _setup_standard_mode() -> None:
283
283
  settings = get_settings()
284
284
  _validate_standard_credentials(settings)
285
285
  _setup_logging(settings.log_level)
286
+ _log_startup_version()
286
287
 
287
288
 
288
289
  def _http_run_kwargs(transport: str, port: int, path: str) -> dict:
@@ -411,6 +412,25 @@ def _setup_logging(log_level_str: str, force: bool = False) -> None:
411
412
  logging.getLogger("fastmcp.server.server").addFilter(ToolValidationLogFilter())
412
413
 
413
414
 
415
+ def _log_startup_version() -> None:
416
+ """Log ha-mcp version at startup, plus a dev-channel banner when relevant.
417
+
418
+ The dev banner only fires for standalone dev installs (Docker ``:dev`` /
419
+ ``:latest``, or ``pip install ha-mcp-dev``). It is suppressed under the HA
420
+ Supervisor because add-on users already pick dev vs stable in the HAOS UI.
421
+ """
422
+ from ha_mcp._version import get_version, is_dev_version, is_running_in_addon
423
+
424
+ version = get_version()
425
+ logger.info(f"ha-mcp {version}")
426
+ if is_dev_version(version) and not is_running_in_addon():
427
+ logger.warning(
428
+ "This is the dev channel. For the stable release use the "
429
+ "'ghcr.io/homeassistant-ai/ha-mcp:stable' Docker tag "
430
+ "(or 'pip install ha-mcp' on PyPI)."
431
+ )
432
+
433
+
414
434
  def _get_timestamped_uvicorn_log_config() -> dict:
415
435
  """Return a Uvicorn log config with human-readable timestamps added."""
416
436
  from uvicorn.config import LOGGING_CONFIG
@@ -580,9 +600,9 @@ def main() -> None:
580
600
  """Run server via CLI using FastMCP's stdio transport."""
581
601
  # Handle --version flag early, before server creation requires config
582
602
  if "--version" in sys.argv or "-V" in sys.argv:
583
- from importlib.metadata import version
603
+ from ha_mcp._version import get_version
584
604
 
585
- print(f"ha-mcp {version('ha-mcp-dev')}")
605
+ print(f"ha-mcp {get_version()}")
586
606
  sys.exit(0)
587
607
 
588
608
  # Check for smoke test flag
@@ -605,6 +625,7 @@ def main() -> None:
605
625
  sys.exit(1)
606
626
 
607
627
  _setup_logging(settings.log_level)
628
+ _log_startup_version()
608
629
 
609
630
  _run_entrypoint(_run_with_graceful_shutdown(), "Server")
610
631
 
@@ -780,6 +801,7 @@ def main_oauth() -> None:
780
801
  for logger_name in ["ha_mcp", "ha_mcp.auth", "ha_mcp.auth.provider"]:
781
802
  logging.getLogger(logger_name).setLevel(getattr(logging, log_level))
782
803
  logger.info(f"OAuth mode logging configured at {log_level} level")
804
+ _log_startup_version()
783
805
 
784
806
  port, path = _get_http_runtime(default_port=8086)
785
807
  base_url = os.getenv("MCP_BASE_URL")
@@ -0,0 +1,58 @@
1
+ """Version resolution for the ha-mcp package.
2
+
3
+ Kept as a standalone module (no other ``ha_mcp`` imports) so it can be used from
4
+ ``__init__.py`` and ``config.py`` without circular-import risk.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib.metadata
10
+ import logging
11
+ import os
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def get_version() -> str:
17
+ """Return the installed ha-mcp version.
18
+
19
+ Resolution order:
20
+ 1. ``HA_MCP_BUILD_VERSION`` env var — set by Docker/add-on builds that can't
21
+ rewrite ``pyproject.toml`` before install, so the dev suffix still reaches
22
+ the running process. Stable builds leave it unset.
23
+ 2. ``ha-mcp`` package metadata — stable PyPI + stable Docker.
24
+ 3. ``ha-mcp-dev`` package metadata — PyPI dev channel (renamed package).
25
+
26
+ If none of the above resolve, logs a warning and returns ``"unknown"``.
27
+ The "unknown" string is itself diagnostic in bug reports and startup logs
28
+ — it tells triagers the install didn't register package metadata (e.g. a
29
+ source checkout without ``pip install -e .``, or a broken Docker layer).
30
+ """
31
+ if override := os.environ.get("HA_MCP_BUILD_VERSION"):
32
+ return override
33
+ for pkg_name in ("ha-mcp", "ha-mcp-dev"):
34
+ try:
35
+ return importlib.metadata.version(pkg_name)
36
+ except importlib.metadata.PackageNotFoundError:
37
+ continue
38
+ logger.warning(
39
+ "ha-mcp package metadata not found and HA_MCP_BUILD_VERSION unset — "
40
+ "version will be reported as 'unknown'. Reinstall the package or set "
41
+ "HA_MCP_BUILD_VERSION if this is an intentional source-tree run."
42
+ )
43
+ return "unknown"
44
+
45
+
46
+ def is_dev_version(version: str) -> bool:
47
+ """Return True when the version string contains a PEP 440 ``.dev`` suffix."""
48
+ return ".dev" in version
49
+
50
+
51
+ def is_running_in_addon() -> bool:
52
+ """Return True when running inside a Home Assistant add-on container.
53
+
54
+ The HA Supervisor injects ``SUPERVISOR_TOKEN`` into every add-on's env.
55
+ Checked so the standalone-Docker ``:stable`` banner isn't shown to add-on
56
+ users, who already see the dev/stable distinction in the HAOS add-on UI.
57
+ """
58
+ return bool(os.environ.get("SUPERVISOR_TOKEN"))
@@ -2,22 +2,20 @@
2
2
  Configuration management for Home Assistant MCP Server.
3
3
  """
4
4
 
5
- import importlib.metadata
6
5
  import os
7
6
 
8
7
  # Load environment variables from .env file with HAMCP_ENV_FILE support
9
8
  # Use absolute path to ensure .env is found regardless of cwd
10
9
  from pathlib import Path
11
10
 
12
- try:
13
- _PACKAGE_VERSION = importlib.metadata.version("ha-mcp")
14
- except importlib.metadata.PackageNotFoundError:
15
- _PACKAGE_VERSION = "unknown"
16
-
17
11
  from dotenv import load_dotenv
18
12
  from pydantic import Field, field_validator, model_validator
19
13
  from pydantic_settings import BaseSettings, SettingsConfigDict
20
14
 
15
+ from ha_mcp._version import get_version
16
+
17
+ _PACKAGE_VERSION = get_version()
18
+
21
19
  project_root = Path(__file__).parent.parent.parent
22
20
 
23
21
  # Demo environment token - use HOMEASSISTANT_TOKEN="demo" to connect to the public demo
@@ -222,8 +222,15 @@ class AreaTools:
222
222
  """
223
223
  Create or update a Home Assistant area (room).
224
224
 
225
- Provide name only to create a new area. Provide area_id to update existing.
226
225
  Areas organize entities by physical location for room-based control.
226
+
227
+ Create: provide name only.
228
+ Update: provide area_id (from ha_config_list_areas) plus any fields to change.
229
+
230
+ EXAMPLES:
231
+ ha_config_set_area(name="Kitchen")
232
+ ha_config_set_area(name="Living Room", icon="mdi:sofa")
233
+ ha_config_set_area(area_id="kitchen", name="Kitchen Renamed", floor_id="ground_floor")
227
234
  """
228
235
  try:
229
236
  # Parse aliases if provided as string
@@ -41,6 +41,7 @@ from .reference_validator import validate_config_references
41
41
  from .util_helpers import (
42
42
  apply_entity_category,
43
43
  coerce_bool_param,
44
+ coerce_to_list,
44
45
  fetch_entity_category,
45
46
  merge_validation_meta,
46
47
  parse_json_param,
@@ -413,7 +414,7 @@ class AutomationConfigTools:
413
414
  BASIC EXAMPLES:
414
415
 
415
416
  Simple time-based automation:
416
- ha_config_set_automation({
417
+ ha_config_set_automation(config={
417
418
  "alias": "Morning Lights",
418
419
  "description": "Turn on bedroom lights at 7 AM to help wake up",
419
420
  "trigger": [{"platform": "time", "at": "07:00:00"}],
@@ -421,7 +422,7 @@ class AutomationConfigTools:
421
422
  })
422
423
 
423
424
  Motion-activated lighting with condition:
424
- ha_config_set_automation({
425
+ ha_config_set_automation(config={
425
426
  "alias": "Motion Light",
426
427
  "trigger": [{"platform": "state", "entity_id": "binary_sensor.motion", "to": "on"}],
427
428
  "condition": [{"condition": "sun", "after": "sunset"}],
@@ -449,7 +450,7 @@ class AutomationConfigTools:
449
450
  BLUEPRINT AUTOMATION EXAMPLES:
450
451
 
451
452
  Create automation from blueprint:
452
- ha_config_set_automation({
453
+ ha_config_set_automation(config={
453
454
  "alias": "Motion Light Kitchen",
454
455
  "use_blueprint": {
455
456
  "path": "homeassistant/motion_light.yaml",
@@ -757,10 +758,14 @@ class AutomationConfigTools:
757
758
  try:
758
759
  parsed_config = parse_json_param(config, "config")
759
760
  except ValueError as e:
760
- raise_tool_error(create_validation_error(
761
- f"Invalid config parameter: {e}",
762
- parameter="config",
763
- invalid_json=True,
761
+ raise_tool_error(create_error_response(
762
+ code=ErrorCode.VALIDATION_INVALID_JSON,
763
+ message=f"Invalid config parameter: {e}",
764
+ suggestions=[
765
+ "Pass 'config' as a dict, not a JSON string, to avoid escaping issues.",
766
+ "Check for JSON syntax errors: unquoted keys, trailing commas, or invalid escape sequences.",
767
+ ],
768
+ context={"parameter": "config"},
764
769
  ))
765
770
 
766
771
  if parsed_config is None or not isinstance(parsed_config, dict):
@@ -788,12 +793,54 @@ class AutomationConfigTools:
788
793
 
789
794
  missing_fields = [f for f in required_fields if f not in config_dict]
790
795
  if missing_fields:
796
+ # If the caller supplied a 'sequence' key, the config looks like a
797
+ # script — point them at ha_config_set_script instead of the generic
798
+ # missing-fields error.
799
+ if "sequence" in config_dict and (
800
+ "trigger" in missing_fields or "action" in missing_fields
801
+ ):
802
+ context: dict[str, Any] = {"missing_fields": missing_fields}
803
+ if identifier:
804
+ context["identifier"] = identifier
805
+ raise_tool_error(create_error_response(
806
+ code=ErrorCode.CONFIG_MISSING_REQUIRED_FIELDS,
807
+ message=f"Missing required fields: {', '.join(missing_fields)}",
808
+ details=(
809
+ "Config contains 'sequence', which belongs to scripts. "
810
+ "Automations use 'trigger' and 'action'; scripts use 'sequence'."
811
+ ),
812
+ suggestions=[
813
+ "Did you mean ha_config_set_script? Scripts use 'sequence' directly.",
814
+ "For an automation, replace 'sequence' with 'action' and add a 'trigger'.",
815
+ ],
816
+ context=context,
817
+ ))
791
818
  raise_tool_error(create_config_error(
792
819
  f"Missing required fields: {', '.join(missing_fields)}",
793
820
  identifier=identifier,
794
821
  missing_fields=missing_fields,
795
822
  ))
796
823
 
824
+ # HA accepts conditions with 'platform' (trigger syntax) but then crashes
825
+ # with an unhelpful 500 rather than a 400 validation error.
826
+ for idx, cond in enumerate(coerce_to_list(config_dict.get("condition"))):
827
+ if not isinstance(cond, dict):
828
+ continue
829
+ if "platform" in cond and "condition" not in cond:
830
+ raise_tool_error(create_error_response(
831
+ code=ErrorCode.VALIDATION_INVALID_PARAMETER,
832
+ message=(
833
+ f"Condition at index {idx} uses 'platform' (trigger syntax). "
834
+ "Conditions use 'condition', not 'platform'."
835
+ ),
836
+ suggestions=[
837
+ f"Replace 'platform' with 'condition': "
838
+ f"{{'condition': '{cond['platform']}', ...}}",
839
+ "Triggers use 'platform'; conditions use 'condition'.",
840
+ ],
841
+ context={"condition_index": idx, "found_key": "platform"},
842
+ ))
843
+
797
844
  # Prevent duplicate creation when config contains an existing automation id
798
845
  if identifier is None and "id" in config_dict:
799
846
  existing_id = config_dict["id"]
@@ -801,16 +801,16 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
801
801
 
802
802
  EXAMPLES (menu-based types + tod, where first-call payload is non-obvious):
803
803
  - template sensor:
804
- ha_config_set_helper("template", "Room Temp",
804
+ ha_config_set_helper(helper_type="template", name="Room Temp",
805
805
  config={"next_step_id": "sensor",
806
806
  "state": "{{ states('sensor.x')|float }}",
807
807
  "unit_of_measurement": "°C"})
808
808
  - group (light):
809
- ha_config_set_helper("group", "Kitchen Lights",
809
+ ha_config_set_helper(helper_type="group", name="Kitchen Lights",
810
810
  config={"group_type": "light",
811
811
  "entities": ["light.a", "light.b"]})
812
812
  - tod (time-of-day indicator, cross-midnight OK):
813
- ha_config_set_helper("tod", "Quiet Hours",
813
+ ha_config_set_helper(helper_type="tod", name="Quiet Hours",
814
814
  config={"after_time": "22:00:00", "before_time": "07:00:00"})
815
815
 
816
816
  For complex schemas and per-type parameter details, use ha_get_helper_schema.
@@ -313,19 +313,20 @@ class ConfigScriptTools:
313
313
  - max: Maximum concurrent executions (for queued/parallel modes)
314
314
  - fields: Input parameters for the script
315
315
 
316
- IMPORTANT: The 'config' parameter must be passed as a proper dictionary/object.
316
+ SCRIPTS vs AUTOMATIONS: Scripts use 'sequence', NOT 'trigger' or 'action'.
317
+ If you need trigger-based execution, use ha_config_set_automation instead.
317
318
 
318
319
  EXAMPLES:
319
320
 
320
321
  Create basic delay script:
321
- ha_config_set_script("wait_script", {
322
+ ha_config_set_script(script_id="wait_script", config={
322
323
  "sequence": [{"delay": {"seconds": 5}}],
323
324
  "alias": "Wait 5 Seconds",
324
325
  "description": "Simple delay script"
325
326
  })
326
327
 
327
328
  Create service call script:
328
- ha_config_set_script("blink_light", {
329
+ ha_config_set_script(script_id="blink_light", config={
329
330
  "sequence": [
330
331
  {"service": "light.turn_on", "target": {"entity_id": "light.living_room"}},
331
332
  {"delay": {"seconds": 2}},
@@ -336,7 +337,7 @@ class ConfigScriptTools:
336
337
  })
337
338
 
338
339
  Create script with parameters:
339
- ha_config_set_script("backup_script", {
340
+ ha_config_set_script(script_id="backup_script", config={
340
341
  "alias": "Backup with Reference",
341
342
  "description": "Create backup with optional reference parameter",
342
343
  "fields": {
@@ -360,7 +361,7 @@ class ConfigScriptTools:
360
361
  })
361
362
 
362
363
  Update script:
363
- ha_config_set_script("morning_routine", {
364
+ ha_config_set_script(script_id="morning_routine", config={
364
365
  "sequence": [
365
366
  {"service": "light.turn_on", "target": {"area_id": "bedroom"}},
366
367
  {"service": "climate.set_temperature", "target": {"entity_id": "climate.bedroom"}, "data": {"temperature": 22}}
@@ -369,7 +370,7 @@ class ConfigScriptTools:
369
370
  })
370
371
 
371
372
  Create blueprint-based script:
372
- ha_config_set_script("notification_script", {
373
+ ha_config_set_script(script_id="notification_script", config={
373
374
  "alias": "My Notification Script",
374
375
  "use_blueprint": {
375
376
  "path": "notification_script.yaml",
@@ -381,7 +382,7 @@ class ConfigScriptTools:
381
382
  })
382
383
 
383
384
  Update blueprint script inputs:
384
- ha_config_set_script("notification_script", {
385
+ ha_config_set_script(script_id="notification_script", config={
385
386
  "alias": "My Notification Script",
386
387
  "use_blueprint": {
387
388
  "path": "notification_script.yaml",
@@ -532,6 +532,13 @@ async def apply_entity_category(
532
532
  )
533
533
 
534
534
 
535
+ def coerce_to_list(value: Any) -> list[Any]:
536
+ """Return value as a list: list → as-is, dict/other → [value], None/falsy → []."""
537
+ if isinstance(value, list):
538
+ return value
539
+ return [value] if value else []
540
+
541
+
535
542
  def merge_validation_meta(
536
543
  result: dict[str, Any], validation_meta: dict[str, Any]
537
544
  ) -> None:
@@ -111,19 +111,22 @@ def _build_proxy_descriptions(search_tool_name: str) -> dict[str, str]:
111
111
  "read": (
112
112
  f"Execute a read-only tool discovered via {search_tool_name}. "
113
113
  f"Safe — does not modify any data or state.\n"
114
- f"{_PROXY_PARAMS_SUFFIX}"
114
+ f"{_PROXY_PARAMS_SUFFIX}\n"
115
+ f'EXAMPLE: ha_call_read_tool(name="ha_get_history", arguments={{"entity_ids": "light.x", "start_time": "24h"}})'
115
116
  ),
116
117
  "write": (
117
118
  f"Execute a write tool discovered via {search_tool_name}. "
118
119
  f"Creates or updates data. Use for any tool that modifies "
119
120
  f"state but does not delete/remove resources.\n"
120
- f"{_PROXY_PARAMS_SUFFIX}"
121
+ f"{_PROXY_PARAMS_SUFFIX}\n"
122
+ f'EXAMPLE: ha_call_write_tool(name="ha_config_set_area", arguments={{"name": "Kitchen"}})'
121
123
  ),
122
124
  "delete": (
123
125
  f"Execute a delete/remove tool discovered via {search_tool_name}. "
124
126
  f"Permanently removes data. Use for tools that delete or "
125
127
  f"remove resources (areas, automations, devices, etc.).\n"
126
- f"{_PROXY_PARAMS_SUFFIX}"
128
+ f"{_PROXY_PARAMS_SUFFIX}\n"
129
+ f'EXAMPLE: ha_call_delete_tool(name="ha_config_remove_area", arguments={{"area_id": "old_area"}})'
127
130
  ),
128
131
  }
129
132
 
@@ -254,13 +257,47 @@ class CategorizedSearchTransform(BM25SearchTransform):
254
257
  async def categorized_call(
255
258
  name: Annotated[str, "The name of the tool to call"],
256
259
  arguments: Annotated[
257
- dict[str, Any] | None, "Arguments to pass to the tool"
260
+ dict[str, Any] | str | None, "Arguments to pass to the tool"
258
261
  ] = None,
259
262
  ctx: Context = None, # type: ignore[assignment]
260
263
  ) -> Any:
261
264
  # Rebuild category cache if catalog has changed
262
265
  await transform._rebuild_category_cache(ctx)
263
266
 
267
+ # Tolerate `arguments` passed as a JSON string — small models
268
+ # sometimes serialize it before sending. Parse once up front so
269
+ # downstream logic can assume a dict (or None).
270
+ if isinstance(arguments, str):
271
+ try:
272
+ parsed = json.loads(arguments)
273
+ except json.JSONDecodeError as e:
274
+ raise ToolError(json.dumps(create_error_response(
275
+ code=ErrorCode.VALIDATION_INVALID_JSON,
276
+ message=f"'arguments' is a string but not valid JSON: {e}",
277
+ suggestions=[
278
+ "Pass 'arguments' as an object, not a JSON string.",
279
+ ],
280
+ context={"proxy_used": proxy_name, "tool_name": name},
281
+ ))) from e
282
+ if not isinstance(parsed, dict):
283
+ raise ToolError(json.dumps(create_error_response(
284
+ code=ErrorCode.VALIDATION_INVALID_PARAMETER,
285
+ message=(
286
+ "'arguments' must be a JSON object "
287
+ f"(got {type(parsed).__name__})."
288
+ ),
289
+ suggestions=[
290
+ "Pass 'arguments' as an object (dict), not a list or scalar.",
291
+ ],
292
+ context={"proxy_used": proxy_name, "tool_name": name},
293
+ )))
294
+ logger.warning(
295
+ "Proxy %s received 'arguments' as a JSON string for tool %s — parsed as fallback",
296
+ proxy_name,
297
+ name,
298
+ )
299
+ arguments = parsed
300
+
264
301
  # Determine which category set to check
265
302
  if category == "read":
266
303
  allowed = transform._read_tools
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.3.0.dev398
3
+ Version: 7.3.0.dev400
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
@@ -5,6 +5,7 @@ pyproject.toml
5
5
  src/ha_mcp/__init__.py
6
6
  src/ha_mcp/__main__.py
7
7
  src/ha_mcp/_pypi_marker
8
+ src/ha_mcp/_version.py
8
9
  src/ha_mcp/config.py
9
10
  src/ha_mcp/errors.py
10
11
  src/ha_mcp/py.typed