ha-mcp-dev 7.5.0.dev565__tar.gz → 7.5.0.dev567__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 (113) hide show
  1. {ha_mcp_dev-7.5.0.dev565/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev567}/PKG-INFO +8 -1
  2. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/README.md +7 -0
  3. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/pyproject.toml +1 -1
  4. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_areas.py +197 -69
  5. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_history.py +267 -147
  6. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_search.py +561 -44
  7. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_services.py +79 -5
  8. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/util_helpers.py +99 -4
  9. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567/src/ha_mcp_dev.egg-info}/PKG-INFO +8 -1
  10. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/LICENSE +0 -0
  11. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/MANIFEST.in +0 -0
  12. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/setup.cfg +0 -0
  13. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/__main__.py +0 -0
  15. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/_pypi_marker +0 -0
  16. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/_version.py +0 -0
  17. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/auth/__init__.py +0 -0
  18. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/auth/consent_form.py +0 -0
  19. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/auth/provider.py +0 -0
  20. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/client/__init__.py +0 -0
  21. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/client/rest_client.py +0 -0
  22. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/client/supervisor_client.py +0 -0
  23. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/client/websocket_client.py +0 -0
  24. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/client/websocket_listener.py +0 -0
  25. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/config.py +0 -0
  26. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/errors.py +0 -0
  27. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/py.typed +0 -0
  28. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  29. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  30. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  31. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  32. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  33. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  34. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  35. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  36. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  37. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  38. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  39. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  40. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  41. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  42. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  43. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  44. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  45. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  46. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  47. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  48. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  49. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  50. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/server.py +0 -0
  51. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/settings_ui.py +0 -0
  52. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/smoke_test.py +0 -0
  53. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/__init__.py +0 -0
  54. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/backup.py +0 -0
  55. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  56. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/device_control.py +0 -0
  57. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/enhanced.py +0 -0
  58. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/helpers.py +0 -0
  59. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/reference_validator.py +0 -0
  60. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/registry.py +0 -0
  61. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/smart_search.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_addons.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_calendar.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_camera.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_categories.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_code.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_energy.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_entities.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_groups.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_hacs.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_integrations.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_labels.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_registry.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_resources.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_service.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_system.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_todo.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_traces.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_updates.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_utility.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/tools/tools_zones.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/transforms/__init__.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/transforms/categorized_search.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/utils/__init__.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/utils/config_hash.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/utils/data_paths.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/utils/domain_handlers.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/utils/operation_manager.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/utils/python_sandbox.py +0 -0
  105. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp/utils/usage_logger.py +0 -0
  106. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  107. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  108. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  109. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  110. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  111. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/tests/__init__.py +0 -0
  112. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/tests/test_constants.py +0 -0
  113. {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev567}/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.5.0.dev565
