Skip to content

Commit 79f3c1e

Browse files
authored
feat: consolidate duplicate tools (108 → 105 tools) (#423)
Consolidating system info and blueprint tools to reduce tool count from 108 to 105 decorated tools (100 at runtime). Changes: - Merged ha_get_system_info and ha_get_system_version into ha_get_overview - Merged ha_list_blueprints into ha_get_blueprint with optional path parameter - Added tool count limit test (max 105 decorated tools) - Updated all E2E tests Related: #420, #424
1 parent 46d6666 commit 79f3c1e

9 files changed

Lines changed: 188 additions & 285 deletions

File tree

src/ha_mcp/tools/tools_blueprints.py

Lines changed: 58 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -18,105 +18,53 @@
1818
def register_blueprint_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1919
"""Register Home Assistant blueprint management tools."""
2020

21-
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["blueprint"], "title": "List Blueprints"})
22-
@log_tool_usage
23-
async def ha_list_blueprints(
24-
domain: Annotated[
25-
str,
26-
Field(
27-
description="Blueprint domain: 'automation' or 'script'",
28-
default="automation",
29-
),
30-
] = "automation",
31-
) -> dict[str, Any]:
32-
"""
33-
List installed blueprints for a specific domain.
21+
def _format_blueprint_list(blueprints_data: dict[str, Any], domain: str) -> dict[str, Any]:
22+
"""Format blueprint data into list response structure.
3423
35-
Returns all blueprints available in Home Assistant for the specified domain,
36-
including their paths and metadata.
37-
38-
EXAMPLES:
39-
- List automation blueprints: ha_list_blueprints("automation")
40-
- List script blueprints: ha_list_blueprints("script")
24+
Args:
25+
blueprints_data: Raw blueprint data from WebSocket API
26+
domain: Blueprint domain (automation or script)
4127
42-
RETURNS:
43-
- List of blueprints with path, name, and domain information
44-
- Each blueprint includes its relative path for use with ha_get_blueprint
28+
Returns:
29+
Formatted response with blueprints list, count, and domain
4530
"""
46-
try:
47-
# Validate domain
48-
valid_domains = ["automation", "script"]
49-
if domain not in valid_domains:
50-
return {
51-
"success": False,
52-
"error": f"Invalid domain '{domain}'. Must be one of: {', '.join(valid_domains)}",
53-
"valid_domains": valid_domains,
54-
}
55-
56-
# Send WebSocket command to list blueprints
57-
response = await client.send_websocket_message(
58-
{"type": "blueprint/list", "domain": domain}
59-
)
60-
61-
if not response.get("success"):
62-
return {
63-
"success": False,
64-
"error": response.get("error", "Failed to list blueprints"),
65-
"domain": domain,
66-
}
67-
68-
# Process the blueprint list
69-
blueprints_data = response.get("result", {})
70-
71-
# Convert to a more usable format
72-
blueprints = []
73-
for path, metadata in blueprints_data.items():
74-
blueprint_info = {
75-
"path": path,
76-
"domain": domain,
77-
"name": metadata.get("name", path.split("/")[-1].replace(".yaml", "")),
78-
}
79-
80-
# Add optional metadata if available
81-
if "metadata" in metadata:
82-
meta = metadata["metadata"]
83-
blueprint_info.update({
84-
"description": meta.get("description"),
85-
"source_url": meta.get("source_url"),
86-
"author": meta.get("author"),
87-
})
88-
89-
blueprints.append(blueprint_info)
90-
91-
return {
92-
"success": True,
31+
blueprints = []
32+
for bp_path, metadata in blueprints_data.items():
33+
blueprint_info = {
34+
"path": bp_path,
9335
"domain": domain,
94-
"count": len(blueprints),
95-
"blueprints": blueprints,
36+
"name": metadata.get("name", bp_path.split("/")[-1].replace(".yaml", "")),
9637
}
9738

98-
except Exception as e:
99-
logger.error(f"Error listing blueprints: {e}")
100-
return {
101-
"success": False,
102-
"domain": domain,
103-
"error": str(e),
104-
"suggestions": [
105-
"Verify Home Assistant connection",
106-
"Check if blueprint integration is enabled",
107-
f"Use domain 'automation' or 'script' (got '{domain}')",
108-
],
109-
}
39+
# Add optional metadata if available
40+
if "metadata" in metadata:
41+
meta = metadata["metadata"]
42+
blueprint_info.update({
43+
"description": meta.get("description"),
44+
"source_url": meta.get("source_url"),
45+
"author": meta.get("author"),
46+
})
47+
48+
blueprints.append(blueprint_info)
49+
50+
return {
51+
"success": True,
52+
"domain": domain,
53+
"count": len(blueprints),
54+
"blueprints": blueprints,
55+
}
11056

111-
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["blueprint"], "title": "Get Blueprint Details"})
57+
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["blueprint"], "title": "Get Blueprint"})
11258
@log_tool_usage
11359
async def ha_get_blueprint(
11460
path: Annotated[
115-
str,
61+
str | None,
11662
Field(
117-
description="Blueprint path (e.g., 'homeassistant/motion_light.yaml' or 'custom/my_blueprint.yaml')"
63+
description="Blueprint path to get details for (e.g., 'homeassistant/motion_light.yaml'). "
64+
"If omitted, lists all blueprints in the domain.",
65+
default=None,
11866
),
119-
],
67+
] = None,
12068
domain: Annotated[
12169
str,
12270
Field(
@@ -126,17 +74,22 @@ async def ha_get_blueprint(
12674
] = "automation",
12775
) -> dict[str, Any]:
12876
"""
129-
Get detailed information about a specific blueprint.
77+
Get blueprint information - list all blueprints or get details for a specific one.
13078
131-
Retrieves the full blueprint configuration including inputs, triggers,
132-
conditions, and actions. Use this to understand what a blueprint does
133-
and what inputs it requires.
79+
Without a path: Lists all installed blueprints for the specified domain.
80+
With a path: Retrieves full blueprint configuration including inputs, triggers,
81+
conditions, and actions.
13482
13583
EXAMPLES:
136-
- Get automation blueprint: ha_get_blueprint("homeassistant/motion_light.yaml", "automation")
137-
- Get script blueprint: ha_get_blueprint("custom/backup_script.yaml", "script")
84+
- List all automation blueprints: ha_get_blueprint(domain="automation")
85+
- List script blueprints: ha_get_blueprint(domain="script")
86+
- Get specific blueprint: ha_get_blueprint(path="homeassistant/motion_light.yaml", domain="automation")
13887
139-
RETURNS:
88+
RETURNS (when listing):
89+
- List of blueprints with path, name, and domain information
90+
- Count of blueprints found
91+
92+
RETURNS (when getting specific blueprint):
14093
- Blueprint metadata (name, description, author, source_url)
14194
- Input definitions with selectors and defaults
14295
- Blueprint configuration (triggers, conditions, actions for automations; sequence for scripts)
@@ -151,22 +104,25 @@ async def ha_get_blueprint(
151104
"valid_domains": valid_domains,
152105
}
153106

