#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
declare const process: {
env: Record<string, string | undefined>;
exit(code?: number): never;
};
// Home Assistant client
interface HAEntity {
entity_id: string;
state: string;
attributes: Record<string, any>;
last_changed: string;
last_updated: string;
}
class HomeAssistantClient {
private baseUrl: string;
private token: string;
constructor(baseUrl: string, token: string) {
this.baseUrl = baseUrl.replace(/\/$/, "");
this.token = token;
}
private async fetch(endpoint: string, options: RequestInit = {}) {
const url = `${this.baseUrl}/api/${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${this.token}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
throw new Error(`Home Assistant API error: ${response.status} ${response.statusText}`);
}
return response.json();
}
async getStates(): Promise<HAEntity[]> {
return this.fetch("states");
}
async getState(entityId: string): Promise<HAEntity> {
return this.fetch(`states/${entityId}`);
}
async callService(domain: string, service: string, data: any = {}) {
return this.fetch(`services/${domain}/${service}`, {
method: "POST",
body: JSON.stringify(data),
});
}
async getServices() {
return this.fetch("services");
}
async getConfig() {
return this.fetch("config");
}
}
// Create MCP server
const HASS_HOST = process.env.HASS_HOST;
const HASS_TOKEN = process.env.HASS_TOKEN;
if (!HASS_HOST || !HASS_TOKEN) {
console.error("Error: HASS_HOST and HASS_TOKEN environment variables must be set");
process.exit(1);
}
const haClient = new HomeAssistantClient(HASS_HOST, HASS_TOKEN);
const server = new Server(
{
name: "homeassistant-mcp",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
}
);
// Define tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_entities",
description: "List all Home Assistant entities (lights, switches, sensors, etc.)",
inputSchema: {
type: "object",
properties: {
domain: {
type: "string",
description: "Filter by domain (e.g., 'light', 'switch', 'sensor'). Optional.",
},
},
},
},
{
name: "get_entity_state",
description: "Get the current state and attributes of a specific entity",
inputSchema: {
type: "object",
properties: {
entity_id: {
type: "string",
description: "The entity ID (e.g., 'light.living_room')",
},
},
required: ["entity_id"],
},
},
{
name: "call_service",
description: "Call a Home Assistant service (e.g., turn on a light, set climate)",
inputSchema: {
type: "object",
properties: {
domain: {
type: "string",
description: "Service domain (e.g., 'light', 'switch', 'climate')",
},
service: {
type: "string",
description: "Service name (e.g., 'turn_on', 'turn_off', 'set_temperature')",
},
entity_id: {
type: "string",
description: "Target entity ID",
},
data: {
type: "object",
description: "Additional service data (e.g., brightness, temperature)",
},
},
required: ["domain", "service"],
},
},
{
name: "list_services",
description: "List all available Home Assistant services",
inputSchema: {
type: "object",
properties: {},
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "list_entities": {
const entities = await haClient.getStates();
const filtered = args?.domain
? entities.filter((e) => e.entity_id.startsWith(`${args.domain}.`))
: entities;
return {
content: [
{
type: "text",
text: JSON.stringify(
filtered.map((e) => ({
entity_id: e.entity_id,
state: e.state,
friendly_name: e.attributes.friendly_name,
})),
null,
2
),
},
],
};
}
case "get_entity_state": {
const entity = await haClient.getState(args?.entity_id as string);
return {
content: [
{
type: "text",
text: JSON.stringify(entity, null, 2),
},
],
};
}
case "call_service": {
const serviceData: any = {};
if (args?.entity_id) {
serviceData.entity_id = args.entity_id;
}
if (args?.data) {
Object.assign(serviceData, args.data);
}
const result = await haClient.callService(
args?.domain as string,
args?.service as string,
serviceData
);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
case "list_services": {
const services = await haClient.getServices();
return {
content: [
{
type: "text",
text: JSON.stringify(services, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
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);
});