From 3b2e9640dba1b5e06e9de38b06a23a9340da9a3f Mon Sep 17 00:00:00 2001 From: jango-blockchained Date: Fri, 31 Jan 2025 23:05:14 +0100 Subject: [PATCH] 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 --- src/index.ts | 246 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 193 insertions(+), 53 deletions(-) diff --git a/src/index.ts b/src/index.ts index 324b05e..8466cab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,9 @@ import { config } from 'dotenv'; import { resolve } from 'path'; import { v4 as uuidv4 } from 'uuid'; 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 const envFile = process.env.NODE_ENV === 'production' @@ -26,6 +29,51 @@ const PORT = process.env.PORT || 3000; 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; + execute: (params: any) => Promise; +} + +// 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 { command: string; entity_id: string; @@ -142,17 +190,66 @@ interface SSEParams { 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; +} + +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() { const hass = await get_hass(); - - // Create MCP server - const server = new LiteMCP('home-assistant', '0.1.0'); + const logger: ILogger = (hass as any).logger; // Add the list devices tool - server.addTool({ + const listDevicesTool = { name: 'list_devices', description: 'List all available Home Assistant devices', - parameters: z.object({}), + parameters: z.object({}).describe('No parameters required'), execute: async () => { try { const response = await fetch(`${HASS_HOST}/api/states`, { @@ -166,35 +263,35 @@ async function main() { throw new Error(`Failed to fetch devices: ${response.statusText}`); } - const states = await response.json() as HassEntity[]; - const devices = states.reduce((acc: Record, state: HassEntity) => { - const domain = state.entity_id.split('.')[0]; - if (!acc[domain]) { - acc[domain] = []; + const states = await response.json() as HassState[]; + const devices: Record = {}; + + // Group devices by domain + states.forEach(state => { + const [domain] = state.entity_id.split('.'); + if (!devices[domain]) { + devices[domain] = []; } - acc[domain].push({ - entity_id: state.entity_id, - state: state.state, - attributes: state.attributes, - }); - return acc; - }, {}); + devices[domain].push(state); + }); return { success: true, - devices, + devices }; } catch (error) { return { 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 - server.addTool({ + const controlTool = { name: 'control', description: 'Control Home Assistant devices and services', parameters: z.object({ @@ -326,10 +423,12 @@ async function main() { }; } } - }); + }; + server.addTool(controlTool); + tools.push(controlTool); // Add the history tool - server.addTool({ + const historyTool = { name: 'get_history', description: 'Get state history for Home Assistant entities', parameters: z.object({ @@ -339,7 +438,7 @@ async function main() { 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'), }), - execute: async (params) => { + execute: async (params: HistoryParams) => { try { const now = new Date(); 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 - server.addTool({ + const sceneTool = { name: 'scene', description: 'Manage and activate Home Assistant scenes', parameters: z.object({ 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)'), }), - execute: async (params) => { + execute: async (params: SceneParams) => { try { if (params.action === 'list') { 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 - server.addTool({ + const notifyTool = { name: 'notify', description: 'Send notifications through Home Assistant', parameters: z.object({ @@ -458,7 +561,7 @@ async function main() { target: z.string().optional().describe('Specific notification target (e.g., mobile_app_phone)'), data: z.record(z.any()).optional().describe('Additional notification data'), }), - execute: async (params) => { + execute: async (params: NotifyParams) => { try { const service = params.target ? `notify.${params.target}` : 'notify.notify'; const [domain, service_name] = service.split('.'); @@ -491,17 +594,19 @@ async function main() { }; } }, - }); + }; + server.addTool(notifyTool); + tools.push(notifyTool); // Add the automation tool - server.addTool({ + const automationTool = { name: 'automation', description: 'Manage Home Assistant automations', parameters: z.object({ 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)'), }), - execute: async (params) => { + execute: async (params: AutomationParams) => { try { if (params.action === 'list') { 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 - server.addTool({ + const addonTool = { name: 'addon', description: 'Manage Home Assistant add-ons', parameters: z.object({ @@ -573,7 +680,7 @@ async function main() { 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)'), }), - execute: async (params) => { + execute: async (params: AddonParams) => { try { if (params.action === 'list') { 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 - server.addTool({ + const packageTool = { name: 'package', description: 'Manage HACS packages and custom components', parameters: z.object({ @@ -678,7 +787,7 @@ async function main() { repository: z.string().optional().describe('Repository URL or name (required for install)'), version: z.string().optional().describe('Version to install'), }), - execute: async (params) => { + execute: async (params: PackageParams) => { try { 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 - server.addTool({ + const automationConfigTool = { name: 'automation_config', description: 'Advanced automation configuration and management', parameters: z.object({ @@ -768,7 +879,7 @@ async function main() { action: z.array(z.any()).describe('List of actions'), }).optional().describe('Automation configuration (required for create and update)'), }), - execute: async (params) => { + execute: async (params: AutomationConfigParams) => { try { switch (params.action) { case 'create': { @@ -895,10 +1006,12 @@ async function main() { }; } }, - }); + }; + server.addTool(automationConfigTool); + tools.push(automationConfigTool); // Add SSE endpoint - server.addTool({ + const subscribeEventsTool = { name: 'subscribe_events', description: 'Subscribe to Home Assistant events via Server-Sent Events (SSE)', parameters: z.object({ @@ -976,10 +1089,12 @@ async function main() { keepAlive: true }; } - }); + }; + server.addTool(subscribeEventsTool); + tools.push(subscribeEventsTool); // Add statistics endpoint - server.addTool({ + const getSSEStatsTool = { name: 'get_sse_stats', description: 'Get SSE connection statistics', parameters: z.object({ @@ -998,18 +1113,43 @@ async function main() { statistics: sseManager.getStatistics() }; } - }); + }; + server.addTool(getSSEStatsTool); + tools.push(getSSEStatsTool); - console.log('Initializing MCP Server...'); + logger.debug('[server:init]', 'Initializing MCP Server...'); // Start the server await server.start(); - console.log(`MCP Server started on port ${PORT}`); - console.log('Home Assistant server running on stdio'); - console.log('SSE endpoints initialized'); + logger.info('[server:init]', `MCP Server started on port ${PORT}`); + logger.info('[server:init]', 'Home Assistant server running on stdio'); + 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 - 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); \ No newline at end of file