"""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 import area_registry as ar from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) DOMAIN = "mcp_bridge" 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: """Return entities exposed to conversation.""" entity_reg = er.async_get(hass) area_reg = ar.async_get(hass) exposed_entities: list[dict[str, Any]] = [] for state in hass.states.async_all(): entity_entry = entity_reg.async_get(state.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": state.entity_id, "state": state.state, "friendly_name": state.attributes.get( "friendly_name", state.entity_id ), "area": area_name, "domain": state.entity_id.split(".", 1)[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), } ) return ServiceResponse( { "entities": exposed_entities, "count": len(exposed_entities), } ) async def get_exposed_scripts(call: ServiceCall) -> ServiceResponse: """Return 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", {}), } ) return ServiceResponse( { "scripts": exposed_scripts, "count": len(exposed_scripts), } ) async def get_entity_metadata(call: ServiceCall) -> ServiceResponse: """Return detailed metadata for a single 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 = ar.async_get(hass) state = hass.states.get(entity_id) if not state: return ServiceResponse( {"error": f"Entity {entity_id} not found"}, success=False, ) entity_entry = entity_reg.async_get(entity_id) 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(".", 1)[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