202 lines
6.3 KiB
Python

"""MCP Bridge integration for Home Assistant.
This integration provides services to expose filtered entities and scripts
to MCP (Model Context Protocol) servers for AI agent control.
"""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
DOMAIN = "mcp_bridge"
# Label to mark scripts as MCP-accessible
MCP_ACCESSIBLE_LABEL = "mcp_accessible"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the MCP Bridge integration."""
_LOGGER.info("Setting up MCP Bridge integration")
async def get_exposed_entities(call: ServiceCall) -> ServiceResponse:
"""Get all entities exposed to conversation / voice assistants."""
entity_reg = er.async_get(hass)
area_reg = hass.helpers.area_registry.async_get(hass)
exposed_entities: list[dict[str, Any]] = []
for state in hass.states.async_all():
entity_id = state.entity_id
entity_entry = entity_reg.async_get(entity_id)
if not entity_entry:
continue
if not entity_entry.options.get("conversation", {}).get(
"should_expose", False
):
continue
area_name = None
if entity_entry.area_id:
area = area_reg.async_get_area(entity_entry.area_id)
if area:
area_name = area.name
exposed_entities.append(
{
"entity_id": entity_id,
"state": state.state,
"friendly_name": state.attributes.get(
"friendly_name", entity_id
),
"area": area_name,
"domain": entity_id.split(".")[0],
"device_class": state.attributes.get("device_class"),
"supported_features": state.attributes.get(
"supported_features", 0
),
"last_changed": state.last_changed.isoformat(),
"last_updated": state.last_updated.isoformat(),
"attributes": dict(state.attributes),
}
)
_LOGGER.debug("Found %s exposed entities", len(exposed_entities))
return ServiceResponse(
{
"entities": exposed_entities,
"count": len(exposed_entities),
}
)
async def get_exposed_scripts(call: ServiceCall) -> ServiceResponse:
"""Get all scripts marked as MCP-accessible."""
entity_reg = er.async_get(hass)
exposed_scripts: list[dict[str, Any]] = []
for entity_id in hass.states.async_entity_ids("script"):
entity_entry = entity_reg.async_get(entity_id)
if not entity_entry:
continue
if MCP_ACCESSIBLE_LABEL not in entity_entry.labels:
continue
state = hass.states.get(entity_id)
if not state:
continue
exposed_scripts.append(
{
"entity_id": entity_id,
"friendly_name": state.attributes.get(
"friendly_name", entity_id
),
"description": state.attributes.get("description", ""),
"fields": state.attributes.get("fields", {}),
}
)
_LOGGER.debug("Found %s exposed scripts", len(exposed_scripts))
return ServiceResponse(
{
"scripts": exposed_scripts,
"count": len(exposed_scripts),
}
)
async def get_entity_metadata(call: ServiceCall) -> ServiceResponse:
"""Get detailed metadata for a specific entity."""
entity_id = call.data.get(CONF_ENTITY_ID)
if not entity_id:
return ServiceResponse(
{"error": "entity_id is required"},
success=False,
)
entity_reg = er.async_get(hass)
area_reg = hass.helpers.area_registry.async_get(hass)
entity_entry = entity_reg.async_get(entity_id)
state = hass.states.get(entity_id)
if not state:
return ServiceResponse(
{"error": f"Entity {entity_id} not found"},
success=False,
)
area_name = None
if entity_entry and entity_entry.area_id:
area = area_reg.async_get_area(entity_entry.area_id)
if area:
area_name = area.name
metadata: dict[str, Any] = {
"entity_id": entity_id,
"state": state.state,
"friendly_name": state.attributes.get(
"friendly_name", entity_id
),
"area": area_name,
"domain": entity_id.split(".")[0],
"device_class": state.attributes.get("device_class"),
"supported_features": state.attributes.get(
"supported_features", 0
),
"last_changed": state.last_changed.isoformat(),
"last_updated": state.last_updated.isoformat(),
"attributes": dict(state.attributes),
}
if entity_entry:
metadata["labels"] = list(entity_entry.labels)
metadata["is_exposed_to_conversation"] = entity_entry.options.get(
"conversation", {}
).get("should_expose", False)
return ServiceResponse(metadata)
hass.services.async_register(
DOMAIN,
"get_exposed_entities",
get_exposed_entities,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
"get_exposed_scripts",
get_exposed_scripts,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
"get_entity_metadata",
get_entity_metadata,
supports_response=SupportsResponse.ONLY,
)
_LOGGER.info("MCP Bridge services registered successfully")
return True