Skip to content

Commit f2f472a

Browse files
feat: Add domain filter and options support to ha_get_integration (#542)
* feat: Add domain filter and options support to ha_get_integration Enhance ha_get_integration tool to expose config entry options (including template sensor Jinja definitions) for auditing purposes. Changes: - Add `domain` parameter to filter entries by integration domain - Add `include_options` parameter to include the options object in list responses - Domain filter auto-enables options inclusion - Update docstring with audit use case and template examples - Add E2E tests for domain filtering, options inclusion, and specific entry retrieval Closes #462 https://claude.ai/code/session_0189BzxxH6FEGSy5GhWdTKZA * fix: Strengthen test_specific_entry_includes_options per review feedback - Find an entry with non-empty options instead of picking an arbitrary one - Assert that options field is present and matches the list endpoint data - Skip test if no integrations with options exist in the test environment https://claude.ai/code/session_0189BzxxH6FEGSy5GhWdTKZA --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 630839d commit f2f472a

2 files changed

Lines changed: 166 additions & 4 deletions

File tree

src/ha_mcp/tools/tools_integrations.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,29 +37,57 @@ async def ha_get_integration(
3737
default=None,
3838
),
3939
] = None,
40+
domain: Annotated[
41+
str | None,
42+
Field(
43+
description="Filter by integration domain (e.g. 'template', 'group'). "
44+
"When set, includes the full options/configuration for each entry.",
45+
default=None,
46+
),
47+
] = None,
48+
include_options: Annotated[
49+
bool | str,
50+
Field(
51+
description="Include the options object for each entry. "
52+
"Automatically enabled when domain filter is set. "
53+
"Useful for auditing template definitions and helper configurations.",
54+
default=False,
55+
),
56+
] = False,
4057
) -> dict[str, Any]:
4158
"""
4259
Get integration (config entry) information - list all or get a specific one.
4360
44-
Without an entry_id: Lists all configured integrations with optional fuzzy search.
45-
With an entry_id: Returns detailed information about a specific config entry.
61+
Without an entry_id: Lists all configured integrations with optional filters.
62+
With an entry_id: Returns detailed information including full options/configuration.
63+
64+
Use this to audit existing configurations (e.g. template sensor Jinja code).
65+
When creating new functionality, prefer UI-based helpers over templates when possible.
4666
4767
EXAMPLES:
4868
- List all integrations: ha_get_integration()
4969
- Search integrations: ha_get_integration(query="zigbee")
5070
- Get specific entry: ha_get_integration(entry_id="abc123")
71+
- List template entries with definitions: ha_get_integration(domain="template")
72+
- List all with options: ha_get_integration(include_options=True)
5173
5274
STATES: 'loaded' (running), 'setup_error', 'setup_retry', 'not_loaded',
5375
'failed_unload', 'migration_error'.
5476
5577
RETURNS (when listing):
5678
- entries: List of integrations with domain, title, state, capabilities
5779
- state_summary: Count of entries in each state
80+
- When domain filter or include_options is set, each entry includes the 'options' object
5881
5982
RETURNS (when getting specific entry):
60-
- entry: Full config entry details
83+
- entry: Full config entry details including options/configuration
6184
"""
6285
try:
86+
include_opts = coerce_bool_param(include_options, "include_options", default=False)
87+
# Auto-enable options when domain filter is set
88+
if domain is not None:
89+
include_opts = True
90+
6391
# If entry_id provided, get specific config entry
6492
if entry_id is not None:
6593
try:
@@ -91,6 +119,11 @@ async def ha_get_integration(
91119

92120
entries = response
93121

122+
# Apply domain filter before formatting
123+
if domain:
124+
domain_lower = domain.strip().lower()
125+
entries = [e for e in entries if e.get("domain", "").lower() == domain_lower]
126+
94127
# Format entries for response
95128
formatted_entries = []
96129
for entry in entries:
@@ -105,6 +138,10 @@ async def ha_get_integration(
105138
"disabled_by": entry.get("disabled_by"),
106139
}
107140

141+
# Include options when requested (for auditing template definitions, etc.)
142+
if include_opts:
143+
formatted_entry["options"] = entry.get("options", {})
144+
108145
# Include pref_disable_new_entities and pref_disable_polling if present
109146
if "pref_disable_new_entities" in entry:
110147
formatted_entry["pref_disable_new_entities"] = entry[
@@ -152,13 +189,16 @@ async def ha_get_integration(
152189
state = entry.get("state", "unknown")
153190
state_summary[state] = state_summary.get(state, 0) + 1
154191

155-
return {
192+
result_data: dict[str, Any] = {
156193
"success": True,
157194
"total": len(formatted_entries),
158195
"entries": formatted_entries,
159196
"state_summary": state_summary,
160197
"query": query if query else None,
161198
}
199+
if domain:
200+
result_data["domain_filter"] = domain.strip().lower()
201+
return result_data
162202

163203
except Exception as e:
164204
logger.error(f"Failed to get integrations: {e}")

tests/src/e2e/workflows/integrations/test_list_integrations.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,128 @@ async def test_entry_details(self, mcp_client):
236236
logger.info(f"All {data['total']} entries have valid structure")
237237

238238

239+
@pytest.mark.integrations
240+
class TestIntegrationFiltering:
241+
"""Test integration domain filtering and options inclusion."""
242+
243+
async def test_filter_by_domain(self, mcp_client):
244+
"""
245+
Test: Filter integrations by domain.
246+
247+
Verifies that the domain parameter filters entries correctly
248+
and auto-includes the options object.
249+
"""
250+
logger.info("Testing ha_get_integration with domain filter...")
251+
252+
# First get all integrations to find a valid domain
253+
all_result = await mcp_client.call_tool("ha_get_integration", {})
254+
all_data = assert_mcp_success(all_result, "get all integrations")
255+
256+
if all_data["total"] == 0:
257+
pytest.skip("No integrations available to test domain filter")
258+
259+
# Pick a domain that exists
260+
test_domain = all_data["entries"][0]["domain"]
261+
262+
result = await mcp_client.call_tool(
263+
"ha_get_integration", {"domain": test_domain}
264+
)
265+
data = assert_mcp_success(result, f"filter by domain {test_domain}")
266+
267+
assert data["total"] > 0, f"Expected entries for domain {test_domain}"
268+
assert data.get("domain_filter") == test_domain
269+
270+
# All entries should be the filtered domain
271+
for entry in data["entries"]:
272+
assert entry["domain"] == test_domain, (
273+
f"Expected domain {test_domain}, got {entry['domain']}"
274+
)
275+
276+
# Domain filter auto-enables options inclusion
277+
for entry in data["entries"]:
278+
assert "options" in entry, "Domain filter should include options"
279+
280+
logger.info(f"Domain filter test passed: {data['total']} {test_domain} entries")
281+
282+
async def test_filter_by_nonexistent_domain(self, mcp_client):
283+
"""
284+
Test: Filter by domain that doesn't exist returns empty results.
285+
"""
286+
result = await mcp_client.call_tool(
287+
"ha_get_integration", {"domain": "nonexistent_domain_xyz"}
288+
)
289+
data = assert_mcp_success(result, "filter by nonexistent domain")
290+
291+
assert data["total"] == 0, "Should have 0 results for nonexistent domain"
292+
assert len(data["entries"]) == 0
293+
294+
async def test_include_options_flag(self, mcp_client):
295+
"""
296+
Test: include_options parameter includes options in list response.
297+
"""
298+
logger.info("Testing ha_get_integration with include_options=True...")
299+
300+
result = await mcp_client.call_tool(
301+
"ha_get_integration", {"include_options": True}
302+
)
303+
data = assert_mcp_success(result, "list with include_options")
304+
305+
if data["total"] == 0:
306+
pytest.skip("No integrations available")
307+
308+
# All entries should have options field
309+
for entry in data["entries"]:
310+
assert "options" in entry, "include_options should add options field"
311+
312+
logger.info(f"include_options test passed: {data['total']} entries with options")
313+
314+
async def test_specific_entry_includes_options(self, mcp_client):
315+
"""
316+
Test: Getting a specific entry by entry_id returns full data including options.
317+
318+
This validates the audit use case from issue #462 - being able to
319+
retrieve template definitions and other config entry options.
320+
"""
321+
logger.info("Testing specific entry includes options...")
322+
323+
# Find an entry that actually has options to validate the audit use case
324+
list_result = await mcp_client.call_tool(
325+
"ha_get_integration", {"include_options": True}
326+
)
327+
list_data = assert_mcp_success(list_result, "list with options")
328+
329+
target_entry = next(
330+
(e for e in list_data["entries"] if e.get("options")), None
331+
)
332+
if not target_entry:
333+
pytest.skip("No integrations with non-empty options found")
334+
335+
entry_id = target_entry["entry_id"]
336+
337+
result = await mcp_client.call_tool(
338+
"ha_get_integration", {"entry_id": entry_id}
339+
)
340+
data = assert_mcp_success(result, "get specific entry")
341+
342+
assert "entry" in data, "Should have entry data"
343+
entry = data["entry"]
344+
345+
# The raw REST API response should include these fields
346+
assert "entry_id" in entry
347+
assert "domain" in entry
348+
349+
# Verify options are present and match what the list endpoint returned
350+
assert "options" in entry, "Specific entry should include options"
351+
assert entry["options"] == target_entry["options"], (
352+
"Options from specific entry should match list endpoint"
353+
)
354+
355+
logger.info(
356+
f"Specific entry test passed: domain={entry.get('domain')}, "
357+
f"options_keys={list(entry['options'].keys())}"
358+
)
359+
360+
239361
@pytest.mark.integrations
240362
async def test_integration_discovery(mcp_client):
241363
"""

0 commit comments

Comments
 (0)