NodeMCU MCP Service
by amanasmuei
Verified
const express = require('express');
const bodyParser = require('body-parser');
const dotenv = require('dotenv');
const { spawn } = require('child_process');
const deviceManager = require('./services/DeviceManager');
// Load environment variables
dotenv.config();
// MCP protocol handlers
class NodeMCUServer {
constructor() {
this.toolDefinitions = {
"list-devices": {
description: "List all registered NodeMCU devices and their status",
parameters: {}
},
"get-device": {
description: "Get detailed information about a specific NodeMCU device",
parameters: {
deviceId: {
type: "string",
description: "The ID of the device to get information about"
}
}
},
"send-command": {
description: "Send a command to a NodeMCU device",
parameters: {
deviceId: {
type: "string",
description: "The ID of the device to send the command to"
},
command: {
type: "string",
description: "The command to send (restart, update, status, etc.)"
},
params: {
type: "object",
description: "Optional parameters for the command"
}
}
},
"update-config": {
description: "Update the configuration of a NodeMCU device",
parameters: {
deviceId: {
type: "string",
description: "The ID of the device to update configuration for"
},
config: {
type: "object",
description: "Configuration parameters to update"
}
}
}
};
}
// Handle MCP initialization
async initialize() {
process.stdout.write(JSON.stringify({
type: "initialize",
protocol_version: "0.1",
tools: this.toolDefinitions
}) + "\n");
}
// Process MCP messages from stdin
async processMessage(message) {
try {
const parsed = JSON.parse(message);
switch (parsed.type) {
case "execute_tool":
await this.handleToolExecution(parsed);
break;
default:
console.error(`Unknown message type: ${parsed.type}`);
}
} catch (error) {
console.error('Error processing message:', error);
this.sendErrorResponse('internal_error', 'Failed to process message');
}
}
// Handle tool execution requests
async handleToolExecution(request) {
const { tool_name, tool_params, request_id } = request;
try {
let result;
switch (tool_name) {
case "list-devices":
result = await this.listDevices();
break;
case "get-device":
result = await this.getDevice(tool_params.deviceId);
break;
case "send-command":
result = await this.sendCommand(tool_params.deviceId, tool_params.command, tool_params.params || {});
break;
case "update-config":
result = await this.updateConfig(tool_params.deviceId, tool_params.config);
break;
default:
throw new Error(`Unknown tool: ${tool_name}`);
}
this.sendToolResponse(request_id, result);
} catch (error) {
console.error(`Error executing tool ${tool_name}:`, error);
this.sendErrorResponse(request_id, 'tool_execution_error', error.message);
}
}
// Tool implementation: List all devices
async listDevices() {
const devices = deviceManager.getAllDevices();
return {
devices: devices.map(device => ({
id: device.id,
name: device.name,
status: device.status,
lastSeen: device.lastSeen
})),
count: devices.length
};
}
// Tool implementation: Get device details
async getDevice(deviceId) {
if (!deviceId) {
throw new Error('Device ID is required');
}
const device = deviceManager.getDevice(deviceId);
if (!device) {
throw new Error(`Device not found: ${deviceId}`);
}
return device;
}
// Tool implementation: Send command to device
async sendCommand(deviceId, command, params) {
if (!deviceId) {
throw new Error('Device ID is required');
}
if (!command) {
throw new Error('Command is required');
}
try {
return await deviceManager.sendCommand(deviceId, command, params);
} catch (error) {
throw new Error(`Failed to send command: ${error.message}`);
}
}
// Tool implementation: Update device configuration
async updateConfig(deviceId, config) {
if (!deviceId) {
throw new Error('Device ID is required');
}
if (!config || Object.keys(config).length === 0) {
throw new Error('Configuration is required');
}
try {
return await deviceManager.updateDeviceConfig(deviceId, config);
} catch (error) {
throw new Error(`Failed to update configuration: ${error.message}`);
}
}
// Send tool execution response
sendToolResponse(requestId, result) {
process.stdout.write(JSON.stringify({
type: "tool_response",
request_id: requestId,
status: "success",
result
}) + "\n");
}
// Send error response
sendErrorResponse(requestId, errorType, errorMessage) {
process.stdout.write(JSON.stringify({
type: "tool_response",
request_id: requestId,
status: "error",
error: {
type: errorType,
message: errorMessage
}
}) + "\n");
}
// Start the MCP server
async run() {
// Initialize and send tool definitions
await this.initialize();
// Process stdin for MCP messages
process.stdin.setEncoding('utf8');
let buffer = '';
process.stdin.on('data', (chunk) => {
buffer += chunk;
// Process complete JSON messages
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep the last incomplete line in buffer
for (const line of lines) {
if (line.trim()) {
this.processMessage(line);
}
}
});
// Also start the regular API server for compatibility
this.startAPIServer();
}
// Start a traditional API server alongside MCP for compatibility
startAPIServer() {
const app = express();
const PORT = process.env.PORT || 3000;
app.use(bodyParser.json());
// Simple endpoint for checking if the server is running
app.get('/health', (req, res) => {
res.status(200).json({
status: 'ok',
message: 'NodeMCU MCP Service is running'
});
});
// Start API server
app.listen(PORT, () => {
console.error(`API Server running on port ${PORT}`);
});
}
}
// Create and run the MCP server
const server = new NodeMCUServer();
server.run().catch(error => {
console.error('Fatal error starting MCP server:', error);
process.exit(1);
});