154-
# First, list blueprints to check if path exists
107+
# Get list of blueprints
155108
list_response = await client.send_websocket_message(
156109
{"type": "blueprint/list", "domain": domain}
157110
)
158111

159112
if not list_response.get("success"):
160113
return {
161114
"success": False,
162-
"error": "Failed to query blueprints",
163-
"path": path,
115+
"error": list_response.get("error", "Failed to query blueprints"),
164116
"domain": domain,
165117
}
166118

167119
blueprints_data = list_response.get("result", {})
168120

169-
# Check if blueprint exists
121+
# If no path provided, return list of all blueprints
122+
if path is None:
123+
return _format_blueprint_list(blueprints_data, domain)
124+
125+
# Path provided - get specific blueprint details
170126
if path not in blueprints_data:
171127
available_paths = list(blueprints_data.keys())[:10]
172128
return {
@@ -176,7 +132,7 @@ async def ha_get_blueprint(
176132
"domain": domain,
177133
"available_blueprints": available_paths,
178134
"suggestions": [
179-
"Use ha_list_blueprints() to see available blueprints",
135+
"Use ha_get_blueprint() without path to see all available blueprints",
180136
"Check the path format (e.g., 'homeassistant/motion_light.yaml')",
181137
],
182138
}
@@ -223,7 +179,7 @@ async def ha_get_blueprint(
223179
"error": str(e),
224180
"suggestions": [
225181
"Verify the blueprint path is correct",
226-
"Use ha_list_blueprints() to find available blueprints",
182+
"Use ha_get_blueprint() without path to see available blueprints",
227183
"Check Home Assistant connection",
228184
],
229185
}
@@ -284,7 +240,7 @@ async def ha_import_blueprint(
284240
]
285241

286242
if "already exists" in str(error_msg).lower():
287-
suggestions.insert(0, "Blueprint already exists - use ha_list_blueprints() to see installed blueprints")
243+
suggestions.insert(0, "Blueprint already exists - use ha_get_blueprint() to see installed blueprints")
288244

289245
return {
290246
"success": False,
@@ -305,7 +261,7 @@ async def ha_import_blueprint(
305261
"name": result_data.get("blueprint", {}).get("name"),
306262
"description": result_data.get("blueprint", {}).get("description"),
307263
},
308-
"message": "Blueprint imported successfully. Use ha_list_blueprints() to see all installed blueprints.",
264+
"message": "Blueprint imported successfully. Use ha_get_blueprint() to see all installed blueprints.",
309265
}
310266

311267
except Exception as e:

src/ha_mcp/tools/tools_search.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,8 @@ async def ha_get_overview(
449449
)
450450
result = cast(dict[str, Any], result)
451451

452-
# Include system info in the overview
452+
# Include comprehensive system info in the overview
453+
# This replaces the deprecated ha_get_system_info and ha_get_system_version tools
453454
try:
454455
config = await client.get_config()
455456
result["system_info"] = {
@@ -464,6 +465,15 @@ async def ha_get_overview(
464465
"latitude": config.get("latitude"),
465466
"longitude": config.get("longitude"),
466467
"elevation": config.get("elevation"),
468+
"config_dir": config.get("config_dir"),
469+
"allowlist_external_dirs": config.get("allowlist_external_dirs", []),
470+
"allowlist_external_urls": config.get("allowlist_external_urls", []),
471+
"components": config.get("components", []),
472+
"components_loaded": len(config.get("components", [])),
473+
"state": config.get("state"),
474+
"safe_mode": config.get("safe_mode", False),
475+
"internal_url": config.get("internal_url"),
476+
"external_url": config.get("external_url"),
467477
}
468478
except Exception as e:
469479
logger.warning(f"Failed to fetch system info for overview: {e}")

src/ha_mcp/tools/tools_system.py

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -304,50 +304,6 @@ async def ha_reload_core(
304304
],
305305
}
306306

307-
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["system"], "title": "Get System Info"})
308-
@log_tool_usage
309-
async def ha_get_system_info() -> dict[str, Any]:
310-
"""
311-
Get Home Assistant system information.
312-
313-
Returns version, location settings, timezone, loaded components, and configuration paths.
314-
"""
315-
try:
316-
config = await client.get_config()
317-
318-
# Extract relevant system information
319-
system_info = {
320-
"success": True,
321-
"version": config.get("version"),
322-
"location_name": config.get("location_name"),
323-
"time_zone": config.get("time_zone"),
324-
"unit_system": config.get("unit_system", {}),
325-
"latitude": config.get("latitude"),
326-
"longitude": config.get("longitude"),
327-
"elevation": config.get("elevation"),
328-
"currency": config.get("currency"),
329-
"country": config.get("country"),
330-
"language": config.get("language"),
331-
"config_dir": config.get("config_dir"),
332-
"allowlist_external_dirs": config.get("allowlist_external_dirs", []),
333-
"allowlist_external_urls": config.get("allowlist_external_urls", []),
334-
"components": config.get("components", []),
335-
"component_count": len(config.get("components", [])),
336-
"state": config.get("state"),
337-
"safe_mode": config.get("safe_mode", False),
338-
"internal_url": config.get("internal_url"),
339-
"external_url": config.get("external_url"),
340-
}
341-
342-
return system_info
343-
344-
except Exception as e:
345-
logger.error(f"Failed to get system info: {e}")
346-
return {
347-
"success": False,
348-
"error": f"Failed to get system info: {str(e)}",
349-
}
350-
351307
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["system"], "title": "Get System Health"})
352308
@log_tool_usage
353309
async def ha_get_system_health() -> dict[str, Any]:
@@ -396,7 +352,7 @@ async def ha_get_system_health() -> dict[str, Any]:
396352
"error": f"Failed to get system health: {str(e)}",
397353
"suggestions": [
398354
"System health may not be available in all HA installations",
399-
"Try ha_get_system_info() for basic system information",
355+
"Try ha_get_overview() for basic system information",
400356
],
401357
}
402358
finally:

