Refactor Home Assistant server initialization with Express and comprehensive endpoint management

- Added Express server with security middleware
- Implemented dynamic tool registration and API endpoint generation
- Enhanced logging with structured initialization messages
- Created centralized tools tracking for automatic endpoint discovery
- Added explicit Express server startup alongside MCP server
- Improved initialization logging with detailed endpoint information
This commit is contained in:
jango-blockchained
2025-01-31 23:05:14 +01:00
parent 662cb1b2fb
commit 3b2e9640db

View File

@@ -3,6 +3,9 @@ import { config } from 'dotenv';
import { resolve } from 'path'; import { resolve } from 'path';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { sseManager } from './sse/index.js'; import { sseManager } from './sse/index.js';
import { ILogger } from "@digital-alchemy/core";
import express from 'express';
import { rateLimiter, securityHeaders } from './security/index.js';
// Load environment variables based on NODE_ENV // Load environment variables based on NODE_ENV
const envFile = process.env.NODE_ENV === 'production' const envFile = process.env.NODE_ENV === 'production'
@@ -26,6 +29,51 @@ const PORT = process.env.PORT || 3000;
console.log('Initializing Home Assistant connection...'); console.log('Initializing Home Assistant connection...');
// Initialize Express app
const app = express();
// Apply security middleware
app.use(securityHeaders);
app.use(rateLimiter);
app.use(express.json());
// Initialize LiteMCP
const server = new LiteMCP('home-assistant', '0.1.0');
// Define Tool interface
interface Tool {
name: string;
description: string;
parameters: z.ZodType<any>;
execute: (params: any) => Promise<any>;
}
// Array to track tools (moved outside main function)
const tools: Tool[] = [];
// Create API endpoint for each tool
app.post('/api/:tool', async (req, res) => {
const toolName = req.params.tool;
const tool = tools.find((t: Tool) => t.name === toolName);
if (!tool) {
return res.status(404).json({
success: false,
message: `Tool '${toolName}' not found`
});
}
try {
const result = await tool.execute(req.body);
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred'
});
}
});
interface CommandParams { interface CommandParams {
command: string; command: string;
entity_id: string; entity_id: string;
@@ -142,17 +190,66 @@ interface SSEParams {
domain?: string; domain?: string;
} }
interface HistoryParams {
entity_id: string;
start_time?: string;
end_time?: string;
minimal_response?: boolean;
significant_changes_only?: boolean;
}
interface SceneParams {
action: 'list' | 'activate';
scene_id?: string;
}
interface NotifyParams {
message: string;
title?: string;
target?: string;
data?: Record<string, any>;
}
interface AutomationParams {
action: 'list' | 'toggle' | 'trigger';
automation_id?: string;
}
interface AddonParams {
action: 'list' | 'info' | 'install' | 'uninstall' | 'start' | 'stop' | 'restart';
slug?: string;
version?: string;
}
interface PackageParams {
action: 'list' | 'install' | 'uninstall' | 'update';
category: 'integration' | 'plugin' | 'theme' | 'python_script' | 'appdaemon' | 'netdaemon';
repository?: string;
version?: string;
}
interface AutomationConfigParams {
action: 'create' | 'update' | 'delete' | 'duplicate';
automation_id?: string;
config?: {
alias: string;
description?: string;
mode?: 'single' | 'parallel' | 'queued' | 'restart';
trigger: any[];
condition?: any[];
action: any[];
};
}
async function main() { async function main() {
const hass = await get_hass(); const hass = await get_hass();
const logger: ILogger = (hass as any).logger;
// Create MCP server
const server = new LiteMCP('home-assistant', '0.1.0');
// Add the list devices tool // Add the list devices tool
server.addTool({ const listDevicesTool = {
name: 'list_devices', name: 'list_devices',
description: 'List all available Home Assistant devices', description: 'List all available Home Assistant devices',
parameters: z.object({}), parameters: z.object({}).describe('No parameters required'),
execute: async () => { execute: async () => {
try { try {
const response = await fetch(`${HASS_HOST}/api/states`, { const response = await fetch(`${HASS_HOST}/api/states`, {
@@ -166,35 +263,35 @@ async function main() {
throw new Error(`Failed to fetch devices: ${response.statusText}`); throw new Error(`Failed to fetch devices: ${response.statusText}`);
} }
const states = await response.json() as HassEntity[]; const states = await response.json() as HassState[];
const devices = states.reduce((acc: Record<string, HassEntity[]>, state: HassEntity) => { const devices: Record<string, HassState[]> = {};
const domain = state.entity_id.split('.')[0];
if (!acc[domain]) { // Group devices by domain
acc[domain] = []; states.forEach(state => {
const [domain] = state.entity_id.split('.');
if (!devices[domain]) {
devices[domain] = [];
} }
acc[domain].push({ devices[domain].push(state);
entity_id: state.entity_id, });
state: state.state,
attributes: state.attributes,
});
return acc;
}, {});
return { return {
success: true, success: true,
devices, devices
}; };
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred', message: error instanceof Error ? error.message : 'Unknown error occurred'
}; };
} }
}, }
}); };
server.addTool(listDevicesTool);
tools.push(listDevicesTool);
// Add the Home Assistant control tool // Add the Home Assistant control tool
server.addTool({ const controlTool = {
name: 'control', name: 'control',
description: 'Control Home Assistant devices and services', description: 'Control Home Assistant devices and services',
parameters: z.object({ parameters: z.object({
@@ -326,10 +423,12 @@ async function main() {
}; };
} }
} }
}); };
server.addTool(controlTool);
tools.push(controlTool);
// Add the history tool // Add the history tool
server.addTool({ const historyTool = {
name: 'get_history', name: 'get_history',
description: 'Get state history for Home Assistant entities', description: 'Get state history for Home Assistant entities',
parameters: z.object({ parameters: z.object({
@@ -339,7 +438,7 @@ async function main() {
minimal_response: z.boolean().optional().describe('Return minimal response to reduce data size'), minimal_response: z.boolean().optional().describe('Return minimal response to reduce data size'),
significant_changes_only: z.boolean().optional().describe('Only return significant state changes'), significant_changes_only: z.boolean().optional().describe('Only return significant state changes'),
}), }),
execute: async (params) => { execute: async (params: HistoryParams) => {
try { try {
const now = new Date(); const now = new Date();
const startTime = params.start_time ? new Date(params.start_time) : new Date(now.getTime() - 24 * 60 * 60 * 1000); const startTime = params.start_time ? new Date(params.start_time) : new Date(now.getTime() - 24 * 60 * 60 * 1000);
@@ -377,17 +476,19 @@ async function main() {
}; };
} }
}, },
}); };
server.addTool(historyTool);
tools.push(historyTool);
// Add the scenes tool // Add the scenes tool
server.addTool({ const sceneTool = {
name: 'scene', name: 'scene',
description: 'Manage and activate Home Assistant scenes', description: 'Manage and activate Home Assistant scenes',
parameters: z.object({ parameters: z.object({
action: z.enum(['list', 'activate']).describe('Action to perform with scenes'), action: z.enum(['list', 'activate']).describe('Action to perform with scenes'),
scene_id: z.string().optional().describe('Scene ID to activate (required for activate action)'), scene_id: z.string().optional().describe('Scene ID to activate (required for activate action)'),
}), }),
execute: async (params) => { execute: async (params: SceneParams) => {
try { try {
if (params.action === 'list') { if (params.action === 'list') {
const response = await fetch(`${HASS_HOST}/api/states`, { const response = await fetch(`${HASS_HOST}/api/states`, {
@@ -446,10 +547,12 @@ async function main() {
}; };
} }
}, },
}); };
server.addTool(sceneTool);
tools.push(sceneTool);
// Add the notification tool // Add the notification tool
server.addTool({ const notifyTool = {
name: 'notify', name: 'notify',
description: 'Send notifications through Home Assistant', description: 'Send notifications through Home Assistant',
parameters: z.object({ parameters: z.object({
@@ -458,7 +561,7 @@ async function main() {
target: z.string().optional().describe('Specific notification target (e.g., mobile_app_phone)'), target: z.string().optional().describe('Specific notification target (e.g., mobile_app_phone)'),
data: z.record(z.any()).optional().describe('Additional notification data'), data: z.record(z.any()).optional().describe('Additional notification data'),
}), }),
execute: async (params) => { execute: async (params: NotifyParams) => {
try { try {
const service = params.target ? `notify.${params.target}` : 'notify.notify'; const service = params.target ? `notify.${params.target}` : 'notify.notify';
const [domain, service_name] = service.split('.'); const [domain, service_name] = service.split('.');
@@ -491,17 +594,19 @@ async function main() {
}; };
} }
}, },
}); };
server.addTool(notifyTool);
tools.push(notifyTool);
// Add the automation tool // Add the automation tool
server.addTool({ const automationTool = {
name: 'automation', name: 'automation',
description: 'Manage Home Assistant automations', description: 'Manage Home Assistant automations',
parameters: z.object({ parameters: z.object({
action: z.enum(['list', 'toggle', 'trigger']).describe('Action to perform with automation'), action: z.enum(['list', 'toggle', 'trigger']).describe('Action to perform with automation'),
automation_id: z.string().optional().describe('Automation ID (required for toggle and trigger actions)'), automation_id: z.string().optional().describe('Automation ID (required for toggle and trigger actions)'),
}), }),
execute: async (params) => { execute: async (params: AutomationParams) => {
try { try {
if (params.action === 'list') { if (params.action === 'list') {
const response = await fetch(`${HASS_HOST}/api/states`, { const response = await fetch(`${HASS_HOST}/api/states`, {
@@ -562,10 +667,12 @@ async function main() {
}; };
} }
}, },
}); };
server.addTool(automationTool);
tools.push(automationTool);
// Add the addon tool // Add the addon tool
server.addTool({ const addonTool = {
name: 'addon', name: 'addon',
description: 'Manage Home Assistant add-ons', description: 'Manage Home Assistant add-ons',
parameters: z.object({ parameters: z.object({
@@ -573,7 +680,7 @@ async function main() {
slug: z.string().optional().describe('Add-on slug (required for all actions except list)'), slug: z.string().optional().describe('Add-on slug (required for all actions except list)'),
version: z.string().optional().describe('Version to install (only for install action)'), version: z.string().optional().describe('Version to install (only for install action)'),
}), }),
execute: async (params) => { execute: async (params: AddonParams) => {
try { try {
if (params.action === 'list') { if (params.action === 'list') {
const response = await fetch(`${HASS_HOST}/api/hassio/store`, { const response = await fetch(`${HASS_HOST}/api/hassio/store`, {
@@ -665,10 +772,12 @@ async function main() {
}; };
} }
}, },
}); };
server.addTool(addonTool);
tools.push(addonTool);
// Add the package tool // Add the package tool
server.addTool({ const packageTool = {
name: 'package', name: 'package',
description: 'Manage HACS packages and custom components', description: 'Manage HACS packages and custom components',
parameters: z.object({ parameters: z.object({
@@ -678,7 +787,7 @@ async function main() {
repository: z.string().optional().describe('Repository URL or name (required for install)'), repository: z.string().optional().describe('Repository URL or name (required for install)'),
version: z.string().optional().describe('Version to install'), version: z.string().optional().describe('Version to install'),
}), }),
execute: async (params) => { execute: async (params: PackageParams) => {
try { try {
const hacsBase = `${HASS_HOST}/api/hacs`; const hacsBase = `${HASS_HOST}/api/hacs`;
@@ -750,10 +859,12 @@ async function main() {
}; };
} }
}, },
}); };
server.addTool(packageTool);
tools.push(packageTool);
// Extend the automation tool with more functionality // Extend the automation tool with more functionality
server.addTool({ const automationConfigTool = {
name: 'automation_config', name: 'automation_config',
description: 'Advanced automation configuration and management', description: 'Advanced automation configuration and management',
parameters: z.object({ parameters: z.object({
@@ -768,7 +879,7 @@ async function main() {
action: z.array(z.any()).describe('List of actions'), action: z.array(z.any()).describe('List of actions'),
}).optional().describe('Automation configuration (required for create and update)'), }).optional().describe('Automation configuration (required for create and update)'),
}), }),
execute: async (params) => { execute: async (params: AutomationConfigParams) => {
try { try {
switch (params.action) { switch (params.action) {
case 'create': { case 'create': {
@@ -895,10 +1006,12 @@ async function main() {
}; };
} }
}, },
}); };
server.addTool(automationConfigTool);
tools.push(automationConfigTool);
// Add SSE endpoint // Add SSE endpoint
server.addTool({ const subscribeEventsTool = {
name: 'subscribe_events', name: 'subscribe_events',
description: 'Subscribe to Home Assistant events via Server-Sent Events (SSE)', description: 'Subscribe to Home Assistant events via Server-Sent Events (SSE)',
parameters: z.object({ parameters: z.object({
@@ -976,10 +1089,12 @@ async function main() {
keepAlive: true keepAlive: true
}; };
} }
}); };
server.addTool(subscribeEventsTool);
tools.push(subscribeEventsTool);
// Add statistics endpoint // Add statistics endpoint
server.addTool({ const getSSEStatsTool = {
name: 'get_sse_stats', name: 'get_sse_stats',
description: 'Get SSE connection statistics', description: 'Get SSE connection statistics',
parameters: z.object({ parameters: z.object({
@@ -998,18 +1113,43 @@ async function main() {
statistics: sseManager.getStatistics() statistics: sseManager.getStatistics()
}; };
} }
}); };
server.addTool(getSSEStatsTool);
tools.push(getSSEStatsTool);
console.log('Initializing MCP Server...'); logger.debug('[server:init]', 'Initializing MCP Server...');
// Start the server // Start the server
await server.start(); await server.start();
console.log(`MCP Server started on port ${PORT}`); logger.info('[server:init]', `MCP Server started on port ${PORT}`);
console.log('Home Assistant server running on stdio'); logger.info('[server:init]', 'Home Assistant server running on stdio');
console.log('SSE endpoints initialized'); logger.info('[server:init]', 'SSE endpoints initialized');
// Log available endpoints using our tracked tools array
logger.info('[server:endpoints]', '\nAvailable API Endpoints:');
tools.forEach((tool: Tool) => {
logger.info('[server:endpoints]', `- ${tool.name}: ${tool.description}`);
});
// Log SSE endpoints
logger.info('[server:endpoints]', '\nAvailable SSE Endpoints:');
logger.info('[server:endpoints]', '- /subscribe_events');
logger.info('[server:endpoints]', ' Parameters:');
logger.info('[server:endpoints]', ' - token: Authentication token (required)');
logger.info('[server:endpoints]', ' - events: List of event types to subscribe to (optional)');
logger.info('[server:endpoints]', ' - entity_id: Specific entity ID to monitor (optional)');
logger.info('[server:endpoints]', ' - domain: Domain to monitor (e.g., "light", "switch") (optional)');
logger.info('[server:endpoints]', '\n- /get_sse_stats');
logger.info('[server:endpoints]', ' Parameters:');
logger.info('[server:endpoints]', ' - token: Authentication token (required)');
// Log successful initialization // Log successful initialization
console.log('Server initialization complete. Ready to handle requests.'); logger.info('[server:init]', '\nServer initialization complete. Ready to handle requests.');
// Start the Express server
app.listen(PORT, () => {
logger.info('[server:init]', `Express server listening on port ${PORT}`);
});
} }
main().catch(console.error); main().catch(console.error);