Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
194
README.md
194
README.md
@ -1,5 +1,7 @@
|
|||||||
# MCP Bridge - Home Assistant Integration
|
# MCP Bridge - Home Assistant Integration
|
||||||
|
|
||||||
|
[](https://github.com/custom-components/hacs)
|
||||||
|
|
||||||
Bridge integration between Home Assistant and MCP (Model Context Protocol) servers, enabling AI agents to control your smart home.
|
Bridge integration between Home Assistant and MCP (Model Context Protocol) servers, enabling AI agents to control your smart home.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@ -16,79 +18,29 @@ MCP (Model Context Protocol) is Anthropic's protocol for connecting AI agents to
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
**Note:** This integration is hosted on Gitea (not GitHub), so it's not available via HACS. Instead, use one of the automated installation methods below.
|
### Via HACS (Recommended)
|
||||||
|
|
||||||
### Method 1: One-Line Install/Update (Easiest)
|
1. **Add Custom Repository**
|
||||||
|
- Open HACS in Home Assistant
|
||||||
|
- Click the three dots (⋮) in top right
|
||||||
|
- Select "Custom repositories"
|
||||||
|
- Repository: `http://your-gitea-server/username/homeassistant-mcp-bridge`
|
||||||
|
- Category: **Integration**
|
||||||
|
- Click "Add"
|
||||||
|
|
||||||
Run this command in your Home Assistant terminal (requires Terminal & SSH addon or SSH access):
|
2. **Install**
|
||||||
|
- HACS → Integrations
|
||||||
|
- Click "+ Explore & Download Repositories"
|
||||||
|
- Search for "MCP Bridge"
|
||||||
|
- Click "Download"
|
||||||
|
- Restart Home Assistant
|
||||||
|
|
||||||
```bash
|
### Manual Installation
|
||||||
curl -sSL http://your-gitea.local:3000/username/homeassistant-mcp-bridge/raw/branch/main/quick_install.sh | \
|
|
||||||
GITEA_URL=http://your-gitea.local:3000 \
|
|
||||||
REPO=username/homeassistant-mcp-bridge \
|
|
||||||
bash
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important:**
|
1. Copy `custom_components/mcp_bridge` to your Home Assistant config directory
|
||||||
- Replace `your-gitea.local:3000` with your Gitea server address
|
2. Restart Home Assistant
|
||||||
- Replace `username` with your Gitea username
|
|
||||||
- Replace `main` with your branch name if different
|
|
||||||
|
|
||||||
After installation completes, restart Home Assistant.
|
See [INSTALLATION_GUIDE.md](INSTALLATION_GUIDE.md) for detailed instructions.
|
||||||
|
|
||||||
### Method 2: Home Assistant Auto-Update Button
|
|
||||||
|
|
||||||
Add this to your `configuration.yaml` to create an update button/service:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
shell_command:
|
|
||||||
update_mcp_bridge: >
|
|
||||||
curl -fsSL http://your-gitea.local:3000/username/homeassistant-mcp-bridge/raw/branch/main/quick_install.sh |
|
|
||||||
GITEA_URL=http://your-gitea.local:3000
|
|
||||||
REPO=username/homeassistant-mcp-bridge
|
|
||||||
bash
|
|
||||||
|
|
||||||
script:
|
|
||||||
update_mcp_bridge:
|
|
||||||
alias: "Update MCP Bridge Integration"
|
|
||||||
sequence:
|
|
||||||
- service: shell_command.update_mcp_bridge
|
|
||||||
- delay:
|
|
||||||
seconds: 5
|
|
||||||
- service: persistent_notification.create
|
|
||||||
data:
|
|
||||||
title: "MCP Bridge Updated"
|
|
||||||
message: "Integration updated from Gitea. Please restart Home Assistant."
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
- Go to Developer Tools → Services
|
|
||||||
- Select `script.update_mcp_bridge`
|
|
||||||
- Click "Call Service"
|
|
||||||
- Restart Home Assistant after update
|
|
||||||
|
|
||||||
**Optional Dashboard Button:**
|
|
||||||
```yaml
|
|
||||||
type: button
|
|
||||||
name: Update MCP Bridge
|
|
||||||
icon: mdi:download
|
|
||||||
tap_action:
|
|
||||||
action: call-service
|
|
||||||
service: script.update_mcp_bridge
|
|
||||||
```
|
|
||||||
|
|
||||||
### Method 3: Manual Installation
|
|
||||||
|
|
||||||
1. Download these files from your Gitea repository:
|
|
||||||
- `custom_components/mcp_bridge/__init__.py`
|
|
||||||
- `custom_components/mcp_bridge/manifest.json`
|
|
||||||
- `custom_components/mcp_bridge/services.yaml`
|
|
||||||
|
|
||||||
2. Copy to `/config/custom_components/mcp_bridge/`
|
|
||||||
|
|
||||||
3. Restart Home Assistant
|
|
||||||
|
|
||||||
See [INSTALLATION_GUIDE.md](INSTALLATION_GUIDE.md) for detailed manual steps.
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@ -156,7 +108,8 @@ Returns scripts marked with `mcp_accessible` label, including their parameters.
|
|||||||
"speed": {
|
"speed": {
|
||||||
"name": "Speed",
|
"name": "Speed",
|
||||||
"description": "0=off, 1=low, 2=medium, 3=high",
|
"description": "0=off, 1=low, 2=medium, 3=high",
|
||||||
"required": true
|
"required": true,
|
||||||
|
"selector": {"number": {"min": 0, "max": 3}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -172,17 +125,6 @@ Get detailed metadata for a specific entity.
|
|||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `entity_id`: The entity to query
|
- `entity_id`: The entity to query
|
||||||
|
|
||||||
## Updating the Integration
|
|
||||||
|
|
||||||
**Using Auto-Update Script/Button:**
|
|
||||||
- Just run the install command again or call the `script.update_mcp_bridge` service
|
|
||||||
- Restart Home Assistant after update
|
|
||||||
|
|
||||||
**Manual Update:**
|
|
||||||
- Download the latest files from Gitea
|
|
||||||
- Replace files in `/config/custom_components/mcp_bridge/`
|
|
||||||
- Restart Home Assistant
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -218,46 +160,24 @@ No configuration required! The integration automatically loads on Home Assistant
|
|||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
- **Home Assistant**: 2024.1.0 or newer
|
- **Home Assistant**: 2024.1.0 or newer
|
||||||
|
- **HACS**: Any version
|
||||||
- **Python**: 3.11+
|
- **Python**: 3.11+
|
||||||
- **Installation**: Requires SSH/Terminal access or manual file management
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Documentation
|
||||||
|
|
||||||
### Installation Script Fails
|
- [Installation Guide](INSTALLATION_GUIDE.md) - Detailed setup instructions
|
||||||
|
- [Integration README](custom_components/mcp_bridge/README.md) - Technical details
|
||||||
**Check:**
|
- [Gitea Setup Guide](GITEA_SETUP_GUIDE.md) - For hosting on your own Gitea
|
||||||
- Home Assistant can reach your Gitea server
|
|
||||||
- Terminal addon has network access
|
|
||||||
- URLs are correct (server address, username, repo name)
|
|
||||||
|
|
||||||
**Test connectivity:**
|
|
||||||
```bash
|
|
||||||
curl http://your-gitea.local:3000/username/homeassistant-mcp-bridge
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration Not Loading
|
|
||||||
|
|
||||||
**Check logs:**
|
|
||||||
- Settings → System → Logs
|
|
||||||
- Filter by "mcp_bridge"
|
|
||||||
|
|
||||||
**Common issues:**
|
|
||||||
- Files not in correct location (`/config/custom_components/mcp_bridge/`)
|
|
||||||
- Python syntax errors (check logs)
|
|
||||||
- Didn't restart Home Assistant after installation
|
|
||||||
|
|
||||||
### Services Not Showing
|
|
||||||
|
|
||||||
**Verify installation:**
|
|
||||||
```bash
|
|
||||||
ls -la /config/custom_components/mcp_bridge/
|
|
||||||
```
|
|
||||||
|
|
||||||
Should show: `__init__.py`, `manifest.json`, `services.yaml`
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Repository Structure
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest custom_components/mcp_bridge/test_init.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
homeassistant-mcp-bridge/
|
homeassistant-mcp-bridge/
|
||||||
@ -266,46 +186,31 @@ homeassistant-mcp-bridge/
|
|||||||
│ ├── __init__.py # Core integration logic
|
│ ├── __init__.py # Core integration logic
|
||||||
│ ├── manifest.json # Integration metadata
|
│ ├── manifest.json # Integration metadata
|
||||||
│ ├── services.yaml # Service definitions
|
│ ├── services.yaml # Service definitions
|
||||||
│ └── test_init.py # Unit tests
|
│ └── README.md # Integration docs
|
||||||
├── quick_install.sh # One-line installer
|
├── hacs.json # HACS configuration
|
||||||
├── install_mcp_bridge.sh # Detailed installer
|
├── info.md # HACS UI description
|
||||||
├── ha_shell_command.yaml # HA shell command config
|
└── README.md # This file
|
||||||
└── README.md # This file
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running Tests
|
## Contributing
|
||||||
|
|
||||||
```bash
|
This is a personal project for homelab use, but suggestions and improvements are welcome!
|
||||||
pytest custom_components/mcp_bridge/test_init.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Versioning
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
Check your installed version:
|
3. Make your changes
|
||||||
```yaml
|
4. Test thoroughly
|
||||||
service: mcp_bridge.get_entity_metadata
|
5. Submit a pull request
|
||||||
data:
|
|
||||||
entity_id: sensor.any_sensor # Any entity
|
|
||||||
```
|
|
||||||
|
|
||||||
The response will show the integration version in logs.
|
|
||||||
|
|
||||||
Or check `manifest.json`:
|
|
||||||
```bash
|
|
||||||
cat /config/custom_components/mcp_bridge/manifest.json | grep version
|
|
||||||
```
|
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [x] Basic entity exposure
|
- [x] Basic entity exposure
|
||||||
- [x] Script discovery with labels
|
- [x] Script discovery with labels
|
||||||
- [x] Complete metadata support
|
- [x] Complete metadata support
|
||||||
- [x] Automated install/update script
|
|
||||||
- [ ] Real-time state updates via WebSocket
|
- [ ] Real-time state updates via WebSocket
|
||||||
- [ ] Custom entity tags beyond voice assistant exposure
|
- [ ] Custom entity tags beyond voice assistant exposure
|
||||||
- [ ] Integration config flow (UI-based setup)
|
- [ ] Integration config flow (UI-based setup)
|
||||||
- [ ] Service call history/logging
|
- [ ] Service call history/logging
|
||||||
- [ ] Version checking and update notifications
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@ -313,14 +218,13 @@ MIT License - Use freely in your homelab!
|
|||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
- **Issues**: Report on your Gitea instance
|
- **Issues**: [Gitea Issues](http://your-gitea-server/username/homeassistant-mcp-bridge/issues)
|
||||||
- **Logs**: Settings → System → Logs (filter by "mcp_bridge")
|
- **Logs**: Settings → System → Logs (filter by "mcp_bridge")
|
||||||
- **Questions**: Check the detailed guides in the repository
|
|
||||||
|
|
||||||
## Related Projects
|
## Acknowledgments
|
||||||
|
|
||||||
- **MCP Server**: Coming next - connects to this integration
|
- Built for use with [Anthropic's MCP](https://modelcontextprotocol.io/)
|
||||||
- **n8n Integration**: Workflow automation with AI agents
|
- Designed to work with n8n workflows
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
@ -1,173 +1,171 @@
|
|||||||
"""MCP Bridge integration for Home Assistant."""
|
"""MCP Bridge integration for Home Assistant.
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
This integration provides services to expose filtered entities and scripts
|
||||||
|
to MCP (Model Context Protocol) servers for AI agent control.
|
||||||
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.const import CONF_ENTITY_ID
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
|
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers import area_registry as ar
|
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.components.script import async_get_script_fields
|
from homeassistant.const import CONF_ENTITY_ID
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "mcp_bridge"
|
DOMAIN = "mcp_bridge"
|
||||||
|
|
||||||
|
# Label to mark scripts as MCP-accessible
|
||||||
MCP_ACCESSIBLE_LABEL = "mcp_accessible"
|
MCP_ACCESSIBLE_LABEL = "mcp_accessible"
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up the MCP Bridge integration."""
|
||||||
_LOGGER.info("Setting up MCP Bridge integration")
|
_LOGGER.info("Setting up MCP Bridge integration")
|
||||||
|
|
||||||
async def get_exposed_entities(call: ServiceCall) -> dict[str, Any]:
|
async def get_exposed_entities(call: ServiceCall) -> dict[str, Any]:
|
||||||
|
"""Get all entities exposed to voice assistants."""
|
||||||
entity_reg = er.async_get(hass)
|
entity_reg = er.async_get(hass)
|
||||||
area_reg = ar.async_get(hass)
|
|
||||||
|
|
||||||
entities: list[dict[str, Any]] = []
|
exposed_entities = []
|
||||||
|
|
||||||
for state in hass.states.async_all():
|
# Iterate through all entities
|
||||||
entity_entry = entity_reg.async_get(state.entity_id)
|
for entity_id, state in hass.states.async_all():
|
||||||
if not entity_entry:
|
entity_entry = entity_reg.async_get(entity_id)
|
||||||
continue
|
|
||||||
|
|
||||||
if not entity_entry.options.get("conversation", {}).get(
|
# Check if entity is exposed to conversation (voice assistant)
|
||||||
"should_expose", False
|
if entity_entry and entity_entry.options.get("conversation", {}).get("should_expose", False):
|
||||||
):
|
# Get area name if available
|
||||||
continue
|
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
|
||||||
|
|
||||||
area_name = None
|
# Build entity data
|
||||||
if entity_entry.area_id:
|
entity_data = {
|
||||||
area = area_reg.async_get_area(entity_entry.area_id)
|
"entity_id": entity_id,
|
||||||
if area:
|
|
||||||
area_name = area.name
|
|
||||||
|
|
||||||
entities.append(
|
|
||||||
{
|
|
||||||
"entity_id": state.entity_id,
|
|
||||||
"state": state.state,
|
"state": state.state,
|
||||||
"friendly_name": state.attributes.get(
|
"friendly_name": state.attributes.get("friendly_name", entity_id),
|
||||||
"friendly_name", state.entity_id
|
|
||||||
),
|
|
||||||
"area": area_name,
|
"area": area_name,
|
||||||
"domain": state.entity_id.split(".", 1)[0],
|
"domain": entity_id.split(".")[0],
|
||||||
"device_class": state.attributes.get("device_class"),
|
"device_class": state.attributes.get("device_class"),
|
||||||
"supported_features": state.attributes.get(
|
"supported_features": state.attributes.get("supported_features", 0),
|
||||||
"supported_features", 0
|
|
||||||
),
|
|
||||||
"last_changed": state.last_changed.isoformat(),
|
"last_changed": state.last_changed.isoformat(),
|
||||||
"last_updated": state.last_updated.isoformat(),
|
"last_updated": state.last_updated.isoformat(),
|
||||||
"attributes": dict(state.attributes),
|
"attributes": dict(state.attributes)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
exposed_entities.append(entity_data)
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Found {len(exposed_entities)} exposed entities")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"entities": entities,
|
"entities": exposed_entities,
|
||||||
"count": len(entities),
|
"count": len(exposed_entities)
|
||||||
}
|
}
|
||||||
|
|
||||||
async def get_exposed_scripts(call: ServiceCall) -> dict[str, Any]:
|
async def get_exposed_scripts(call: ServiceCall) -> dict[str, Any]:
|
||||||
|
"""Get all scripts marked as MCP-accessible."""
|
||||||
entity_reg = er.async_get(hass)
|
entity_reg = er.async_get(hass)
|
||||||
scripts: list[dict[str, Any]] = []
|
|
||||||
|
|
||||||
|
exposed_scripts = []
|
||||||
|
|
||||||
|
# Get all script entities
|
||||||
for entity_id in hass.states.async_entity_ids("script"):
|
for entity_id in hass.states.async_entity_ids("script"):
|
||||||
entity_entry = entity_reg.async_get(entity_id)
|
entity_entry = entity_reg.async_get(entity_id)
|
||||||
if not entity_entry:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if MCP_ACCESSIBLE_LABEL not in entity_entry.labels:
|
# Check if script has the mcp_accessible label
|
||||||
continue
|
if entity_entry and MCP_ACCESSIBLE_LABEL in entity_entry.labels:
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
if not state:
|
||||||
|
continue
|
||||||
|
|
||||||
fields = await async_get_script_fields(hass, entity_id)
|
# Get script attributes
|
||||||
|
script_data = {
|
||||||
state = hass.states.get(entity_id)
|
|
||||||
|
|
||||||
scripts.append(
|
|
||||||
{
|
|
||||||
"entity_id": entity_id,
|
"entity_id": entity_id,
|
||||||
"friendly_name": state.attributes.get(
|
"friendly_name": state.attributes.get("friendly_name", entity_id),
|
||||||
"friendly_name", entity_id
|
"description": state.attributes.get("description", ""),
|
||||||
)
|
"fields": state.attributes.get("fields", {}),
|
||||||
if state
|
|
||||||
else entity_id,
|
|
||||||
"description": state.attributes.get("description", "")
|
|
||||||
if state
|
|
||||||
else "",
|
|
||||||
"fields": fields or {},
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
exposed_scripts.append(script_data)
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Found {len(exposed_scripts)} exposed scripts")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"scripts": scripts,
|
"scripts": exposed_scripts,
|
||||||
"count": len(scripts),
|
"count": len(exposed_scripts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def get_entity_metadata(call: ServiceCall) -> dict[str, Any]:
|
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)
|
entity_id = call.data.get(CONF_ENTITY_ID)
|
||||||
|
|
||||||
if not entity_id:
|
if not entity_id:
|
||||||
return {"error": "entity_id is required"}
|
_LOGGER.error("entity_id not provided")
|
||||||
|
return {"error": "entity_id required"}
|
||||||
|
|
||||||
entity_reg = er.async_get(hass)
|
entity_reg = er.async_get(hass)
|
||||||
area_reg = ar.async_get(hass)
|
entity_entry = entity_reg.async_get(entity_id)
|
||||||
|
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
if not state:
|
if not state:
|
||||||
|
_LOGGER.error(f"Entity {entity_id} not found")
|
||||||
return {"error": f"Entity {entity_id} not found"}
|
return {"error": f"Entity {entity_id} not found"}
|
||||||
|
|
||||||
entity_entry = entity_reg.async_get(entity_id)
|
# Get area name if available
|
||||||
|
|
||||||
area_name = None
|
area_name = None
|
||||||
if entity_entry and entity_entry.area_id:
|
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)
|
area = area_reg.async_get_area(entity_entry.area_id)
|
||||||
if area:
|
if area:
|
||||||
area_name = area.name
|
area_name = area.name
|
||||||
|
|
||||||
data: dict[str, Any] = {
|
metadata = {
|
||||||
"entity_id": entity_id,
|
"entity_id": entity_id,
|
||||||
"state": state.state,
|
"state": state.state,
|
||||||
"friendly_name": state.attributes.get(
|
"friendly_name": state.attributes.get("friendly_name", entity_id),
|
||||||
"friendly_name", entity_id
|
|
||||||
),
|
|
||||||
"area": area_name,
|
"area": area_name,
|
||||||
"domain": entity_id.split(".", 1)[0],
|
"domain": entity_id.split(".")[0],
|
||||||
"device_class": state.attributes.get("device_class"),
|
"device_class": state.attributes.get("device_class"),
|
||||||
"supported_features": state.attributes.get(
|
"supported_features": state.attributes.get("supported_features", 0),
|
||||||
"supported_features", 0
|
|
||||||
),
|
|
||||||
"last_changed": state.last_changed.isoformat(),
|
"last_changed": state.last_changed.isoformat(),
|
||||||
"last_updated": state.last_updated.isoformat(),
|
"last_updated": state.last_updated.isoformat(),
|
||||||
"attributes": dict(state.attributes),
|
"attributes": dict(state.attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
if entity_entry:
|
if entity_entry:
|
||||||
data["labels"] = list(entity_entry.labels)
|
metadata["labels"] = list(entity_entry.labels)
|
||||||
data["is_exposed_to_conversation"] = entity_entry.options.get(
|
metadata["is_exposed_to_conversation"] = entity_entry.options.get(
|
||||||
"conversation", {}
|
"conversation", {}
|
||||||
).get("should_expose", False)
|
).get("should_expose", False)
|
||||||
|
|
||||||
return data
|
return metadata
|
||||||
|
|
||||||
|
# Register services
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
"get_exposed_entities",
|
"get_exposed_entities",
|
||||||
get_exposed_entities,
|
get_exposed_entities,
|
||||||
supports_response=SupportsResponse.ONLY,
|
schema=None
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
"get_exposed_scripts",
|
"get_exposed_scripts",
|
||||||
get_exposed_scripts,
|
get_exposed_scripts,
|
||||||
supports_response=SupportsResponse.ONLY,
|
schema=None
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
"get_entity_metadata",
|
"get_entity_metadata",
|
||||||
get_entity_metadata,
|
get_entity_metadata,
|
||||||
supports_response=SupportsResponse.ONLY,
|
schema=None
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.info("MCP Bridge services registered successfully")
|
_LOGGER.info("MCP Bridge services registered successfully")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -4,9 +4,7 @@
|
|||||||
"documentation": "https://www.git.quarantinedstudio.com/mvezina/homeassistant-mcp-bridge",
|
"documentation": "https://www.git.quarantinedstudio.com/mvezina/homeassistant-mcp-bridge",
|
||||||
"issue_tracker": "https://www.git.quarantinedstudio.com/mvezina/homeassistant-mcp-bridge/issues",
|
"issue_tracker": "https://www.git.quarantinedstudio.com/mvezina/homeassistant-mcp-bridge/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"codeowners": [
|
"codeowners": [],
|
||||||
"@mvezina"
|
|
||||||
],
|
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"config_flow": false
|
"config_flow": false
|
||||||
|
|||||||
@ -1,130 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# MCP Bridge Installation Diagnostic Script
|
|
||||||
# Run this manually in HA terminal to see what's happening
|
|
||||||
|
|
||||||
echo "=== MCP Bridge Installation Diagnostic ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 1. Check network connectivity
|
|
||||||
echo "1. Testing Gitea connectivity..."
|
|
||||||
GITEA_URL="${GITEA_URL:-https://www.git.quarantinedstudio.com}"
|
|
||||||
REPO="${REPO:-mvezina/homeassistant-mcp-bridge}"
|
|
||||||
|
|
||||||
echo " Attempting to reach: $GITEA_URL/$REPO"
|
|
||||||
if curl -I "$GITEA_URL/$REPO" 2>&1 | head -n 1; then
|
|
||||||
echo " ✓ Can reach Gitea server"
|
|
||||||
else
|
|
||||||
echo " ✗ Cannot reach Gitea server"
|
|
||||||
echo " Please check:"
|
|
||||||
echo " - Is Gitea running?"
|
|
||||||
echo " - Is the URL correct?"
|
|
||||||
echo " - Can HA reach your Gitea server?"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 2. Check if curl exists
|
|
||||||
echo "2. Checking for curl..."
|
|
||||||
if command -v curl &> /dev/null; then
|
|
||||||
echo " ✓ curl is available: $(which curl)"
|
|
||||||
curl --version | head -n 1
|
|
||||||
else
|
|
||||||
echo " ✗ curl is NOT available"
|
|
||||||
echo " Checking for wget..."
|
|
||||||
if command -v wget &> /dev/null; then
|
|
||||||
echo " ✓ wget is available: $(which wget)"
|
|
||||||
else
|
|
||||||
echo " ✗ Neither curl nor wget available!"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 3. Check config directory
|
|
||||||
echo "3. Checking config directory..."
|
|
||||||
CONFIG_DIR="/config"
|
|
||||||
if [ -d "$CONFIG_DIR" ]; then
|
|
||||||
echo " ✓ /config exists"
|
|
||||||
else
|
|
||||||
echo " ✗ /config does not exist, trying alternate..."
|
|
||||||
CONFIG_DIR="${HOME}/.homeassistant"
|
|
||||||
if [ -d "$CONFIG_DIR" ]; then
|
|
||||||
echo " ✓ Using $CONFIG_DIR"
|
|
||||||
else
|
|
||||||
echo " ✗ Cannot find config directory!"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 4. Check/create custom_components directory
|
|
||||||
echo "4. Checking custom_components directory..."
|
|
||||||
CUSTOM_DIR="$CONFIG_DIR/custom_components"
|
|
||||||
if [ -d "$CUSTOM_DIR" ]; then
|
|
||||||
echo " ✓ $CUSTOM_DIR exists"
|
|
||||||
else
|
|
||||||
echo " ! $CUSTOM_DIR does not exist, creating..."
|
|
||||||
mkdir -p "$CUSTOM_DIR" && echo " ✓ Created successfully" || echo " ✗ Failed to create"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 5. Check write permissions
|
|
||||||
echo "5. Checking write permissions..."
|
|
||||||
TEST_FILE="$CUSTOM_DIR/test_write_$$"
|
|
||||||
if touch "$TEST_FILE" 2>/dev/null; then
|
|
||||||
echo " ✓ Can write to $CUSTOM_DIR"
|
|
||||||
rm "$TEST_FILE"
|
|
||||||
else
|
|
||||||
echo " ✗ Cannot write to $CUSTOM_DIR"
|
|
||||||
echo " Permission issue!"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 6. Try downloading a test file
|
|
||||||
echo "6. Testing file download..."
|
|
||||||
BRANCH="${BRANCH:-main}"
|
|
||||||
TEST_URL="$GITEA_URL/$REPO/raw/branch/$BRANCH/custom_components/mcp_bridge/manifest.json"
|
|
||||||
echo " URL: $TEST_URL"
|
|
||||||
|
|
||||||
TEMP_FILE="/tmp/mcp_test_$$"
|
|
||||||
if curl -fsSL "$TEST_URL" -o "$TEMP_FILE" 2>/dev/null; then
|
|
||||||
echo " ✓ Successfully downloaded test file"
|
|
||||||
echo " File size: $(wc -c < "$TEMP_FILE") bytes"
|
|
||||||
echo " First line: $(head -n 1 "$TEMP_FILE")"
|
|
||||||
rm "$TEMP_FILE"
|
|
||||||
else
|
|
||||||
echo " ✗ Failed to download test file"
|
|
||||||
echo " Possible issues:"
|
|
||||||
echo " - Wrong repository name or path"
|
|
||||||
echo " - Wrong branch name"
|
|
||||||
echo " - File doesn't exist at that path"
|
|
||||||
echo " - Network/firewall blocking"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 7. Check existing installation
|
|
||||||
echo "7. Checking for existing installation..."
|
|
||||||
INSTALL_DIR="$CONFIG_DIR/custom_components/mcp_bridge"
|
|
||||||
if [ -d "$INSTALL_DIR" ]; then
|
|
||||||
echo " ✓ Installation directory exists: $INSTALL_DIR"
|
|
||||||
echo " Files:"
|
|
||||||
ls -lh "$INSTALL_DIR" 2>/dev/null || echo " (empty or cannot list)"
|
|
||||||
|
|
||||||
if [ -f "$INSTALL_DIR/manifest.json" ]; then
|
|
||||||
VERSION=$(grep -oP '"version":\s*"\K[^"]+' "$INSTALL_DIR/manifest.json" 2>/dev/null || echo "unknown")
|
|
||||||
echo " Current version: $VERSION"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " ! Installation directory does not exist yet"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 8. Provide installation command
|
|
||||||
echo "8. Suggested installation command:"
|
|
||||||
echo ""
|
|
||||||
echo " export GITEA_URL=\"$GITEA_URL\""
|
|
||||||
echo " export REPO=\"$REPO\""
|
|
||||||
echo " export BRANCH=\"$BRANCH\""
|
|
||||||
echo " curl -fsSL \"\$GITEA_URL/\$REPO/raw/branch/\$BRANCH/quick_install.sh\" | bash"
|
|
||||||
echo ""
|
|
||||||
echo "=== Diagnostic Complete ==="
|
|
||||||
echo ""
|
|
||||||
echo "If all checks passed, run the installation command above."
|
|
||||||
echo "If checks failed, fix the issues indicated and try again."
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
# Add this to your configuration.yaml
|
|
||||||
|
|
||||||
shell_command:
|
|
||||||
# Update MCP Bridge integration from Gitea
|
|
||||||
update_mcp_bridge: >
|
|
||||||
curl -fsSL https://www.git.quarantinedstudio.com/mvezina/homeassistant-mcp-bridge/raw/branch/main/quick_install.sh |
|
|
||||||
GITEA_URL=https://www.git.quarantinedstudio.com
|
|
||||||
REPO=mvezina/homeassistant-mcp-bridge
|
|
||||||
bash
|
|
||||||
|
|
||||||
# Then create an automation or script to call it:
|
|
||||||
|
|
||||||
script:
|
|
||||||
update_mcp_bridge:
|
|
||||||
alias: "Update MCP Bridge Integration"
|
|
||||||
sequence:
|
|
||||||
- service: shell_command.update_mcp_bridge
|
|
||||||
- delay:
|
|
||||||
seconds: 5
|
|
||||||
- service: system_log.write
|
|
||||||
data:
|
|
||||||
message: "MCP Bridge updated. Please restart Home Assistant."
|
|
||||||
level: warning
|
|
||||||
- service: persistent_notification.create
|
|
||||||
data:
|
|
||||||
title: "MCP Bridge Updated"
|
|
||||||
message: "The MCP Bridge integration has been updated. Please restart Home Assistant for changes to take effect."
|
|
||||||
|
|
||||||
# Optional: Add a button to your dashboard
|
|
||||||
button:
|
|
||||||
- type: button
|
|
||||||
name: Update MCP Bridge
|
|
||||||
icon: mdi:download
|
|
||||||
tap_action:
|
|
||||||
action: call-service
|
|
||||||
service: script.update_mcp_bridge
|
|
||||||
6
hacs.json
Normal file
6
hacs.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "MCP Bridge",
|
||||||
|
"content_in_root": false,
|
||||||
|
"render_readme": true,
|
||||||
|
"homeassistant": "2024.1.0"
|
||||||
|
}
|
||||||
35
info.md
Normal file
35
info.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# MCP Bridge
|
||||||
|
|
||||||
|
Bridge integration between Home Assistant and MCP (Model Context Protocol) servers for AI agent control.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ Expose entities to AI agents via voice assistant settings
|
||||||
|
- ✅ Control which scripts are accessible using labels
|
||||||
|
- ✅ Complete entity metadata including area, device class, and attributes
|
||||||
|
- ✅ Secure: Only exposes what you explicitly mark
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
- `mcp_bridge.get_exposed_entities` - Get all exposed entities
|
||||||
|
- `mcp_bridge.get_exposed_scripts` - Get MCP-accessible scripts
|
||||||
|
- `mcp_bridge.get_entity_metadata` - Get detailed entity info
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
No configuration needed! The integration auto-loads after installation.
|
||||||
|
|
||||||
|
### Exposing Entities
|
||||||
|
|
||||||
|
Go to: **Settings → Voice Assistants → Expose**
|
||||||
|
|
||||||
|
Toggle the entities you want the AI agent to control.
|
||||||
|
|
||||||
|
### Exposing Scripts
|
||||||
|
|
||||||
|
1. Create or edit a script
|
||||||
|
2. Click the settings icon ⚙️
|
||||||
|
3. Add the label: `mcp_accessible`
|
||||||
|
4. Save
|
||||||
|
|
||||||
|
That's it! The script is now accessible to AI agents.
|
||||||
@ -1,69 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# MCP Bridge Quick Installer with Logging
|
|
||||||
# Logs to /config/mcp_bridge_install.log
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
GITEA_URL="${GITEA_URL:-http://your-gitea.local:3000}"
|
|
||||||
REPO="${REPO:-username/homeassistant-mcp-bridge}"
|
|
||||||
BRANCH="${BRANCH:-main}"
|
|
||||||
BASE_URL="$GITEA_URL/$REPO/raw/branch/$BRANCH/custom_components/mcp_bridge"
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_FILE="/config/mcp_bridge_install.log"
|
|
||||||
exec 1> >(tee -a "$LOG_FILE")
|
|
||||||
exec 2>&1
|
|
||||||
|
|
||||||
echo "=== MCP Bridge Installation Started at $(date) ==="
|
|
||||||
echo "Configuration:"
|
|
||||||
echo " GITEA_URL: $GITEA_URL"
|
|
||||||
echo " REPO: $REPO"
|
|
||||||
echo " BRANCH: $BRANCH"
|
|
||||||
echo " BASE_URL: $BASE_URL"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Determine config directory
|
|
||||||
CONFIG_DIR="${CONFIG_DIR:-/config}"
|
|
||||||
if [ ! -d "$CONFIG_DIR" ]; then
|
|
||||||
CONFIG_DIR="${HOME}/.homeassistant"
|
|
||||||
echo "Using alternate config dir: $CONFIG_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
DEST="$CONFIG_DIR/custom_components/mcp_bridge"
|
|
||||||
echo "Installation directory: $DEST"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Create directory
|
|
||||||
echo "Creating directory..."
|
|
||||||
mkdir -p "$DEST" || {
|
|
||||||
echo "ERROR: Failed to create directory $DEST"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
echo "Directory created: $DEST"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Download files
|
|
||||||
echo "Downloading files..."
|
|
||||||
cd "$DEST" || exit 1
|
|
||||||
|
|
||||||
for file in __init__.py manifest.json services.yaml; do
|
|
||||||
echo " Downloading $file from $BASE_URL/$file"
|
|
||||||
|
|
||||||
if curl -fsSL "$BASE_URL/$file" -o "$file"; then
|
|
||||||
echo " ✓ $file downloaded successfully ($(wc -c < "$file") bytes)"
|
|
||||||
else
|
|
||||||
echo " ✗ FAILED to download $file"
|
|
||||||
echo " URL attempted: $BASE_URL/$file"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Installation Summary ==="
|
|
||||||
echo "Files installed in: $DEST"
|
|
||||||
ls -lh "$DEST"
|
|
||||||
echo ""
|
|
||||||
echo "✅ MCP Bridge installed successfully!"
|
|
||||||
echo "⚠️ IMPORTANT: Restart Home Assistant to activate"
|
|
||||||
echo ""
|
|
||||||
echo "Installation completed at $(date)"
|
|
||||||
echo "=== End of Installation Log ==="
|
|
||||||
Loading…
x
Reference in New Issue
Block a user