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 { 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<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 {
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<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() {
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<string, HassEntity[]>, state: HassEntity) => {
const domain = state.entity_id.split('.')[0];
if (!acc[domain]) {
acc[domain] = [];
const states = await response.json() as HassState[];
const devices: Record<string, HassState[]> = {};
// 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,
devices[domain].push(state);
});
return acc;
}, {});
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);