ha-mcp-dev 7.1.0.dev291__tar.gz → 7.1.0.dev293__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 (97) hide show
  1. {ha_mcp_dev-7.1.0.dev291/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.1.0.dev293}/PKG-INFO +1 -2
  2. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/pyproject.toml +1 -2
  3. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/config.py +1 -1
  4. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/smart_search.py +20 -6
  5. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_config_dashboards.py +19 -237
  6. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -2
  7. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp_dev.egg-info/requires.txt +0 -3
  8. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/LICENSE +0 -0
  9. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/MANIFEST.in +0 -0
  10. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/README.md +0 -0
  11. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/setup.cfg +0 -0
  12. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/__init__.py +0 -0
  13. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/__main__.py +0 -0
  14. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/_pypi_marker +0 -0
  15. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/auth/__init__.py +0 -0
  16. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/auth/consent_form.py +0 -0
  17. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/auth/provider.py +0 -0
  18. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/client/__init__.py +0 -0
  19. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/client/rest_client.py +0 -0
  20. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/client/websocket_client.py +0 -0
  21. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/client/websocket_listener.py +0 -0
  22. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/errors.py +0 -0
  23. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/py.typed +0 -0
  24. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/card_types.json +0 -0
  25. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/dashboard_guide.md +0 -0
  26. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  27. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  28. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  29. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  30. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  31. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  32. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  33. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  34. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  35. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  36. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  37. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  38. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  39. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  40. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  41. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  42. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/server.py +0 -0
  43. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/smoke_test.py +0 -0
  44. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/__init__.py +0 -0
  45. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/backup.py +0 -0
  46. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  47. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/device_control.py +0 -0
  48. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/enhanced.py +0 -0
  49. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/helpers.py +0 -0
  50. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/registry.py +0 -0
  51. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_addons.py +0 -0
  52. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_areas.py +0 -0
  53. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  54. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  55. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_calendar.py +0 -0
  56. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_camera.py +0 -0
  57. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  58. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  59. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  60. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_config_info.py +0 -0
  61. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  62. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_entities.py +0 -0
  63. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  64. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_groups.py +0 -0
  65. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_hacs.py +0 -0
  66. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_history.py +0 -0
  67. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_integrations.py +0 -0
  68. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_labels.py +0 -0
  69. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  70. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_registry.py +0 -0
  71. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_resources.py +0 -0
  72. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_search.py +0 -0
  73. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_service.py +0 -0
  74. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_services.py +0 -0
  75. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_system.py +0 -0
  76. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_todo.py +0 -0
  77. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_traces.py +0 -0
  78. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_updates.py +0 -0
  79. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_utility.py +0 -0
  80. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  81. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/tools_zones.py +0 -0
  82. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/tools/util_helpers.py +0 -0
  83. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/transforms/__init__.py +0 -0
  84. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/transforms/categorized_search.py +0 -0
  85. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/utils/__init__.py +0 -0
  86. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/utils/domain_handlers.py +0 -0
  87. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  88. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/utils/operation_manager.py +0 -0
  89. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/utils/python_sandbox.py +0 -0
  90. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp/utils/usage_logger.py +0 -0
  91. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  92. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  93. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  94. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  95. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/tests/__init__.py +0 -0
  96. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/tests/test_constants.py +0 -0
  97. {ha_mcp_dev-7.1.0.dev291 → ha_mcp_dev-7.1.0.dev293}/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.1.0.dev291
3
+ Version: 7.1.0.dev293
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
@@ -20,7 +20,6 @@ Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: fastmcp==3.1.1
22
22
  Requires-Dist: httpx[socks]==0.28.1
23
- Requires-Dist: jq==1.11.0; sys_platform != "win32"
24
23
  Requires-Dist: pydantic==2.12.5
25
24
  Requires-Dist: python-dotenv==1.2.2
