ha-mcp-dev 7.2.0.dev335__tar.gz → 7.2.0.dev336__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 (101) hide show
  1. {ha_mcp_dev-7.2.0.dev335/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.2.0.dev336}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/server.py +36 -27
  4. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_entities.py +205 -40
  5. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_registry.py +6 -453
  6. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_search.py +51 -71
  7. ha_mcp_dev-7.2.0.dev336/src/ha_mcp/tools/tools_todo.py +512 -0
  8. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  9. ha_mcp_dev-7.2.0.dev335/src/ha_mcp/tools/tools_todo.py +0 -530
  10. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/LICENSE +0 -0
  11. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/MANIFEST.in +0 -0
  12. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/README.md +0 -0
  13. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/setup.cfg +0 -0
  14. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/__init__.py +0 -0
  15. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/__main__.py +0 -0
  16. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/_pypi_marker +0 -0
  17. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/auth/__init__.py +0 -0
  18. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/auth/consent_form.py +0 -0
  19. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/auth/provider.py +0 -0
  20. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/client/__init__.py +0 -0
  21. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/client/rest_client.py +0 -0
  22. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/client/websocket_client.py +0 -0
  23. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/client/websocket_listener.py +0 -0
  24. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/config.py +0 -0
  25. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/errors.py +0 -0
  26. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/py.typed +0 -0
  27. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  28. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  29. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  30. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  31. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  32. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  33. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  34. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  35. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  36. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  37. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  38. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  39. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  40. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  41. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  42. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  43. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  44. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  45. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  46. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  47. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/smoke_test.py +0 -0
  48. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/__init__.py +0 -0
  49. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/backup.py +0 -0
  50. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  51. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/device_control.py +0 -0
  52. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/enhanced.py +0 -0
  53. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/helpers.py +0 -0
  54. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/registry.py +0 -0
  55. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/smart_search.py +0 -0
  56. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_addons.py +0 -0
  57. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_areas.py +0 -0
  58. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  59. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  60. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_calendar.py +0 -0
  61. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_camera.py +0 -0
  62. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_categories.py +0 -0
  63. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  64. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  65. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  66. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  67. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  68. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  69. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_groups.py +0 -0
  70. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_hacs.py +0 -0
  71. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_history.py +0 -0
  72. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_integrations.py +0 -0
  73. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_labels.py +0 -0
  74. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  75. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_resources.py +0 -0
  76. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_service.py +0 -0
  77. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_services.py +0 -0
  78. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_system.py +0 -0
  79. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_traces.py +0 -0
  80. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_updates.py +0 -0
  81. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_utility.py +0 -0
  82. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  83. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  84. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_zones.py +0 -0
  85. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/util_helpers.py +0 -0
  86. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/transforms/__init__.py +0 -0
  87. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/transforms/categorized_search.py +0 -0
  88. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/utils/__init__.py +0 -0
  89. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/utils/domain_handlers.py +0 -0
  90. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  91. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/utils/operation_manager.py +0 -0
  92. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/utils/python_sandbox.py +0 -0
  93. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/utils/usage_logger.py +0 -0
  94. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  95. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  96. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  97. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  98. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  99. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/tests/__init__.py +0 -0
  100. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/tests/test_constants.py +0 -0
  101. {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/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.2.0.dev335
3
+ Version: 7.2.0.dev336
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.2.0.dev335"
7
+ version = "7.2.0.dev336"
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"
@@ -95,6 +95,7 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
95
95
  """Lazily create and return the Home Assistant client."""
96
96
  if self._client is None:
97
97
  from .client.rest_client import HomeAssistantClient
98
+
98
99
  self._client = HomeAssistantClient()
99
100
  logger.debug("Lazily created HomeAssistantClient")
100
101
  return self._client
@@ -104,6 +105,7 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
104
105
  """Lazily create and return the smart search tools."""
105
106
  if self._smart_tools is None:
106
107
  from .tools.smart_search import create_smart_search_tools
108
+
107
109
  self._smart_tools = create_smart_search_tools(self.client)
108
110
  logger.debug("Lazily created SmartSearchTools")
109
111
  return self._smart_tools
@@ -113,6 +115,7 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
113
115
  """Lazily create and return the device control tools."""
114
116
  if self._device_tools is None:
115
117
  from .tools.device_control import create_device_control_tools
118
+
116
119
  self._device_tools = create_device_control_tools(self.client)
117
120
  logger.debug("Lazily created DeviceControlTools")
118
121
  return self._device_tools
@@ -122,6 +125,7 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
122
125
  """Lazily create and return the tools registry."""
123
126
  if self._tools_registry is None:
124
127
  from .tools.registry import ToolsRegistry
128
+
125
129
  self._tools_registry = ToolsRegistry(
126
130
  self, enabled_modules=self.settings.enabled_tool_modules
127
131
  )
@@ -228,7 +232,7 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
228
232
  "This server uses search-based tool discovery. Most tools "
229
233
  "are NOT listed directly \u2014 use ha_search_tools to find them.\n\n"
230
234
  "WORKFLOW:\n"
231
- "1. Call ha_search_tools(query=\"...\") to find relevant tools\n"
235
+ '1. Call ha_search_tools(query="...") to find relevant tools\n'
232
236
  "2. Results include name, description, parameters, and "
233
237
  "annotations (readOnlyHint/destructiveHint)\n"
234
238
  "3. Execute the discovered tool \u2014 two options:\n"
@@ -286,9 +290,7 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
286
290
 
287
291
  return frontmatter
288
292
 
289
- def _build_skill_block(
290
- self, skill_name: str, main_file: Path
291
- ) -> str | None:
293
+ def _build_skill_block(self, skill_name: str, main_file: Path) -> str | None:
292
294
  """Build an instruction block for a single skill.
293
295
 
294
296
  Reads the description field from YAML frontmatter and includes it
@@ -367,18 +369,14 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
367
369
  "get entity state attributes details single specific entity_id"
368
370
  ),
369
371
  "ha_get_state": (
370
- "get current state value single entity check status"
371
- ),
372
- "ha_get_states": (
373
- "get all states entities bulk overview list"
372
+ "get current state value single entity check status bulk multiple states"
374
373
  ),
375
374
  "ha_config_set_automation": (
376
375
  "create update modify edit automation triggers conditions actions "
377
376
  "new automation write save"
378
377
  ),
379
378
  "ha_config_set_script": (
380
- "create update modify edit script sequence actions "
381
- "new script write save"
379
+ "create update modify edit script sequence actions new script write save"
382
380
  ),
383
381
  "ha_config_set_yaml": (
384
382
  "edit yaml configuration.yaml packages template sensor "
@@ -450,10 +448,12 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
450
448
  # Original tool docstrings are unchanged.
451
449
  from .transforms import SearchKeywordsTransform
452
450
 
453
- self.mcp.add_transform(SearchKeywordsTransform(
454
- keywords=self._SEARCH_KEYWORDS,
455
- overrides=self._SEARCH_DESCRIPTION_OVERRIDES,
456
- ))
451
+ self.mcp.add_transform(
452
+ SearchKeywordsTransform(
453
+ keywords=self._SEARCH_KEYWORDS,
454
+ overrides=self._SEARCH_DESCRIPTION_OVERRIDES,
455
+ )
456
+ )
457
457
 
458
458
  self.mcp.add_transform(
459
459
  CategorizedSearchTransform(
@@ -462,9 +462,7 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
462
462
  search_tool_description=description,
463
463
  )
464
464
  )
465
- logger.info(
466
- "Tool search transform applied (%d pinned tools)", len(pinned)
467
- )
465
+ logger.info("Tool search transform applied (%d pinned tools)", len(pinned))
468
466
  except Exception:
469
467
  logger.exception("Failed to apply tool search transform")
470
468
 
@@ -499,9 +497,11 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
499
497
  )
500
498
  return
501
499
 
502
- self.mcp.add_provider(SkillsDirectoryProvider(
503
- roots=[skills_dir], supporting_files="resources"
504
- ))
500
+ self.mcp.add_provider(
501
+ SkillsDirectoryProvider(
502
+ roots=[skills_dir], supporting_files="resources"
503
+ )
504
+ )
505
505
  logger.info("Registered bundled skills as MCP resources")
506
506
  except Exception:
507
507
  logger.exception("Failed to register skills as resources")
@@ -590,7 +590,9 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
590
590
 
591
591
  # Use factory to capture ref_files in closure
592
592
  def _make_skill_handler(
593
- s_name: str, s_uri: str, files: list[dict[str, str]],
593
+ s_name: str,
594
+ s_uri: str,
595
+ files: list[dict[str, str]],
594
596
  ) -> Callable[[], Coroutine[Any, Any, dict[str, Any]]]:
595
597
  async def handler() -> dict[str, Any]:
596
598
  return {
@@ -603,6 +605,7 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
603
605
  ),
604
606
  "available_files": files,
605
607
  }
608
+
606
609
  return handler
607
610
 
608
611
  self.mcp.tool(
@@ -624,9 +627,12 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
624
627
  self, query: str, domain_filter: str | None = None, limit: int = 10
625
628
  ) -> dict[str, Any]:
626
629
  """Bridge method to existing smart search implementation."""
627
- return cast(dict[str, Any], await self.smart_tools.smart_entity_search(
628
- query=query, limit=limit, include_attributes=False
629
- ))
630
+ return cast(
631
+ dict[str, Any],
632
+ await self.smart_tools.smart_entity_search(
633
+ query=query, limit=limit, include_attributes=False
634
+ ),
635
+ )
630
636
 
631
637
  async def get_entity_state(self, entity_id: str) -> dict[str, Any]:
632
638
  """Bridge method to existing entity state implementation."""
@@ -647,9 +653,12 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
647
653
 
648
654
  async def get_entities_by_area(self, area_name: str) -> dict[str, Any]:
649
655
  """Bridge method to existing area functionality."""
650
- return cast(dict[str, Any], await self.smart_tools.get_entities_by_area(
651
- area_query=area_name, group_by_domain=True
652
- ))
656
+ return cast(
657
+ dict[str, Any],
658
+ await self.smart_tools.get_entities_by_area(
659
+ area_query=area_name, group_by_domain=True
660
+ ),
661
+ )
653
662
 
654
663
  async def start(self) -> None:
655
664
  """Start the Smart MCP server with async compatibility."""
@@ -7,13 +7,18 @@ via the Home Assistant entity registry API.
7
7
 
8
8
  import asyncio
9
9
  import logging
10
+ import re
10
11
  from typing import Annotated, Any, Literal
11
12
 
12
13
  from fastmcp.exceptions import ToolError
13
14
  from pydantic import Field
14
15
 
15
16
  from ..errors import ErrorCode, create_error_response
16
- from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
17
+ from .helpers import (
18
+ exception_to_structured_error,
19
+ log_tool_usage,
20
+ raise_tool_error,
21
+ )
17
22
  from .tools_voice_assistant import KNOWN_ASSISTANTS
18
23
  from .util_helpers import coerce_bool_param, parse_json_param, parse_string_list_param
19
24
 
@@ -68,6 +73,8 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
68
73
  parsed_labels: list[str] | None,
69
74
  label_operation: str,
70
75
  parsed_expose_to: dict[str, bool] | None,
76
+ new_entity_id: str | None = None,
77
+ new_device_name: str | None = None,
71
78
  ) -> dict[str, Any]:
72
79
  """Update a single entity. Returns the response dict."""
73
80
  # For add/remove operations, we need to fetch current labels first
@@ -117,10 +124,12 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
117
124
  try:
118
125
  enabled_bool = coerce_bool_param(enabled, "enabled")
119
126
  except ValueError as e:
120
- raise_tool_error(create_error_response(
121
- ErrorCode.VALIDATION_INVALID_PARAMETER,
122
- str(e),
123
- ))
127
+ raise_tool_error(
128
+ create_error_response(
129
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
130
+ str(e),
131
+ )
132
+ )
124
133
  message["disabled_by"] = None if enabled_bool else "user"
125
134
  updates_made.append("enabled" if enabled_bool else "disabled")
126
135
 
@@ -128,10 +137,12 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
128
137
  try:
129
138
  hidden_bool = coerce_bool_param(hidden, "hidden")
130
139
  except ValueError as e:
131
- raise_tool_error(create_error_response(
132
- ErrorCode.VALIDATION_INVALID_PARAMETER,
133
- str(e),
134
- ))
140
+ raise_tool_error(
141
+ create_error_response(
142
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
143
+ str(e),
144
+ )
145
+ )
135
146
  message["hidden_by"] = "user" if hidden_bool else None
136
147
  updates_made.append("hidden" if hidden_bool else "visible")
137
148
 
@@ -154,20 +165,58 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
154
165
  f"labels removed: {parsed_labels} -> {final_labels}"
155
166
  )
156
167
 
168
+ if new_entity_id is not None:
169
+ entity_pattern = r"^[a-z_]+\.[a-z0-9_]+$"
170
+ if not re.match(entity_pattern, new_entity_id):
171
+ raise_tool_error(
172
+ create_error_response(
173
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
174
+ f"Invalid new_entity_id format: {new_entity_id}",
175
+ suggestions=[
176
+ "Use format: domain.object_id (lowercase letters, numbers, underscores only)"
177
+ ],
178
+ context={"new_entity_id": new_entity_id},
179
+ )
180
+ )
181
+ current_domain = entity_id.split(".")[0]
182
+ new_domain = new_entity_id.split(".")[0]
183
+ if current_domain != new_domain:
184
+ raise_tool_error(
185
+ create_error_response(
186
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
187
+ f"Domain mismatch: cannot change from '{current_domain}' to '{new_domain}'",
188
+ suggestions=[
189
+ f"New entity_id must start with '{current_domain}.'"
190
+ ],
191
+ context={
192
+ "entity_id": entity_id,
193
+ "new_entity_id": new_entity_id,
194
+ },
195
+ )
196
+ )
197
+ message["new_entity_id"] = new_entity_id
198
+ updates_made.append(f"entity_id -> {new_entity_id}")
199
+
157
200
  if parsed_expose_to is not None:
158
201
  updates_made.append(f"expose_to={parsed_expose_to}")
159
202
 
203
+ if new_device_name is not None:
204
+ updates_made.append(f"device_name -> {new_device_name}")
205
+
160
206
  if not updates_made:
161
207
  raise_tool_error(
162
208
  create_error_response(
163
209
  ErrorCode.VALIDATION_INVALID_PARAMETER,
164
210
  "No updates specified",
165
211
  suggestions=[
166
- "Provide at least one of: area_id, name, icon, enabled, hidden, aliases, categories, labels, or expose_to"
212
+ "Provide at least one of: area_id, name, icon, enabled, hidden, aliases, categories, labels, expose_to, new_entity_id, or new_device_name"
167
213
  ],
168
214
  )
169
215
  )
170
216
 
217
+ # Save original entity_id before potential rename
218
+ original_entity_id = entity_id
219
+
171
220
  # Send entity registry update (covers all fields except expose_to)
172
221
  has_registry_updates = len(message) > 2 # more than just type + entity_id
173
222
  entity_entry: dict[str, Any] = {}
@@ -188,21 +237,75 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
188
237
  if isinstance(error, dict)
189
238
  else str(error)
190
239
  )
240
+ suggestions = [
241
+ "Verify the entity_id exists using ha_search_entities()",
242
+ ]
243
+ if new_entity_id is not None:
244
+ suggestions.extend([
245
+ "Check that the new entity_id doesn't already exist",
246
+ "Ensure the entity has a unique_id (some legacy entities cannot be renamed)",
247
+ ])
248
+ else:
249
+ suggestions.extend([
250
+ "Check that area_id exists if specified",
251
+ "Some entities may not support all update options",
252
+ ])
191
253
  raise_tool_error(
192
254
  create_error_response(
193
255
  ErrorCode.SERVICE_CALL_FAILED,
194
256
  f"Failed to update entity: {error_msg}",
195
257
  context={"entity_id": entity_id},
196
- suggestions=[
197
- "Verify the entity_id exists using ha_search_entities()",
198
- "Check that area_id exists if specified",
199
- "Some entities may not support all update options",
200
- ],
258
+ suggestions=suggestions,
201
259
  )
202
260
  )
203
261
 
204
262
  entity_entry = result.get("result", {}).get("entity_entry", {})
205
263
 
264
+ # If entity was renamed, update entity_id for subsequent operations
265
+ if new_entity_id:
266
+ entity_id = new_entity_id
267
+
268
+ # Handle new_device_name — rename the associated device
269
+ # Normalize empty string to None (no-op, don't clear device name)
270
+ if new_device_name is not None and not new_device_name.strip():
271
+ new_device_name = None
272
+ device_rename_result: dict[str, Any] | None = None
273
+ if new_device_name is not None:
274
+ # If no registry update was sent, fetch entity_entry to get device_id
275
+ if not entity_entry:
276
+ device_lookup_msg: dict[str, Any] = {
277
+ "type": "config/entity_registry/get",
278
+ "entity_id": entity_id,
279
+ }
280
+ get_result = await client.send_websocket_message(device_lookup_msg)
281
+ if get_result.get("success"):
282
+ entity_entry = get_result.get("result", {})
283
+ else:
284
+ logger.warning(f"Entity registry lookup failed for {entity_id}: {get_result.get('error')}")
285
+ device_rename_result = {
286
+ "warning": "Entity registry lookup failed — could not determine device. Retry may succeed.",
287
+ }
288
+
289
+ device_id = entity_entry.get("device_id") if not device_rename_result else None
290
+ if not device_id:
291
+ device_rename_result = {
292
+ "warning": "Entity has no associated device — device rename skipped",
293
+ }
294
+ else:
295
+ device_msg: dict[str, Any] = {
296
+ "type": "config/device_registry/update",
297
+ "device_id": device_id,
298
+ "name_by_user": new_device_name if new_device_name else None,
299
+ }
300
+ device_result = await client.send_websocket_message(device_msg)
301
+ if device_result.get("success"):
302
+ device_rename_result = {"success": True, "device_id": device_id}
303
+ else:
304
+ device_rename_result = {
305
+ "warning": f"Entity updated but device rename failed: {device_result.get('error', 'Unknown error')}",
306
+ "device_id": device_id,
307
+ }
308
+
206
309
  # Handle expose_to via separate WebSocket API
207
310
  exposure_result: dict[str, bool] | None = None
208
311
  if parsed_expose_to is not None:
@@ -289,9 +392,26 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
289
392
  "message": f"Entity updated: {', '.join(updates_made)}",
290
393
  }
291
394
 
395
+ # Include old_entity_id and rename warning when a rename was performed
396
+ if new_entity_id is not None:
397
+ response_data["old_entity_id"] = original_entity_id
398
+ response_data["warning"] = (
399
+ "Remember to update any automations, scripts, or dashboards "
400
+ "that reference the old entity_id"
401
+ )
402
+
292
403
  if exposure_result is not None:
293
404
  response_data["exposure"] = exposure_result
294
405
 
406
+ if device_rename_result is not None:
407
+ response_data["device_rename"] = device_rename_result
408
+ # Only mark partial when device rename was attempted and failed
409
+ # (not when entity simply has no device)
410
+ if "warning" in device_rename_result and device_rename_result.get(
411
+ "device_id"
412
+ ):
413
+ response_data["partial"] = True
414
+
295
415
  return response_data
296
416
 
297
417
  @mcp.tool(
@@ -299,8 +419,8 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
299
419
  annotations={
300
420
  "destructiveHint": True,
301
421
  "idempotentHint": True,
302
- "title": "Set Entity"
303
- }
422
+ "title": "Set Entity",
423
+ },
304
424
  )
305
425
  @log_tool_usage
306
426
  async def ha_set_entity(
@@ -396,29 +516,69 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
396
516
  default=None,
397
517
  ),
398
518
  ] = None,
519
+ new_entity_id: Annotated[
520
+ str | None,
521
+ Field(
522
+ description=(
523
+ "New entity ID to rename to (e.g., 'light.new_name'). "
524
+ "Domain must match the original. Single entity only."
525
+ ),
526
+ default=None,
527
+ ),
528
+ ] = None,
529
+ new_device_name: Annotated[
530
+ str | None,
531
+ Field(
532
+ description=(
533
+ "New display name for the associated device. "
534
+ "If provided, both entity and device are updated in one operation. Single entity only."
535
+ ),
536
+ default=None,
537
+ ),
538
+ ] = None,
399
539
  ) -> dict[str, Any]:
400
540
  """Update entity properties in the entity registry.
401
541
 
402
542
  Allows modifying entity metadata such as area assignment, display name,
403
- icon, enabled/disabled state, visibility, aliases, labels, and voice
404
- assistant exposure in a single call.
543
+ icon, enabled/disabled state, visibility, aliases, labels, voice
544
+ assistant exposure, and entity_id rename in a single call.
405
545
 
406
546
  BULK OPERATIONS:
407
- When entity_id is a list, only labels and expose_to parameters are supported.
408
- Other parameters (area_id, name, icon, enabled, hidden, aliases) require single entity.
547
+ When entity_id is a list, only labels, expose_to, and categories parameters are supported.
548
+ Other parameters (area_id, name, icon, enabled, hidden, aliases, new_entity_id, new_device_name) require single entity.
409
549
 
410
550
  LABEL OPERATIONS:
411
551
  - label_operation="set" (default): Replace all labels with the provided list. Use [] to clear.
412
552
  - label_operation="add": Add labels to existing ones without removing any.
413
553
  - label_operation="remove": Remove specified labels from the entity.
414
554
 
555
+ ENTITY ID RENAME:
556
+ Use new_entity_id to change an entity's ID (e.g., sensor.old -> sensor.new).
557
+ Domain must match. Voice exposure settings are preserved automatically.
558
+
559
+ WARNING: Renaming an entity_id does NOT update references in automations,
560
+ scripts, templates, or dashboards. All consumers of the old entity_id must
561
+ be updated manually — HA does not propagate the rename automatically.
562
+
563
+ Rename limitations:
564
+ - Entity history is preserved (HA 2022.4+)
565
+ - Entities without unique IDs cannot be renamed
566
+ - Entities disabled by their integration cannot be renamed
567
+
568
+ DEVICE RENAME:
569
+ Use new_device_name to rename the associated device. Can be combined with
570
+ new_entity_id to rename both in one call. The device is looked up automatically.
571
+
415
572
  Use ha_search_entities() or ha_get_device() to find entity IDs.
416
573
  Use ha_config_get_label() to find available label IDs.
417
574
 
418
575
  EXAMPLES:
419
576
  Single entity:
420
577
  - Assign to area: ha_set_entity("sensor.temp", area_id="living_room")
421
- - Rename: ha_set_entity("sensor.temp", name="Living Room Temperature")
578
+ - Rename display name: ha_set_entity("sensor.temp", name="Living Room Temperature")
579
+ - Rename entity_id: ha_set_entity("light.old_name", new_entity_id="light.new_name")
580
+ - Rename entity and device: ha_set_entity("light.old", new_entity_id="light.new", new_device_name="New Lamp")
581
+ - Rename entity_id with friendly name: ha_set_entity("sensor.old", new_entity_id="sensor.new", name="New Name")
422
582
  - Set labels: ha_set_entity("light.lamp", labels=["outdoor", "smart"])
423
583
  - Add labels: ha_set_entity("light.lamp", labels=["new_label"], label_operation="add")
424
584
  - Remove labels: ha_set_entity("light.lamp", labels=["old_label"], label_operation="remove")
@@ -430,8 +590,6 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
430
590
  - Add labels to multiple: ha_set_entity(["light.a", "light.b"], labels=["new"], label_operation="add")
431
591
  - Expose multiple to Alexa: ha_set_entity(["light.a", "light.b"], expose_to={"cloud.alexa": True})
432
592
 
433
- NOTE: To rename an entity_id (e.g., sensor.old -> sensor.new), use ha_rename_entity() instead.
434
-
435
593
  ENABLED/DISABLED WARNING:
436
594
  Setting enabled=False performs a **registry-level disable** — the entity is completely
437
595
  removed from the Home Assistant state machine and hidden from the UI. It will NOT appear
@@ -483,6 +641,8 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
483
641
  "enabled": enabled,
484
642
  "hidden": hidden,
485
643
  "aliases": aliases,
644
+ "new_entity_id": new_entity_id,
645
+ "new_device_name": new_device_name,
486
646
  }
