Skip to content

Commit 7f57e2c

Browse files
Tool consolidation to address issue #424 (#453)
* feat: consolidate list/get tool pairs to reduce tool count Consolidate 5 pairs of ha_list_*/ha_get_* tools into single tools with optional parameters, following the pattern established in issue #420 for ha_get_blueprint. Tool consolidations: - ha_list_updates + ha_get_release_notes → ha_get_updates(entity_id=None) - ha_list_zones → ha_get_zone(zone_id=None) - ha_list_devices + ha_get_device → ha_get_device(device_id=None, manufacturer=None) - ha_list_exposed_entities + ha_get_entity_exposure → ha_get_entity_exposure(entity_id=None) - ha_list_integrations + ha_get_config_entry → ha_get_integration(entry_id=None) Net reduction: 4 tools (from 105 to 101) The consolidated tools now support both modes: - Without identifier: Lists all items (with optional filters) - With identifier: Returns detailed info for specific item Closes #424 * chore: update uv.lock * feat: consolidate additional list tools (round 2) - reduce to 97 tools Consolidate 4 more tool pairs to further reduce tool count: - ha_config_list_dashboards → ha_config_get_dashboard(list_only=True) - ha_config_list_labels → ha_config_get_label(label_id=None) - ha_list_addons + ha_list_available_addons → ha_get_addon(source=None) - ha_list_todo_lists + ha_get_todo_items → ha_get_todo(entity_id=None) Tool count: 101 → 97 (saved 4 tools) Combined with first round: 105 → 97 (saved 8 tools total) All consolidated tools follow the established pattern: - When optional ID/filter param is None → list mode - When param provided → get specific item mode Updated tests and documentation accordingly. Closes #424 * fix: update voice assistant tests to match consolidated tool API The ha_get_entity_exposure tool returns different response structures: - When entity_id is provided: returns exposed_to, is_exposed_anywhere, etc. - When listing all: returns exposed_entities, count, assistant_filter Updated tests to match the actual implementation: - Use assistant_filter instead of filters_applied.assistant - Use entity-specific response keys when entity_id is provided * fix: remove `or True` expressions that ignored user-provided False values The `or True` pattern used to satisfy the type checker was causing `coerce_bool_param(..., default=True) or True` to always return True, even when the user explicitly passed False for preserve_voice_exposure, parallel, or report_errors parameters. Fixed by replacing `or True` with an assert for type narrowing, which correctly preserves user input while satisfying the type checker. Affected tools: - ha_rename_entity(): preserve_voice_exposure parameter - ha_rename_entity_and_device(): preserve_voice_exposure parameter - ha_bulk_control(): parallel parameter - ha_render_template(): report_errors parameter Fixes bug identified by Gemini Code Assist review. * chore: update uv.lock * fix: validate credentials in standard mode entry points The config now has OAuth-mode placeholder defaults for homeassistant_url and homeassistant_token. In standard mode (main, main_web, main_sse), validate that real credentials are provided - not the OAuth placeholders. Also updates test mocks to match current function signatures: - mock_run_async now accepts show_banner kwarg - mock_run_http now accepts default_port kwarg - HTTP entry point tests now provide mock credentials This restores the expected behavior where missing credentials show a friendly error message and exit with non-zero status. * fix: update e2e tests to use consolidated tool names The tool consolidation PR renamed/consolidated these tools: - ha_config_list_dashboards -> ha_config_get_dashboard(list_only=True) - ha_get_config_entry -> ha_get_integration Update e2e tests to use the new consolidated tool names. * fix: update voice assistant tests to match consolidated tool API Change ha_get_entity_exposure response to use filters_applied dict instead of assistant_filter to match the expected e2e test API. - Rename assistant_filter -> filters_applied["assistant"] - Update unit test to match new response structure * fix: add pre-flight HACS availability check for install tests Add pre-flight HACS availability check using ha_hacs_info before attempting HACS install operations. This prevents flaky test failures when HACS is rate-limited or temporarily unavailable. Affects: - test_install_mcp_tools_basic - test_install_mcp_tools_idempotent - test_check_mcp_tools_in_hacs --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent aa194e1 commit 7f57e2c