26
25
  Requires-Dist: truststore==0.10.4
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.1.0.dev291"
7
+ version = "7.1.0.dev293"
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"
@@ -26,7 +26,6 @@ classifiers = [
26
26
  dependencies = [
27
27
  "fastmcp==3.1.1",
28
28
  "httpx[socks]==0.28.1",
29
- 'jq==1.11.0; sys_platform != "win32"',
30
29
  "pydantic==2.12.5",
31
30
  "python-dotenv==1.2.2",
32
31
  "truststore==0.10.4",
@@ -80,7 +80,7 @@ class Settings(BaseSettings):
80
80
  # Examples: "tools_config_automations,tools_config_scripts,tools_traces"
81
81
  enabled_tool_modules: str = Field("all", alias="ENABLED_TOOL_MODULES")
82
82
 
83
- # Dashboard partial update tools (jq_transform, find_card)
83
+ # Dashboard partial update tools (python_transform, find_card)
84
84
  # These are token-efficient alternatives to full config replacement.
85
85
  # Disable when using clients with programmatic tool use (future).
86
86
  enable_dashboard_partial_tools: bool = Field(True, alias="ENABLE_DASHBOARD_PARTIAL_TOOLS")
@@ -4,6 +4,7 @@ Smart search tools for Home Assistant MCP server.
4
4
 
5
5
  import asyncio
6
6
  import logging
7
+ import random
7
8
  import time
8
9
  from typing import Any
9
10
 
@@ -387,9 +388,20 @@ class SmartSearchTools:
387
388
  return_exceptions=True,
388
389
  )
389
390
 
390
- # Process results, handling any exceptions gracefully
391
- entities = results[0] if not isinstance(results[0], Exception) else []
392
- services = results[1] if not isinstance(results[1], Exception) else []
391
+ # Entities are mandatory surface connection/auth errors immediately.
392
+ # Services failure is logged at warning (affects total count and service catalog).
393
+ # Registry failures are logged at debug (area enrichment only).
394
+ if isinstance(results[0], Exception):
395
+ raise results[0]
396
+
397
+ entities = results[0]
398
+ partial_warnings: list[str] = []
399
+ if isinstance(results[1], Exception):
400
+ logger.warning(f"Could not fetch services: {results[1]}")
401
+ partial_warnings.append(f"Services unavailable: {results[1]}")
402
+ services = []
403
+ else:
404
+ services = results[1]
393
405
 
394
406
  # Handle area registry result
395
407
  area_registry: list[dict[str, Any]] = []
@@ -537,8 +549,6 @@ class SmartSearchTools:
537
549
  }
538
550
 
539
551
  # Prepare domain stats with entity filtering and truncation info
540
- import random
541
-
542
552
  formatted_domain_stats = {}
543
553
  for domain, stats in sorted_domains:
544
554
  all_entities = stats["all_entities"]
@@ -569,7 +579,7 @@ class SmartSearchTools:
569
579
  }
570
580
 
571
581
  # Build base response
