Initial commit: Home Assistant MCP Server

This commit is contained in:
2026-02-03 09:38:31 +08:00
commit 6a7dfb88a5
9 changed files with 1921 additions and 0 deletions

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
# Home Assistant Configuration
HA_URL=http://localhost:8123
HA_TOKEN=your_long_lived_access_token_here

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
dist/
.env
*.log

98
README.md Normal file
View File

@@ -0,0 +1,98 @@
# Home Assistant MCP Server
An MCP (Model Context Protocol) server that enables LLMs to interact with Home Assistant through its REST API.
## Features
- **get_states** - Get entity states (all or specific)
- **call_service** - Call Home Assistant services (turn on/off lights, switches, etc.)
- **get_history** - Get historical state data for entities
- **get_config** - Get Home Assistant configuration
- **get_services** - List available services
- **render_template** - Render Jinja2 templates
- **get_logbook** - Get logbook entries
- **fire_event** - Fire custom events
## Setup
### 1. Generate a Long-Lived Access Token
1. Log in to your Home Assistant instance
2. Go to your Profile (click on your name in the sidebar)
3. Scroll down to "Long-Lived Access Tokens"
4. Click "Create Token"
5. Give it a name (e.g., "MCP Server")
6. Copy the token immediately (it won't be shown again!)
### 2. Configure Environment
```bash
cp .env.example .env
```
Edit `.env` with your Home Assistant URL and token:
```
HA_URL=http://your-home-assistant-ip:8123
HA_TOKEN=your_long_lived_access_token
```
### 3. Install and Build
```bash
npm install
npm run build
```
## Usage
### With Claude Desktop
Add to your Claude Desktop configuration (`~/.config/Claude/claude_desktop_config.json` on Linux):
```json
{
"mcpServers": {
"home-assistant": {
"command": "node",
"args": ["/path/to/home-assistant-mcp/dist/index.js"],
"env": {
"HA_URL": "http://your-home-assistant-ip:8123",
"HA_TOKEN": "your_long_lived_access_token"
}
}
}
}
```
### With Other MCP Clients
Run the server:
```bash
npm start
```
The server communicates via stdio using the MCP protocol.
## Example Commands
Once connected, you can ask the LLM things like:
- "What's the current temperature in the living room?"
- "Turn on the kitchen lights"
- "Show me what happened in the last hour"
- "What's the status of all my lights?"
- "Set the thermostat to 22 degrees"
## Development
Watch mode for TypeScript:
```bash
npm run dev
```
## License
MIT

1178
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "home-assistant-mcp",
"version": "1.0.0",
"description": "MCP Server for Home Assistant integration",
"type": "module",
"main": "dist/index.js",
"bin": {
"home-assistant-mcp": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc --watch"
},
"keywords": [
"mcp",
"home-assistant",
"llm",
"ai"
],
"author": "",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"dotenv": "^16.3.1"
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.0"
}
}

4
run_server.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
export HA_URL="http://10.8.0.17:8123"
export HA_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3MjkxZDFhNmQ1ZDQ0MGI1YmQ5ODgwYTZlZTZjYmIxYiIsImlhdCI6MTc2OTkxNzcyNSwiZXhwIjoyMDg1Mjc3NzI1fQ.Bpqs8Tr7Nxle377HbIP5u8fQZnSL5rQjYxBYaeEJMuw"
exec /usr/bin/node /home/wartana/myApp/home-assistant-mcp/dist/index.js

220
src/client.ts Normal file
View File

