ha-mcp-dev 7.2.0.dev348__tar.gz → 7.2.0.dev349__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.dev348/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.2.0.dev349}/PKG-INFO +4 -4
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/README.md +3 -3
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/pyproject.toml +3 -15
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_areas.py +189 -107
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_blueprints.py +34 -9
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_calendar.py +38 -9
- ha_mcp_dev-7.2.0.dev349/src/ha_mcp/tools/tools_config_entry_flow.py +574 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_config_scripts.py +93 -55
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_groups.py +125 -73
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_integrations.py +225 -165
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_mcp_component.py +152 -109
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_service.py +137 -88
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_services.py +127 -106
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_system.py +228 -169
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_todo.py +201 -129
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349/src/ha_mcp_dev.egg-info}/PKG-INFO +4 -4
- ha_mcp_dev-7.2.0.dev348/src/ha_mcp/tools/tools_config_entry_flow.py +0 -493
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/LICENSE +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/setup.cfg +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/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.
|
|
3
|
+
Version: 7.2.0.dev349
|
|
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
|
|
@@ -37,7 +37,7 @@ Dynamic: license-file
|
|
|
37
37
|
<!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
|
|
38
38
|
|
|
39
39
|
<p align="center">
|
|
40
|
-
<img src="https://img.shields.io/badge/tools-
|
|
40
|
+
<img src="https://img.shields.io/badge/tools-86-blue" alt="95+ Tools">
|
|
41
41
|
<a href="https://github.com/homeassistant-ai/ha-mcp/releases"><img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release"></a>
|
|
42
42
|
<a href="https://github.com/homeassistant-ai/ha-mcp/actions/workflows/e2e-tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests"></a>
|
|
43
43
|
<a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
|
|
@@ -160,7 +160,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
160
160
|
<details>
|
|
161
161
|
<!-- TOOLS_TABLE_START -->
|
|
162
162
|
|
|
163
|
-
<summary><b>Complete Tool List (
|
|
163
|
+
<summary><b>Complete Tool List (86 tools)</b></summary>
|
|
164
164
|
|
|
165
165
|
| Category | Tools |
|
|
166
166
|
|----------|-------|
|
|
@@ -177,7 +177,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
177
177
|
| **Groups** | `ha_config_list_groups`, `ha_config_remove_group`, `ha_config_set_group` |
|
|
178
178
|
| **HACS** | `ha_hacs_add_repository`, `ha_hacs_download`, `ha_hacs_repository_info`, `ha_hacs_search` |
|
|
179
179
|
| **Helper Entities** | `ha_config_list_helpers`, `ha_config_remove_helper`, `ha_config_set_helper`, `ha_get_helper_schema`, `ha_set_config_entry_helper` |
|
|
180
|
-
| **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs
|
|
180
|
+
| **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs` |
|
|
181
181
|
| **Integrations** | `ha_delete_config_entry`, `ha_get_integration`, `ha_set_integration_enabled` |
|
|
182
182
|
| **Labels & Categories** | `ha_config_get_category`, `ha_config_get_label`, `ha_config_remove_category`, `ha_config_remove_label`, `ha_config_set_category`, `ha_config_set_label` |
|
|
183
183
|
| **Scripts** | `ha_config_get_script`, `ha_config_remove_script`, `ha_config_set_script` |
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
|
-
<img src="https://img.shields.io/badge/tools-
|
|
11
|
+
<img src="https://img.shields.io/badge/tools-86-blue" alt="95+ Tools">
|
|
12
12
|
<a href="https://github.com/homeassistant-ai/ha-mcp/releases"><img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release"></a>
|
|
13
13
|
<a href="https://github.com/homeassistant-ai/ha-mcp/actions/workflows/e2e-tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests"></a>
|
|
14
14
|
<a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
|
|
@@ -131,7 +131,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
131
131
|
<details>
|
|
132
132
|
<!-- TOOLS_TABLE_START -->
|
|
133
133
|
|
|
134
|
-
<summary><b>Complete Tool List (
|
|
134
|
+
<summary><b>Complete Tool List (86 tools)</b></summary>
|
|
135
135
|
|
|
136
136
|
| Category | Tools |
|
|
137
137
|
|----------|-------|
|
|
@@ -148,7 +148,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
148
148
|
| **Groups** | `ha_config_list_groups`, `ha_config_remove_group`, `ha_config_set_group` |
|
|
149
149
|
| **HACS** | `ha_hacs_add_repository`, `ha_hacs_download`, `ha_hacs_repository_info`, `ha_hacs_search` |
|
|
150
150
|
| **Helper Entities** | `ha_config_list_helpers`, `ha_config_remove_helper`, `ha_config_set_helper`, `ha_get_helper_schema`, `ha_set_config_entry_helper` |
|
|
151
|
-
| **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs
|
|
151
|
+
| **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs` |
|
|
152
152
|
| **Integrations** | `ha_delete_config_entry`, `ha_get_integration`, `ha_set_integration_enabled` |
|
|
153
153
|
| **Labels & Categories** | `ha_config_get_category`, `ha_config_get_label`, `ha_config_remove_category`, `ha_config_remove_label`, `ha_config_set_category`, `ha_config_set_label` |
|
|
154
154
|
| **Scripts** | `ha_config_get_script`, `ha_config_remove_script`, `ha_config_set_script` |
|
|
@@ -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.dev349"
|
|
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"
|
|
@@ -133,8 +133,8 @@ ignore = [
|
|
|
133
133
|
[tool.ruff.lint.per-file-ignores]
|
|
134
134
|
"__init__.py" = ["F401"]
|
|
135
135
|
"tests/**/*" = ["E501", "B011"]
|
|
136
|
-
# C901 ignores for
|
|
137
|
-
# Remove lines as
|
|
136
|
+
# C901 ignores for tools files with complex methods (see #925).
|
|
137
|
+
# Remove lines as individual methods are simplified below threshold.
|
|
138
138
|
"src/ha_mcp/tools/backup.py" = ["C901"]
|
|
139
139
|
"src/ha_mcp/tools/best_practice_checker.py" = ["C901"]
|
|
140
140
|
"src/ha_mcp/tools/device_control.py" = ["C901"]
|
|
@@ -142,28 +142,16 @@ ignore = [
|
|
|
142
142
|
"src/ha_mcp/tools/registry.py" = ["C901"]
|
|
143
143
|
"src/ha_mcp/tools/smart_search.py" = ["C901"]
|
|
144
144
|
"src/ha_mcp/tools/tools_addons.py" = ["C901"]
|
|
145
|
-
"src/ha_mcp/tools/tools_areas.py" = ["C901"]
|
|
146
|
-
"src/ha_mcp/tools/tools_blueprints.py" = ["C901"]
|
|
147
|
-
"src/ha_mcp/tools/tools_calendar.py" = ["C901"]
|
|
148
145
|
"src/ha_mcp/tools/tools_config_automations.py" = ["C901"]
|
|
149
146
|
"src/ha_mcp/tools/tools_config_dashboards.py" = ["C901"]
|
|
150
|
-
"src/ha_mcp/tools/tools_config_entry_flow.py" = ["C901"]
|
|
151
147
|
"src/ha_mcp/tools/tools_config_helpers.py" = ["C901"]
|
|
152
|
-
"src/ha_mcp/tools/tools_config_scripts.py" = ["C901"]
|
|
153
148
|
"src/ha_mcp/tools/tools_entities.py" = ["C901"]
|
|
154
149
|
"src/ha_mcp/tools/tools_filesystem.py" = ["C901"]
|
|
155
|
-
"src/ha_mcp/tools/tools_groups.py" = ["C901"]
|
|
156
150
|
"src/ha_mcp/tools/tools_hacs.py" = ["C901"]
|
|
157
151
|
"src/ha_mcp/tools/tools_history.py" = ["C901"]
|
|
158
|
-
"src/ha_mcp/tools/tools_integrations.py" = ["C901"]
|
|
159
|
-
"src/ha_mcp/tools/tools_mcp_component.py" = ["C901"]
|
|
160
152
|
"src/ha_mcp/tools/tools_registry.py" = ["C901"]
|
|
161
153
|
"src/ha_mcp/tools/tools_resources.py" = ["C901"]
|
|
162
154
|
"src/ha_mcp/tools/tools_search.py" = ["C901"]
|
|
163
|
-
"src/ha_mcp/tools/tools_service.py" = ["C901"]
|
|
164
|
-
"src/ha_mcp/tools/tools_services.py" = ["C901"]
|
|
165
|
-
"src/ha_mcp/tools/tools_system.py" = ["C901"]
|
|
166
|
-
"src/ha_mcp/tools/tools_todo.py" = ["C901"]
|
|
167
155
|
"src/ha_mcp/tools/tools_traces.py" = ["C901"]
|
|
168
156
|
"src/ha_mcp/tools/tools_updates.py" = ["C901"]
|
|
169
157
|
"src/ha_mcp/tools/tools_utility.py" = ["C901"]
|
|
@@ -9,25 +9,130 @@ import logging
|
|
|
9
9
|
from typing import Annotated, Any
|
|
10
10
|
|
|
11
11
|
from fastmcp.exceptions import ToolError
|
|
12
|
+
from fastmcp.tools import tool
|
|
12
13
|
from pydantic import Field
|
|
13
14
|
|
|
14
15
|
from ..errors import ErrorCode, create_error_response
|
|
15
|
-
from .helpers import
|
|
16
|
+
from .helpers import (
|
|
17
|
+
exception_to_structured_error,
|
|
18
|
+
log_tool_usage,
|
|
19
|
+
raise_tool_error,
|
|
20
|
+
register_tool_methods,
|
|
21
|
+
)
|
|
16
22
|
from .util_helpers import parse_string_list_param
|
|
17
23
|
|
|
18
24
|
logger = logging.getLogger(__name__)
|
|
19
25
|
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
"""
|
|
27
|
+
class AreaTools:
|
|
28
|
+
"""Area and floor management tools for Home Assistant."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, client: Any) -> None:
|
|
31
|
+
self._client = client
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def _build_area_update_message(
|
|
35
|
+
area_id: str,
|
|
36
|
+
name: str | None,
|
|
37
|
+
floor_id: str | None,
|
|
38
|
+
icon: str | None,
|
|
39
|
+
parsed_aliases: list[str] | None,
|
|
40
|
+
picture: str | None,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
"""Build a WebSocket message for updating an existing area."""
|
|
43
|
+
message: dict[str, Any] = {
|
|
44
|
+
"type": "config/area_registry/update",
|
|
45
|
+
"area_id": area_id,
|
|
46
|
+
}
|
|
47
|
+
if name is not None:
|
|
48
|
+
message["name"] = name
|
|
49
|
+
if floor_id is not None:
|
|
50
|
+
message["floor_id"] = floor_id if floor_id else None
|
|
51
|
+
if icon is not None:
|
|
52
|
+
message["icon"] = icon if icon else None
|
|
53
|
+
if parsed_aliases is not None:
|
|
54
|
+
message["aliases"] = parsed_aliases
|
|
55
|
+
if picture is not None:
|
|
56
|
+
message["picture"] = picture if picture else None
|
|
57
|
+
return message
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _build_area_create_message(
|
|
61
|
+
name: str,
|
|
62
|
+
floor_id: str | None,
|
|
63
|
+
icon: str | None,
|
|
64
|
+
parsed_aliases: list[str] | None,
|
|
65
|
+
picture: str | None,
|
|
66
|
+
) -> dict[str, Any]:
|
|
67
|
+
"""Build a WebSocket message for creating a new area."""
|
|
68
|
+
message: dict[str, Any] = {
|
|
69
|
+
"type": "config/area_registry/create",
|
|
70
|
+
"name": name,
|
|
71
|
+
}
|
|
72
|
+
if floor_id:
|
|
73
|
+
message["floor_id"] = floor_id
|
|
74
|
+
if icon:
|
|
75
|
+
message["icon"] = icon
|
|
76
|
+
if parsed_aliases:
|
|
77
|
+
message["aliases"] = parsed_aliases
|
|
78
|
+
if picture:
|
|
79
|
+
message["picture"] = picture
|
|
80
|
+
return message
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _build_floor_update_message(
|
|
84
|
+
floor_id: str,
|
|
85
|
+
name: str | None,
|
|
86
|
+
level: int | None,
|
|
87
|
+
icon: str | None,
|
|
88
|
+
parsed_aliases: list[str] | None,
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
"""Build a WebSocket message for updating an existing floor."""
|
|
91
|
+
message: dict[str, Any] = {
|
|
92
|
+
"type": "config/floor_registry/update",
|
|
93
|
+
"floor_id": floor_id,
|
|
94
|
+
}
|
|
95
|
+
if name is not None:
|
|
96
|
+
message["name"] = name
|
|
97
|
+
if level is not None:
|
|
98
|
+
message["level"] = level
|
|
99
|
+
if icon is not None:
|
|
100
|
+
message["icon"] = icon if icon else None
|
|
101
|
+
if parsed_aliases is not None:
|
|
102
|
+
message["aliases"] = parsed_aliases
|
|
103
|
+
return message
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _build_floor_create_message(
|
|
107
|
+
name: str,
|
|
108
|
+
level: int | None,
|
|
109
|
+
icon: str | None,
|
|
110
|
+
parsed_aliases: list[str] | None,
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
"""Build a WebSocket message for creating a new floor."""
|
|
113
|
+
message: dict[str, Any] = {
|
|
114
|
+
"type": "config/floor_registry/create",
|
|
115
|
+
"name": name,
|
|
116
|
+
}
|
|
117
|
+
if level is not None:
|
|
118
|
+
message["level"] = level
|
|
119
|
+
if icon:
|
|
120
|
+
message["icon"] = icon
|
|
121
|
+
if parsed_aliases:
|
|
122
|
+
message["aliases"] = parsed_aliases
|
|
123
|
+
return message
|
|
23
124
|
|
|
24
125
|
# ============================================================
|
|
25
126
|
# AREA TOOLS
|
|
26
127
|
# ============================================================
|
|
27
128
|
|
|
28
|
-
@
|
|
129
|
+
@tool(
|
|
130
|
+
name="ha_config_list_areas",
|
|
131
|
+
tags={"Areas & Floors"},
|
|
132
|
+
annotations={"idempotentHint": True, "readOnlyHint": True, "title": "List Areas"},
|
|
133
|
+
)
|
|
29
134
|
@log_tool_usage
|
|
30
|
-
async def ha_config_list_areas() -> dict[str, Any]:
|
|
135
|
+
async def ha_config_list_areas(self) -> dict[str, Any]:
|
|
31
136
|
"""
|
|
32
137
|
List all Home Assistant areas (rooms).
|
|
33
138
|
|
|
@@ -38,7 +143,7 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
38
143
|
"type": "config/area_registry/list",
|
|
39
144
|
}
|
|
40
145
|
|
|
41
|
-
result = await
|
|
146
|
+
result = await self._client.send_websocket_message(message)
|
|
42
147
|
|
|
43
148
|
if result.get("success"):
|
|
44
149
|
areas = result.get("result", [])
|
|
@@ -63,9 +168,14 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
63
168
|
"Verify WebSocket connection is active",
|
|
64
169
|
])
|
|
65
170
|
|
|
66
|
-
@
|
|
171
|
+
@tool(
|
|
172
|
+
name="ha_config_set_area",
|
|
173
|
+
tags={"Areas & Floors"},
|
|
174
|
+
annotations={"destructiveHint": True, "title": "Create or Update Area"},
|
|
175
|
+
)
|
|
67
176
|
@log_tool_usage
|
|
68
177
|
async def ha_config_set_area(
|
|
178
|
+
self,
|
|
69
179
|
name: Annotated[
|
|
70
180
|
str | None,
|
|
71
181
|
Field(
|
|
@@ -127,27 +237,11 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
127
237
|
|
|
128
238
|
# Determine if this is a create or update operation
|
|
129
239
|
if area_id:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"area_id": area_id,
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
# Only add fields that were explicitly provided
|
|
137
|
-
if name is not None:
|
|
138
|
-
message["name"] = name
|
|
139
|
-
if floor_id is not None:
|
|
140
|
-
message["floor_id"] = floor_id if floor_id else None
|
|
141
|
-
if icon is not None:
|
|
142
|
-
message["icon"] = icon if icon else None
|
|
143
|
-
if parsed_aliases is not None:
|
|
144
|
-
message["aliases"] = parsed_aliases
|
|
145
|
-
if picture is not None:
|
|
146
|
-
message["picture"] = picture if picture else None
|
|
147
|
-
|
|
240
|
+
message = self._build_area_update_message(
|
|
241
|
+
area_id, name, floor_id, icon, parsed_aliases, picture,
|
|
242
|
+
)
|
|
148
243
|
operation = "update"
|
|
149
244
|
else:
|
|
150
|
-
# CREATE operation - name is required
|
|
151
245
|
if not name:
|
|
152
246
|
raise_tool_error(create_error_response(
|
|
153
247
|
ErrorCode.VALIDATION_MISSING_PARAMETER,
|
|
@@ -155,24 +249,12 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
155
249
|
context={"operation": "create_area"},
|
|
156
250
|
suggestions=["Provide a name for the new area"],
|
|
157
251
|
))
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"name": name,
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if floor_id:
|
|
165
|
-
message["floor_id"] = floor_id
|
|
166
|
-
if icon:
|
|
167
|
-
message["icon"] = icon
|
|
168
|
-
if parsed_aliases:
|
|
169
|
-
message["aliases"] = parsed_aliases
|
|
170
|
-
if picture:
|
|
171
|
-
message["picture"] = picture
|
|
172
|
-
|
|
252
|
+
message = self._build_area_create_message(
|
|
253
|
+
name, floor_id, icon, parsed_aliases, picture,
|
|
254
|
+
)
|
|
173
255
|
operation = "create"
|
|
174
256
|
|
|
175
|
-
result = await
|
|
257
|
+
result = await self._client.send_websocket_message(message)
|
|
176
258
|
|
|
177
259
|
if result.get("success"):
|
|
178
260
|
area_data = result.get("result", {})
|
|
@@ -183,19 +265,19 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
183
265
|
"area_id": area_data.get("area_id", area_id),
|
|
184
266
|
"message": f"Successfully {operation}d area: {area_name}",
|
|
185
267
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
268
|
+
|
|
269
|
+
error = result.get("error", {})
|
|
270
|
+
error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
|
|
271
|
+
ctx: dict[str, Any] = {"operation": operation}
|
|
272
|
+
if name:
|
|
273
|
+
ctx["name"] = name
|
|
274
|
+
if area_id:
|
|
275
|
+
ctx["area_id"] = area_id
|
|
276
|
+
raise_tool_error(create_error_response(
|
|
277
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
278
|
+
f"Failed to {operation} area: {error_msg}",
|
|
279
|
+
context=ctx,
|
|
280
|
+
))
|
|
199
281
|
|
|
200
282
|
except ToolError:
|
|
201
283
|
raise
|
|
@@ -208,9 +290,14 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
208
290
|
"If assigning to a floor, verify floor_id exists",
|
|
209
291
|
])
|
|
210
292
|
|
|
211
|
-
@
|
|
293
|
+
@tool(
|
|
294
|
+
name="ha_config_remove_area",
|
|
295
|
+
tags={"Areas & Floors"},
|
|
296
|
+
annotations={"destructiveHint": True, "idempotentHint": True, "title": "Remove Area"},
|
|
297
|
+
)
|
|
212
298
|
@log_tool_usage
|
|
213
299
|
async def ha_config_remove_area(
|
|
300
|
+
self,
|
|
214
301
|
area_id: Annotated[
|
|
215
302
|
str,
|
|
216
303
|
Field(description="Area ID to delete (use ha_config_list_areas to find IDs)"),
|
|
@@ -228,7 +315,7 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
228
315
|
"area_id": area_id,
|
|
229
316
|
}
|
|
230
317
|
|
|
231
|
-
result = await
|
|
318
|
+
result = await self._client.send_websocket_message(message)
|
|
232
319
|
|
|
233
320
|
if result.get("success"):
|
|
234
321
|
return {
|
|
@@ -258,9 +345,13 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
258
345
|
# FLOOR TOOLS
|
|
259
346
|
# ============================================================
|
|
260
347
|
|
|
261
|
-
@
|
|
348
|
+
@tool(
|
|
349
|
+
name="ha_config_list_floors",
|
|
350
|
+
tags={"Areas & Floors"},
|
|
351
|
+
annotations={"idempotentHint": True, "readOnlyHint": True, "title": "List Floors"},
|
|
352
|
+
)
|
|
262
353
|
@log_tool_usage
|
|
263
|
-
async def ha_config_list_floors() -> dict[str, Any]:
|
|
354
|
+
async def ha_config_list_floors(self) -> dict[str, Any]:
|
|
264
355
|
"""
|
|
265
356
|
List all Home Assistant floors.
|
|
266
357
|
|
|
@@ -271,7 +362,7 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
271
362
|
"type": "config/floor_registry/list",
|
|
272
363
|
}
|
|
273
364
|
|
|
274
|
-
result = await
|
|
365
|
+
result = await self._client.send_websocket_message(message)
|
|
275
366
|
|
|
276
367
|
if result.get("success"):
|
|
277
368
|
floors = result.get("result", [])
|
|
@@ -296,9 +387,14 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
296
387
|
"Verify WebSocket connection is active",
|
|
297
388
|
])
|
|
298
389
|
|
|
299
|
-
@
|
|
390
|
+
@tool(
|
|
391
|
+
name="ha_config_set_floor",
|
|
392
|
+
tags={"Areas & Floors"},
|
|
393
|
+
annotations={"destructiveHint": True, "title": "Create or Update Floor"},
|
|
394
|
+
)
|
|
300
395
|
@log_tool_usage
|
|
301
396
|
async def ha_config_set_floor(
|
|
397
|
+
self,
|
|
302
398
|
name: Annotated[
|
|
303
399
|
str | None,
|
|
304
400
|
Field(
|
|
@@ -353,25 +449,11 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
353
449
|
|
|
354
450
|
# Determine if this is a create or update operation
|
|
355
451
|
if floor_id:
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
"floor_id": floor_id,
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
# Only add fields that were explicitly provided
|
|
363
|
-
if name is not None:
|
|
364
|
-
message["name"] = name
|
|
365
|
-
if level is not None:
|
|
366
|
-
message["level"] = level
|
|
367
|
-
if icon is not None:
|
|
368
|
-
message["icon"] = icon if icon else None
|
|
369
|
-
if parsed_aliases is not None:
|
|
370
|
-
message["aliases"] = parsed_aliases
|
|
371
|
-
|
|
452
|
+
message = self._build_floor_update_message(
|
|
453
|
+
floor_id, name, level, icon, parsed_aliases,
|
|
454
|
+
)
|
|
372
455
|
operation = "update"
|
|
373
456
|
else:
|
|
374
|
-
# CREATE operation - name is required
|
|
375
457
|
if not name:
|
|
376
458
|
raise_tool_error(create_error_response(
|
|
377
459
|
ErrorCode.VALIDATION_MISSING_PARAMETER,
|
|
@@ -379,22 +461,12 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
379
461
|
context={"operation": "create_floor"},
|
|
380
462
|
suggestions=["Provide a name for the new floor"],
|
|
381
463
|
))
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
"name": name,
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if level is not None:
|
|
389
|
-
message["level"] = level
|
|
390
|
-
if icon:
|
|
391
|
-
message["icon"] = icon
|
|
392
|
-
if parsed_aliases:
|
|
393
|
-
message["aliases"] = parsed_aliases
|
|
394
|
-
|
|
464
|
+
message = self._build_floor_create_message(
|
|
465
|
+
name, level, icon, parsed_aliases,
|
|
466
|
+
)
|
|
395
467
|
operation = "create"
|
|
396
468
|
|
|
397
|
-
result = await
|
|
469
|
+
result = await self._client.send_websocket_message(message)
|
|
398
470
|
|
|
399
471
|
if result.get("success"):
|
|
400
472
|
floor_data = result.get("result", {})
|
|
@@ -405,19 +477,19 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
405
477
|
"floor_id": floor_data.get("floor_id", floor_id),
|
|
406
478
|
"message": f"Successfully {operation}d floor: {floor_name}",
|
|
407
479
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
480
|
+
|
|
481
|
+
error = result.get("error", {})
|
|
482
|
+
error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
|
|
483
|
+
ctx: dict[str, Any] = {"operation": operation}
|
|
484
|
+
if name:
|
|
485
|
+
ctx["name"] = name
|
|
486
|
+
if floor_id:
|
|
487
|
+
ctx["floor_id"] = floor_id
|
|
488
|
+
raise_tool_error(create_error_response(
|
|
489
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
490
|
+
f"Failed to {operation} floor: {error_msg}",
|
|
491
|
+
context=ctx,
|
|
492
|
+
))
|
|
421
493
|
|
|
422
494
|
except ToolError:
|
|
423
495
|
raise
|
|
@@ -429,9 +501,14 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
429
501
|
"For update: Verify the floor_id exists using ha_config_list_floors()",
|
|
430
502
|
])
|
|
431
503
|
|
|
432
|
-
@
|
|
504
|
+
@tool(
|
|
505
|
+
name="ha_config_remove_floor",
|
|
506
|
+
tags={"Areas & Floors"},
|
|
507
|
+
annotations={"destructiveHint": True, "idempotentHint": True, "title": "Remove Floor"},
|
|
508
|
+
)
|
|
433
509
|
@log_tool_usage
|
|
434
510
|
async def ha_config_remove_floor(
|
|
511
|
+
self,
|
|
435
512
|
floor_id: Annotated[
|
|
436
513
|
str,
|
|
437
514
|
Field(description="Floor ID to delete (use ha_config_list_floors to find IDs)"),
|
|
@@ -449,7 +526,7 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
449
526
|
"floor_id": floor_id,
|
|
450
527
|
}
|
|
451
528
|
|
|
452
|
-
result = await
|
|
529
|
+
result = await self._client.send_websocket_message(message)
|
|
453
530
|
|
|
454
531
|
if result.get("success"):
|
|
455
532
|
return {
|
|
@@ -474,3 +551,8 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
474
551
|
"Check Home Assistant connection",
|
|
475
552
|
"Verify the floor_id exists using ha_config_list_floors()",
|
|
476
553
|
])
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
557
|
+
"""Register Home Assistant area and floor management tools."""
|
|
558
|
+
register_tool_methods(mcp, AreaTools(client))
|
|
@@ -9,17 +9,27 @@ import logging
|
|
|
9
9
|
from typing import Annotated, Any
|
|
10
10
|
|
|
11
11
|
from fastmcp.exceptions import ToolError
|
|
12
|
+
from fastmcp.tools import tool
|
|
12
13
|
from pydantic import Field
|
|
13
14
|
|
|
14
15
|
from ..errors import ErrorCode, create_error_response
|
|
15
|
-
from .helpers import
|
|
16
|
+
from .helpers import (
|
|
17
|
+
exception_to_structured_error,
|
|
18
|
+
log_tool_usage,
|
|
19
|
+
raise_tool_error,
|
|
20
|
+
register_tool_methods,
|
|
21
|
+
)
|
|
16
22
|
|
|
17
23
|
logger = logging.getLogger(__name__)
|
|
18
24
|
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
"""
|
|
26
|
+
class BlueprintTools:
|
|
27
|
+
"""Blueprint management tools for Home Assistant."""
|
|
22
28
|
|
|
29
|
+
def __init__(self, client: Any) -> None:
|
|
30
|
+
self._client = client
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
23
33
|
def _format_blueprint_list(blueprints_data: dict[str, Any], domain: str) -> dict[str, Any]:
|
|
24
34
|
"""Format blueprint data into list response structure.
|
|
25
35
|
|
|
@@ -56,9 +66,14 @@ def register_blueprint_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
56
66
|
"blueprints": blueprints,
|
|
57
67
|
}
|
|
58
68
|
|
|
59
|
-
@
|
|
69
|
+
@tool(
|
|
70
|
+
name="ha_get_blueprint",
|
|
71
|
+
tags={"Blueprints"},
|
|
72
|
+
annotations={"idempotentHint": True, "readOnlyHint": True, "title": "Get Blueprint"},
|
|
73
|
+
)
|
|
60
74
|
@log_tool_usage
|
|
61
75
|
async def ha_get_blueprint(
|
|
76
|
+
self,
|
|
62
77
|
path: Annotated[
|
|
63
78
|
str | None,
|
|
64
79
|
Field(
|
|
@@ -107,7 +122,7 @@ def register_blueprint_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
107
122
|
))
|
|
108
123
|
|
|
109
124
|
# Get list of blueprints
|
|
110
|
-
list_response = await
|
|
125
|
+
list_response = await self._client.send_websocket_message(
|
|
111
126
|
{"type": "blueprint/list", "domain": domain}
|
|
112
127
|
)
|
|
113
128
|
|
|
@@ -122,7 +137,7 @@ def register_blueprint_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
122
137
|
|
|
123
138
|
# If no path provided, return list of all blueprints
|
|
124
139
|
if path is None:
|
|
125
|
-
return _format_blueprint_list(blueprints_data, domain)
|
|
140
|
+
return self._format_blueprint_list(blueprints_data, domain)
|
|
126
141
|
|
|
127
142
|
# Path provided - get specific blueprint details
|
|
128
143
|
if path not in blueprints_data:
|
|
@@ -183,9 +198,14 @@ def register_blueprint_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
183
198
|
],
|
|
184
199
|
)
|
|
185
200
|
|
|
186
|
-
@
|
|
201
|
+
@tool(
|
|
202
|
+
name="ha_import_blueprint",
|
|
203
|
+
tags={"Blueprints"},
|
|
204
|
+
annotations={"destructiveHint": True, "title": "Import Blueprint"},
|
|
205
|
+
)
|
|
187
206
|
@log_tool_usage
|
|
188
207
|
async def ha_import_blueprint(
|
|
208
|
+
self,
|
|
189
209
|
url: Annotated[
|
|
190
210
|
str,
|
|
191
211
|
Field(
|
|
@@ -224,7 +244,7 @@ def register_blueprint_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
224
244
|
))
|
|
225
245
|
|
|
226
246
|
# Send WebSocket command to import blueprint
|
|
227
|
-
response = await
|
|
247
|
+
response = await self._client.send_websocket_message(
|
|
228
248
|
{"type": "blueprint/import", "url": url}
|
|
229
249
|
)
|
|
230
250
|
|
|
@@ -272,7 +292,7 @@ def register_blueprint_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
272
292
|
suggested_filename = suggested_filename + ".yaml"
|
|
273
293
|
|
|
274
294
|
# Save the blueprint to disk (blueprint/import only validates)
|
|
275
|
-
save_response = await
|
|
295
|
+
save_response = await self._client.send_websocket_message(
|
|
276
296
|
{
|
|
277
297
|
"type": "blueprint/save",
|
|
278
298
|
"domain": domain,
|
|
@@ -333,3 +353,8 @@ def register_blueprint_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
333
353
|
"Try importing from a different source (GitHub, Community, direct URL)",
|
|
334
354
|
],
|
|
335
355
|
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def register_blueprint_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
359
|
+
"""Register Home Assistant blueprint management tools."""
|
|
360
|
+
register_tool_methods(mcp, BlueprintTools(client))
|