ha-mcp-dev 7.4.1.dev451__tar.gz → 7.4.1.dev453__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 (108) hide show
  1. {ha_mcp_dev-7.4.1.dev451/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev453}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/device_control.py +40 -3
  4. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/smart_search.py +53 -0
  5. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_hacs.py +32 -0
  6. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_history.py +30 -2
  7. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_search.py +3 -0
  8. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_service.py +5 -2
  9. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_traces.py +43 -3
  10. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  11. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/LICENSE +0 -0
  12. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/MANIFEST.in +0 -0
  13. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/README.md +0 -0
  14. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/setup.cfg +0 -0
  15. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/__init__.py +0 -0
  16. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/__main__.py +0 -0
  17. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/_pypi_marker +0 -0
  18. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/_version.py +0 -0
  19. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/auth/__init__.py +0 -0
  20. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/auth/consent_form.py +0 -0
  21. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/auth/provider.py +0 -0
  22. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/client/__init__.py +0 -0
  23. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/client/rest_client.py +0 -0
  24. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/client/websocket_client.py +0 -0
  25. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/client/websocket_listener.py +0 -0
  26. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/config.py +0 -0
  27. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/errors.py +0 -0
  28. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/py.typed +0 -0
  29. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  30. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  31. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  32. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  33. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  34. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  35. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  36. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  37. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  38. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  39. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  40. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  41. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  42. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  43. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  44. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  45. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  46. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  47. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  48. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  49. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/server.py +0 -0
  50. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/settings_ui.py +0 -0
  51. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/smoke_test.py +0 -0
  52. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/__init__.py +0 -0
  53. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/backup.py +0 -0
  54. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  55. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/enhanced.py +0 -0
  56. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/helpers.py +0 -0
  57. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/reference_validator.py +0 -0
  58. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/registry.py +0 -0
  59. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_addons.py +0 -0
  60. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_areas.py +0 -0
  61. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  62. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  63. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_calendar.py +0 -0
  64. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_camera.py +0 -0
  65. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_categories.py +0 -0
  66. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_code.py +0 -0
  67. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  68. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  69. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  70. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  71. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  72. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_energy.py +0 -0
  73. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_entities.py +0 -0
  74. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  75. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_groups.py +0 -0
  76. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_integrations.py +0 -0
  77. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_labels.py +0 -0
  78. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  79. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_registry.py +0 -0
  80. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_resources.py +0 -0
  81. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_services.py +0 -0
  82. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_system.py +0 -0
  83. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_todo.py +0 -0
  84. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_updates.py +0 -0
  85. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_utility.py +0 -0
  86. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  87. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  88. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/tools_zones.py +0 -0
  89. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/tools/util_helpers.py +0 -0
  90. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/transforms/__init__.py +0 -0
  91. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/transforms/categorized_search.py +0 -0
  92. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/utils/__init__.py +0 -0
  93. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/utils/config_hash.py +0 -0
  94. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/utils/data_paths.py +0 -0
  95. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/utils/domain_handlers.py +0 -0
  96. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  97. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  98. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/utils/operation_manager.py +0 -0
  99. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/utils/python_sandbox.py +0 -0
  100. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp/utils/usage_logger.py +0 -0
  101. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  102. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  103. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  104. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  105. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  106. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/tests/__init__.py +0 -0
  107. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/tests/test_constants.py +0 -0
  108. {ha_mcp_dev-7.4.1.dev451 → ha_mcp_dev-7.4.1.dev453}/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.dev451
3
+ Version: 7.4.1.dev453
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.dev451"
7
+ version = "7.4.1.dev453"
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"
@@ -10,6 +10,7 @@ import json
10
10
  import logging
11
11
  from typing import Any, ClassVar
12
12
 
13
+ from fastmcp import Context
13
14
  from fastmcp.exceptions import ToolError
14
15
 
15
16
  from ..client.rest_client import HomeAssistantClient
@@ -534,7 +535,10 @@ class DeviceControlTools:
534
535
  return valid
535
536
 
536
537
  async def bulk_device_control(
537
- self, operations: list[dict[str, Any]], parallel: bool = True
538
+ self,
539
+ operations: list[dict[str, Any]],
540
+ parallel: bool = True,
541
+ ctx: Context | None = None,
538
542
  ) -> dict[str, Any]:
539
543
  """
540
544
  Control multiple devices with bulk operation support.
@@ -542,6 +546,7 @@ class DeviceControlTools:
542
546
  Args:
543
547
  operations: List of device control operations
544
548
  parallel: Whether to execute operations in parallel
549
+ ctx: Optional FastMCP Context for progress reporting
545
550
 
546
551
  Returns:
547
552
  Bulk operation results
@@ -563,11 +568,35 @@ class DeviceControlTools:
563
568
  operations, skipped_operations
564
569
  )
565
570
 
571
+ if ctx is not None:
572
+ await ctx.info(
573
+ f"bulk_device_control: {len(valid_operations)} valid op(s), "
574
+ f"{len(skipped_operations)} skipped, "
575
+ f"mode={'parallel' if parallel else 'sequential'}"
576
+ )
577
+ await ctx.report_progress(
578
+ progress=0,
579
+ total=len(valid_operations),
580
+ message="dispatching operations",
581
+ )
582
+
566
583
  # Execute only valid operations
567
584
  if parallel:
568
585
  await self._execute_parallel(valid_operations, results, operation_ids)
569
586
  else:
570
- await self._execute_sequential(valid_operations, results, operation_ids)
587
+ await self._execute_sequential(
588
+ valid_operations, results, operation_ids, ctx=ctx
589
+ )
590
+
591
+ if ctx is not None:
592
+ await ctx.report_progress(
593
+ progress=len(valid_operations),
594
+ total=len(valid_operations),
595
+ message=(
596
+ f"dispatched {len(operation_ids)} op(s); "
597
+ "use get_bulk_operation_status to verify completion"
598
+ ),
599
+ )
571
600
 
572
601
  return self._build_bulk_response(
573
602
  operations, results, operation_ids, skipped_operations, parallel
@@ -631,8 +660,10 @@ class DeviceControlTools:
631
660
  valid_operations: list[tuple[int, dict[str, Any], str, str]],
632
661
  results: list[dict[str, Any]],
633
662
  operation_ids: list[str],
663
+ ctx: Context | None = None,
634
664
  ) -> None:
635
- for _i, op, entity_id, action in valid_operations:
665
+ total = len(valid_operations)
666
+ for i, (_orig_index, op, entity_id, action) in enumerate(valid_operations):
636
667
  try:
637
668
  result = await self.control_device_smart(
638
669
  entity_id=entity_id,
@@ -651,6 +682,12 @@ class DeviceControlTools:
651
682
  ErrorCode.SERVICE_CALL_FAILED,
652
683
  f"Exception during execution: {e!s}",
653
684
  ))
685
+ if ctx is not None:
686
+ await ctx.report_progress(
687
+ progress=i + 1,
688
+ total=total,
689
+ message=f"{entity_id} {action} dispatched",
690
+ )
654
691
 
655
692
  def _build_bulk_response(
656
693
  self,
@@ -9,6 +9,8 @@ import random
9
9
  import time
10
10
  from typing import Any
11
11
 
12
+ from fastmcp import Context
13
+
12
14
  from ..client.rest_client import HomeAssistantClient
13
15
  from ..config import get_global_settings
14
16
  from ..utils.fuzzy_search import (
@@ -802,6 +804,7 @@ class SmartSearchTools:
802
804
  include_config: bool = False,
803
805
  concurrency_limit: int = DEFAULT_CONCURRENCY_LIMIT,
804
806
  exact_match: bool = True,
807
+ ctx: Context | None = None,
805
808
  ) -> dict[str, Any]:
806
809
  """
807
810
  Deep search across automation, script, helper, and dashboard definitions.
@@ -834,8 +837,26 @@ class SmartSearchTools:
834
837
 
835
838
  query_lower = query.lower().strip()
836
839
 
840
+ total_phases = len(search_types) + 1 # +1 for initial state fetch
841
+ if ctx is not None:
842
+ await ctx.info(
843
+ f"deep_search starting: query={query!r} types={search_types}"
844
+ )
845
+ await ctx.report_progress(
846
+ progress=0,
847
+ total=total_phases,
848
+ message="fetching entity states",
849
+ )
850
+
837
851
  # Fetch all entities once at the beginning to avoid repeated calls
838
852
  all_entities = await self.client.get_states()
853
+ phase_done = 1
854
+ if ctx is not None:
855
+ await ctx.report_progress(
856
+ progress=phase_done,
857
+ total=total_phases,
858
+ message=f"fetched {len(all_entities)} entity states",
859
+ )
839
860
 
840
861
  # Pre-resolve unique_ids from cached entity states to avoid redundant API calls
841
862
  automation_unique_id_map = {}
@@ -1009,6 +1030,14 @@ class SmartSearchTools:
1009
1030
  }
1010
1031
  )
1011
1032
 