src/ha_mcp/tools/tools_updates.py

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -271,76 +271,6 @@ async def ha_get_release_notes(
271271
"error": f"Failed to get release notes: {str(e)}",
272272
}
273273

274-
@mcp.tool(annotations={"idempotentHint": True, "readOnlyHint": True, "tags": ["system"], "title": "Get System Version"})
275-
@log_tool_usage
276-
async def ha_get_system_version() -> dict[str, Any]:
277-
"""
278-
Get Home Assistant version and basic system information.
279-
280-
Returns current version, installation type, and system details.
281-
Useful for:
282-
- Checking current version before updates
283-
- Verifying update success
284-
- Compatibility checks for integrations
285-
- Debugging system configuration
286-
287-
Returns:
288-
Dictionary containing:
289-
- version: Current Home Assistant Core version
290-
- location_name: Configured location name
291-
- config_dir: Configuration directory path
292-
- timezone: System timezone
293-
- components_loaded: Number of loaded components
294-
- unit_system: Metric or imperial
295-
- internal_url: Internal access URL (if configured)
296-
- external_url: External access URL (if configured)
297-
"""
298-
try:
299-
# Get configuration via REST API
300-
config = await client.get_config()
301-
302-
# Extract relevant information
303-
version_info = {
304-
"success": True,
305-
"version": config.get("version"),
306-
"location_name": config.get("location_name"),
307-
"config_dir": config.get("config_dir"),
308-
"timezone": config.get("time_zone"),
309-
"elevation": config.get("elevation"),
310-
"latitude": config.get("latitude"),
311-
"longitude": config.get("longitude"),
312-
"unit_system": config.get("unit_system", {}).get(
313-
"temperature", "unknown"
314-
),
315-
"components_loaded": len(config.get("components", [])),
316-
"allowlist_external_dirs": config.get("allowlist_external_dirs", []),
317-
"allowlist_external_urls": config.get("allowlist_external_urls", []),
318-
"internal_url": config.get("internal_url"),
319-
"external_url": config.get("external_url"),
320-
"currency": config.get("currency"),
321-
"country": config.get("country"),
322-
"language": config.get("language"),
323-
"safe_mode": config.get("safe_mode", False),
324-
"state": config.get("state"),
325-
}
326-
327-
# Clean up None values
328-
version_info = {k: v for k, v in version_info.items() if v is not None}
329-
330-
return version_info
331-
332-
except Exception as e:
333-
logger.error(f"Failed to get system version: {e}")
334-
return {
335-
"success": False,
336-
"error": f"Failed to get system version: {str(e)}",
337-
"suggestions": [
338-
"Check Home Assistant connection",
339-
"Verify API access permissions",
340-
],
341-
}
342-
343-
344274
def _supports_release_notes(entity_id: str, attributes: dict[str, Any]) -> bool:
345275
"""
346276
Determine if an update entity supports fetching release notes.

0 commit comments

Comments
 (0)