initial commit
This commit is contained in:
143
custom_components/mcp_bridge/README.md
Normal file
143
custom_components/mcp_bridge/README.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# MCP Bridge - Home Assistant Custom Integration
|
||||
|
||||
This integration provides services to expose filtered entities and custom scripts to MCP (Model Context Protocol) servers, enabling AI agents to control your Home Assistant instance.
|
||||
|
||||
## Installation
|
||||
|
||||
### Via HACS (Recommended)
|
||||
|
||||
1. **Install HACS** (if not already installed): https://hacs.xyz/docs/setup/download
|
||||
|
||||
2. **Add this repository as a custom repository:**
|
||||
- Open HACS in Home Assistant
|
||||
- Click the three dots in the top right
|
||||
- Select "Custom repositories"
|
||||
- Add your Gitea URL: `http://your-gitea-server/username/homeassistant-mcp-bridge`
|
||||
- Category: Integration
|
||||
- Click "Add"
|
||||
|
||||
3. **Install the integration:**
|
||||
- Go to HACS → Integrations
|
||||
- Click "+ Explore & Download Repositories"
|
||||
- Search for "MCP Bridge"
|
||||
- Click "Download"
|
||||
- Restart Home Assistant
|
||||
|
||||
### Manual Installation
|
||||
|
||||
1. Copy the `mcp_bridge` folder to your Home Assistant `custom_components` directory:
|
||||
```
|
||||
/config/custom_components/mcp_bridge/
|
||||
```
|
||||
|
||||
2. Restart Home Assistant
|
||||
|
||||
3. The integration will be automatically loaded (no configuration needed)
|
||||
|
||||
## Services
|
||||
|
||||
### `mcp_bridge.get_exposed_entities`
|
||||
|
||||
Returns all entities that are exposed to voice assistants.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"entities": [...],
|
||||
"count": 42
|
||||
}
|
||||
```
|
||||
|
||||
### `mcp_bridge.get_exposed_scripts`
|
||||
|
||||
Returns all custom scripts marked with the `mcp_accessible` label.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"scripts": [...],
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
### `mcp_bridge.get_entity_metadata`
|
||||
|
||||
Get detailed metadata for a specific entity.
|
||||
|
||||
**Parameters:**
|
||||
- `entity_id` (required): The entity to query
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"entity_id": "light.living_room",
|
||||
"state": "on",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Exposing Entities to AI Agent
|
||||
|
||||
1. Go to Settings → Voice Assistants → Expose
|
||||
2. Toggle entities you want the AI agent to control
|
||||
3. These entities will be returned by `mcp_bridge.get_exposed_entities`
|
||||
|
||||
### Exposing Scripts to AI Agent
|
||||
|
||||
1. Create your custom script in HA
|
||||
2. Add the label `mcp_accessible` to the script:
|
||||
- Go to Settings → Automations & Scenes → Scripts
|
||||
- Click on your script
|
||||
- Click the settings icon (top right)
|
||||
- Under "Labels", add `mcp_accessible`
|
||||
3. The script will now be returned by `mcp_bridge.get_exposed_scripts`
|
||||
|
||||
## Example: Marking a Script as MCP-Accessible
|
||||
|
||||
```yaml
|
||||
# In your scripts.yaml or via UI
|
||||
set_master_bedroom_fan_speed:
|
||||
alias: "Set Master Bedroom Fan Speed"
|
||||
description: "[MCP] Set the speed of the master bedroom fan."
|
||||
# Add label via UI: Settings → Scripts → (your script) → Labels → mcp_accessible
|
||||
fields:
|
||||
fan_state:
|
||||
name: Fan speed
|
||||
description: "0=off, 1=low, 2=medium, 3=high"
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 3
|
||||
step: 1
|
||||
sequence:
|
||||
# Your script actions here
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
You can test the services in Home Assistant's Developer Tools → Services:
|
||||
|
||||
1. Select `mcp_bridge.get_exposed_entities`
|
||||
2. Click "Call Service"
|
||||
3. View the response in the "Response" tab
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**No entities returned:**
|
||||
- Make sure you've exposed entities via Settings → Voice Assistants → Expose
|
||||
|
||||
**No scripts returned:**
|
||||
- Ensure your scripts have the `mcp_accessible` label
|
||||
- Check Home Assistant logs for errors: Settings → System → Logs
|
||||
|
||||
**Integration not loading:**
|
||||
- Check the logs for errors
|
||||
- Ensure the folder structure is correct
|
||||
- Restart Home Assistant
|
||||
|
||||
## Version
|
||||
|
||||
0.1.0 - Initial release
|
||||
171
custom_components/mcp_bridge/__init__.py
Normal file
171
custom_components/mcp_bridge/__init__.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""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.
|
||||
"""
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.const import CONF_ENTITY_ID
|
||||
|
||||
_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) -> dict[str, Any]:
|
||||
"""Get all entities exposed to voice assistants."""
|
||||
entity_reg = er.async_get(hass)
|
||||
|
||||
exposed_entities = []
|
||||
|
||||
# Iterate through all entities
|
||||
for entity_id, state in hass.states.async_all():
|
||||
entity_entry = entity_reg.async_get(entity_id)
|
||||
|
||||
# Check if entity is exposed to conversation (voice assistant)
|
||||
if entity_entry and entity_entry.options.get("conversation", {}).get("should_expose", False):
|
||||
# Get area name if available
|
||||
area_name = None
|
||||
if entity_entry.area_id:
|
||||
area_reg = hass.helpers.area_registry.async_get(hass)
|
||||
area = area_reg.async_get_area(entity_entry.area_id)
|
||||
if area:
|
||||
area_name = area.name
|
||||
|
||||
# Build entity data
|
||||
entity_data = {
|
||||
"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)
|
||||
}
|
||||
|
||||
exposed_entities.append(entity_data)
|
||||
|
||||
_LOGGER.debug(f"Found {len(exposed_entities)} exposed entities")
|
||||
|
||||
return {
|
||||
"entities": exposed_entities,
|
||||
"count": len(exposed_entities)
|
||||
}
|
||||
|
||||
async def get_exposed_scripts(call: ServiceCall) -> dict[str, Any]:
|
||||
"""Get all scripts marked as MCP-accessible."""
|
||||
entity_reg = er.async_get(hass)
|
||||
|
||||
exposed_scripts = []
|
||||
|
||||
# Get all script entities
|
||||
for entity_id in hass.states.async_entity_ids("script"):
|
||||
entity_entry = entity_reg.async_get(entity_id)
|
||||
|
||||
# Check if script has the mcp_accessible label
|
||||
if entity_entry and MCP_ACCESSIBLE_LABEL in entity_entry.labels:
|
||||
state = hass.states.get(entity_id)
|
||||
if not state:
|
||||
continue
|
||||
|
||||
# Get script attributes
|
||||
script_data = {
|
||||
"entity_id": entity_id,
|
||||
"friendly_name": state.attributes.get("friendly_name", entity_id),
|
||||
"description": state.attributes.get("description", ""),
|
||||
"fields": state.attributes.get("fields", {}),
|
||||
}
|
||||
|
||||
exposed_scripts.append(script_data)
|
||||
|
||||
_LOGGER.debug(f"Found {len(exposed_scripts)} exposed scripts")
|
||||
|
||||
return {
|
||||
"scripts": exposed_scripts,
|
||||
"count": len(exposed_scripts)
|
||||
}
|
||||
|
||||
async def get_entity_metadata(call: ServiceCall) -> dict[str, Any]:
|
||||
"""Get detailed metadata for a specific entity."""
|
||||
entity_id = call.data.get(CONF_ENTITY_ID)
|
||||
|
||||
if not entity_id:
|
||||
_LOGGER.error("entity_id not provided")
|
||||
return {"error": "entity_id required"}
|
||||
|
||||
entity_reg = er.async_get(hass)
|
||||
entity_entry = entity_reg.async_get(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
if not state:
|
||||
_LOGGER.error(f"Entity {entity_id} not found")
|
||||
return {"error": f"Entity {entity_id} not found"}
|
||||
|
||||
# Get area name if available
|
||||
area_name = None
|
||||
if entity_entry and entity_entry.area_id:
|
||||
area_reg = hass.helpers.area_registry.async_get(hass)
|
||||
area = area_reg.async_get_area(entity_entry.area_id)
|
||||
if area:
|
||||
area_name = area.name
|
||||
|
||||
metadata = {
|
||||
"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 metadata
|
||||
|
||||
# Register services
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"get_exposed_entities",
|
||||
get_exposed_entities,
|
||||
schema=None
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"get_exposed_scripts",
|
||||
get_exposed_scripts,
|
||||
schema=None
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"get_entity_metadata",
|
||||
get_entity_metadata,
|
||||
schema=None
|
||||
)
|
||||
|
||||
_LOGGER.info("MCP Bridge services registered successfully")
|
||||
|
||||
return True
|
||||
11
custom_components/mcp_bridge/manifest.json
Normal file
11
custom_components/mcp_bridge/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "mcp_bridge",
|
||||
"name": "MCP Bridge",
|
||||
"documentation": "https://www.git.quarantinedstudio.com/mvezina/homeassistant-mcp-bridge",
|
||||
"issue_tracker": "https://www.git.quarantinedstudio.com/mvezina/homeassistant-mcp-bridge/issues",
|
||||
"requirements": [],
|
||||
"codeowners": [],
|
||||
"version": "0.1.0",
|
||||
"iot_class": "local_polling",
|
||||
"config_flow": false
|
||||
}
|
||||
21
custom_components/mcp_bridge/services.yaml
Normal file
21
custom_components/mcp_bridge/services.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
get_exposed_entities:
|
||||
name: Get Exposed Entities
|
||||
description: Returns all entities exposed to voice assistants with full metadata.
|
||||
fields: {}
|
||||
|
||||
get_exposed_scripts:
|
||||
name: Get Exposed Scripts
|
||||
description: Returns all custom scripts marked as MCP-accessible (with mcp_accessible label).
|
||||
fields: {}
|
||||
|
||||
get_entity_metadata:
|
||||
name: Get Entity Metadata
|
||||
description: Get detailed metadata for a specific entity.
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity ID
|
||||
description: The entity ID to get metadata for
|
||||
required: true
|
||||
example: "light.living_room"
|
||||
selector:
|
||||
entity:
|
||||
Reference in New Issue
Block a user