feat: Enhance MCP tool execution and device listing with advanced filtering
- Refactor MCP execution endpoint to improve error handling and result reporting - Update health check endpoint with MCP version and supported tools - Extend list_devices tool with optional domain, area, and floor filtering - Improve device listing response with more detailed device metadata - Standardize tool import and initialization in main index file
This commit is contained in:
115
src/index.ts
115
src/index.ts
@@ -27,6 +27,18 @@ import { speechService } from "./speech/index.js";
|
||||
import { APP_CONFIG } from "./config/app.config.js";
|
||||
import { loadEnvironmentVariables } from "./config/loadEnv.js";
|
||||
import { MCP_SCHEMA } from "./mcp/schema.js";
|
||||
import {
|
||||
listDevicesTool,
|
||||
controlTool,
|
||||
subscribeEventsTool,
|
||||
getSSEStatsTool,
|
||||
automationConfigTool,
|
||||
addonTool,
|
||||
packageTool,
|
||||
sceneTool,
|
||||
notifyTool,
|
||||
historyTool,
|
||||
} from "./tools/index.js";
|
||||
|
||||
// Load environment variables based on NODE_ENV
|
||||
await loadEnvironmentVariables();
|
||||
@@ -46,67 +58,18 @@ export interface Tool {
|
||||
}
|
||||
|
||||
// Array to store tools
|
||||
const tools: Tool[] = [];
|
||||
|
||||
// Define the list devices tool
|
||||
const listDevicesTool: Tool = {
|
||||
name: "list_devices",
|
||||
description: "List all available Home Assistant devices",
|
||||
parameters: z.object({}),
|
||||
execute: async () => {
|
||||
try {
|
||||
const devices = await list_devices();
|
||||
return {
|
||||
success: true,
|
||||
devices,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Add tools to the array
|
||||
tools.push(listDevicesTool);
|
||||
|
||||
// Add the Home Assistant control tool
|
||||
const controlTool: Tool = {
|
||||
name: "control",
|
||||
description: "Control Home Assistant devices and services",
|
||||
parameters: z.object({
|
||||
command: z.enum([
|
||||
...commonCommands,
|
||||
...coverCommands,
|
||||
...climateCommands,
|
||||
] as [string, ...string[]]),
|
||||
entity_id: z.string().describe("The ID of the entity to control"),
|
||||
}),
|
||||
execute: async (params: { command: Command; entity_id: string }) => {
|
||||
try {
|
||||
const [domain] = params.entity_id.split(".");
|
||||
await call_service(domain, params.command, {
|
||||
entity_id: params.entity_id,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: `Command ${params.command} executed successfully on ${params.entity_id}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Add the control tool to the array
|
||||
tools.push(controlTool);
|
||||
const tools: Tool[] = [
|
||||
listDevicesTool,
|
||||
controlTool,
|
||||
subscribeEventsTool,
|
||||
getSSEStatsTool,
|
||||
automationConfigTool,
|
||||
addonTool,
|
||||
packageTool,
|
||||
sceneTool,
|
||||
notifyTool,
|
||||
historyTool,
|
||||
];
|
||||
|
||||
// Initialize Elysia app with middleware
|
||||
const app = new Elysia()
|
||||
@@ -119,24 +82,40 @@ const app = new Elysia()
|
||||
.use(errorHandler);
|
||||
|
||||
// Mount API routes
|
||||
app.get("/api/mcp", () => MCP_SCHEMA);
|
||||
app.post("/api/mcp/execute", async ({ body }: { body: { tool: string; parameters: Record<string, unknown> } }) => {
|
||||
const { tool: toolName, parameters } = body;
|
||||
app.get("/api/mcp/schema", () => MCP_SCHEMA);
|
||||
|
||||
app.post("/api/mcp/execute", async ({ body }: { body: { name: string; parameters: Record<string, unknown> } }) => {
|
||||
const { name: toolName, parameters } = body;
|
||||
const tool = tools.find((t) => t.name === toolName);
|
||||
|
||||
if (!tool) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Tool '${toolName}' not found`,
|
||||
};
|
||||
}
|
||||
return await tool.execute(parameters);
|
||||
|
||||
try {
|
||||
const result = await tool.execute(parameters);
|
||||
return {
|
||||
success: true,
|
||||
result,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : "Unknown error occurred",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get("/health", () => ({
|
||||
// Health check endpoint with MCP info
|
||||
app.get("/api/mcp/health", () => ({
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
version: "0.1.0",
|
||||
version: "1.0.0",
|
||||
mcp_version: "1.0",
|
||||
supported_tools: tools.map(t => t.name),
|
||||
speech_enabled: APP_CONFIG.SPEECH.ENABLED,
|
||||
wake_word_enabled: APP_CONFIG.SPEECH.WAKE_WORD_ENABLED,
|
||||
speech_to_text_enabled: APP_CONFIG.SPEECH.SPEECH_TO_TEXT_ENABLED,
|
||||
|
||||
@@ -6,8 +6,26 @@ import { HassState } from "../types/index.js";
|
||||
export const listDevicesTool: Tool = {
|
||||
name: "list_devices",
|
||||
description: "List all available Home Assistant devices",
|
||||
parameters: z.object({}).describe("No parameters required"),
|
||||
execute: async () => {
|
||||
parameters: z.object({
|
||||
domain: z.enum([
|
||||
"light",
|
||||
"climate",
|
||||
"alarm_control_panel",
|
||||
"cover",
|
||||
"switch",
|
||||
"contact",
|
||||
"media_player",
|
||||
"fan",
|
||||
"lock",
|
||||
"vacuum",
|
||||
"scene",
|
||||
"script",
|
||||
"camera",
|
||||
]).optional(),
|
||||
area: z.string().optional(),
|
||||
floor: z.string().optional(),
|
||||
}).describe("Filter devices by domain, area, or floor"),
|
||||
execute: async (params: { domain?: string; area?: string; floor?: string }) => {
|
||||
try {
|
||||
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/states`, {
|
||||
headers: {
|
||||
@@ -21,10 +39,23 @@ export const listDevicesTool: Tool = {
|
||||
}
|
||||
|
||||
const states = (await response.json()) as HassState[];
|
||||
let filteredStates = states;
|
||||
|
||||
// Apply filters
|
||||
if (params.domain) {
|
||||
filteredStates = filteredStates.filter(state => state.entity_id.startsWith(`${params.domain}.`));
|
||||
}
|
||||
if (params.area) {
|
||||
filteredStates = filteredStates.filter(state => state.attributes?.area_id === params.area);
|
||||
}
|
||||
if (params.floor) {
|
||||
filteredStates = filteredStates.filter(state => state.attributes?.floor === params.floor);
|
||||
}
|
||||
|
||||
const devices: Record<string, HassState[]> = {};
|
||||
|
||||
// Group devices by domain
|
||||
states.forEach(state => {
|
||||
filteredStates.forEach(state => {
|
||||
const [domain] = state.entity_id.split('.');
|
||||
if (!devices[domain]) {
|
||||
devices[domain] = [];
|
||||
@@ -47,12 +78,14 @@ export const listDevicesTool: Tool = {
|
||||
sample: entities.slice(0, 2).map(e => ({
|
||||
id: e.entity_id,
|
||||
state: e.state,
|
||||
name: e.attributes?.friendly_name || e.entity_id
|
||||
name: e.attributes?.friendly_name || e.entity_id,
|
||||
area: e.attributes?.area_id,
|
||||
floor: e.attributes?.floor,
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
const totalDevices = states.length;
|
||||
const totalDevices = filteredStates.length;
|
||||
const deviceTypes = Object.keys(devices);
|
||||
|
||||
const deviceSummary = {
|
||||
|
||||
Reference in New Issue
Block a user