UNPKG

leshan-mcp-server

Version:

A standards-compliant MCP server for Leshan LwM2M, exposing Leshan as Model Context Protocol tools.

312 lines (271 loc) 10.1 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; // Import config and utilities import { validateEnvironment } from "./config/env.js"; import { configureLogger } from "./utils/loggerConfig.js"; import logger from "./utils/loggerConfig.js"; import healthChecker from "./utils/healthCheck.js"; import rateLimiter from "./utils/rateLimiter.js"; // Import handlers import { listDevices } from "./handlers/listDevices.js"; import { getDeviceInfo } from "./handlers/getDeviceInfo.js"; import { readResource } from "./handlers/readResource.js"; import { writeResource } from "./handlers/writeResource.js"; import { executeResource } from "./handlers/executeResource.js"; import { observeResource } from "./handlers/observeResource.js"; import { cancelObservation } from "./handlers/cancelObservation.js"; // Import prompts import { deviceDiagnosticsPrompt } from "./prompts/deviceDiagnosticsPrompt.js"; import { deviceMonitoringPrompt } from "./prompts/deviceMonitoringPrompt.js"; import { lwm2mTroubleshootingPrompt } from "./prompts/lwm2mTroubleshootingPrompt.js"; import { deviceConfigurationPrompt } from "./prompts/deviceConfigurationPrompt.js"; // Configure environment and logging const env = validateEnvironment(); await configureLogger(); // Create MCP server const server = new McpServer({ name: "leshan-mcp-server", version: "2.0.0", }); // Register tools with comprehensive schemas server.tool( "list-devices", "List all registered LwM2M devices in the Leshan server", {}, listDevices ); server.tool( "get-device-info", "Get detailed information about a specific LwM2M device", { deviceId: z.string() .min(1, "Device ID cannot be empty") .max(64, "Device ID too long") .describe("The device endpoint identifier"), }, getDeviceInfo ); server.tool( "read-resource", "Read a resource value from a LwM2M device", { deviceId: z.string().min(1).max(64).describe("Device endpoint identifier"), objectId: z.string().regex(/^\d+$/, "Must be numeric").describe("LwM2M Object ID (e.g., '3' for Device Object)"), instanceId: z.string().regex(/^\d+$/, "Must be numeric").describe("Object Instance ID (typically '0')"), resourceId: z.string().regex(/^\d+$/, "Must be numeric").describe("Resource ID within the object instance"), }, readResource ); server.tool( "write-resource", "Write a value to a LwM2M device resource", { deviceId: z.string().min(1).max(64).describe("Device endpoint identifier"), objectId: z.string().regex(/^\d+$/, "Must be numeric").describe("LwM2M Object ID"), instanceId: z.string().regex(/^\d+$/, "Must be numeric").describe("Object Instance ID"), resourceId: z.string().regex(/^\d+$/, "Must be numeric").describe("Resource ID"), value: z.any().describe("Value to write to the resource"), }, writeResource ); server.tool( "execute-resource", "Execute a resource on a LwM2M device", { deviceId: z.string().min(1).max(64).describe("Device endpoint identifier"), objectId: z.string().regex(/^\d+$/, "Must be numeric").describe("LwM2M Object ID"), instanceId: z.string().regex(/^\d+$/, "Must be numeric").describe("Object Instance ID"), resourceId: z.string().regex(/^\d+$/, "Must be numeric").describe("Resource ID"), arguments: z.any().optional().describe("Optional execution arguments"), }, executeResource ); server.tool( "observe-resource", "Start observing a LwM2M resource for changes", { deviceId: z.string().min(1).max(64).describe("Device endpoint identifier"), objectId: z.string().regex(/^\d+$/, "Must be numeric").describe("LwM2M Object ID"), instanceId: z.string().regex(/^\d+$/, "Must be numeric").describe("Object Instance ID"), resourceId: z.string().regex(/^\d+$/, "Must be numeric").describe("Resource ID"), }, observeResource ); server.tool( "cancel-observation", "Cancel an active observation on a LwM2M resource", { deviceId: z.string().min(1).max(64).describe("Device endpoint identifier"), objectId: z.string().regex(/^\d+$/, "Must be numeric").describe("LwM2M Object ID"), instanceId: z.string().regex(/^\d+$/, "Must be numeric").describe("Object Instance ID"), resourceId: z.string().regex(/^\d+$/, "Must be numeric").describe("Resource ID"), }, cancelObservation ); // Add health check and system monitoring tools server.tool( "health-check", "Check the health status of the MCP server and Leshan connection", {}, async () => { const operationId = `health-check-${Date.now()}`; try { logger.info("Health check requested", { operationId }); const health = await healthChecker.checkHealth(); const stats = healthChecker.getHealthStats(); const rateLimiterStats = rateLimiter.getStats(); return { content: [{ type: "text", text: JSON.stringify({ ...health, statistics: stats, rateLimiter: rateLimiterStats, configuration: { leshanUrl: env.LESHAN_URL, timeout: env.LESHAN_TIMEOUT, retries: env.LESHAN_RETRIES, maxConcurrentRequests: env.MAX_CONCURRENT_REQUESTS, logLevel: env.LOG_LEVEL } }, null, 2) }] }; } catch (error) { logger.error("Health check tool failed", { operationId, error: error.message }); return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error.message, operationId, timestamp: new Date().toISOString() }, null, 2) }], isError: true }; } } ); server.tool( "system-stats", "Get detailed system and performance statistics", {}, async () => { const operationId = `system-stats-${Date.now()}`; try { logger.info("System stats requested", { operationId }); const stats = { timestamp: new Date().toISOString(), operationId, system: { uptime: process.uptime(), memoryUsage: process.memoryUsage(), cpuUsage: process.cpuUsage(), nodeVersion: process.version, platform: process.platform, arch: process.arch, pid: process.pid }, healthChecker: healthChecker.getHealthStats(), rateLimiter: rateLimiter.getStats(), environment: { nodeEnv: env.NODE_ENV, leshanUrl: env.LESHAN_URL, logLevel: env.LOG_LEVEL } }; return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] }; } catch (error) { logger.error("System stats tool failed", { operationId, error: error.message }); return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error.message, operationId, timestamp: new Date().toISOString() }, null, 2) }], isError: true }; } } ); // Register prompts server.prompt("device-diagnostics", "Comprehensive diagnostic analysis for LwM2M devices", { deviceId: z.string().optional().describe("Device endpoint to diagnose") }, deviceDiagnosticsPrompt); server.prompt("device-monitoring-setup", "Set up continuous monitoring for LwM2M devices", { deviceId: z.string().optional().describe("Device endpoint to monitor"), monitoringType: z.enum(["basic", "advanced", "custom"]).default("basic").describe("Type of monitoring to configure") }, deviceMonitoringPrompt); server.prompt("lwm2m-troubleshooting", "Systematic troubleshooting guide for LwM2M device issues", { deviceId: z.string().optional().describe("Device endpoint with issues"), issueType: z.enum(["connectivity", "registration", "resource", "performance", "general"]).default("general").describe("Type of issue"), symptoms: z.string().optional().describe("Description of observed symptoms") }, lwm2mTroubleshootingPrompt); server.prompt("device-configuration", "Configure and optimize LwM2M device settings", { deviceId: z.string().optional().describe("Device endpoint to configure"), configurationType: z.enum(["initial", "optimization", "security"]).default("initial").describe("Type of configuration") }, deviceConfigurationPrompt); // Global error handlers process.on("unhandledRejection", (reason, promise) => { logger.error("Unhandled promise rejection", { reason: reason?.message || reason, stack: reason?.stack, promise: promise.toString() }); }); process.on("uncaughtException", (error) => { logger.error("Uncaught exception", { error: error.message, stack: error.stack }); process.exit(1); }); // Graceful shutdown handlers const shutdown = async (signal) => { logger.info(`Received ${signal}, initiating graceful shutdown`); try { // Clear rate limiter queue rateLimiter.clearQueue(); // Close logger await logger.close(); process.exit(0); } catch (error) { logger.error("Error during shutdown", { error: error.message }); process.exit(1); } }; process.on("SIGINT", () => shutdown("SIGINT")); process.on("SIGTERM", () => shutdown("SIGTERM")); // Server connection function async function connectServer() { try { logger.info("Initializing Leshan MCP Server", { version: "1.0.12", leshanUrl: env.LESHAN_URL, nodeEnv: env.NODE_ENV, maxConcurrentRequests: env.MAX_CONCURRENT_REQUESTS }); // Perform initial health check await healthChecker.checkHealth(); const transport = new StdioServerTransport(); await server.connect(transport); logger.info("Leshan MCP Server connected successfully"); } catch (error) { logger.error("Error initializing Leshan MCP Server", { error: error.message }); process.exit(1); } } connectServer().catch(console.error);