572
- base_response = {
582
+ base_response: dict[str, Any] = {
573
583
  "success": True,
574
584
  "system_summary": {
575
585
  "total_entities": len(entities),
@@ -582,6 +592,10 @@ class SmartSearchTools:
582
592
  "ai_insights": ai_insights,
583
593
  }
584
594
 
595
+ if partial_warnings:
596
+ base_response["partial"] = True
597
+ base_response["warnings"] = partial_warnings
598
+
585
599
  # Add level-specific fields
586
600
  if detail_level == "full":
587
601
  # Full: Add device types and service catalog
@@ -28,26 +28,6 @@ from .util_helpers import parse_json_param
28
28
 
29
29
  logger = logging.getLogger(__name__)
30
30
 
31
- # Try to import jq - it's not available on Windows ARM64
32
- try:
33
- import jq # noqa: F401
34
-
35
- JQ_AVAILABLE = True
36
- except ImportError:
37
- JQ_AVAILABLE = False
38
- logger.warning(
39
- "jq library not available - jq_transform features will be disabled. "
40
- "This is expected on Windows ARM64 where jq cannot be compiled."
41
- )
42
-
43
- # Error message when jq_transform is used without jq available
44
- _JQ_UNAVAILABLE_ERROR = (
45
- "jq_transform is not available - jq library could not be imported. "
46
- "This is a known limitation on Windows ARM64 where jq cannot be compiled. "
47
- "Please use the 'config' parameter for full config replacement instead, "
48
- "or use ha-mcp on Windows x64, Linux, or macOS where jq is supported."
49
- )
50
-
51
31
  # Card documentation base URL
52
32
  CARD_DOCS_BASE_URL = (
53
33
  "https://raw.githubusercontent.com/home-assistant/home-assistant.io/"
@@ -127,47 +107,6 @@ async def _verify_config_unchanged(
127
107
  return {"success": True}
128
108
 
129
109
 
130
- def _apply_jq_transform(
131
- config: dict[str, Any], expression: str
132
- ) -> tuple[dict[str, Any] | None, str | None]:
133
- """
134
- Apply a jq transformation to dashboard config.
135
-
136
- Returns:
137
- tuple: (transformed_config, error_message)
138
- - On success: (dict, None)
139
- - On failure: (None, error_string)
140
- """
141
- # Check if jq is available
142
- if not JQ_AVAILABLE:
143
- return None, _JQ_UNAVAILABLE_ERROR
144
-
145
- import jq
146
-
147
- try:
148
- # Compile and validate the jq expression
149
- program = jq.compile(expression)
150
- except ValueError as e:
151
- return None, f"Invalid jq expression: {e}"
152
-
153
- try:
154
- # Execute the transformation
155
- result = program.input_value(config).first()
156
- except StopIteration:
157
- return None, "jq expression produced no output"
158
- except Exception as e:
159
- return None, f"jq transformation error: {e}"
160
-
161
- # Validate result is still a valid dashboard structure
162
- if not isinstance(result, dict):
163
- return None, f"jq result must be a dict, got {type(result).__name__}"
164
-
165
- if "views" not in result and "strategy" not in result:
166
- return None, "jq result missing required 'views' or 'strategy' key"
167
-
168
- return result, None
169
-
170
-
171
110
  def _find_cards_in_config(
172
111
  config: dict[str, Any],
173
112
  entity_id: str | None = None,
@@ -393,11 +332,11 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
393
332
  if config_size >= 10000:
394
333
  result["hint"] = (
395
334
  f"Large config ({config_size:,} bytes). For edits, use "
396
- "ha_dashboard_find_card() + ha_config_set_dashboard(jq_transform=...) "
335
+ "ha_dashboard_find_card() + ha_config_set_dashboard(python_transform=...) "
397
336
  "instead of full config replacement."
398
337
  )
399
338
 
400
- return cast(dict[str, Any], result)
339
+ return result
401
340
  except ToolError:
402
341
  raise
403
342
  except Exception as e:
@@ -438,26 +377,14 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
438
377
  description="Dashboard configuration with views and cards. "
439
378
  "Can be dict or JSON string. "
440
379
  "Omit or set to None to create dashboard without initial config. "
441
- "Mutually exclusive with jq_transform."
442
- ),
443
- ] = None,
444
- jq_transform: Annotated[
445
- str | None,
446
- Field(
447
- description="jq expression to transform existing dashboard config. "
448
- "Mutually exclusive with config and python_transform. Requires config_hash for validation. "
449
- "Examples: '.views[0].sections[1].cards[0].icon = \"mdi:thermometer\"', "
450
- '\'.views[0].cards += [{"type": "button", "entity": "light.bedroom"}]\', '
451
- "'del(.views[0].sections[0].cards[2])'. "
452
- "MULTI-OP: Chain with '|': 'del(.views[0].cards[2]) | .views[0].cards[0].icon = \"mdi:new\"'. "
453
- "Use ha_dashboard_find_card() to get jq_path for targeted edits."
380
+ "Mutually exclusive with python_transform."
454
381
  ),
455
382
  ] = None,
456
383
  python_transform: Annotated[
457
384
  str | None,
458
385
  Field(
459
386
  description="Python expression to transform existing dashboard config. "
460
- "Mutually exclusive with config and jq_transform. "
387
+ "Mutually exclusive with config. "
461
388
  "Requires config_hash for validation. "
462
389
  "See PYTHON TRANSFORM SECURITY below for allowed operations. "
463
390
  "Examples: "
@@ -471,7 +398,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
471
398
  str | None,
472
399
  Field(
473
400
  description="Config hash from ha_config_get_dashboard for optimistic locking. "
474
- "REQUIRED for jq_transform (validates dashboard unchanged). "
401
+ "REQUIRED for python_transform (validates dashboard unchanged). "
475
402
  "Optional for config (validates before full replacement if provided)."
476
403
  ),
477
404
  ] = None,
@@ -505,31 +432,20 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
505
432
  Create or update a Home Assistant dashboard.
506
433
 
507
434
  Creates a new dashboard or updates an existing one with the provided configuration.
508
- Supports three modes: full config replacement, Python transformation, OR jq-based transformation.
435
+ Supports two modes: full config replacement OR Python transformation.
509
436
 
510
437
  Use 'default' or 'lovelace' to target the built-in default dashboard.
511
438
  New dashboards require a hyphenated url_path (e.g., 'my-dashboard').
512
439
 
513
440
  WHEN TO USE WHICH MODE:
514
441
  - python_transform: RECOMMENDED for edits. Surgical/pattern-based updates, works on all platforms.
515
- - jq_transform: Legacy mode. Requires jq binary (not available on Windows ARM64).
516
442
  - config: New dashboards only, or full restructure. Replaces everything.
517
443
 
518
- JQ TRANSFORM EXAMPLES:
519
- - Update card icon: '.views[0].sections[1].cards[0].icon = "mdi:thermometer"'
520
- - Add card: '.views[0].cards += [{"type": "button", "entity": "light.bedroom"}]'
521
- - Delete card: 'del(.views[0].sections[0].cards[2])'
522
- - Update by selection: '(.views[0].cards[] | select(.entity == "light.living_room")).icon = "mdi:lamp"'
523
-
524
- MULTI-OPERATION (chain with |):
525
- - Delete then update: 'del(.views[0].cards[2]) | .views[0].cards[0].icon = "mdi:new"'
526
- - Multiple updates: '.views[0].cards[0].icon = "mdi:a" | .views[0].cards[1].icon = "mdi:b"'
527
-
528
- IMPORTANT: After delete/add operations, indices shift! Subsequent jq_transform calls
444
+ IMPORTANT: After delete/add operations, indices shift! Subsequent python_transform calls
529
445
  must use fresh config_hash from ha_dashboard_find_card() or ha_config_get_dashboard()
530
446
  to get updated structure. Chain multiple ops in ONE expression when possible.
531
447
 
532
- TIP: Use ha_dashboard_find_card() to get the jq_path for any card.
448
+ TIP: Use ha_dashboard_find_card() to get the path for any card.
533
449
 
534
450
  PYTHON TRANSFORM EXAMPLES (RECOMMENDED):
535
451
  - Update card icon: 'config["views"][0]["cards"][0]["icon"] = "mdi:thermometer"'
@@ -587,12 +503,6 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
587
503
  }
588
504
  )
589
505
 
590
- Update card using jq_transform (efficient for small changes):
591
- ha_config_set_dashboard(
592
- url_path="home-dashboard",
593
- jq_transform='.views[0].sections[0].cards[0].features += [{"type": "climate-hvac-modes"}]'
594
- )
595
-
596
506
  Create strategy-based dashboard (auto-generated):
597
507
  ha_config_set_dashboard(
598
508
  url_path="my-home",
@@ -645,24 +555,15 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
645
555
  context={"action": "set", "url_path": url_path},
646
556
  ))
647
557
 
648
- # Validate mutual exclusivity of config, jq_transform, and python_transform
649
- transforms_provided = sum(
650
- [
651
- config is not None,
652
- jq_transform is not None,
653
- python_transform is not None,
654
- ]
655
- )
656
-
657
- if transforms_provided > 1:
558
+ # Validate mutual exclusivity of config and python_transform
559
+ if config is not None and python_transform is not None:
658
560
  raise_tool_error(create_error_response(
659
561
  ErrorCode.VALIDATION_INVALID_PARAMETER,
660
- "Cannot use multiple transform methods simultaneously",
562
+ "Cannot use both config and python_transform simultaneously",
661
563
  suggestions=[
662
- "Use only ONE of: config, jq_transform, or python_transform",
564
+ "Use only ONE of: config or python_transform",
663
565
  "config: Full replacement",
664
- "jq_transform: jq-based edits (requires jq installation)",
665
- "python_transform: Python-based edits (recommended, works everywhere)",
566
+ "python_transform: Python-based edits (recommended)",
666
567
  ],
667
568
  context={"action": "set", "url_path": url_path},
668
569
  ))
@@ -783,125 +684,6 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
783
684
  "message": f"Dashboard {url_path} updated via Python transform",
784
685
  }