31 files changed

Lines changed: 959 additions & 1023 deletions

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,23 +120,23 @@ Spend less time configuring, more time enjoying your smart home.
120120
| **Automations** | `ha_config_get_automation`, `ha_config_set_automation`, `ha_config_remove_automation` |
121121
| **Scripts** | `ha_config_get_script`, `ha_config_set_script`, `ha_config_remove_script` |
122122
| **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_config_remove_helper` |
123-
| **Dashboards** | `ha_config_list_dashboards`, `ha_config_get_dashboard`, `ha_config_set_dashboard`, `ha_config_update_dashboard_metadata`, `ha_config_delete_dashboard`, `ha_get_dashboard_guide`, `ha_get_card_types`, `ha_get_card_documentation` |
123+
| **Dashboards** | `ha_config_get_dashboard`, `ha_config_set_dashboard`, `ha_config_update_dashboard_metadata`, `ha_config_delete_dashboard`, `ha_get_dashboard_guide`, `ha_get_card_types`, `ha_get_card_documentation` |
124124
| **Areas & Floors** | `ha_config_list_areas`, `ha_config_set_area`, `ha_config_remove_area`, `ha_config_list_floors`, `ha_config_set_floor`, `ha_config_remove_floor` |
125-
| **Labels** | `ha_config_list_labels`, `ha_config_get_label`, `ha_config_set_label`, `ha_config_remove_label`, `ha_manage_entity_labels` |
126-
| **Zones** | `ha_list_zones`, `ha_create_zone`, `ha_update_zone`, `ha_delete_zone` |
125+
| **Labels** | `ha_config_get_label`, `ha_config_set_label`, `ha_config_remove_label`, `ha_manage_entity_labels` |
126+
| **Zones** | `ha_get_zone`, `ha_create_zone`, `ha_update_zone`, `ha_delete_zone` |
127127
| **Groups** | `ha_config_list_groups`, `ha_config_set_group`, `ha_config_remove_group` |
128-
| **Todo Lists** | `ha_list_todo_lists`, `ha_get_todo_items`, `ha_add_todo_item`, `ha_update_todo_item`, `ha_remove_todo_item` |
128+
| **Todo Lists** | `ha_get_todo`, `ha_add_todo_item`, `ha_update_todo_item`, `ha_remove_todo_item` |
129129
| **Calendar** | `ha_config_get_calendar_events`, `ha_config_set_calendar_event`, `ha_config_remove_calendar_event` |
130130
| **Blueprints** | `ha_list_blueprints`, `ha_get_blueprint`, `ha_import_blueprint` |
131-
| **Device Registry** | `ha_list_devices`, `ha_get_device`, `ha_update_device`, `ha_remove_device`, `ha_rename_entity` |
131+
| **Device Registry** | `ha_get_device`, `ha_update_device`, `ha_remove_device`, `ha_rename_entity` |
132132
| **ZHA & Integrations** | `ha_get_zha_devices`, `ha_get_entity_integration_source` |
133-
| **Add-ons** | `ha_list_addons`, `ha_list_available_addons` |
133+
| **Add-ons** | `ha_get_addon` |
134134
| **Camera** | `ha_get_camera_image` |
135135
| **History & Statistics** | `ha_get_history`, `ha_get_statistics` |
136136
| **Automation Traces** | `ha_get_automation_traces` |
137-
| **System & Updates** | `ha_check_config`, `ha_restart`, `ha_reload_core`, `ha_get_system_info`, `ha_get_system_health`, `ha_list_updates`, `ha_get_release_notes`, `ha_get_system_version` |
137+
| **System & Updates** | `ha_check_config`, `ha_restart`, `ha_reload_core`, `ha_get_system_info`, `ha_get_system_health`, `ha_get_updates` |
138138
| **Backup & Restore** | `ha_backup_create`, `ha_backup_restore` |
139-
| **Utility** | `ha_get_logbook`, `ha_eval_template`, `ha_get_domain_docs`, `ha_list_integrations` |
139+
| **Utility** | `ha_get_logbook`, `ha_eval_template`, `ha_get_domain_docs`, `ha_get_integration` |
140140

141141
</details>
142142

homeassistant-addon/DOCS.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -291,19 +291,19 @@ The add-on provides 80+ MCP tools for controlling Home Assistant:
291291
- **Scripts**: `ha_config_get_script`, `ha_config_set_script`, `ha_config_remove_script`
292292
- **Automations**: `ha_config_get_automation`, `ha_config_set_automation`, `ha_config_remove_automation`
293293
- **Groups**: `ha_config_list_groups`, `ha_config_set_group`, `ha_config_remove_group`
294-
- **Dashboards**: `ha_config_list_dashboards`, `ha_config_get_dashboard`, `ha_config_set_dashboard`, `ha_config_delete_dashboard`
294+
- **Dashboards**: `ha_config_get_dashboard`, `ha_config_set_dashboard`, `ha_config_delete_dashboard`
295295
- **Areas & Floors**: `ha_config_list_areas`, `ha_config_set_area`, `ha_config_remove_area`, `ha_config_list_floors`, `ha_config_set_floor`, `ha_config_remove_floor`
296-
- **Labels**: `ha_config_list_labels`, `ha_config_set_label`, `ha_config_remove_label`, `ha_manage_entity_labels`
297-
- **Zones**: `ha_list_zones`, `ha_create_zone`, `ha_update_zone`, `ha_delete_zone`
296+
- **Labels**: `ha_config_get_label`, `ha_config_set_label`, `ha_config_remove_label`, `ha_manage_entity_labels`
297+
- **Zones**: `ha_get_zone`, `ha_create_zone`, `ha_update_zone`, `ha_delete_zone`
298298

299299
### Todo & Calendar
300-
- **Todo Lists**: `ha_list_todo_lists`, `ha_get_todo_items`, `ha_add_todo_item`, `ha_update_todo_item`, `ha_remove_todo_item`
300+
- **Todo Lists**: `ha_get_todo`, `ha_add_todo_item`, `ha_update_todo_item`, `ha_remove_todo_item`
301301
- **Calendar**: `ha_config_get_calendar_events`, `ha_config_set_calendar_event`, `ha_config_remove_calendar_event`
302302

303303
### Device Control
304304
- `ha_bulk_control` - Multi-device control with verification
305305
- `ha_get_operation_status` - Check operation status
306-
- `ha_list_devices`, `ha_get_device`, `ha_update_device`, `ha_remove_device`
306+
- `ha_get_device`, `ha_update_device`, `ha_remove_device`
307307
- `ha_rename_entity` - Rename entity ID
308308

309309
### History & Monitoring
@@ -317,13 +317,12 @@ The add-on provides 80+ MCP tools for controlling Home Assistant:
317317
- `ha_get_entity_integration_source` - Get integration source for any entity
318318

319319
### Add-ons (Supervisor only)
320-
- `ha_list_addons` - List installed add-ons
321-
- `ha_list_available_addons` - List available add-ons from repositories
320+
- `ha_get_addon` - List installed or available add-ons (source="installed" or "available")
322321

323322
### System & Updates
324323
- `ha_check_config`, `ha_restart`, `ha_reload_core`
325324
- `ha_get_system_info`, `ha_get_system_health`
326-
- `ha_list_updates`, `ha_get_release_notes`, `ha_get_system_version`
325+
- `ha_get_updates` - List updates or get details for a specific update entity
327326

328327
### Blueprints
329328
- `ha_list_blueprints`, `ha_get_blueprint`, `ha_import_blueprint`
@@ -336,7 +335,7 @@ The add-on provides 80+ MCP tools for controlling Home Assistant:
336335
- `ha_get_logbook` - Historical events
337336
- `ha_eval_template` - Evaluate Jinja2 templates
338337
- `ha_get_domain_docs` - Domain documentation
339-
- `ha_list_integrations` - List installed integrations
338+
- `ha_get_integration` - List or get integration info
340339

341340
See the [main repository](https://github.com/homeassistant-ai/ha-mcp) for detailed tool documentation and examples.
342341

src/ha_mcp/__main__.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,22 @@ def main() -> None:
334334
# Configure logging before server creation
335335
from ha_mcp.config import get_settings
336336
settings = get_settings()
337+
338+
# In standard mode (not OAuth), validate that real credentials are provided
339+
# The config has defaults for OAuth mode, but standard mode requires real values
340+
missing_vars = []
341+
if settings.homeassistant_url == "http://oauth-mode":
342+
missing_vars.append(" - HOMEASSISTANT_URL")
343+
if settings.homeassistant_token == "oauth-mode-token":
344+
missing_vars.append(" - HOMEASSISTANT_TOKEN")
345+
346+
if missing_vars:
347+
print(
348+
_CONFIG_ERROR_MESSAGE.format(missing_vars="\n".join(missing_vars)),
349+
file=sys.stderr,
350+
)
351+
sys.exit(1)
352+
337353
logging.basicConfig(
338354
level=getattr(logging, settings.log_level),
339355
format='%(asctime)s %(name)s %(levelname)s: %(message)s'
@@ -483,6 +499,21 @@ def main_web() -> None:
483499
# Configure logging before server creation
484500
from ha_mcp.config import get_settings
485501
settings = get_settings()
502+
503+
# Validate credentials (required in non-OAuth HTTP mode)
504+
missing_vars = []
505+
if settings.homeassistant_url == "http://oauth-mode":
506+
missing_vars.append(" - HOMEASSISTANT_URL")
507+
if settings.homeassistant_token == "oauth-mode-token":
508+
missing_vars.append(" - HOMEASSISTANT_TOKEN")
509+
510+
if missing_vars:
511+
print(
512+
_CONFIG_ERROR_MESSAGE.format(missing_vars="\n".join(missing_vars)),
513+
file=sys.stderr,
514+
)
515+
sys.exit(1)
516+
486517
logging.basicConfig(
487518
level=getattr(logging, settings.log_level),
488519
format='%(asctime)s %(name)s %(levelname)s: %(message)s'
@@ -503,6 +534,21 @@ def main_sse() -> None:
503534
# Configure logging before server creation
504535
from ha_mcp.config import get_settings
505536
settings = get_settings()
537+
538+
# Validate credentials (required in non-OAuth SSE mode)
539+
missing_vars = []
540+
if settings.homeassistant_url == "http://oauth-mode":
541+
missing_vars.append(" - HOMEASSISTANT_URL")
542+
if settings.homeassistant_token == "oauth-mode-token":
543+
missing_vars.append(" - HOMEASSISTANT_TOKEN")
544+
545+
if missing_vars:
546+
print(
547+
_CONFIG_ERROR_MESSAGE.format(missing_vars="\n".join(missing_vars)),
548+
file=sys.stderr,
549+
)
550+
sys.exit(1)
551+
506552
logging.basicConfig(
507553
level=getattr(logging, settings.log_level),
508554
format='%(asctime)s %(name)s %(levelname)s: %(message)s'

src/ha_mcp/tools/tools_addons.py

Lines changed: 44 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -263,86 +263,81 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs) -> Non
263263
**kwargs: Additional arguments (ignored, for auto-discovery compatibility)
264264
"""
265265

266-
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["addon"], "title": "List Installed Add-ons"})
266+
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["addon"], "title": "Get Add-ons"})
267267
@log_tool_usage
268-
async def ha_list_addons(
268+
async def ha_get_addon(
269+
source: Annotated[
270+
str | None,
271+
Field(
272+
description="Add-on source: 'installed' (default) for currently installed add-ons, "
273+
"'available' for add-ons in the store that can be installed.",
274+
default=None,
275+
),
276+
] = None,
269277
include_stats: Annotated[
270278
bool,
271279
Field(
272-
description="Include CPU/memory usage statistics for each add-on",
280+
description="Include CPU/memory usage statistics (only for source='installed')",
273281
default=False,
274282
),
275283
] = False,
276-
) -> dict[str, Any]:
277-
"""
278-
List installed Home Assistant add-ons.
279-
280-
Returns add-ons with version, state (started/stopped), and update availability.
281-
Optionally includes CPU/memory usage statistics.
282-
283-
**Note:** This tool only works with Home Assistant OS or Supervised installations.
284-
285-
**Response includes:**
286-
- Add-on name, slug, description
287-
- Current version and state
288-
- Whether an update is available
289-
- Repository the add-on came from
290-
- Optional: CPU/memory usage stats
291-
292-
**Example Usage:**
293-
- List all add-ons: ha_list_addons()
294-
- List with resource usage: ha_list_addons(include_stats=True)
295-
296-
**Use Cases:**
297-
- Check which add-ons are installed and running
298-
- Monitor add-on health and resource usage
299-
- Find add-ons with available updates
300-
"""
301-
return await list_addons(client, include_stats)
302-
303-
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["addon"], "title": "List Available Add-ons"})
304-
@log_tool_usage
305-
async def ha_list_available_addons(
306284
repository: Annotated[
307285
str | None,
308286
Field(
309-
description="Filter by repository slug (e.g., 'core', 'community')",
287+
description="Filter by repository slug, e.g., 'core', 'community' (only for source='available')",
310288
default=None,
311289
),
312290
] = None,
313291
query: Annotated[
314292
str | None,
315293
Field(
316-
description="Search filter for add-on names/descriptions",
294+
description="Search filter for add-on names/descriptions (only for source='available')",
317295
default=None,
318296
),
319297
] = None,
320298
) -> dict[str, Any]:
321299
"""
322-
List add-ons available in the Home Assistant add-on store.
300+
Get Home Assistant add-ons - list installed or available from store.
323301
324-
Returns add-ons from official and custom repositories that can be installed.
302+
This tool retrieves add-on information based on the source parameter:
303+
- source='installed' (default): Lists currently installed add-ons
304+
- source='available': Lists add-ons available in the add-on store
325305
326306
**Note:** This tool only works with Home Assistant OS or Supervised installations.
327307
328-
**Response includes:**
329-
- List of configured repositories
330-
- Available add-ons with name, slug, description, version
331-
- Installation status for each add-on
332-
- Summary statistics
308+
**INSTALLED ADD-ONS (source='installed'):**
309+
Returns add-ons with version, state (started/stopped), and update availability.
310+
- include_stats: Optionally include CPU/memory usage statistics
333311
334-
**Parameters:**
335-
- repository: Filter results to a specific repository
312+
**AVAILABLE ADD-ONS (source='available'):**
313+
Returns add-ons from official and custom repositories that can be installed.
314+
- repository: Filter by repository slug (e.g., 'core', 'community')
336315
- query: Search by name or description (case-insensitive)
337316
338317
**Example Usage:**
339-
- List all available add-ons: ha_list_available_addons()
340-
- Search for MQTT: ha_list_available_addons(query="mqtt")
341-
- List official add-ons: ha_list_available_addons(repository="core")
318+
- List installed add-ons: ha_get_addon()
319+
- List with resource usage: ha_get_addon(include_stats=True)
320+
- List available add-ons: ha_get_addon(source="available")
321+
- Search for MQTT: ha_get_addon(source="available", query="mqtt")
322+
- List official add-ons: ha_get_addon(source="available", repository="core")
342323
343324
**Use Cases:**
325+
- Check which add-ons are installed and running
326+
- Monitor add-on health and resource usage
327+
- Find add-ons with available updates
344328
- Find add-ons to recommend for user's needs
345329
- Check if a specific add-on is available
346-
- Explore add-on store contents
347330
"""
348-
return await list_available_addons(client, repository, query)
331+
# Default to installed if not specified
332+
effective_source = (source or "installed").lower()
333+
334+
if effective_source == "available":
335+
return await list_available_addons(client, repository, query)
336+
elif effective_source == "installed":
337+
return await list_addons(client, include_stats)
338+
else:
339+
return {
340+
"success": False,
341+
"error": f"Invalid source: {source}. Must be 'installed' or 'available'.",
342+
"valid_sources": ["installed", "available"],
343+
}

0 commit comments

Comments
 (0)