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.
- {ha_mcp_dev-7.2.0.dev335/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.2.0.dev336}/PKG-INFO +1 -1
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/pyproject.toml +1 -1
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/server.py +36 -27
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_entities.py +205 -40
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_registry.py +6 -453
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_search.py +51 -71
- ha_mcp_dev-7.2.0.dev336/src/ha_mcp/tools/tools_todo.py +512 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- ha_mcp_dev-7.2.0.dev335/src/ha_mcp/tools/tools_todo.py +0 -530
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/LICENSE +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/README.md +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/setup.cfg +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/py.typed +0 -0
- {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
- {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
- {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
- {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
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {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
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {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
- {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
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {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
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.2.0.dev335 → ha_mcp_dev-7.2.0.dev336}/tests/test_env_manager.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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(
|
|
454
|
-
|
|
455
|
-
|
|
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(
|
|
503
|
-
|
|
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,
|
|
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(
|
|
628
|
-
|
|
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(
|
|
651
|
-
|
|
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
|
|
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(
|
|
121
|
-
|
|
122
|
-
|
|
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(
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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(
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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(
|