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.
Files changed (101) hide show
  1. {ha_mcp_dev-7.2.0.dev348/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.2.0.dev349}/PKG-INFO +4 -4
  2. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/README.md +3 -3
  3. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/pyproject.toml +3 -15
  4. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_areas.py +189 -107
  5. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_blueprints.py +34 -9
  6. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_calendar.py +38 -9
  7. ha_mcp_dev-7.2.0.dev349/src/ha_mcp/tools/tools_config_entry_flow.py +574 -0
  8. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_config_scripts.py +93 -55
  9. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_groups.py +125 -73
  10. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_integrations.py +225 -165
  11. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_mcp_component.py +152 -109
  12. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_service.py +137 -88
  13. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_services.py +127 -106
  14. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_system.py +228 -169
  15. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_todo.py +201 -129
  16. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349/src/ha_mcp_dev.egg-info}/PKG-INFO +4 -4
  17. ha_mcp_dev-7.2.0.dev348/src/ha_mcp/tools/tools_config_entry_flow.py +0 -493
  18. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/LICENSE +0 -0
  19. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/MANIFEST.in +0 -0
  20. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/setup.cfg +0 -0
  21. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/__init__.py +0 -0
  22. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/__main__.py +0 -0
  23. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/_pypi_marker +0 -0
  24. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/auth/__init__.py +0 -0
  25. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/auth/consent_form.py +0 -0
  26. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/auth/provider.py +0 -0
  27. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/client/__init__.py +0 -0
  28. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/client/rest_client.py +0 -0
  29. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/client/websocket_client.py +0 -0
  30. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/client/websocket_listener.py +0 -0
  31. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/config.py +0 -0
  32. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/errors.py +0 -0
  33. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/py.typed +0 -0
  34. {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
  35. {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
  36. {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
  37. {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
  38. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  39. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  40. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  41. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  42. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  43. {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
  44. {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
  45. {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
  46. {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
  47. {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
  48. {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
  49. {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
  50. {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
  51. {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
  52. {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
  53. {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
  54. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/server.py +0 -0
  55. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/smoke_test.py +0 -0
  56. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/__init__.py +0 -0
  57. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/backup.py +0 -0
  58. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  59. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/device_control.py +0 -0
  60. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/enhanced.py +0 -0
  61. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/helpers.py +0 -0
  62. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/registry.py +0 -0
  63. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/smart_search.py +0 -0
  64. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_addons.py +0 -0
  65. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  66. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_camera.py +0 -0
  67. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_categories.py +0 -0
  68. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  69. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  70. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  71. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_entities.py +0 -0
  72. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  73. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_hacs.py +0 -0
  74. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_history.py +0 -0
  75. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_labels.py +0 -0
  76. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_registry.py +0 -0
  77. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_resources.py +0 -0
  78. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_search.py +0 -0
  79. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_traces.py +0 -0
  80. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_updates.py +0 -0
  81. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_utility.py +0 -0
  82. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  83. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  84. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/tools_zones.py +0 -0
  85. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/tools/util_helpers.py +0 -0
  86. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/transforms/__init__.py +0 -0
  87. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/transforms/categorized_search.py +0 -0
  88. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/utils/__init__.py +0 -0
  89. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/utils/domain_handlers.py +0 -0
  90. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  91. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/utils/operation_manager.py +0 -0
  92. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/utils/python_sandbox.py +0 -0
  93. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp/utils/usage_logger.py +0 -0
  94. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  95. {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
  96. {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
  97. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  98. {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
  99. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/tests/__init__.py +0 -0
  100. {ha_mcp_dev-7.2.0.dev348 → ha_mcp_dev-7.2.0.dev349}/tests/test_constants.py +0 -0
  101. {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.dev348
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-87-blue" alt="95+ 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 (87 tools)</b></summary>
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`, `ha_get_statistics` |
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-87-blue" alt="95+ 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 (87 tools)</b></summary>
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`, `ha_get_statistics` |
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.dev348"
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 unmigrated tools files (see #925).
137
- # Remove lines as files are migrated to class-based pattern.
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 exception_to_structured_error, log_tool_usage, raise_tool_error
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
- def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
22
- """Register Home Assistant area and floor management tools."""
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
- @mcp.tool(tags={"Areas & Floors"}, annotations={"idempotentHint": True, "readOnlyHint": True, "title": "List Areas"})
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 client.send_websocket_message(message)
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
- @mcp.tool(tags={"Areas & Floors"}, annotations={"destructiveHint": True, "title": "Create or Update Area"})
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
- # UPDATE operation
131
- message: dict[str, Any] = {
132
- "type": "config/area_registry/update",
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
- message = {
160
- "type": "config/area_registry/create",
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 client.send_websocket_message(message)
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
- else:
187
- error = result.get("error", {})
188
- error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
189
- ctx: dict[str, Any] = {"operation": operation}
190
- if name:
191
- ctx["name"] = name
192
- if area_id:
193
- ctx["area_id"] = area_id
194
- raise_tool_error(create_error_response(
195
- ErrorCode.SERVICE_CALL_FAILED,
196
- f"Failed to {operation} area: {error_msg}",
197
- context=ctx,
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
- @mcp.tool(tags={"Areas & Floors"}, annotations={"destructiveHint": True, "idempotentHint": True, "title": "Remove Area"})
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 client.send_websocket_message(message)
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
- @mcp.tool(tags={"Areas & Floors"}, annotations={"idempotentHint": True, "readOnlyHint": True, "title": "List Floors"})
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 client.send_websocket_message(message)
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
- @mcp.tool(tags={"Areas & Floors"}, annotations={"destructiveHint": True, "title": "Create or Update Floor"})
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
- # UPDATE operation
357
- message: dict[str, Any] = {
358
- "type": "config/floor_registry/update",
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
- message = {
384
- "type": "config/floor_registry/create",
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 client.send_websocket_message(message)
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
- else:
409
- error = result.get("error", {})
410
- error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
411
- ctx: dict[str, Any] = {"operation": operation}
412
- if name:
413
- ctx["name"] = name
414
- if floor_id:
415
- ctx["floor_id"] = floor_id
416
- raise_tool_error(create_error_response(
417
- ErrorCode.SERVICE_CALL_FAILED,
418
- f"Failed to {operation} floor: {error_msg}",
419
- context=ctx,
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
- @mcp.tool(tags={"Areas & Floors"}, annotations={"destructiveHint": True, "idempotentHint": True, "title": "Remove Floor"})
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 client.send_websocket_message(message)
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 exception_to_structured_error, log_tool_usage, raise_tool_error
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
- def register_blueprint_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
21
- """Register Home Assistant blueprint management tools."""
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
- @mcp.tool(tags={"Blueprints"}, annotations={"idempotentHint": True, "readOnlyHint": True, "title": "Get Blueprint"})
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 client.send_websocket_message(
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
- @mcp.tool(tags={"Blueprints"}, annotations={"destructiveHint": True, "title": "Import Blueprint"})
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 client.send_websocket_message(
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 client.send_websocket_message(
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))