785
686
 
786
- # Handle jq_transform mode
787
- if jq_transform is not None:
788
- # config_hash is REQUIRED for jq_transform
789
- if config_hash is None:
790
- raise_tool_error(create_error_response(
791
- ErrorCode.VALIDATION_INVALID_PARAMETER,
792
- "config_hash is required for jq_transform",
793
- suggestions=[
794
- "Call ha_config_get_dashboard() or ha_dashboard_find_card() first",
795
- "Use the config_hash from that response",
796
- ],
797
- context={"action": "jq_transform", "url_path": url_path},
798
- ))
799
-
800
- # Fetch current dashboard config
801
- get_data = {"type": "lovelace/config", "force": True}
802
- if url_path:
803
- get_data["url_path"] = url_path
804
-
805
- response = await client.send_websocket_message(get_data)
806
-
807
- if isinstance(response, dict) and not response.get("success", True):
808
- error_msg = response.get("error", {})
809
- if isinstance(error_msg, dict):
810
- error_msg = error_msg.get("message", str(error_msg))
811
- raise_tool_error(create_error_response(
812
- ErrorCode.SERVICE_CALL_FAILED,
813
- f"Dashboard not found or inaccessible: {error_msg}",
814
- suggestions=[
815
- "jq_transform requires an existing dashboard",
816
- "Use 'config' parameter to create a new dashboard",
817
- "Verify dashboard exists with ha_config_get_dashboard(list_only=True)",
818
- ],
819
- context={"action": "jq_transform", "url_path": url_path},
820
- ))
821
-
822
- current_config = (
823
- response.get("result") if isinstance(response, dict) else response
824
- )
825
- if not isinstance(current_config, dict):
826
- raise_tool_error(create_error_response(
827
- ErrorCode.SERVICE_CALL_FAILED,
828
- "Current dashboard config is invalid",
829
- suggestions=[
830
- "Initialize dashboard with 'config' parameter first"
831
- ],
832
- context={"action": "jq_transform", "url_path": url_path},
833
- ))
834
-
835
- # Validate config_hash for optimistic locking
836
- current_hash = _compute_config_hash(current_config)
837
- if current_hash != config_hash:
838
- raise_tool_error(create_error_response(
839
- ErrorCode.SERVICE_CALL_FAILED,
840
- "Dashboard modified since last read (conflict)",
841
- suggestions=[
842
- "Call ha_config_get_dashboard() or ha_dashboard_find_card() again",
843
- "Use the fresh config_hash from that response",
844
- "Indices may have changed - re-locate cards with ha_dashboard_find_card()",
845
- ],
846
- context={"action": "jq_transform", "url_path": url_path},
847
- ))
848
-
849
- # Apply jq transformation
850
- jq_result, error = _apply_jq_transform(
851
- current_config, jq_transform
852
- )
853
- if error:
854
- raise_tool_error(create_error_response(
855
- ErrorCode.VALIDATION_FAILED,
856
- error,
857
- suggestions=[
858
- "Verify jq syntax: https://jqlang.github.io/jq/manual/",
859
- "Use ha_dashboard_find_card() to get correct jq_path",
860
- "Test expression locally: echo '<config>' | jq '<expression>'",
861
- ],
862
- context={"action": "jq_transform", "url_path": url_path},
863
- ))
864
- transformed_config = cast(dict[str, Any], jq_result)
865
-
866
- # Save transformed config
867
- save_data = {
868
- "type": "lovelace/config/save",
869
- "config": transformed_config,
870
- }
871
- if url_path:
872
- save_data["url_path"] = url_path
873
-
874
- save_result = await client.send_websocket_message(save_data)
875
-
876
- if isinstance(save_result, dict) and not save_result.get(
877
- "success", True
878
- ):
879
- error_msg = save_result.get("error", {})
880
- if isinstance(error_msg, dict):
881
- error_msg = error_msg.get("message", str(error_msg))
882
- raise_tool_error(create_error_response(
883
- ErrorCode.SERVICE_CALL_FAILED,
884
- f"Failed to save transformed config: {error_msg}",
885
- suggestions=[
886
- "jq expression may have produced invalid dashboard structure",
887
- "Verify config format is valid Lovelace JSON",
888
- ],
889
- context={"action": "jq_transform", "url_path": url_path},
890
- ))
891
-
892
- # Compute new hash for potential chaining
893
- # transformed_config is guaranteed to be a dict here (validated above)
894
- new_config_hash = _compute_config_hash(transformed_config)
895
-
896
- return {
897
- "success": True,
898
- "action": "jq_transform",
899
- "url_path": url_path,
900
- "config_hash": new_config_hash,
901
- "jq_expression": jq_transform,
902
- "message": f"Dashboard {url_path} updated via jq transform",
903
- }
904
-
905
687
  # Check if dashboard exists