487
647
  non_null_single_params = [
488
648
  k for k, v in single_entity_params.items() if v is not None
@@ -517,25 +677,28 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
517
677
 
518
678
  if _enabled_check is False:
519
679
  blocked = [
520
- eid for eid in entity_ids
680
+ eid
681
+ for eid in entity_ids
521
682
  if eid.split(".")[0] in ("automation", "script")
522
683
  ]
523
684
  if blocked:
524
685
  _domain = blocked[0].split(".")[0]
525
686
  _service_hint = f"{_domain}.turn_off"
526
- raise_tool_error(create_error_response(
527
- ErrorCode.VALIDATION_INVALID_PARAMETER,
528
- f"Cannot registry-disable {_domain} entities with ha_set_entity(enabled=False). "
529
- f"This removes the entity from the state machine and hides it from the UI "
530
- f"until it is re-enabled AND the {_domain}s are reloaded. "
531
- f"Use ha_call_service('{_domain}', 'turn_off', entity_id='{blocked[0]}') instead "
532
- f"to disable it without removing it.",
533
- suggestions=[
534
- f"Use {_service_hint} to disable the {_domain} (keeps it visible and manageable)",
535
- f"Use {_domain}.turn_on to re-enable it later",
536
- "ha_set_entity(enabled=False) is for registry-level disable — it fully hides the entity",
537
- ],
538
- ))
687
+ raise_tool_error(
688
+ create_error_response(
689
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
690
+ f"Cannot registry-disable {_domain} entities with ha_set_entity(enabled=False). "
691
+ f"This removes the entity from the state machine and hides it from the UI "
692
+ f"until it is re-enabled AND the {_domain}s are reloaded. "
693
+ f"Use ha_call_service('{_domain}', 'turn_off', entity_id='{blocked[0]}') instead "
694
+ f"to disable it without removing it.",
695
+ suggestions=[
696
+ f"Use {_service_hint} to disable the {_domain} (keeps it visible and manageable)",
697
+ f"Use {_domain}.turn_on to re-enable it later",
698
+ "ha_set_entity(enabled=False) is for registry-level disable — it fully hides the entity",
699
+ ],
700
+ )
701
+ )
539
702
 
540
703
  # Parse list parameters if provided as strings
541
704
  parsed_aliases = None
@@ -654,6 +817,8 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
654
817
  parsed_labels,
655
818
  label_operation,
656
819
  parsed_expose_to,
820
+ new_entity_id=new_entity_id,
821
+ new_device_name=new_device_name,
657
822
  )
658
823
 
659
824
  # Bulk case - process each entity
@@ -733,8 +898,8 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
733
898
  annotations={
734
899
  "readOnlyHint": True,
735
900
  "idempotentHint": True,
736
- "title": "Get Entity"
737
- }
901
+ "title": "Get Entity",
902
+ },
738
903
  )
739
904
  @log_tool_usage
740
905
  async def ha_get_entity(