Hass-MCP
by voska
Verified
- hass-mcp
- app
import functools
import logging
import json
import httpx
from typing import List, Dict, Any, Optional, Callable, Awaitable, TypeVar, cast
# Set up logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
from app.hass import (
get_hass_version, get_entity_state, call_service, get_entities,
get_automations, restart_home_assistant,
cleanup_client, filter_fields, summarize_domain, get_system_overview,
get_hass_error_log
)
# Type variable for generic functions
T = TypeVar('T')
# Create an MCP server
from mcp.server.fastmcp import FastMCP, Context, Image
from mcp.server.stdio import stdio_server
import mcp.types as types
mcp = FastMCP("Hass-MCP", capabilities={
"resources": {},
"tools": {},
"prompts": {}
})
def async_handler(command_type: str):
"""
Simple decorator that logs the command
Args:
command_type: The type of command (for logging)
"""
def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
@functools.wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> T:
logger.info(f"Executing command: {command_type}")
return await func(*args, **kwargs)
return cast(Callable[..., Awaitable[T]], wrapper)
return decorator
@mcp.tool()
@async_handler("get_version")
async def get_version() -> str:
"""
Get the Home Assistant version
Returns:
A string with the Home Assistant version (e.g., "2025.3.0")
"""
logger.info("Getting Home Assistant version")
return await get_hass_version()
@mcp.tool()
@async_handler("get_entity")
async def get_entity(entity_id: str, fields: Optional[List[str]] = None, detailed: bool = False) -> dict:
"""
Get the state of a Home Assistant entity with optional field filtering
Args:
entity_id: The entity ID to get (e.g. 'light.living_room')
fields: Optional list of fields to include (e.g. ['state', 'attr.brightness'])
detailed: If True, returns all entity fields without filtering
Examples:
entity_id="light.living_room" - basic state check
entity_id="light.living_room", fields=["state", "attr.brightness"] - specific fields
entity_id="light.living_room", detailed=True - all details
"""
logger.info(f"Getting entity state: {entity_id}")
if detailed:
# Return all fields
return await get_entity_state(entity_id, lean=False)
elif fields:
# Return only the specified fields
return await get_entity_state(entity_id, fields=fields)
else:
# Return lean format with essential fields
return await get_entity_state(entity_id, lean=True)
@mcp.tool()
@async_handler("entity_action")
async def entity_action(entity_id: str, action: str, **params) -> dict:
"""
Perform an action on a Home Assistant entity (on, off, toggle)
Args:
entity_id: The entity ID to control (e.g. 'light.living_room')
action: The action to perform ('on', 'off', 'toggle')
**params: Additional parameters for the service call
Returns:
The response from Home Assistant
Examples:
entity_id="light.living_room", action="on", brightness=255
entity_id="switch.garden_lights", action="off"
entity_id="climate.living_room", action="on", temperature=22.5
Domain-Specific Parameters:
- Lights: brightness (0-255), color_temp, rgb_color, transition, effect
- Covers: position (0-100), tilt_position
- Climate: temperature, target_temp_high, target_temp_low, hvac_mode
- Media players: source, volume_level (0-1)
"""
if action not in ["on", "off", "toggle"]:
return {"error": f"Invalid action: {action}. Valid actions are 'on', 'off', 'toggle'"}
# Map action to service name
service = action if action == "toggle" else f"turn_{action}"
# Extract the domain from the entity_id
domain = entity_id.split(".")[0]
# Prepare service data
data = {"entity_id": entity_id, **params}
logger.info(f"Performing action '{action}' on entity: {entity_id} with params: {params}")
return await call_service(domain, service, data)
@mcp.resource("hass://entities/{entity_id}")
@async_handler("get_entity_resource")
async def get_entity_resource(entity_id: str) -> str:
"""
Get the state of a Home Assistant entity as a resource
This endpoint provides a standard view with common entity information.
For comprehensive attribute details, use the /detailed endpoint.
Args:
entity_id: The entity ID to get information for
"""
logger.info(f"Getting entity resource: {entity_id}")
# Get the entity state with caching (using lean format for token efficiency)
state = await get_entity_state(entity_id, use_cache=True, lean=True)
# Check if there was an error
if "error" in state:
return f"# Entity: {entity_id}\n\nError retrieving entity: {state['error']}"
# Format the entity as markdown
result = f"# Entity: {entity_id}\n\n"
# Get friendly name if available
friendly_name = state.get("attributes", {}).get("friendly_name")
if friendly_name and friendly_name != entity_id:
result += f"**Name**: {friendly_name}\n\n"
# Add state
result += f"**State**: {state.get('state')}\n\n"
# Add domain info
domain = entity_id.split(".")[0]
result += f"**Domain**: {domain}\n\n"
# Add key attributes based on domain type
attributes = state.get("attributes", {})
# Add a curated list of important attributes
important_attrs = []
# Common attributes across many domains
common_attrs = ["device_class", "unit_of_measurement", "friendly_name"]
# Domain-specific important attributes
if domain == "light":
important_attrs = ["brightness", "color_temp", "rgb_color", "supported_features", "supported_color_modes"]
elif domain == "sensor":
important_attrs = ["unit_of_measurement", "device_class", "state_class"]
elif domain == "climate":
important_attrs = ["hvac_mode", "hvac_action", "temperature", "current_temperature", "target_temp_*"]
elif domain == "media_player":
important_attrs = ["media_title", "media_artist", "source", "volume_level", "media_content_type"]
elif domain == "switch" or domain == "binary_sensor":
important_attrs = ["device_class", "is_on"]
# Combine with common attributes
important_attrs.extend(common_attrs)
# Deduplicate the list while preserving order
important_attrs = list(dict.fromkeys(important_attrs))
# Create and add the important attributes section
result += "## Key Attributes\n\n"
# Display only the important attributes that exist
displayed_attrs = 0
for attr_name in important_attrs:
# Handle wildcard attributes (e.g., target_temp_*)
if attr_name.endswith("*"):
prefix = attr_name[:-1]
matching_attrs = [name for name in attributes if name.startswith(prefix)]
for name in matching_attrs:
result += f"- **{name}**: {attributes[name]}\n"
displayed_attrs += 1
# Regular attribute match
elif attr_name in attributes:
attr_value = attributes[attr_name]
if isinstance(attr_value, (list, dict)) and len(str(attr_value)) > 100:
result += f"- **{attr_name}**: *[Complex data]*\n"
else:
result += f"- **{attr_name}**: {attr_value}\n"
displayed_attrs += 1
# If no important attributes were found, show a message
if displayed_attrs == 0:
result += "No key attributes found for this entity type.\n\n"
# Add attribute count and link to detailed view
total_attr_count = len(attributes)
if total_attr_count > displayed_attrs:
hidden_count = total_attr_count - displayed_attrs
result += f"\n**Note**: Showing {displayed_attrs} of {total_attr_count} total attributes. "
result += f"{hidden_count} additional attributes are available in the [detailed view](/api/resource/hass://entities/{entity_id}/detailed).\n\n"
# Add last updated time if available
if "last_updated" in state:
result += f"**Last Updated**: {state['last_updated']}\n"
return result
@mcp.tool()
@async_handler("list_entities")
async def list_entities(
domain: Optional[str] = None,
search_query: Optional[str] = None,
limit: int = 100,
fields: Optional[List[str]] = None,
detailed: bool = False
) -> List[Dict[str, Any]]:
"""
Get a list of Home Assistant entities with optional filtering
Args:
domain: Optional domain to filter by (e.g., 'light', 'switch', 'sensor')
search_query: Optional search term to filter entities by name, id, or attributes
(Note: Does not support wildcards. To get all entities, leave this empty)
limit: Maximum number of entities to return (default: 100)
fields: Optional list of specific fields to include in each entity
detailed: If True, returns all entity fields without filtering
Returns:
A list of entity dictionaries with lean formatting by default
Examples:
domain="light" - get all lights
search_query="kitchen", limit=20 - search entities
domain="sensor", detailed=True - full sensor details
Best Practices:
- Use lean format (default) for most operations
- Prefer domain filtering over no filtering
- For domain overviews, use domain_summary_tool instead of list_entities
- Only request detailed=True when necessary for full attribute inspection
- To get all entity types/domains, use list_entities without a domain filter,
then extract domains from entity_ids
"""
log_message = "Getting entities"
if domain:
log_message += f" for domain: {domain}"
if search_query:
log_message += f" matching: '{search_query}'"
if limit != 100:
log_message += f" (limit: {limit})"
if detailed:
log_message += " (detailed format)"
elif fields:
log_message += f" (custom fields: {fields})"
else:
log_message += " (lean format)"
logger.info(log_message)
# Handle special case where search_query is a wildcard/asterisk - just ignore it
if search_query == "*":
search_query = None
logger.info("Converting '*' search query to None (retrieving all entities)")
# Use the updated get_entities function with field filtering
return await get_entities(
domain=domain,
search_query=search_query,
limit=limit,
fields=fields,
lean=not detailed # Use lean format unless detailed is requested
)
@mcp.resource("hass://entities")
@async_handler("get_all_entities_resource")
async def get_all_entities_resource() -> str:
"""
Get a list of all Home Assistant entities as a resource
This endpoint returns a complete list of all entities in Home Assistant,
organized by domain. For token efficiency with large installations,
consider using domain-specific endpoints or the domain summary instead.
Returns:
A markdown formatted string listing all entities grouped by domain
Examples:
```
# Get all entities
entities = mcp.get_resource("hass://entities")
```
Best Practices:
- WARNING: This endpoint can return large amounts of data with many entities
- Prefer domain-filtered endpoints: hass://entities/domain/{domain}
- For overview information, use domain summaries instead of full entity lists
- Consider starting with a search if looking for specific entities
"""
logger.info("Getting all entities as a resource")
entities = await get_entities(lean=True)
# Check if there was an error
if isinstance(entities, dict) and "error" in entities:
return f"Error retrieving entities: {entities['error']}"
if len(entities) == 1 and isinstance(entities[0], dict) and "error" in entities[0]:
return f"Error retrieving entities: {entities[0]['error']}"
# Format the entities as a string
result = "# Home Assistant Entities\n\n"
result += f"Total entities: {len(entities)}\n\n"
result += "⚠️ **Note**: For better performance and token efficiency, consider using:\n"
result += "- Domain filtering: `hass://entities/domain/{domain}`\n"
result += "- Domain summaries: `hass://entities/domain/{domain}/summary`\n"
result += "- Entity search: `hass://search/{query}`\n\n"
# Group entities by domain for better organization
domains = {}
for entity in entities:
domain = entity["entity_id"].split(".")[0]
if domain not in domains:
domains[domain] = []
domains[domain].append(entity)
# Build the string with entities grouped by domain
for domain in sorted(domains.keys()):
domain_count = len(domains[domain])
result += f"## {domain.capitalize()} ({domain_count})\n\n"
for entity in sorted(domains[domain], key=lambda e: e["entity_id"]):
# Get a friendly name if available
friendly_name = entity.get("attributes", {}).get("friendly_name", "")
result += f"- **{entity['entity_id']}**: {entity['state']}"
if friendly_name and friendly_name != entity["entity_id"]:
result += f" ({friendly_name})"
result += "\n"
result += "\n"
return result
@mcp.tool()
@async_handler("search_entities_tool")
async def search_entities_tool(query: str, limit: int = 20) -> Dict[str, Any]:
"""
Search for entities matching a query string
Args:
query: The search query to match against entity IDs, names, and attributes.
(Note: Does not support wildcards. To get all entities, leave this blank or use list_entities tool)
limit: Maximum number of results to return (default: 20)
Returns:
A dictionary containing search results and metadata:
- count: Total number of matching entities found
- results: List of matching entities with essential information
- domains: Map of domains with counts (e.g. {"light": 3, "sensor": 2})
Examples:
query="temperature" - find temperature entities
query="living room", limit=10 - find living room entities
query="", limit=500 - list all entity types
"""
logger.info(f"Searching for entities matching: '{query}' with limit: {limit}")
# Special case - treat "*" as empty query to just return entities without filtering
if query == "*":
query = ""
logger.info("Converting '*' to empty query (retrieving all entities up to limit)")
# Handle empty query as a special case to just return entities up to the limit
if not query or not query.strip():
logger.info(f"Empty query - retrieving up to {limit} entities without filtering")
entities = await get_entities(limit=limit, lean=True)
# Check if there was an error
if isinstance(entities, dict) and "error" in entities:
return {"error": entities["error"], "count": 0, "results": [], "domains": {}}
# No query, but we'll return a structured result anyway
domains_count = {}
simplified_entities = []
for entity in entities:
domain = entity["entity_id"].split(".")[0]
# Count domains
if domain not in domains_count:
domains_count[domain] = 0
domains_count[domain] += 1
# Create simplified entity representation
simplified_entity = {
"entity_id": entity["entity_id"],
"state": entity["state"],
"domain": domain,
"friendly_name": entity.get("attributes", {}).get("friendly_name", entity["entity_id"])
}
# Add key attributes based on domain
attributes = entity.get("attributes", {})
# Include domain-specific important attributes
if domain == "light" and "brightness" in attributes:
simplified_entity["brightness"] = attributes["brightness"]
elif domain == "sensor" and "unit_of_measurement" in attributes:
simplified_entity["unit"] = attributes["unit_of_measurement"]
elif domain == "climate" and "temperature" in attributes:
simplified_entity["temperature"] = attributes["temperature"]
elif domain == "media_player" and "media_title" in attributes:
simplified_entity["media_title"] = attributes["media_title"]
simplified_entities.append(simplified_entity)
# Return structured response for empty query
return {
"count": len(simplified_entities),
"results": simplified_entities,
"domains": domains_count,
"query": "all entities (no filtering)"
}
# Normal search with non-empty query
entities = await get_entities(search_query=query, limit=limit, lean=True)
# Check if there was an error
if isinstance(entities, dict) and "error" in entities:
return {"error": entities["error"], "count": 0, "results": [], "domains": {}}
# Prepare the results
domains_count = {}
simplified_entities = []
for entity in entities:
domain = entity["entity_id"].split(".")[0]
# Count domains
if domain not in domains_count:
domains_count[domain] = 0
domains_count[domain] += 1
# Create simplified entity representation
simplified_entity = {
"entity_id": entity["entity_id"],
"state": entity["state"],
"domain": domain,
"friendly_name": entity.get("attributes", {}).get("friendly_name", entity["entity_id"])
}
# Add key attributes based on domain
attributes = entity.get("attributes", {})
# Include domain-specific important attributes
if domain == "light" and "brightness" in attributes:
simplified_entity["brightness"] = attributes["brightness"]
elif domain == "sensor" and "unit_of_measurement" in attributes:
simplified_entity["unit"] = attributes["unit_of_measurement"]
elif domain == "climate" and "temperature" in attributes:
simplified_entity["temperature"] = attributes["temperature"]
elif domain == "media_player" and "media_title" in attributes:
simplified_entity["media_title"] = attributes["media_title"]
simplified_entities.append(simplified_entity)
# Return structured response
return {
"count": len(simplified_entities),
"results": simplified_entities,
"domains": domains_count,
"query": query
}
@mcp.resource("hass://search/{query}/{limit}")
@async_handler("search_entities_resource_with_limit")
async def search_entities_resource_with_limit(query: str, limit: str) -> str:
"""
Search for entities matching a query string with a specified result limit
This endpoint extends the basic search functionality by allowing you to specify
a custom limit on the number of results returned. It's useful for both broader
searches (larger limit) and more focused searches (smaller limit).
Args:
query: The search query to match against entity IDs, names, and attributes
limit: Maximum number of entities to return (as a string, will be converted to int)
Returns:
A markdown formatted string with search results and a JSON summary
Examples:
```
# Search with a larger limit (up to 50 results)
results = mcp.get_resource("hass://search/sensor/50")
# Search with a smaller limit for focused results
results = mcp.get_resource("hass://search/kitchen/5")
```
Best Practices:
- Use smaller limits (5-10) for focused searches where you need just a few matches
- Use larger limits (30-50) for broader searches when you need more comprehensive results
- Balance larger limits against token usage - more results means more tokens
- Consider domain-specific searches for better precision: "light kitchen" instead of just "kitchen"
"""
try:
limit_int = int(limit)
if limit_int <= 0:
limit_int = 20
except ValueError:
limit_int = 20
logger.info(f"Searching for entities matching: '{query}' with custom limit: {limit_int}")
if not query or not query.strip():
return "# Entity Search\n\nError: No search query provided"
entities = await get_entities(search_query=query, limit=limit_int, lean=True)
# Check if there was an error
if isinstance(entities, dict) and "error" in entities:
return f"# Entity Search\n\nError retrieving entities: {entities['error']}"
# Format the search results
result = f"# Entity Search Results for '{query}' (Limit: {limit_int})\n\n"
if not entities:
result += "No entities found matching your search query.\n"
return result
result += f"Found {len(entities)} matching entities:\n\n"
# Group entities by domain for better organization
domains = {}
for entity in entities:
domain = entity["entity_id"].split(".")[0]
if domain not in domains:
domains[domain] = []
domains[domain].append(entity)
# Build the string with entities grouped by domain
for domain in sorted(domains.keys()):
result += f"## {domain.capitalize()}\n\n"
for entity in sorted(domains[domain], key=lambda e: e["entity_id"]):
# Get a friendly name if available
friendly_name = entity.get("attributes", {}).get("friendly_name", entity["entity_id"])
result += f"- **{entity['entity_id']}**: {entity['state']}"
if friendly_name != entity["entity_id"]:
result += f" ({friendly_name})"
result += "\n"
result += "\n"
# Add a more structured summary section for easy LLM processing
result += "## Summary in JSON format\n\n"
result += "```json\n"
# Create a simplified JSON representation with only essential fields
simplified_entities = []
for entity in entities:
simplified_entity = {
"entity_id": entity["entity_id"],
"state": entity["state"],
"domain": entity["entity_id"].split(".")[0],
"friendly_name": entity.get("attributes", {}).get("friendly_name", entity["entity_id"])
}
# Add key attributes based on domain type if they exist
domain = entity["entity_id"].split(".")[0]
attributes = entity.get("attributes", {})
# Include domain-specific important attributes
if domain == "light" and "brightness" in attributes:
simplified_entity["brightness"] = attributes["brightness"]
elif domain == "sensor" and "unit_of_measurement" in attributes:
simplified_entity["unit"] = attributes["unit_of_measurement"]
elif domain == "climate" and "temperature" in attributes:
simplified_entity["temperature"] = attributes["temperature"]
elif domain == "media_player" and "media_title" in attributes:
simplified_entity["media_title"] = attributes["media_title"]
simplified_entities.append(simplified_entity)
result += json.dumps(simplified_entities, indent=2)
result += "\n```\n"
return result
# The domain_summary_tool is already implemented, no need to duplicate it
@mcp.tool()
@async_handler("domain_summary")
async def domain_summary_tool(domain: str, example_limit: int = 3) -> Dict[str, Any]:
"""
Get a summary of entities in a specific domain
Args:
domain: The domain to summarize (e.g., 'light', 'switch', 'sensor')
example_limit: Maximum number of examples to include for each state
Returns:
A dictionary containing:
- total_count: Number of entities in the domain
- state_distribution: Count of entities in each state
- examples: Sample entities for each state
- common_attributes: Most frequently occurring attributes
Examples:
domain="light" - get light summary
domain="climate", example_limit=5 - climate summary with more examples
Best Practices:
- Use this before retrieving all entities in a domain to understand what's available """
logger.info(f"Getting domain summary for: {domain}")
return await summarize_domain(domain, example_limit)
@mcp.tool()
@async_handler("system_overview")
async def system_overview() -> Dict[str, Any]:
"""
Get a comprehensive overview of the entire Home Assistant system
Returns:
A dictionary containing:
- total_entities: Total count of all entities
- domains: Dictionary of domains with their entity counts and state distributions
- domain_samples: Representative sample entities for each domain (2-3 per domain)
- domain_attributes: Common attributes for each domain
- area_distribution: Entities grouped by area (if available)
Examples:
Returns domain counts, sample entities, and common attributes
Best Practices:
- Use this as the first call when exploring an unfamiliar Home Assistant instance
- Perfect for building context about the structure of the smart home
- After getting an overview, use domain_summary_tool to dig deeper into specific domains
"""
logger.info("Generating complete system overview")
return await get_system_overview()
@mcp.resource("hass://entities/{entity_id}/detailed")
@async_handler("get_entity_resource_detailed")
async def get_entity_resource_detailed(entity_id: str) -> str:
"""
Get detailed information about a Home Assistant entity as a resource
Use this detailed view selectively when you need to:
- Understand all available attributes of an entity
- Debug entity behavior or capabilities
- See comprehensive state information
For routine operations where you only need basic state information,
prefer the standard entity endpoint or specify fields in the get_entity tool.
Args:
entity_id: The entity ID to get information for
"""
logger.info(f"Getting detailed entity resource: {entity_id}")
# Get all fields, no filtering (detailed view explicitly requests all data)
state = await get_entity_state(entity_id, use_cache=True, lean=False)
# Check if there was an error
if "error" in state:
return f"# Entity: {entity_id}\n\nError retrieving entity: {state['error']}"
# Format the entity as markdown
result = f"# Entity: {entity_id} (Detailed View)\n\n"
# Get friendly name if available
friendly_name = state.get("attributes", {}).get("friendly_name")
if friendly_name and friendly_name != entity_id:
result += f"**Name**: {friendly_name}\n\n"
# Add state
result += f"**State**: {state.get('state')}\n\n"
# Add domain and entity type information
domain = entity_id.split(".")[0]
result += f"**Domain**: {domain}\n\n"
# Add usage guidance
result += "## Usage Note\n"
result += "This is the detailed view showing all entity attributes. For token-efficient interactions, "
result += "consider using the standard entity endpoint or the get_entity tool with field filtering.\n\n"
# Add all attributes with full details
attributes = state.get("attributes", {})
if attributes:
result += "## Attributes\n\n"
# Sort attributes for better organization
sorted_attrs = sorted(attributes.items())
# Format each attribute with complete information
for attr_name, attr_value in sorted_attrs:
# Format the attribute value
if isinstance(attr_value, (list, dict)):
attr_str = json.dumps(attr_value, indent=2)
result += f"- **{attr_name}**:\n```json\n{attr_str}\n```\n"
else:
result += f"- **{attr_name}**: {attr_value}\n"
# Add context data section
result += "\n## Context Data\n\n"
# Add last updated time if available
if "last_updated" in state:
result += f"**Last Updated**: {state['last_updated']}\n"
# Add last changed time if available
if "last_changed" in state:
result += f"**Last Changed**: {state['last_changed']}\n"
# Add entity ID and context information
if "context" in state:
context = state["context"]
result += f"**Context ID**: {context.get('id', 'N/A')}\n"
if "parent_id" in context:
result += f"**Parent Context**: {context['parent_id']}\n"
if "user_id" in context:
result += f"**User ID**: {context['user_id']}\n"
# Add related entities suggestions
related_domains = []
if domain == "light":
related_domains = ["switch", "scene", "automation"]
elif domain == "sensor":
related_domains = ["binary_sensor", "input_number", "utility_meter"]
elif domain == "climate":
related_domains = ["sensor", "switch", "fan"]
elif domain == "media_player":
related_domains = ["remote", "switch", "sensor"]
if related_domains:
result += "\n## Related Entity Types\n\n"
result += "You may want to check entities in these related domains:\n"
for related in related_domains:
result += f"- {related}\n"
return result
@mcp.resource("hass://entities/domain/{domain}")
@async_handler("list_states_by_domain_resource")
async def list_states_by_domain_resource(domain: str) -> str:
"""
Get a list of entities for a specific domain as a resource
This endpoint provides all entities of a specific type (domain). It's much more
token-efficient than retrieving all entities when you only need entities of a
specific type.
Args:
domain: The domain to filter by (e.g., 'light', 'switch', 'sensor')
Returns:
A markdown formatted string with all entities in the specified domain
Examples:
```
# Get all lights
lights = mcp.get_resource("hass://entities/domain/light")
# Get all climate devices
climate = mcp.get_resource("hass://entities/domain/climate")
# Get all sensors
sensors = mcp.get_resource("hass://entities/domain/sensor")
```
Best Practices:
- Use this endpoint when you need detailed information about all entities of a specific type
- For a more concise overview, use the domain summary endpoint: hass://entities/domain/{domain}/summary
- For sensors and other high-count domains, consider using a search to further filter results
"""
logger.info(f"Getting entities for domain: {domain}")
# Fixed pagination values for now
page = 1
page_size = 50
# Get all entities for the specified domain (using lean format for token efficiency)
entities = await get_entities(domain=domain, lean=True)
# Check if there was an error
if isinstance(entities, dict) and "error" in entities:
return f"Error retrieving entities: {entities['error']}"
# Format the entities as a string
result = f"# {domain.capitalize()} Entities\n\n"
# Pagination info (fixed for now due to MCP limitations)
total_entities = len(entities)
# List the entities
for entity in sorted(entities, key=lambda e: e["entity_id"]):
# Get a friendly name if available
friendly_name = entity.get("attributes", {}).get("friendly_name", entity["entity_id"])
result += f"- **{entity['entity_id']}**: {entity['state']}"
if friendly_name != entity["entity_id"]:
result += f" ({friendly_name})"
result += "\n"
# Add link to summary
result += f"\n## Related Resources\n\n"
result += f"- [View domain summary](/api/resource/hass://entities/domain/{domain}/summary)\n"
return result
# Automation management MCP tools
@mcp.tool()
@async_handler("list_automations")
async def list_automations() -> List[Dict[str, Any]]:
"""
Get a list of all automations from Home Assistant
This function retrieves all automations configured in Home Assistant,
including their IDs, entity IDs, state, and display names.
Returns:
A list of automation dictionaries, each containing id, entity_id,
state, and alias (friendly name) fields.
Examples:
Returns all automation objects with state and friendly names
"""
logger.info("Getting all automations")
try:
# Get automations will now return data from states API, which is more reliable
automations = await get_automations()
# Handle error responses that might still occur
if isinstance(automations, dict) and "error" in automations:
logger.warning(f"Error getting automations: {automations['error']}")
return []
# Handle case where response is a list with error
if isinstance(automations, list) and len(automations) == 1 and isinstance(automations[0], dict) and "error" in automations[0]:
logger.warning(f"Error getting automations: {automations[0]['error']}")
return []
return automations
except Exception as e:
logger.error(f"Error in list_automations: {str(e)}")
return []
# We already have a list_automations tool, so no need to duplicate functionality
@mcp.tool()
@async_handler("restart_ha")
async def restart_ha() -> Dict[str, Any]:
"""
Restart Home Assistant
⚠️ WARNING: Temporarily disrupts all Home Assistant operations
Returns:
Result of restart operation
"""
logger.info("Restarting Home Assistant")
return await restart_home_assistant()
@mcp.tool()
@async_handler("call_service")
async def call_service_tool(domain: str, service: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Call any Home Assistant service (low-level API access)
Args:
domain: The domain of the service (e.g., 'light', 'switch', 'automation')
service: The service to call (e.g., 'turn_on', 'turn_off', 'toggle')
data: Optional data to pass to the service (e.g., {'entity_id': 'light.living_room'})
Returns:
The response from Home Assistant (usually empty for successful calls)
Examples:
domain='light', service='turn_on', data={'entity_id': 'light.x', 'brightness': 255}
domain='automation', service='reload'
domain='fan', service='set_percentage', data={'entity_id': 'fan.x', 'percentage': 50}
"""
logger.info(f"Calling Home Assistant service: {domain}.{service} with data: {data}")
return await call_service(domain, service, data or {})
# Prompt functionality
@mcp.prompt()
def create_automation(trigger_type: str, entity_id: str = None):
"""
Guide a user through creating a Home Assistant automation
This prompt provides a step-by-step guided conversation for creating
a new automation in Home Assistant based on the specified trigger type.
Args:
trigger_type: The type of trigger for the automation (state, time, etc.)
entity_id: Optional entity to use as the trigger source
Returns:
A list of messages for the interactive conversation
"""
# Define the initial system message
system_message = """You are an automation creation assistant for Home Assistant.
You'll guide the user through creating an automation with the following steps:
1. Define the trigger conditions based on their specified trigger type
2. Specify the actions to perform
3. Add any conditions (optional)
4. Review and confirm the automation"""
# Define the first user message based on parameters
trigger_description = {
"state": "an entity changing state",
"time": "a specific time of day",
"numeric_state": "a numeric value crossing a threshold",
"zone": "entering or leaving a zone",
"sun": "sun events (sunrise/sunset)",
"template": "a template condition becoming true"
}
description = trigger_description.get(trigger_type, trigger_type)
if entity_id:
user_message = f"I want to create an automation triggered by {description} for {entity_id}."
else:
user_message = f"I want to create an automation triggered by {description}."
# Return the conversation starter messages
return [
{"role": "system", "content": system_message},
{"role": "user", "content": user_message}
]
@mcp.prompt()
def debug_automation(automation_id: str):
"""
Help a user troubleshoot an automation that isn't working
This prompt guides the user through the process of diagnosing and fixing
issues with an existing Home Assistant automation.
Args:
automation_id: The entity ID of the automation to troubleshoot
Returns:
A list of messages for the interactive conversation
"""
system_message = """You are a Home Assistant automation troubleshooting expert.
You'll help the user diagnose problems with their automation by checking:
1. Trigger conditions and whether they're being met
2. Conditions that might be preventing execution
3. Action configuration issues
4. Entity availability and connectivity
5. Permissions and scope issues"""
user_message = f"My automation {automation_id} isn't working properly. Can you help me troubleshoot it?"
return [
{"role": "system", "content": system_message},
{"role": "user", "content": user_message}
]
@mcp.prompt()
def troubleshoot_entity(entity_id: str):
"""
Guide a user through troubleshooting issues with an entity
This prompt helps diagnose and resolve problems with a specific
Home Assistant entity that isn't functioning correctly.
Args:
entity_id: The entity ID having issues
Returns:
A list of messages for the interactive conversation
"""
system_message = """You are a Home Assistant entity troubleshooting expert.
You'll help the user diagnose problems with their entity by checking:
1. Entity status and availability
2. Integration status
3. Device connectivity
4. Recent state changes and error patterns
5. Configuration issues
6. Common problems with this entity type"""
user_message = f"My entity {entity_id} isn't working properly. Can you help me troubleshoot it?"
return [
{"role": "system", "content": system_message},
{"role": "user", "content": user_message}
]
@mcp.prompt()
def routine_optimizer():
"""
Analyze usage patterns and suggest optimized routines based on actual behavior
This prompt helps users analyze their Home Assistant usage patterns and create
more efficient routines, automations, and schedules based on real usage data.
Returns:
A list of messages for the interactive conversation
"""
system_message = """You are a Home Assistant optimization expert specializing in routine analysis.
You'll help the user analyze their usage patterns and create optimized routines by:
1. Reviewing entity state histories to identify patterns
2. Analyzing when lights, climate controls, and other devices are used
3. Finding correlations between different device usages
4. Suggesting automations based on detected routines
5. Optimizing existing automations to better match actual usage
6. Creating schedules that adapt to the user's lifestyle
7. Identifying energy-saving opportunities based on usage patterns"""
user_message = "I'd like to optimize my home automations based on my actual usage patterns. Can you help analyze how I use my smart home and suggest better routines?"
return [
{"role": "system", "content": system_message},
{"role": "user", "content": user_message}
]
@mcp.prompt()
def automation_health_check():
"""
Review all automations, find conflicts, redundancies, or improvement opportunities
This prompt helps users perform a comprehensive review of their Home Assistant
automations to identify issues, optimize performance, and improve reliability.
Returns:
A list of messages for the interactive conversation
"""
system_message = """You are a Home Assistant automation expert specializing in system optimization.
You'll help the user perform a comprehensive audit of their automations by:
1. Reviewing all automations for potential conflicts (e.g., opposing actions)
2. Identifying redundant automations that could be consolidated
3. Finding inefficient trigger patterns that might cause unnecessary processing
4. Detecting missing conditions that could improve reliability
5. Suggesting template optimizations for more efficient processing
6. Uncovering potential race conditions between automations
7. Recommending structural improvements to the automation organization
8. Highlighting best practices and suggesting implementation changes"""
user_message = "I'd like to do a health check on all my Home Assistant automations. Can you help me review them for conflicts, redundancies, and potential improvements?"
return [
{"role": "system", "content": system_message},
{"role": "user", "content": user_message}
]
@mcp.prompt()
def entity_naming_consistency():
"""
Audit entity names and suggest standardization improvements
This prompt helps users analyze their entity naming conventions and create
a more consistent, organized naming system across their Home Assistant instance.
Returns:
A list of messages for the interactive conversation
"""
system_message = """You are a Home Assistant organization expert specializing in entity naming conventions.
You'll help the user audit and improve their entity naming by:
1. Analyzing current entity IDs and friendly names for inconsistencies
2. Identifying patterns in existing naming conventions
3. Suggesting standardized naming schemes based on entity types and locations
4. Creating clear guidelines for future entity naming
5. Proposing specific name changes for entities that don't follow conventions
6. Showing how to implement these changes without breaking automations
7. Explaining benefits of consistent naming for automation and UI organization"""
user_message = "I'd like to make my Home Assistant entity names more consistent and organized. Can you help me audit my current naming conventions and suggest improvements?"
return [
{"role": "system", "content": system_message},
{"role": "user", "content": user_message}
]
@mcp.prompt()
def dashboard_layout_generator():
"""
Create optimized dashboards based on user preferences and usage patterns
This prompt helps users design effective, user-friendly dashboards
for their Home Assistant instance based on their specific needs.
Returns:
A list of messages for the interactive conversation
"""
system_message = """You are a Home Assistant UI design expert specializing in dashboard creation.
You'll help the user create optimized dashboards by:
1. Analyzing which entities they interact with most frequently
2. Identifying logical groupings of entities (by room, function, or use case)
3. Suggesting dashboard layouts with the most important controls prominently placed
4. Creating specialized views for different contexts (mobile, tablet, wall-mounted)
5. Designing intuitive card arrangements that minimize scrolling/clicking
6. Recommending specialized cards and custom components that enhance usability
7. Balancing information density with visual clarity
8. Creating consistent visual patterns that aid in quick recognition"""
user_message = "I'd like to redesign my Home Assistant dashboards to be more functional and user-friendly. Can you help me create optimized layouts based on how I actually use my system?"
return [
{"role": "system", "content": system_message},
{"role": "user", "content": user_message}
]
# Documentation endpoint
@mcp.tool()
@async_handler("get_history")
async def get_history(entity_id: str, hours: int = 24) -> Dict[str, Any]:
"""
Get the history of an entity's state changes
Args:
entity_id: The entity ID to get history for
hours: Number of hours of history to retrieve (default: 24)
Returns:
A dictionary containing:
- entity_id: The entity ID requested
- states: List of state objects with timestamps
- count: Number of state changes found
- first_changed: Timestamp of earliest state change
- last_changed: Timestamp of most recent state change
Examples:
entity_id="light.living_room" - get 24h history
entity_id="sensor.temperature", hours=168 - get 7 day history
Best Practices:
- Keep hours reasonable (24-72) for token efficiency
- Use for entities with discrete state changes rather than continuously changing sensors
- Consider the state distribution rather than every individual state
"""
logger.info(f"Getting history for entity: {entity_id}, hours: {hours}")
try:
# Get current state to ensure entity exists
current = await get_entity_state(entity_id, detailed=True)
if isinstance(current, dict) and "error" in current:
return {
"entity_id": entity_id,
"error": current["error"],
"states": [],
"count": 0
}
# For now, this is a stub that returns minimal dummy data
# In a real implementation, this would call the Home Assistant history API
now = current.get("last_updated", "2023-03-15T12:00:00.000Z")
# Create a dummy history (would be replaced with real API call)
states = [
{
"state": current.get("state", "unknown"),
"last_changed": now,
"attributes": current.get("attributes", {})
}
]
# Add a note about this being placeholder data
return {
"entity_id": entity_id,
"states": states,
"count": len(states),
"first_changed": now,
"last_changed": now,
"note": "This is placeholder data. Future versions will include real historical data."
}
except Exception as e:
logger.error(f"Error retrieving history for {entity_id}: {str(e)}")
return {
"entity_id": entity_id,
"error": f"Error retrieving history: {str(e)}",
"states": [],
"count": 0
}
@mcp.tool()
@async_handler("get_error_log")
async def get_error_log() -> Dict[str, Any]:
"""
Get the Home Assistant error log for troubleshooting
Returns:
A dictionary containing:
- log_text: The full error log text
- error_count: Number of ERROR entries found
- warning_count: Number of WARNING entries found
- integration_mentions: Map of integration names to mention counts
- error: Error message if retrieval failed
Examples:
Returns errors, warnings count and integration mentions
Best Practices:
- Use this tool when troubleshooting specific Home Assistant errors
- Look for patterns in repeated errors
- Pay attention to timestamps to correlate errors with events
- Focus on integrations with many mentions in the log
"""
logger.info("Getting Home Assistant error log")
return await get_hass_error_log()