906
688
  result = await client.send_websocket_message(
907
689
  {"type": "lovelace/dashboards/list"}
@@ -1067,7 +849,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1067
849
  if existing_config_size >= 10000:
1068
850
  hint = (
1069
851
  f"Replaced large config ({existing_config_size:,} bytes). "
1070
- "Consider jq_transform for targeted edits."
852
+ "Consider python_transform for targeted edits."
1071
853
  )
1072
854
 
1073
855
  # Build save config message
@@ -1421,7 +1203,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1421
1203
 
1422
1204
  # =========================================================================
1423
1205
  # Card Search Tool (partial update tools - controlled by feature flag)
1424
- # Card add/update/remove replaced by jq_transform in ha_config_set_dashboard
1206
+ # Card add/update/remove replaced by python_transform in ha_config_set_dashboard
1425
1207
  # =========================================================================
1426
1208
 
1427
1209
  # Check feature flag for partial update tools (lazy check, default enabled)
@@ -1476,8 +1258,8 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1476
1258
  """
1477
1259
  Find cards in a dashboard by entity_id, type, or heading text.
1478
1260
 
1479
- Returns card locations (view_index, section_index, card_index) and jq_path
1480
- for use with ha_config_set_dashboard(jq_transform=...).
1261
+ Returns card locations (view_index, section_index, card_index) and path
1262
+ for use with ha_config_set_dashboard(python_transform=...).
1481
1263
 
1482
1264
  Use this tool BEFORE targeted updates to find exact card positions without
1483
1265
  manually parsing the full dashboard config.
@@ -1509,7 +1291,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1509
1291
  3. ha_config_set_dashboard(
1510
1292
  url_path="my-dash",
1511
1293
  config_hash=find["config_hash"],
1512
- jq_transform=f'{find["matches"][0]["jq_path"]}.icon = "mdi:lamp"'
1294
+ python_transform=f'config{find["matches"][0]["jq_path"]}["icon"] = "mdi:lamp"'
1513
1295
  )
1514
1296
  """
1515
1297
  try:
@@ -1593,7 +1375,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1593
1375
  },
1594
1376
  "matches": matches,
1595
1377
  "match_count": len(matches),
1596
- "hint": "Use jq_path with ha_config_set_dashboard(jq_transform=...) for targeted updates"
1378
+ "hint": "Use jq_path with ha_config_set_dashboard(python_transform=...) for targeted updates"
1597
1379
  if matches
1598
1380
  else "No matches found. Try broader search criteria.",
1599
1381
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.1.0.dev291
3
+ Version: 7.1.0.dev293
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
@@ -20,7 +20,6 @@ Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: fastmcp==3.1.1
22
22
  Requires-Dist: httpx[socks]==0.28.1
23
- Requires-Dist: jq==1.11.0; sys_platform != "win32"
24
23
  Requires-Dist: pydantic==2.12.5
25
24
  Requires-Dist: python-dotenv==1.2.2
26
25
  Requires-Dist: truststore==0.10.4
@@ -5,6 +5,3 @@ python-dotenv==1.2.2
5
5
  truststore==0.10.4
6
6
  websockets==16.0
7
7
  cryptography==46.0.5
8
-
9
- [:sys_platform != "win32"]
10
- jq==1.11.0