1033
+ phase_done += 1
1034
+ if ctx is not None:
1035
+ await ctx.report_progress(
1036
+ progress=phase_done,
1037
+ total=total_phases,
1038
+ message=f"automations searched ({len(results['automations'])} matches)",
1039
+ )
1040
+
1012
1041
  # ================================================================
1013
1042
  # SCRIPT SEARCH (same 3-tier strategy: REST bulk -> WS bulk -> individual)
1014
1043
  # ================================================================
@@ -1165,6 +1194,14 @@ class SmartSearchTools:
1165
1194
  }
1166
1195
  )
1167
1196
 
1197
+ phase_done += 1
1198
+ if ctx is not None:
1199
+ await ctx.report_progress(
1200
+ progress=phase_done,
1201
+ total=total_phases,
1202
+ message=f"scripts searched ({len(results['scripts'])} matches)",
1203
+ )
1204
+
1168
1205
  # Search helpers with parallel WebSocket calls
1169
1206
  if "helper" in search_types:
1170
1207
  helper_types = [
@@ -1248,6 +1285,14 @@ class SmartSearchTools:
1248
1285
  elif isinstance(result, Exception):
1249
1286
  logger.debug(f"Helper list fetch failed: {result}")
1250
1287
 
1288
+ phase_done += 1
1289
+ if ctx is not None:
1290
+ await ctx.report_progress(
1291
+ progress=phase_done,
1292
+ total=total_phases,
1293
+ message=f"helpers searched ({len(results['helpers'])} matches)",
1294
+ )
1295
+
1251
1296
  # ================================================================
1252
1297
  # DASHBOARD SEARCH
1253
1298
  # Fetches all storage-mode dashboards and the default dashboard,
@@ -1340,6 +1385,14 @@ class SmartSearchTools:
1340
1385
  logger.error(f"Dashboard search error: {e}")
1341
1386
  raise
1342
1387
 
1388
+ phase_done += 1
1389
+ if ctx is not None:
1390
+ await ctx.report_progress(
1391
+ progress=phase_done,
1392
+ total=total_phases,
1393
+ message=f"dashboards searched ({len(results['dashboards'])} matches)",
1394
+ )
1395
+
1343
1396
  # Merge all results with their category, sort by score, and paginate
1344
1397
  tagged_results: list[tuple[str, dict[str, Any]]] = []
1345
1398
  for category, items in results.items():
@@ -8,6 +8,7 @@ to discover custom integrations, Lovelace cards, themes, and more.
8
8
  import logging
9
9
  from typing import Annotated, Any, Literal
10
10
 
11
+ from fastmcp import Context
11
12
  from fastmcp.exceptions import ToolError
12
13
  from fastmcp.tools import tool
13
14
  from pydantic import Field
@@ -137,6 +138,7 @@ class HacsTools:
137
138
  description="Number of results to skip for pagination (default: 0)",
138
139
  ),
139
140
  ] = 0,
141
+ ctx: Context | None = None,
140
142
  ) -> dict[str, Any]:
141
143
  """Search HACS store for repositories, or list installed repositories.
142
144
 
@@ -181,6 +183,17 @@ class HacsTools:
181
183
  min_value=0,
182
184
  )
183
185
 
186
+ if ctx is not None:
187
+ await ctx.info(
188
+ f"ha_hacs_search starting: query={query!r} "
189
+ f"category={category} installed_only={installed_only_bool}"
190
+ )
191
+ await ctx.report_progress(
192
+ progress=0,
193
+ total=3,
194
+ message="checking HACS availability",
195
+ )
196
+
184
197
  # Check if HACS is available
185
198
  await _assert_hacs_available()
186
199
 
@@ -195,6 +208,13 @@ class HacsTools:
195
208
  hacs_category = CATEGORY_MAP.get(category, category)
196
209
  kwargs_cmd["categories"] = [hacs_category]
197
210
 
211
+ if ctx is not None:
212
+ await ctx.report_progress(
213
+ progress=1,
214
+ total=3,
215
+ message="fetching HACS repository list",
216
+ )
217
+
198
218
  response = await ws_client.send_command(
199
219
  "hacs/repositories/list", **kwargs_cmd
200
220
  )
@@ -211,9 +231,21 @@ class HacsTools:
211
231
  )
212
232
 
213
233
  all_repositories = response.get("result", [])
234
+ if ctx is not None:
235
+ await ctx.report_progress(
236
+ progress=2,
237
+ total=3,
238
+ message=f"filtering {len(all_repositories)} repositories",
239
+ )
214
240
  matches = _filter_and_score_repos(
215
241
  all_repositories, query, installed_only_bool
216
242
  )
243
+ if ctx is not None:
244
+ await ctx.report_progress(
245
+ progress=3,
246
+ total=3,
247
+ message=f"matched {len(matches)} repositories",
248
+ )
217
249
 
218
250
  limited_matches = matches[offset_int : offset_int + max_results_int]
219
251
  has_more = (offset_int + len(limited_matches)) < len(matches)
@@ -14,6 +14,7 @@ import re
14
14
  from datetime import UTC, datetime, timedelta
15
15
  from typing import Annotated, Any, Literal
16
16
 
17
+ from fastmcp import Context
17
18
  from fastmcp.exceptions import ToolError
18
19
  from fastmcp.tools import tool
19
20
  from pydantic import Field
@@ -205,6 +206,7 @@ class HistoryTools:
205
206
  default=None,
206
207
  ),
207
208
  ] = None,
209
+ ctx: Context | None = None,
208
210
  ) -> dict[str, Any]:
209
211
  """
