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
JavaScript
#!/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);