refactor: optimize configuration and tool implementations
- Standardized error handling across tool implementations - Improved return type consistency for tool execution results - Simplified configuration parsing and type definitions - Enhanced type safety for various configuration schemas - Cleaned up and normalized tool response structures - Updated SSE and event subscription tool implementations
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
import { Router } from 'express';
|
||||
import { APP_CONFIG } from '../config/app.config.js';
|
||||
import { Router } from "express";
|
||||
import { APP_CONFIG } from "../config/app.config.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Health check endpoint
|
||||
router.get('/', (_req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: APP_CONFIG.VERSION
|
||||
});
|
||||
router.get("/", (_req, res) => {
|
||||
res.json({
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
version: APP_CONFIG.VERSION,
|
||||
});
|
||||
});
|
||||
|
||||
export { router as healthRoutes };
|
||||
export { router as healthRoutes };
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/**
|
||||
* API Routes Module
|
||||
*
|
||||
*
|
||||
* This module exports the main router that combines all API routes
|
||||
* into a single router instance. Each route group is mounted under
|
||||
* its respective path prefix.
|
||||
*
|
||||
*
|
||||
* @module routes
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { mcpRoutes } from './mcp.routes.js';
|
||||
import { sseRoutes } from './sse.routes.js';
|
||||
import { toolRoutes } from './tool.routes.js';
|
||||
import { healthRoutes } from './health.routes.js';
|
||||
import { Router } from "express";
|
||||
import { mcpRoutes } from "./mcp.routes.js";
|
||||
import { sseRoutes } from "./sse.routes.js";
|
||||
import { toolRoutes } from "./tool.routes.js";
|
||||
import { healthRoutes } from "./health.routes.js";
|
||||
|
||||
/**
|
||||
* Create main router instance
|
||||
@@ -27,13 +27,13 @@ const router = Router();
|
||||
* - /tools: Tool management endpoints
|
||||
* - /health: Health check endpoint
|
||||
*/
|
||||
router.use('/mcp', mcpRoutes);
|
||||
router.use('/sse', sseRoutes);
|
||||
router.use('/tools', toolRoutes);
|
||||
router.use('/health', healthRoutes);
|
||||
router.use("/mcp", mcpRoutes);
|
||||
router.use("/sse", sseRoutes);
|
||||
router.use("/tools", toolRoutes);
|
||||
router.use("/health", healthRoutes);
|
||||
|
||||
/**
|
||||
* Export the configured router
|
||||
* This will be mounted in the main application
|
||||
*/
|
||||
export { router as apiRoutes };
|
||||
export { router as apiRoutes };
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* MCP Routes Module
|
||||
*
|
||||
*
|
||||
* This module provides routes for accessing and executing MCP functionality.
|
||||
* It includes endpoints for retrieving the MCP schema and executing MCP tools.
|
||||
*
|
||||
*
|
||||
* @module mcp-routes
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { MCP_SCHEMA } from '../mcp/schema.js';
|
||||
import { APP_CONFIG } from '../config/app.config.js';
|
||||
import { Tool } from '../types/index.js';
|
||||
import { Router } from "express";
|
||||
import { MCP_SCHEMA } from "../mcp/schema.js";
|
||||
import { APP_CONFIG } from "../config/app.config.js";
|
||||
import { Tool } from "../types/index.js";
|
||||
|
||||
/**
|
||||
* Create router instance for MCP routes
|
||||
@@ -28,15 +28,15 @@ const tools: Tool[] = [];
|
||||
* Returns the MCP schema without requiring authentication
|
||||
* This endpoint allows clients to discover available tools and their parameters
|
||||
*/
|
||||
router.get('/', (_req, res) => {
|
||||
res.json(MCP_SCHEMA);
|
||||
router.get("/", (_req, res) => {
|
||||
res.json(MCP_SCHEMA);
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /mcp/execute
|
||||
* Execute a tool with the provided parameters
|
||||
* Requires authentication via Bearer token
|
||||
*
|
||||
*
|
||||
* @param {Object} req.body.tool - Name of the tool to execute
|
||||
* @param {Object} req.body.parameters - Parameters for the tool
|
||||
* @returns {Object} Tool execution result
|
||||
@@ -44,42 +44,43 @@ router.get('/', (_req, res) => {
|
||||
* @throws {404} If tool is not found
|
||||
* @throws {500} If execution fails
|
||||
*/
|
||||
router.post('/execute', async (req, res) => {
|
||||
try {
|
||||
// Get token from Authorization header
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
router.post("/execute", async (req, res) => {
|
||||
try {
|
||||
// Get token from Authorization header
|
||||
const token = req.headers.authorization?.replace("Bearer ", "");
|
||||
|
||||
if (!token || token !== APP_CONFIG.HASS_TOKEN) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Unauthorized - Invalid token'
|
||||
});
|
||||
}
|
||||
|
||||
const { tool: toolName, parameters } = req.body;
|
||||
|
||||
// Find the requested tool
|
||||
const tool = tools.find(t => t.name === toolName);
|
||||
if (!tool) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: `Tool '${toolName}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
// Execute the tool with the provided parameters
|
||||
const result = await tool.execute(parameters);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
});
|
||||
if (!token || token !== APP_CONFIG.HASS_TOKEN) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Unauthorized - Invalid token",
|
||||
});
|
||||
}
|
||||
|
||||
const { tool: toolName, parameters } = req.body;
|
||||
|
||||
// Find the requested tool
|
||||
const tool = tools.find((t) => t.name === toolName);
|
||||
if (!tool) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: `Tool '${toolName}' not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Execute the tool with the provided parameters
|
||||
const result = await tool.execute(parameters);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Export the configured router
|
||||
* This will be mounted under /api/mcp in the main application
|
||||
*/
|
||||
export { router as mcpRoutes };
|
||||
export { router as mcpRoutes };
|
||||
|
||||
@@ -1,108 +1,115 @@
|
||||
import { Router } from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { sseManager } from '../sse/index.js';
|
||||
import { TokenManager } from '../security/index.js';
|
||||
import { middleware } from '../middleware/index.js';
|
||||
import { Router } from "express";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { sseManager } from "../sse/index.js";
|
||||
import { TokenManager } from "../security/index.js";
|
||||
import { middleware } from "../middleware/index.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// SSE endpoints
|
||||
router.get('/subscribe_events', middleware.wsRateLimiter, (req, res) => {
|
||||
try {
|
||||
// Get token from query parameter and validate
|
||||
const token = req.query.token?.toString() || '';
|
||||
const clientIp = req.ip || req.socket.remoteAddress || '';
|
||||
const validationResult = TokenManager.validateToken(token, clientIp);
|
||||
router.get("/subscribe_events", middleware.wsRateLimiter, (req, res) => {
|
||||
try {
|
||||
// Get token from query parameter and validate
|
||||
const token = req.query.token?.toString() || "";
|
||||
const clientIp = req.ip || req.socket.remoteAddress || "";
|
||||
const validationResult = TokenManager.validateToken(token, clientIp);
|
||||
|
||||
if (!validationResult.valid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Unauthorized',
|
||||
error: validationResult.error,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Set SSE headers with enhanced security
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Credentials': 'true'
|
||||
});
|
||||
|
||||
// Send initial connection message
|
||||
res.write(`data: ${JSON.stringify({
|
||||
type: 'connection',
|
||||
status: 'connected',
|
||||
timestamp: new Date().toISOString()
|
||||
})}\n\n`);
|
||||
|
||||
const clientId = uuidv4();
|
||||
const client = {
|
||||
id: clientId,
|
||||
ip: clientIp,
|
||||
connectedAt: new Date(),
|
||||
send: (data: string) => {
|
||||
res.write(`data: ${data}\n\n`);
|
||||
}
|
||||
};
|
||||
|
||||
// Add client to SSE manager with enhanced tracking
|
||||
const sseClient = sseManager.addClient(client, token);
|
||||
if (!sseClient || !sseClient.authenticated) {
|
||||
const errorMessage = JSON.stringify({
|
||||
type: 'error',
|
||||
message: sseClient ? 'Authentication failed' : 'Maximum client limit reached',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
res.write(`data: ${errorMessage}\n\n`);
|
||||
return res.end();
|
||||
}
|
||||
|
||||
// Handle client disconnect
|
||||
req.on('close', () => {
|
||||
sseManager.removeClient(clientId);
|
||||
console.log(`Client ${clientId} disconnected at ${new Date().toISOString()}`);
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
req.on('error', (error) => {
|
||||
console.error(`SSE Error for client ${clientId}:`, error);
|
||||
const errorMessage = JSON.stringify({
|
||||
type: 'error',
|
||||
message: 'Connection error',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
res.write(`data: ${errorMessage}\n\n`);
|
||||
sseManager.removeClient(clientId);
|
||||
res.end();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('SSE Setup Error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal Server Error',
|
||||
error: error instanceof Error ? error.message : 'An unexpected error occurred',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
if (!validationResult.valid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Unauthorized",
|
||||
error: validationResult.error,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Set SSE headers with enhanced security
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
Connection: "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
});
|
||||
|
||||
// Send initial connection message
|
||||
res.write(
|
||||
`data: ${JSON.stringify({
|
||||
type: "connection",
|
||||
status: "connected",
|
||||
timestamp: new Date().toISOString(),
|
||||
})}\n\n`,
|
||||
);
|
||||
|
||||
const clientId = uuidv4();
|
||||
const client = {
|
||||
id: clientId,
|
||||
ip: clientIp,
|
||||
connectedAt: new Date(),
|
||||
send: (data: string) => {
|
||||
res.write(`data: ${data}\n\n`);
|
||||
},
|
||||
};
|
||||
|
||||
// Add client to SSE manager with enhanced tracking
|
||||
const sseClient = sseManager.addClient(client, token);
|
||||
if (!sseClient || !sseClient.authenticated) {
|
||||
const errorMessage = JSON.stringify({
|
||||
type: "error",
|
||||
message: sseClient
|
||||
? "Authentication failed"
|
||||
: "Maximum client limit reached",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
res.write(`data: ${errorMessage}\n\n`);
|
||||
return res.end();
|
||||
}
|
||||
|
||||
// Handle client disconnect
|
||||
req.on("close", () => {
|
||||
sseManager.removeClient(clientId);
|
||||
console.log(
|
||||
`Client ${clientId} disconnected at ${new Date().toISOString()}`,
|
||||
);
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
req.on("error", (error) => {
|
||||
console.error(`SSE Error for client ${clientId}:`, error);
|
||||
const errorMessage = JSON.stringify({
|
||||
type: "error",
|
||||
message: "Connection error",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
res.write(`data: ${errorMessage}\n\n`);
|
||||
sseManager.removeClient(clientId);
|
||||
res.end();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("SSE Setup Error:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error",
|
||||
error:
|
||||
error instanceof Error ? error.message : "An unexpected error occurred",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get SSE stats endpoint
|
||||
router.get('/stats', async (req, res) => {
|
||||
try {
|
||||
const stats = await sseManager.getStatistics();
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
});
|
||||
}
|
||||
router.get("/stats", async (req, res) => {
|
||||
try {
|
||||
const stats = await sseManager.getStatistics();
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import { APP_CONFIG } from '../config/app.config.js';
|
||||
import { Tool } from '../types/index.js';
|
||||
import { Router } from "express";
|
||||
import { APP_CONFIG } from "../config/app.config.js";
|
||||
import { Tool } from "../types/index.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -8,68 +8,70 @@ const router = Router();
|
||||
const tools: Tool[] = [];
|
||||
|
||||
// List devices endpoint
|
||||
router.get('/devices', async (req, res) => {
|
||||
try {
|
||||
// Get token from Authorization header
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
router.get("/devices", async (req, res) => {
|
||||
try {
|
||||
// Get token from Authorization header
|
||||
const token = req.headers.authorization?.replace("Bearer ", "");
|
||||
|
||||
if (!token || token !== APP_CONFIG.HASS_TOKEN) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Unauthorized - Invalid token'
|
||||
});
|
||||
}
|
||||
|
||||
const tool = tools.find(t => t.name === 'list_devices');
|
||||
if (!tool) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Tool not found'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await tool.execute({ token });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
});
|
||||
if (!token || token !== APP_CONFIG.HASS_TOKEN) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Unauthorized - Invalid token",
|
||||
});
|
||||
}
|
||||
|
||||
const tool = tools.find((t) => t.name === "list_devices");
|
||||
if (!tool) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Tool not found",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await tool.execute({ token });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Control device endpoint
|
||||
router.post('/control', async (req, res) => {
|
||||
try {
|
||||
// Get token from Authorization header
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
router.post("/control", async (req, res) => {
|
||||
try {
|
||||
// Get token from Authorization header
|
||||
const token = req.headers.authorization?.replace("Bearer ", "");
|
||||
|
||||
if (!token || token !== APP_CONFIG.HASS_TOKEN) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Unauthorized - Invalid token'
|
||||
});
|
||||
}
|
||||
|
||||
const tool = tools.find(t => t.name === 'control');
|
||||
if (!tool) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Tool not found'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await tool.execute({
|
||||
...req.body,
|
||||
token
|
||||
});
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
});
|
||||
if (!token || token !== APP_CONFIG.HASS_TOKEN) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Unauthorized - Invalid token",
|
||||
});
|
||||
}
|
||||
|
||||
const tool = tools.find((t) => t.name === "control");
|
||||
if (!tool) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Tool not found",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await tool.execute({
|
||||
...req.body,
|
||||
token,
|
||||
});
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export { router as toolRoutes };
|
||||
export { router as toolRoutes };
|
||||
|
||||
Reference in New Issue
Block a user