210
212
  Retrieve historical data from Home Assistant's recorder.
@@ -288,6 +290,18 @@ class HistoryTools:
288
290
  # Parse time parameters
289
291
  start_dt, end_dt = _parse_time_range(start_time, end_time, default_hours)
290
292
 
293
+ if ctx is not None:
294
+ await ctx.info(
295
+ f"ha_get_history starting: source={source} "
296
+ f"entities={len(entity_id_list)} "
297
+ f"window={start_dt.isoformat()}..{end_dt.isoformat()}"
298
+ )
299
+ await ctx.report_progress(
300
+ progress=0,
301
+ total=3,
302
+ message="connecting to Home Assistant WebSocket",
303
+ )
304
+
291
305
  # Connect to WebSocket (shared by both sources)
292
306
  ws_client, error = await get_connected_ws_client(
293
307
  self._client.base_url,
@@ -300,20 +314,34 @@ class HistoryTools:
300
314
  "Failed to connect to Home Assistant WebSocket",
301
315
  ))
302
316
 
317
+ if ctx is not None:
318
+ await ctx.report_progress(
319
+ progress=1,
320
+ total=3,
321
+ message=f"querying recorder ({source})",
322
+ )
323
+
303
324
  try:
304
325
  if source == "statistics":
305
- return await _fetch_statistics(
326
+ result = await _fetch_statistics(
306
327
  ws_client, self._client, entity_id_list,
307
328
  start_dt, end_dt, period, statistic_types,
308
329
  limit, offset,
309
330
  )
310
331
  else:
311
- return await _fetch_history(
332
+ result = await _fetch_history(
312
333
  ws_client, self._client, entity_id_list,
313
334
  start_dt, end_dt, minimal_response,
314
335
  significant_changes_only, limit, offset,
315
336
  _DEFAULT_HISTORY_LIMIT, _MAX_HISTORY_LIMIT,
316
337
  )
338
+ if ctx is not None:
339
+ await ctx.report_progress(
340
+ progress=3,
341
+ total=3,
342
+ message="recorder query complete",
343
+ )
344
+ return result
317
345
  finally:
318
346
  if ws_client:
319
347
  await ws_client.disconnect()
@@ -8,6 +8,7 @@ import asyncio
8
8
  import logging
9
9
  from typing import Annotated, Any, Literal, cast
10
10
 
11
+ from fastmcp import Context
11
12
  from fastmcp.exceptions import ToolError
12
13
  from pydantic import Field
13
14
 
@@ -823,6 +824,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
823
824
  ),
824
825
  ),
825
826
  ] = True,
827
+ ctx: Context | None = None,
826
828
  ) -> dict[str, Any]:
