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 { APP_CONFIG } from "./config/app.config.js";
|
||||||
import { loadEnvironmentVariables } from "./config/loadEnv.js";
|
import { loadEnvironmentVariables } from "./config/loadEnv.js";
|
||||||
import { MCP_SCHEMA } from "./mcp/schema.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
|
// Load environment variables based on NODE_ENV
|
||||||
await loadEnvironmentVariables();
|
await loadEnvironmentVariables();
|
||||||
@@ -46,67 +58,18 @@ export interface Tool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Array to store tools
|
// Array to store tools
|
||||||
const tools: Tool[] = [];
|
const tools: Tool[] = [
|
||||||
|
listDevicesTool,
|
||||||
// Define the list devices tool
|
controlTool,
|
||||||
const listDevicesTool: Tool = {
|
subscribeEventsTool,
|
||||||
name: "list_devices",
|
getSSEStatsTool,
|
||||||
description: "List all available Home Assistant devices",
|
automationConfigTool,
|
||||||
parameters: z.object({}),
|
addonTool,
|
||||||
execute: async () => {
|
packageTool,
|
||||||
try {
|
sceneTool,
|
||||||
const devices = await list_devices();
|
notifyTool,
|
||||||
return {
|
historyTool,
|
||||||
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);
|
|
||||||
|
|
||||||
// Initialize Elysia app with middleware
|
// Initialize Elysia app with middleware
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
@@ -119,24 +82,40 @@ const app = new Elysia()
|
|||||||
.use(errorHandler);
|
.use(errorHandler);
|
||||||
|
|
||||||
// Mount API routes
|
// Mount API routes
|
||||||
app.get("/api/mcp", () => MCP_SCHEMA);
|
app.get("/api/mcp/schema", () => MCP_SCHEMA);
|
||||||
app.post("/api/mcp/execute", async ({ body }: { body: { tool: string; parameters: Record<string, unknown> } }) => {
|
|
||||||
const { tool: toolName, parameters } = body;
|
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);
|
const tool = tools.find((t) => t.name === toolName);
|
||||||
|
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: `Tool '${toolName}' not found`,
|
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
|
// Health check endpoint with MCP info
|
||||||
app.get("/health", () => ({
|
app.get("/api/mcp/health", () => ({
|
||||||
status: "ok",
|
status: "ok",
|
||||||
timestamp: new Date().toISOString(),
|
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,
|
speech_enabled: APP_CONFIG.SPEECH.ENABLED,
|
||||||
wake_word_enabled: APP_CONFIG.SPEECH.WAKE_WORD_ENABLED,
|
wake_word_enabled: APP_CONFIG.SPEECH.WAKE_WORD_ENABLED,
|
||||||
speech_to_text_enabled: APP_CONFIG.SPEECH.SPEECH_TO_TEXT_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 = {
|
export const listDevicesTool: Tool = {
|
||||||
name: "list_devices",
|
name: "list_devices",
|
||||||
description: "List all available Home Assistant devices",
|
description: "List all available Home Assistant devices",
|
||||||
parameters: z.object({}).describe("No parameters required"),
|
parameters: z.object({
|
||||||
execute: async () => {
|
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 {
|
try {
|
||||||
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/states`, {
|
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/states`, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -21,10 +39,23 @@ export const listDevicesTool: Tool = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const states = (await response.json()) as HassState[];
|
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[]> = {};
|
const devices: Record<string, HassState[]> = {};
|
||||||
|
|
||||||
// Group devices by domain
|
// Group devices by domain
|
||||||
states.forEach(state => {
|
filteredStates.forEach(state => {
|
||||||
const [domain] = state.entity_id.split('.');
|
const [domain] = state.entity_id.split('.');
|
||||||
if (!devices[domain]) {
|
if (!devices[domain]) {
|
||||||
devices[domain] = [];
|
devices[domain] = [];
|
||||||
@@ -47,12 +78,14 @@ export const listDevicesTool: Tool = {
|
|||||||
sample: entities.slice(0, 2).map(e => ({
|
sample: entities.slice(0, 2).map(e => ({
|
||||||
id: e.entity_id,
|
id: e.entity_id,
|
||||||
state: e.state,
|
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 deviceTypes = Object.keys(devices);
|
||||||
|
|
||||||
const deviceSummary = {
|
const deviceSummary = {
|
||||||
|
|||||||
Reference in New Issue
Block a user