3
+ Version: 7.5.0.dev567
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
@@ -383,6 +383,13 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
383
383
  - **[@w3z315](https://github.com/w3z315)** — Financial support via [GitHub Sponsors](https://github.com/sponsors/julienld). Thank you! ☕
384
384
  - **[@griffinmartin](https://github.com/griffinmartin)** — Added OpenCode (by Anomaly) as a selectable AI client in the setup wizard, with both stdio and streamable HTTP support.
385
385
  - **[@hhopke](https://github.com/hhopke)** — Fixed addon API calls to route through HA Core ingress proxy instead of direct container connections, fixing `ha_manage_addon` proxy mode on addon installs.
386
+ - **[@tomwilkie](https://github.com/tomwilkie)** — JMESPath middleware exploration (#1147) whose review-time token-measurement data informed the design of #1199 and #1225.
387
+ - **[@SealKan](https://github.com/SealKan)** — `fields=`/`attribute_keys=` projection on six read-heavy tools (#1225), `ha_call_event` tool (#1239), and dashboards-list helper refactor (#1207).
388
+ - **[@KarelTestSpecial](https://github.com/KarelTestSpecial)** — Cached YAML instance to prevent CPU spikes during bulk edits (#1371).
389
+ - **[@corgan2222](https://github.com/corgan2222)** — HA brand assets for custom integration (#1317).
390
+ - **[@drseanwing](https://github.com/drseanwing)** — Progress emission via FastMCP `Context` in long-running tools (#1124); tool-discovery / categorized-search docs (#1123).
391
+ - **[@fnordpig](https://github.com/fnordpig)** — Config subentry support (#1393) and Assist pipeline management tool (#1392).
392
+ - **[@paul43210](https://github.com/paul43210)** — `array_patch` mode in `ha_manage_addon` for atomic GET-modify-POST (#1063).
386
393
 
387
394
  ---
388
395
 
@@ -353,6 +353,13 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
353
353
  - **[@w3z315](https://github.com/w3z315)** — Financial support via [GitHub Sponsors](https://github.com/sponsors/julienld). Thank you! ☕
354
354
  - **[@griffinmartin](https://github.com/griffinmartin)** — Added OpenCode (by Anomaly) as a selectable AI client in the setup wizard, with both stdio and streamable HTTP support.
355
355
  - **[@hhopke](https://github.com/hhopke)** — Fixed addon API calls to route through HA Core ingress proxy instead of direct container connections, fixing `ha_manage_addon` proxy mode on addon installs.
356
+ - **[@tomwilkie](https://github.com/tomwilkie)** — JMESPath middleware exploration (#1147) whose review-time token-measurement data informed the design of #1199 and #1225.
357
+ - **[@SealKan](https://github.com/SealKan)** — `fields=`/`attribute_keys=` projection on six read-heavy tools (#1225), `ha_call_event` tool (#1239), and dashboards-list helper refactor (#1207).
358
+ - **[@KarelTestSpecial](https://github.com/KarelTestSpecial)** — Cached YAML instance to prevent CPU spikes during bulk edits (#1371).
359
+ - **[@corgan2222](https://github.com/corgan2222)** — HA brand assets for custom integration (#1317).
360
+ - **[@drseanwing](https://github.com/drseanwing)** — Progress emission via FastMCP `Context` in long-running tools (#1124); tool-discovery / categorized-search docs (#1123).
361
+ - **[@fnordpig](https://github.com/fnordpig)** — Config subentry support (#1393) and Assist pipeline management tool (#1392).
362
+ - **[@paul43210](https://github.com/paul43210)** — `array_patch` mode in `ha_manage_addon` for atomic GET-modify-POST (#1063).
356
363
 
357
364
  ---
358
365
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.5.0.dev565"
7
+ version = "7.5.0.dev567"
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"
@@ -12,7 +12,7 @@ from fastmcp.exceptions import ToolError
12
12
  from fastmcp.tools import tool
13
13
  from pydantic import Field
14
14
 
15
- from ..errors import ErrorCode, create_error_response
15
+ from ..errors import ErrorCode, create_error_response, create_validation_error
16
16
  from .helpers import (
17
17
  exception_to_structured_error,
18
18
  log_tool_usage,
@@ -20,7 +20,12 @@ from .helpers import (
20
20
  register_tool_methods,
21
21
  validate_identifier_not_empty,
22
22
  )
23
- from .util_helpers import parse_string_list_param
23
+ from .util_helpers import (
24
+ parse_string_list_param,
25
+ project_fields,
26
+ project_records,
27
+ result_fields_warning,
28
+ )
24
29
 
25
30
  logger = logging.getLogger(__name__)
26
31
 
@@ -130,15 +135,65 @@ class AreaTools:
130
135
  @tool(
131
136
  name="ha_config_list_areas",
132
137
  tags={"Areas & Floors"},
133
- annotations={"idempotentHint": True, "readOnlyHint": True, "title": "List Areas"},
138
+ annotations={
139
+ "idempotentHint": True,
140
+ "readOnlyHint": True,
141
+ "title": "List Areas",
142
+ },
134
143
  )
135
144
  @log_tool_usage
136
- async def ha_config_list_areas(self) -> dict[str, Any]:
145
+ async def ha_config_list_areas(
146
+ self,
147
+ fields: Annotated[
148
+ str | list[str] | None,
149
+ Field(
150
+ default=None,
151
+ description=(
152
+ "Return only the specified top-level response keys to reduce "
153
+ 'response size (e.g. ["areas"]). '
154
+ "None = full response (default). "
155
+ "Available keys: success, count, areas, message."
156
+ ),
157
+ ),
158
+ ] = None,
159
+ area_fields: Annotated[
160
+ str | list[str] | None,
161
+ Field(
162
+ default=None,
163
+ description=(
164
+ "Project each area record to only the specified keys. "
165
+ 'E.g. ["area_id", "name"] returns slim area records. '
166
+ "None = full records (default). Unknown keys yield empty records. "
167
+ "Available keys: area_id, name, icon, floor_id, aliases, picture, labels."
168
+ ),
169
+ ),
170
+ ] = None,
171
+ ) -> dict[str, Any]:
137
172
  """
138
173
  List all Home Assistant areas (rooms).
139
174
 
140
175
  Returns area ID, name, icon, floor assignment, aliases, and picture URL.
141
176
  """
177
+ parsed_fields: list[str] | None = None
178
+ if fields is not None:
179
+ try:
180
+ parsed_fields = parse_string_list_param(
181
+ fields, "fields", allow_csv=True
182
+ )
183
+ except ValueError as exc:
184
+ raise_tool_error(create_validation_error(str(exc), parameter="fields"))
185
+ parsed_area_fields: list[str] | None = None
186
+ if area_fields is not None:
187
+ try:
188
+ parsed_area_fields = parse_string_list_param(
189
+ area_fields, "area_fields", allow_csv=True
190
+ )
191
+ if parsed_area_fields is not None and len(parsed_area_fields) == 0:
192
+ raise ValueError("area_fields must contain at least one key")
193
+ except ValueError as exc:
194
+ raise_tool_error(
195
+ create_validation_error(str(exc), parameter="area_fields")
196
+ )
142
197
  try:
143
198
  message: dict[str, Any] = {
144
199
  "type": "config/area_registry/list",
@@ -148,26 +203,42 @@ class AreaTools:
148
203
 
149
204
  if result.get("success"):
150
205
  areas = result.get("result", [])
151
- return {
206
+ _orig_areas = areas
207
+ if parsed_area_fields is not None:
208
+ areas = project_records(areas, parsed_area_fields)
209
+ response: dict[str, Any] = {
152
210
  "success": True,
153
211
  "count": len(areas),
154
212
  "areas": areas,
155
213
  "message": f"Found {len(areas)} area(s)",
156
214
  }
215
+ if parsed_area_fields is not None:
216
+ _warn = result_fields_warning(
217
+ _orig_areas, areas, parsed_area_fields, param_name="area_fields"
218
+ )
219
+ if _warn:
220
+ response.setdefault("warnings", []).append(_warn)
221
+ return project_fields(response, parsed_fields)
157
222
  else:
158
- raise_tool_error(create_error_response(
159
- ErrorCode.SERVICE_CALL_FAILED,
160
- result.get("error", "Failed to list areas"),
161
- ))
223
+ raise_tool_error(
224
+ create_error_response(
225
+ ErrorCode.SERVICE_CALL_FAILED,
226
+ result.get("error", "Failed to list areas"),
227
+ )
228
+ )
162
229
 
163
230
  except ToolError:
164
231
  raise
165
232
  except Exception as e:
166
233
  logger.error(f"Error listing areas: {e}")
167
- exception_to_structured_error(e, context={"operation": "list_areas"}, suggestions=[
168
- "Check Home Assistant connection",
169
- "Verify WebSocket connection is active",
170
- ])
234
+ exception_to_structured_error(
235
+ e,
236
+ context={"operation": "list_areas"},
237
+ suggestions=[
238
+ "Check Home Assistant connection",
239
+ "Verify WebSocket connection is active",
240
+ ],
241
+ )
171
242
 
172
243
  # ============================================================
173
244
  # FLOOR TOOLS
@@ -176,7 +247,11 @@ class AreaTools:
176
247
  @tool(
177
248
  name="ha_config_list_floors",
178
249
  tags={"Areas & Floors"},
179
- annotations={"idempotentHint": True, "readOnlyHint": True, "title": "List Floors"},
250
+ annotations={
251
+ "idempotentHint": True,
252
+ "readOnlyHint": True,
253
+ "title": "List Floors",
254
+ },
180
255
  )
181
256
  @log_tool_usage
182
257
  async def ha_config_list_floors(self) -> dict[str, Any]:
@@ -201,24 +276,34 @@ class AreaTools:
201
276
  "message": f"Found {len(floors)} floor(s)",
202
277
  }
203
278
  else:
204
- raise_tool_error(create_error_response(
205
- ErrorCode.SERVICE_CALL_FAILED,
206
- result.get("error", "Failed to list floors"),
207
- ))
279
+ raise_tool_error(
280
+ create_error_response(
281
+ ErrorCode.SERVICE_CALL_FAILED,
282
+ result.get("error", "Failed to list floors"),
283
+ )
284
+ )
208
285
 
209
286
  except ToolError:
210
287
  raise
211
288
  except Exception as e:
212
289
  logger.error(f"Error listing floors: {e}")
213
- exception_to_structured_error(e, context={"operation": "list_floors"}, suggestions=[
214
- "Check Home Assistant connection",
215
- "Verify WebSocket connection is active",
216
- ])
290
+ exception_to_structured_error(
291
+ e,
292
+ context={"operation": "list_floors"},
293
+ suggestions=[
294
+ "Check Home Assistant connection",
295
+ "Verify WebSocket connection is active",
296
+ ],
297
+ )
217
298
 
218
299
  @tool(
219
300
  name="ha_list_floors_areas",
220
301
  tags={"Areas & Floors"},
221
- annotations={"idempotentHint": True, "readOnlyHint": True, "title": "List Floors and Areas"},
302
+ annotations={
303
+ "idempotentHint": True,
304
+ "readOnlyHint": True,
305
+ "title": "List Floors and Areas",
306
+ },
222
307
  )
223
308
  @log_tool_usage
224
309
  async def ha_list_floors_areas(self) -> dict[str, Any]:
@@ -251,20 +336,22 @@ class AreaTools:
251
336
  areas_ok = areas_result.get("success") and "result" in areas_result
252
337
  floors_ok = floors_result.get("success") and "result" in floors_result
253
338
  if not (areas_ok and floors_ok):
254
- raise_tool_error(create_error_response(
255
- ErrorCode.SERVICE_CALL_FAILED,
256
- "Failed to retrieve area or floor registry",
257
- context={
258
- "areas_success": areas_result.get("success"),
259
- "floors_success": floors_result.get("success"),
260
- "areas_response_keys": sorted(areas_result.keys()),
261
- "floors_response_keys": sorted(floors_result.keys()),
262
- },
263
- suggestions=[
264
- "Check Home Assistant connection",
265
- "Verify WebSocket connection is active",
266
- ],
267
- ))
339
+ raise_tool_error(
340
+ create_error_response(
341
+ ErrorCode.SERVICE_CALL_FAILED,
342
+ "Failed to retrieve area or floor registry",
343
+ context={
344
+ "areas_success": areas_result.get("success"),
345
+ "floors_success": floors_result.get("success"),
346
+ "areas_response_keys": sorted(areas_result.keys()),
347
+ "floors_response_keys": sorted(floors_result.keys()),
348
+ },
349
+ suggestions=[
350
+ "Check Home Assistant connection",
351
+ "Verify WebSocket connection is active",
352
+ ],
353
+ )
354
+ )
268
355
 
269
356
  areas = areas_result["result"]
270
357
  floors = floors_result["result"]
@@ -361,7 +448,10 @@ class AreaTools:
361
448
  @tool(
362
449
  name="ha_set_area_or_floor",
363
450
  tags={"Areas & Floors"},
364
- annotations={"destructiveHint": True, "title": "Create or Update Area or Floor"},
451
+ annotations={
452
+ "destructiveHint": True,
453
+ "title": "Create or Update Area or Floor",
454
+ },
365
455
  )
366
456
  @log_tool_usage
367
457
  async def ha_set_area_or_floor(
@@ -439,10 +529,12 @@ class AreaTools:
439
529
  try:
440
530
  parsed_aliases = parse_string_list_param(aliases, "aliases")
441
531
  except ValueError as e:
442
- raise_tool_error(create_error_response(
443
- ErrorCode.VALIDATION_INVALID_PARAMETER,
444
- f"Invalid aliases parameter: {e}",
445
- ))
532
+ raise_tool_error(
533
+ create_error_response(
534
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
535
+ f"Invalid aliases parameter: {e}",
536
+ )
537
+ )
446
538
 
447
539
  # Reject cross-kind params loudly so silent intent loss can't happen
448
540
  # (e.g., kind='floor' with picture='...' previously dropped the picture
@@ -456,15 +548,17 @@ class AreaTools:
456
548
  if picture is not None:
457
549
  cross_kind_params.append("picture")
458
550
  if cross_kind_params:
459
- raise_tool_error(create_error_response(
460
- ErrorCode.VALIDATION_INVALID_PARAMETER,
461
- f"Parameter(s) {cross_kind_params} are not valid for kind={kind!r}",
462
- context={"kind": kind, "invalid_parameters": cross_kind_params},
463
- suggestions=[
464
- "For kind='area' use: name, id, floor_id, icon, aliases, picture",
465
- "For kind='floor' use: name, id, level, icon, aliases",
466
- ],
467
- ))
551
+ raise_tool_error(
552
+ create_error_response(
553
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
554
+ f"Parameter(s) {cross_kind_params} are not valid for kind={kind!r}",
555
+ context={"kind": kind, "invalid_parameters": cross_kind_params},
556
+ suggestions=[
557
+ "For kind='area' use: name, id, floor_id, icon, aliases, picture",
558
+ "For kind='floor' use: name, id, level, icon, aliases",
559
+ ],
560
+ )
561
+ )
468
562
 
469
563
  # ``None`` stays the documented "create-new" sentinel; explicit
470
564
  # empty/whitespace would silently route to the ``if id:`` create
@@ -483,7 +577,12 @@ class AreaTools:
483
577
  if kind == "area":
484
578
  if id:
485
579
  message = self._build_area_update_message(
486
- id, name, floor_id, icon, parsed_aliases, picture,
580
+ id,
581
+ name,
582
+ floor_id,
583
+ icon,
584
+ parsed_aliases,
585
+ picture,
487
586
  )
488
587
  operation = "update"
489
588
  else:
@@ -497,7 +596,11 @@ class AreaTools:
497
596
  suggestions=["Provide a non-empty name for the new area"],
498
597
  )
499
598
  message = self._build_area_create_message(
500
- name, floor_id, icon, parsed_aliases, picture,
599
+ name,
600
+ floor_id,
601
+ icon,
602
+ parsed_aliases,
603
+ picture,
501
604
  )
502
605
  operation = "create"
503
606
  result_key = "area"
@@ -505,7 +608,11 @@ class AreaTools:
505
608
  else: # kind == "floor"
506
609
  if id:
507
610
  message = self._build_floor_update_message(
508
- id, name, level, icon, parsed_aliases,
611
+ id,
612
+ name,
613
+ level,
614
+ icon,
615
+ parsed_aliases,
509
616
  )
510
617
  operation = "update"
511
618
  else:
@@ -517,7 +624,10 @@ class AreaTools:
517
624
  suggestions=["Provide a non-empty name for the new floor"],
518
625
  )
519
626
  message = self._build_floor_create_message(
520
- name, level, icon, parsed_aliases,
627
+ name,
628
+ level,
629
+ icon,
630
+ parsed_aliases,
521
631
  )
522
632
  operation = "create"
523
633
  result_key = "floor"
@@ -538,17 +648,23 @@ class AreaTools:
538
648
  }
539
649
 
540
650
  error = result.get("error", {})
541
- error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
651
+ error_msg = (
652
+ error.get("message", str(error))
653
+ if isinstance(error, dict)
654
+ else str(error)
655
+ )
542
656
  ctx: dict[str, Any] = {"operation": operation, "kind": kind}
543
657
  if name:
544
658
  ctx["name"] = name
545
659
  if id:
546
660
  ctx[id_key] = id
547
- raise_tool_error(create_error_response(
548
- ErrorCode.SERVICE_CALL_FAILED,
549
- f"Failed to {operation} {kind}: {error_msg}",
550
- context=ctx,
551
- ))
661
+ raise_tool_error(
662
+ create_error_response(
663
+ ErrorCode.SERVICE_CALL_FAILED,
664
+ f"Failed to {operation} {kind}: {error_msg}",
665
+ context=ctx,
666
+ )
667
+ )
552
668
 
553
669
  except ToolError:
554
670
  raise
@@ -570,7 +686,11 @@ class AreaTools:
570
686
  @tool(
571
687
  name="ha_remove_area_or_floor",
572
688
  tags={"Areas & Floors"},
573
- annotations={"destructiveHint": True, "idempotentHint": True, "title": "Remove Area or Floor"},
689
+ annotations={
690
+ "destructiveHint": True,
691
+ "idempotentHint": True,
692
+ "title": "Remove Area or Floor",
693
+ },
574
694
  )
575
695
  @log_tool_usage
576
696
  async def ha_remove_area_or_floor(
@@ -581,7 +701,9 @@ class AreaTools:
581
701
  ],
582
702
  id: Annotated[ # noqa: A002
583
703
  str,
584
- Field(description="Area ID or floor ID to delete (use ha_list_floors_areas to find IDs)"),
704
+ Field(
705
+ description="Area ID or floor ID to delete (use ha_list_floors_areas to find IDs)"
706
+ ),
585
707
  ],
586
708
  ) -> dict[str, Any]:
587
709
  """Remove a Home Assistant area or floor.
@@ -618,12 +740,18 @@ class AreaTools:
618
740
  }
619
741
 
620
742
  error = result.get("error", {})
621
- error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
622
- raise_tool_error(create_error_response(
623
- ErrorCode.SERVICE_CALL_FAILED,
624
- f"Failed to remove {kind}: {error_msg}",
625
- context={"kind": kind, id_key: id},
626
- ))
743
+ error_msg = (
744
+ error.get("message", str(error))
745
+ if isinstance(error, dict)
746
+ else str(error)
747
+ )
748
+ raise_tool_error(
749
+ create_error_response(
750
+ ErrorCode.SERVICE_CALL_FAILED,
751
+ f"Failed to remove {kind}: {error_msg}",
752
+ context={"kind": kind, id_key: id},
753
+ )
754
+ )
627
755
 
628
756
  except ToolError:
629
757
  raise