Add comprehensive WebSocket, context, performance, and security modules

- Introduced WebSocket client for real-time Home Assistant event streaming
- Created context management system for tracking resource relationships and state
- Implemented performance monitoring and optimization utilities
- Added security middleware with token validation, rate limiting, and input sanitization
- Extended tool registry with enhanced tool registration and execution capabilities
- Expanded test coverage for new modules and added comprehensive test scenarios
- Improved type safety and added robust error handling across new modules
This commit is contained in:
jango-blockchained
2025-01-30 09:18:17 +01:00
parent d7e5fcf764
commit 110f2a308c
8 changed files with 2428 additions and 129 deletions

View File

@@ -46,6 +46,73 @@ interface HassEntity {
};
}
interface HassState {
entity_id: string;
state: string;
attributes: {
friendly_name?: string;
description?: string;
[key: string]: any;
};
}
interface HassAddon {
name: string;
slug: string;
description: string;
version: string;
installed: boolean;
available: boolean;
state: string;
}
interface HassAddonResponse {
data: {
addons: HassAddon[];
};
}
interface HassAddonInfoResponse {
data: {
name: string;
slug: string;
description: string;
version: string;
state: string;
status: string;
options: Record<string, any>;
[key: string]: any;
};
}
interface HacsRepository {
name: string;
description: string;
category: string;
installed: boolean;
version_installed: string;
available_version: string;
authors: string[];
domain: string;
}
interface HacsResponse {
repositories: HacsRepository[];
}
interface AutomationConfig {
alias: string;
description?: string;
mode?: 'single' | 'parallel' | 'queued' | 'restart';
trigger: any[];
condition?: any[];
action: any[];
}
interface AutomationResponse {
automation_id: string;
}
async function main() {
const hass = await get_hass();
@@ -232,6 +299,570 @@ async function main() {
}
});
// Add the history tool
server.addTool({
name: 'get_history',
description: 'Get state history for Home Assistant entities',
parameters: z.object({
entity_id: z.string().describe('The entity ID to get history for'),
start_time: z.string().optional().describe('Start time in ISO format. Defaults to 24 hours ago'),
end_time: z.string().optional().describe('End time in ISO format. Defaults to now'),
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) => {
try {
const now = new Date();
const startTime = params.start_time ? new Date(params.start_time) : new Date(now.getTime() - 24 * 60 * 60 * 1000);
const endTime = params.end_time ? new Date(params.end_time) : now;
// Build query parameters
const queryParams = new URLSearchParams({
filter_entity_id: params.entity_id,
minimal_response: String(!!params.minimal_response),
significant_changes_only: String(!!params.significant_changes_only),
start_time: startTime.toISOString(),
end_time: endTime.toISOString(),
});
const response = await fetch(`${HASS_HOST}/api/history/period/${startTime.toISOString()}?${queryParams.toString()}`, {
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch history: ${response.statusText}`);
}
const history = await response.json();
return {
success: true,
history,
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
});
// Add the scenes tool
server.addTool({
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) => {
try {
if (params.action === 'list') {
const response = await fetch(`${HASS_HOST}/api/states`, {
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch scenes: ${response.statusText}`);
}
const states = (await response.json()) as HassState[];
const scenes = states.filter((state) => state.entity_id.startsWith('scene.'));
return {
success: true,
scenes: scenes.map((scene) => ({
entity_id: scene.entity_id,
name: scene.attributes.friendly_name || scene.entity_id.split('.')[1],
description: scene.attributes.description,
})),
};
} else if (params.action === 'activate') {
if (!params.scene_id) {
throw new Error('Scene ID is required for activate action');
}
const response = await fetch(`${HASS_HOST}/api/services/scene/turn_on`, {
method: 'POST',
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
entity_id: params.scene_id,
}),
});
if (!response.ok) {
throw new Error(`Failed to activate scene: ${response.statusText}`);
}
return {
success: true,
message: `Successfully activated scene ${params.scene_id}`,
};
}
throw new Error('Invalid action specified');
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
});
// Add the notification tool
server.addTool({
name: 'notify',
description: 'Send notifications through Home Assistant',
parameters: z.object({
message: z.string().describe('The notification message'),
title: z.string().optional().describe('The notification title'),
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) => {
try {
const service = params.target ? `notify.${params.target}` : 'notify.notify';
const [domain, service_name] = service.split('.');
const response = await fetch(`${HASS_HOST}/api/services/${domain}/${service_name}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: params.message,
title: params.title,
data: params.data,
}),
});
if (!response.ok) {
throw new Error(`Failed to send notification: ${response.statusText}`);
}
return {
success: true,
message: 'Notification sent successfully',
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
});
// Add the automation tool
server.addTool({
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) => {
try {
if (params.action === 'list') {
const response = await fetch(`${HASS_HOST}/api/states`, {
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch automations: ${response.statusText}`);
}
const states = (await response.json()) as HassState[];
const automations = states.filter((state) => state.entity_id.startsWith('automation.'));
return {
success: true,
automations: automations.map((automation) => ({
entity_id: automation.entity_id,
name: automation.attributes.friendly_name || automation.entity_id.split('.')[1],
state: automation.state,
last_triggered: automation.attributes.last_triggered,
})),
};
} else {
if (!params.automation_id) {
throw new Error('Automation ID is required for toggle and trigger actions');
}
const service = params.action === 'toggle' ? 'toggle' : 'trigger';
const response = await fetch(`${HASS_HOST}/api/services/automation/${service}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
entity_id: params.automation_id,
}),
});
if (!response.ok) {
throw new Error(`Failed to ${service} automation: ${response.statusText}`);
}
return {
success: true,
message: `Successfully ${service}d automation ${params.automation_id}`,
};
}
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
});
// Add the addon tool
server.addTool({
name: 'addon',
description: 'Manage Home Assistant add-ons',
parameters: z.object({
action: z.enum(['list', 'info', 'install', 'uninstall', 'start', 'stop', 'restart']).describe('Action to perform with add-on'),
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) => {
try {
if (params.action === 'list') {
const response = await fetch(`${HASS_HOST}/api/hassio/store`, {
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch add-ons: ${response.statusText}`);
}
const data = await response.json() as HassAddonResponse;
return {
success: true,
addons: data.data.addons.map((addon) => ({
name: addon.name,
slug: addon.slug,
description: addon.description,
version: addon.version,
installed: addon.installed,
available: addon.available,
state: addon.state,
})),
};
} else {
if (!params.slug) {
throw new Error('Add-on slug is required for this action');
}
let endpoint = '';
let method = 'GET';
const body: Record<string, any> = {};
switch (params.action) {
case 'info':
endpoint = `/api/hassio/addons/${params.slug}/info`;
break;
case 'install':
endpoint = `/api/hassio/addons/${params.slug}/install`;
method = 'POST';
if (params.version) {
body.version = params.version;
}
break;
case 'uninstall':
endpoint = `/api/hassio/addons/${params.slug}/uninstall`;
method = 'POST';
break;
case 'start':
endpoint = `/api/hassio/addons/${params.slug}/start`;
method = 'POST';
break;
case 'stop':
endpoint = `/api/hassio/addons/${params.slug}/stop`;
method = 'POST';
break;
case 'restart':
endpoint = `/api/hassio/addons/${params.slug}/restart`;
method = 'POST';
break;
}
const response = await fetch(`${HASS_HOST}${endpoint}`, {
method,
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
...(Object.keys(body).length > 0 && { body: JSON.stringify(body) }),
});
if (!response.ok) {
throw new Error(`Failed to ${params.action} add-on: ${response.statusText}`);
}
const data = await response.json() as HassAddonInfoResponse;
return {
success: true,
message: `Successfully ${params.action}ed add-on ${params.slug}`,
data: data.data,
};
}
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
});
// Add the package tool
server.addTool({
name: 'package',
description: 'Manage HACS packages and custom components',
parameters: z.object({
action: z.enum(['list', 'install', 'uninstall', 'update']).describe('Action to perform with package'),
category: z.enum(['integration', 'plugin', 'theme', 'python_script', 'appdaemon', 'netdaemon'])
.describe('Package category'),
repository: z.string().optional().describe('Repository URL or name (required for install)'),
version: z.string().optional().describe('Version to install'),
}),
execute: async (params) => {
try {
const hacsBase = `${HASS_HOST}/api/hacs`;
if (params.action === 'list') {
const response = await fetch(`${hacsBase}/repositories?category=${params.category}`, {
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch packages: ${response.statusText}`);
}
const data = await response.json() as HacsResponse;
return {
success: true,
packages: data.repositories,
};
} else {
if (!params.repository) {
throw new Error('Repository is required for this action');
}
let endpoint = '';
const body: Record<string, any> = {
category: params.category,
repository: params.repository,
};
switch (params.action) {
case 'install':
endpoint = '/repository/install';
if (params.version) {
body.version = params.version;
}
break;
case 'uninstall':
endpoint = '/repository/uninstall';
break;
case 'update':
endpoint = '/repository/update';
break;
}
const response = await fetch(`${hacsBase}${endpoint}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Failed to ${params.action} package: ${response.statusText}`);
}
return {
success: true,
message: `Successfully ${params.action}ed package ${params.repository}`,
};
}
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
});
// Extend the automation tool with more functionality
server.addTool({
name: 'automation_config',
description: 'Advanced automation configuration and management',
parameters: z.object({
action: z.enum(['create', 'update', 'delete', 'duplicate']).describe('Action to perform with automation config'),
automation_id: z.string().optional().describe('Automation ID (required for update, delete, and duplicate)'),
config: z.object({
alias: z.string().describe('Friendly name for the automation'),
description: z.string().optional().describe('Description of what the automation does'),
mode: z.enum(['single', 'parallel', 'queued', 'restart']).optional().describe('How multiple triggerings are handled'),
trigger: z.array(z.any()).describe('List of triggers'),
condition: z.array(z.any()).optional().describe('List of conditions'),
action: z.array(z.any()).describe('List of actions'),
}).optional().describe('Automation configuration (required for create and update)'),
}),
execute: async (params) => {
try {
switch (params.action) {
case 'create': {
if (!params.config) {
throw new Error('Configuration is required for creating automation');
}
const response = await fetch(`${HASS_HOST}/api/config/automation/config`, {
method: 'POST',
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(params.config),
});
if (!response.ok) {
throw new Error(`Failed to create automation: ${response.statusText}`);
}
return {
success: true,
message: 'Successfully created automation',
automation_id: (await response.json()).automation_id,
};
}
case 'update': {
if (!params.automation_id || !params.config) {
throw new Error('Automation ID and configuration are required for updating automation');
}
const response = await fetch(`${HASS_HOST}/api/config/automation/config/${params.automation_id}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(params.config),
});
if (!response.ok) {
throw new Error(`Failed to update automation: ${response.statusText}`);
}
return {
success: true,
message: `Successfully updated automation ${params.automation_id}`,
};
}
case 'delete': {
if (!params.automation_id) {
throw new Error('Automation ID is required for deleting automation');
}
const response = await fetch(`${HASS_HOST}/api/config/automation/config/${params.automation_id}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to delete automation: ${response.statusText}`);
}
return {
success: true,
message: `Successfully deleted automation ${params.automation_id}`,
};
}
case 'duplicate': {
if (!params.automation_id) {
throw new Error('Automation ID is required for duplicating automation');
}
// First, get the existing automation config
const getResponse = await fetch(`${HASS_HOST}/api/config/automation/config/${params.automation_id}`, {
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!getResponse.ok) {
throw new Error(`Failed to get automation config: ${getResponse.statusText}`);
}
const config = await getResponse.json() as AutomationConfig;
config.alias = `${config.alias} (Copy)`;
// Create new automation with modified config
const createResponse = await fetch(`${HASS_HOST}/api/config/automation/config`, {
method: 'POST',
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(config),
});
if (!createResponse.ok) {
throw new Error(`Failed to create duplicate automation: ${createResponse.statusText}`);
}
const newAutomation = await createResponse.json() as AutomationResponse;
return {
success: true,
message: `Successfully duplicated automation ${params.automation_id}`,
new_automation_id: newAutomation.automation_id,
};
}
}
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
});
// Start the server
await server.start();
console.log('MCP Server started');