@@ -0,0 +1,220 @@
/**
* Home Assistant API Client
* Provides HTTP client for interacting with Home Assistant REST API
*/
export interface HAState {
entity_id: string;
state: string;
attributes: Record<string, unknown>;
last_changed: string;
last_updated: string;
}
export interface HAConfig {
components: string[];
config_dir: string;
elevation: number;
latitude: number;
longitude: number;
location_name: string;
time_zone: string;
unit_system: {
length: string;
mass: string;
temperature: string;
volume: string;
};
version: string;
}
export interface HAService {
domain: string;
services: Record<string, {
description?: string;
fields?: Record<string, unknown>;
}>;
}
export interface HAHistoryEntry {
entity_id: string;
state: string;
attributes?: Record<string, unknown>;
last_changed: string;
last_updated?: string;
}
export interface HALogbookEntry {
context_user_id: string | null;
domain: string;
entity_id: string;
message: string;
name: string;
when: string;
}
export class HomeAssistantClient {
private baseUrl: string;
private token: string;
constructor(baseUrl: string, token: string) {
// Remove trailing slash if present
this.baseUrl = baseUrl.replace(/\/$/, '');
this.token = token;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Home Assistant API error (${response.status}): ${errorText}`);
}
// Some endpoints return plain text
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
return response.json() as Promise<T>;
}
return response.text() as unknown as T;
}
/**
* Check if the API is running
*/
async checkApi(): Promise<{ message: string }> {
return this.request<{ message: string }>('/api/');
}
/**
* Get Home Assistant configuration
*/
async getConfig(): Promise<HAConfig> {
return this.request<HAConfig>('/api/config');
}
/**
* Get all entity states
*/
async getStates(): Promise<HAState[]> {
return this.request<HAState[]>('/api/states');
}
/**
* Get state of a specific entity
*/
async getState(entityId: string): Promise<HAState> {
return this.request<HAState>(`/api/states/${entityId}`);
}
/**
* Get all available services
*/
async getServices(): Promise<HAService[]> {
return this.request<HAService[]>('/api/services');
}
/**
* Call a service
*/
async callService(
domain: string,
service: string,
data?: Record<string, unknown>,
returnResponse = false
): Promise<HAState[] | Record<string, unknown>> {
const endpoint = `/api/services/${domain}/${service}${returnResponse ? '?return_response' : ''}`;
return this.request<HAState[] | Record<string, unknown>>(endpoint, {
method: 'POST',
body: JSON.stringify(data || {}),
});
}
/**
* Get entity history
*/
async getHistory(
entityId: string,
startTime?: string,
endTime?: string,
minimalResponse = true
): Promise<HAHistoryEntry[][]> {
const params = new URLSearchParams();
params.set('filter_entity_id', entityId);
if (minimalResponse) {
params.set('minimal_response', '');
}
if (endTime) {
params.set('end_time', endTime);
}
const timestamp = startTime || '';
const endpoint = `/api/history/period/${timestamp}?${params.toString()}`;
return this.request<HAHistoryEntry[][]>(endpoint);
}
/**
* Get logbook entries
*/
async getLogbook(
entityId?: string,
startTime?: string,
endTime?: string
): Promise<HALogbookEntry[]> {
const params = new URLSearchParams();
if (entityId) {
params.set('entity', entityId);
}
if (endTime) {
params.set('end_time', endTime);
}
const timestamp = startTime || '';
const queryString = params.toString();
const endpoint = `/api/logbook/${timestamp}${queryString ? '?' + queryString : ''}`;
return this.request<HALogbookEntry[]>(endpoint);
}
/**
* Render a template
*/
async renderTemplate(template: string): Promise<string> {
return this.request<string>('/api/template', {
method: 'POST',
body: JSON.stringify({ template }),
});
}
/**
* Get error log
*/
async getErrorLog(): Promise<string> {
return this.request<string>('/api/error_log');
}
/**
* Fire an event
*/
async fireEvent(
eventType: string,
eventData?: Record<string, unknown>
): Promise<{ message: string }> {
return this.request<{ message: string }>(`/api/events/${eventType}`, {
method: 'POST',
body: JSON.stringify(eventData || {}),
});
}
}

356
src/index.ts Normal file
View File

@@ -0,0 +1,356 @@
#!/usr/bin/env node
/**
* Home Assistant MCP Server
* Provides tools for LLMs to interact with Home Assistant
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { config } from 'dotenv';
import { HomeAssistantClient } from './client.js';
// Load environment variables
config();
const HA_URL = process.env.HA_URL || 'http://localhost:8123';
const HA_TOKEN = process.env.HA_TOKEN || '';
if (!HA_TOKEN) {
console.error('Error: HA_TOKEN environment variable is required');
console.error('Please set HA_TOKEN to your Home Assistant Long-Lived Access Token');
process.exit(1);
}
// Initialize Home Assistant client
const haClient = new HomeAssistantClient(HA_URL, HA_TOKEN);
// Define available tools
const tools: Tool[] = [
{
name: 'get_states',
description: 'Get the current state of entities in Home Assistant. Returns all entities if no entity_id is specified, or a specific entity if entity_id is provided.',
inputSchema: {
type: 'object',
properties: {
entity_id: {
type: 'string',
description: 'Optional. The entity ID to get state for (e.g., "light.living_room", "sensor.temperature"). If not provided, returns all entities.',
},
},
},
},
{
name: 'call_service',
description: 'Call a Home Assistant service to control devices. Common services include: light.turn_on, light.turn_off, switch.turn_on, switch.turn_off, climate.set_temperature, etc.',
inputSchema: {
type: 'object',
properties: {
domain: {
type: 'string',
description: 'The service domain (e.g., "light", "switch", "climate", "automation")',
},
service: {
type: 'string',
description: 'The service to call (e.g., "turn_on", "turn_off", "toggle", "set_temperature")',
},
data: {
type: 'object',
description: 'Optional service data. Usually includes "entity_id" and service-specific parameters.',
},
},
required: ['domain', 'service'],
},
},
{
name: 'get_history',
description: 'Get the state history of an entity over a time period. Useful for checking historical values of sensors, when devices were turned on/off, etc.',
inputSchema: {
type: 'object',
properties: {
entity_id: {
type: 'string',
description: 'The entity ID to get history for (e.g., "sensor.temperature")',
},
start_time: {
type: 'string',
description: 'Optional. Start time in ISO format (e.g., "2024-01-01T00:00:00"). Defaults to 24 hours ago.',
},
end_time: {
type: 'string',
description: 'Optional. End time in ISO format. Defaults to now.',
},
},
required: ['entity_id'],
},
},
{
name: 'get_config',
description: 'Get Home Assistant configuration including version, location, timezone, and installed components.',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_services',
description: 'Get a list of all available services in Home Assistant. Optionally filter by domain.',
inputSchema: {
type: 'object',
properties: {
domain: {
type: 'string',
description: 'Optional. Filter services by domain (e.g., "light", "switch")',
},
},
},
},
{
name: 'render_template',
description: 'Render a Home Assistant Jinja2 template. Useful for getting computed values or formatted information.',
inputSchema: {
type: 'object',
properties: {
template: {
type: 'string',
description: 'The Jinja2 template to render. Example: "{{ states(\'sensor.temperature\') }} °C"',
},
},
required: ['template'],
},
},
{
name: 'get_logbook',
description: 'Get logbook entries showing what happened in Home Assistant. Shows events like state changes, automations triggered, etc.',
inputSchema: {
type: 'object',
properties: {
entity_id: {
type: 'string',
description: 'Optional. Filter logbook entries by entity ID.',
},
start_time: {
type: 'string',
description: 'Optional. Start time in ISO format. Defaults to 24 hours ago.',
},
end_time: {
type: 'string',
description: 'Optional. End time in ISO format. Defaults to now.',
},
},
},
},
{
name: 'fire_event',
description: 'Fire a custom event in Home Assistant. Useful for triggering automations or custom integrations.',
inputSchema: {
type: 'object',
properties: {
event_type: {
type: 'string',
description: 'The event type to fire (e.g., "custom_event", "my_automation_trigger")',
},
event_data: {
type: 'object',
description: 'Optional. Data to include with the event.',
},
},
required: ['event_type'],
},
},
];
// Create server
const server = new Server(
{
name: 'home-assistant-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Handle list tools request
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'get_states': {
const entityId = args?.entity_id as string | undefined;
if (entityId) {
const state = await haClient.getState(entityId);
return {
content: [
{
type: 'text',
text: JSON.stringify(state, null, 2),
},
],
};
} else {
const states = await haClient.getStates();
// Return summary for all states to avoid overwhelming response
const summary = states.map((s) => ({
entity_id: s.entity_id,
state: s.state,
friendly_name: s.attributes.friendly_name,
}));
return {
content: [
{
type: 'text',
text: JSON.stringify(summary, null, 2),
},
],
};
}
}
case 'call_service': {
const domain = args?.domain as string;
const service = args?.service as string;
const data = args?.data as Record<string, unknown> | undefined;
const result = await haClient.callService(domain, service, data);
return {
content: [
{
type: 'text',
text: `Service ${domain}.${service} called successfully.\n\nChanged states:\n${JSON.stringify(result, null, 2)}`,
},
],
};
}
case 'get_history': {
const entityId = args?.entity_id as string;
const startTime = args?.start_time as string | undefined;
const endTime = args?.end_time as string | undefined;
const history = await haClient.getHistory(entityId, startTime, endTime);
return {
content: [
{
type: 'text',
text: JSON.stringify(history, null, 2),
},
],
};
}
case 'get_config': {
const config = await haClient.getConfig();
return {
content: [
{
type: 'text',
text: JSON.stringify(config, null, 2),
},
],
};
}
case 'get_services': {
const domain = args?.domain as string | undefined;
let services = await haClient.getServices();
if (domain) {
services = services.filter((s) => s.domain === domain);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(services, null, 2),
},
],
};
}
case 'render_template': {
const template = args?.template as string;
const result = await haClient.renderTemplate(template);
return {
content: [
{
type: 'text',
text: result,
},
],
};
}
case 'get_logbook': {
const entityId = args?.entity_id as string | undefined;
const startTime = args?.start_time as string | undefined;
const endTime = args?.end_time as string | undefined;
const logbook = await haClient.getLogbook(entityId, startTime, endTime);
return {
content: [
{
type: 'text',
text: JSON.stringify(logbook, null, 2),
},
],
};
}
case 'fire_event': {
const eventType = args?.event_type as string;
const eventData = args?.event_data as Record<string, unknown> | undefined;
const result = await haClient.fireEvent(eventType, eventData);
return {
content: [
{
type: 'text',
text: result.message,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Home Assistant MCP Server running on stdio');
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

27
tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": [
"ES2022"
],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}