827
829
  """Search inside automation, script, helper, and dashboard *configurations* — not for finding entity IDs.
828
830
 
@@ -865,6 +867,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
865
867
  offset,
866
868
  include_config_bool,
867
869
  exact_match=exact_match_bool,
870
+ ctx=ctx,
868
871
  )
869
872
  return cast(dict[str, Any], result)
870
873
  except ToolError:
@@ -8,6 +8,7 @@ import logging
8
8
  from typing import Annotated, Any, cast
9
9
 
10
10
  import httpx
11
+ from fastmcp import Context
11
12
  from fastmcp.exceptions import ToolError
12
13
  from fastmcp.tools import tool
13
14
  from pydantic import Field
@@ -393,7 +394,9 @@ class ServiceTools:
393
394
  @log_tool_usage
394
395
  async def ha_bulk_control(
395
396
  self,
396
- operations: str | list[dict[str, Any]], parallel: bool | str = True
397
+ operations: str | list[dict[str, Any]],
398
+ parallel: bool | str = True,
399
+ ctx: Context | None = None,
397
400
  ) -> dict[str, Any]:
398
401
  """Control multiple devices with bulk operation support and WebSocket tracking."""
399
402
  # Coerce boolean parameter that may come as string from XML-style calls
@@ -424,7 +427,7 @@ class ServiceTools:
424
427
 
425
428
  operations_list = cast(list[dict[str, Any]], parsed_operations)
426
429
  result = await self._device_tools.bulk_device_control(
427
- operations=operations_list, parallel=parallel_bool
430
+ operations=operations_list, parallel=parallel_bool, ctx=ctx
428
431
  )
429
432
  return cast(dict[str, Any], result)
430
433
 
@@ -9,6 +9,7 @@ import json
9
9
  import logging
10
10
  from typing import Annotated, Any
11
11
 
12
+ from fastmcp import Context
12
13
  from fastmcp.exceptions import ToolError
13
14
  from fastmcp.tools import tool
14
15
  from pydantic import Field
@@ -91,6 +92,7 @@ class TraceTools:
91
92
  default=None,
92
93
  ),
93
94
  ] = None,
95
+ ctx: Context | None = None,
94
96
  ) -> dict[str, Any]:
95
97
  """
96
98
  Retrieve execution traces for automations and scripts to debug issues.
@@ -165,6 +167,17 @@ class TraceTools:
165
167
  # Extract the object_id (part after the domain) as fallback
166
168
  object_id = automation_id.split(".", 1)[1]
167
169
 
170
+ if ctx is not None:
171
+ await ctx.info(
172
+ f"ha_get_automation_traces starting: id={automation_id} "
173
+ f"run_id={run_id or '<list>'}"
174
+ )
175
+ await ctx.report_progress(
176
+ progress=0,
177
+ total=3,
178
+ message="connecting to Home Assistant WebSocket",
179
+ )
180
+
168
181
  # Connect to WebSocket
169
182
  ws_client, error = await get_connected_ws_client(
170
183
  self._client.base_url,
@@ -185,6 +198,13 @@ class TraceTools:
185
198
  ws_client, automation_id, object_id
186
199
  )
187
200
 
201
+ if ctx is not None:
202
+ await ctx.report_progress(
203
+ progress=1,
204
+ total=3,
205
+ message=f"fetching trace {'detail' if run_id else 'list'}",
206
+ )
207
+
188
208
  if run_id:
189
209
  # Get specific trace details
190
210
  result = await ws_client.send_command(
@@ -195,16 +215,20 @@ class TraceTools:
195
215
  )
196
216
 
197
217
  if not result.get("success"):
198
- ctx = {"automation_id": automation_id}
218
+ err_ctx: dict[str, str] = {"automation_id": automation_id}
199
219
  if run_id:
200
- ctx["run_id"] = run_id
220
+ err_ctx["run_id"] = run_id
201
221
  raise_tool_error(create_error_response(
202
222
  ErrorCode.SERVICE_CALL_FAILED,
203
223
  result.get("error", "Failed to retrieve trace"),
204
- context=ctx,
224
+ context=err_ctx,
205
225
  ))
206
226
 
207
227
  trace_data = result.get("result", {})
228
+ if ctx is not None:
229
+ await ctx.report_progress(
230
+ progress=3, total=3, message="formatting trace"
231
+ )
208
232
  return _format_detailed_trace(
209
233
  automation_id, run_id, trace_data,
210
234
  deduplicate=deduplicate, detailed=detailed,
@@ -229,13 +253,29 @@ class TraceTools:
229
253
 
230
254
  # If traces are empty, gather diagnostic information
231
255
  if not traces_data:
256
+ if ctx is not None:
257
+ await ctx.report_progress(
258
+ progress=2,
259
+ total=3,
260
+ message="no traces; gathering diagnostics",
261
+ )
232
262
  diagnostics = await _gather_diagnostics(
233
263
  ws_client, self._client, automation_id, domain
234
264
  )
265
+ if ctx is not None:
266
+ await ctx.report_progress(
267
+ progress=3, total=3, message="diagnostics complete"
268
+ )
235
269
  return _format_trace_list(
236
270
  automation_id, traces_data, limit, diagnostics
237
271
  )
238
272
 
273
+ if ctx is not None:
274
+ await ctx.report_progress(
275
+ progress=3,
276
+ total=3,
277
+ message=f"listed {len(traces_data)} traces",
278
+ )
239
279
  return _format_trace_list(automation_id, traces_data, limit)
240
280
 
241
281
  finally:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.4.1.dev451
3
+ Version: 7.4.1.dev453
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